From 6a3a95352b2771447057cb6d26415c29558bf668 Mon Sep 17 00:00:00 2001 From: LindyHopperGT <91915878+LindyHopperGT@users.noreply.github.com> Date: Mon, 30 Mar 2026 10:14:55 -0700 Subject: [PATCH 1/7] Flow Preload and Policy changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Flow] Replaced the stubbed, nonfunctional preload mechanism in FlowGraph with a policy-driven, async-safe & per-project extendable system. Core Interface (IFlowPreloadableInterface) Replaced ad-hoc PreloadContentAsync (TFunction callback) with PreloadContent() → EFlowPreloadResult (Completed / PreloadInProgress) and FlushContent(). Async C++ nodes return PreloadInProgress and call NotifyPreloadComplete() from their completion delegate. Async Blueprint nodes override K2_PreloadContent and call NotifyPreloadComplete() on self. Sync nodes return Completed unchanged. Policy System (FFlowPreloadPolicy, FFlowPinConnectionPolicy) Introduced FFlowPreloadPolicy instanced-struct policy controlling per-node preload timing (OnGraphInitialize, OnActivate, ManualOnly, Never) and flush timing (OnGraphDeinitialize, OnNodeFinish, ManualOnly, Never). UFlowSettings exposes default policies as nullable const T* accessors. UFlowAsset holds a resolved policy instance with check() validation. Preload Helper (FFlowPreloadHelper / FFlowPreloadHelper_Standard) New instanced-struct helper allocated per-node at InitializeInstance for any node (or addon) implementing IFlowPreloadableInterface. Tracks PendingPreloadCount (not a simple bool) so node + multiple addon participants are counted atomically before any PreloadContent call fires — prevents premature AllPreloadsComplete. Lifecycle hooks: OnNodeInitializeInstance, OnNodeActivate, OnNodeCleanup, OnNodeDeinitializeInstance, OnNodeExecuteInput. Safety flush on DeinitializeInstance regardless of flush timing policy. Context Pins Preloadable nodes gain Preload Content and Flush Content exec input pins and All Preloads Complete exec output pin automatically via GetContextInputs/GetContextOutputs. AllPreloadsComplete fires only when all participants (node + all preloadable addons) have reported completion. AddOn Participation TryInitializePreloadHelper allocates a helper when any addon implements IFlowPreloadableInterface, even if the node itself does not. TriggerPreload / TriggerFlush iterate preloadable addons alongside the node. Added UFlowNodeAddOn::NotifyPreloadComplete() (BlueprintCallable) — delegates to owning node, mirroring the Finish/TriggerOutput pattern. Implementing Nodes UFlowNode_PlayLevelSequence — stores FStreamableHandle, binds CreateWeakLambda → NotifyPreloadComplete(), returns PreloadInProgress; FlushContent cancels the handle. UFlowNode_ExecuteComponent — delegates to component; PreloadInProgress from a component is guarded with ensureAlwaysMsgf (no callback path exists) and treated as Completed. UFlowNode_SubGraph — synchronous CreateSubFlow, returns Completed. --- LICENSE | 2 +- Source/Flow/Private/AddOns/FlowNodeAddOn.cpp | 8 + .../FlowNodeAddOn_PredicateCompareValues.cpp | 113 ++++------ Source/Flow/Private/FlowAsset.cpp | 173 +++++++++++---- Source/Flow/Private/FlowSettings.cpp | 18 ++ Source/Flow/Private/FlowSubsystem.cpp | 5 - .../Interfaces/FlowPreloadableInterface.cpp | 10 + .../Nodes/Actor/FlowNode_ExecuteComponent.cpp | 44 ++-- .../Actor/FlowNode_PlayLevelSequence.cpp | 37 +++- Source/Flow/Private/Nodes/FlowNode.cpp | 157 +++++++++++++- Source/Flow/Private/Nodes/FlowNodeBase.cpp | 20 -- .../Private/Nodes/Graph/FlowNode_SubGraph.cpp | 16 +- .../Policies/FlowPinConnectionPolicy.cpp | 167 ++++++++++++++ .../Policies/FlowPinTypeMatchPolicy.cpp | 5 + .../Private/Policies/FlowPreloadHelper.cpp | 200 +++++++++++++++++ .../Private/Policies/FlowPreloadPolicy.cpp | 5 + .../Policies/FlowStandardPreloadPolicies.cpp | 32 +++ .../Types/FlowPinTypeNamesStandard.cpp | 205 ++++-------------- Source/Flow/Public/AddOns/FlowNodeAddOn.h | 6 + .../FlowNodeAddOn_PredicateCompareValues.h | 30 +-- Source/Flow/Public/FlowAsset.h | 44 +++- Source/Flow/Public/FlowSettings.h | 17 ++ .../Interfaces/FlowCoreExecutableInterface.h | 10 - .../Interfaces/FlowPreloadableInterface.h | 51 +++++ .../Nodes/Actor/FlowNode_ExecuteComponent.h | 12 +- .../Nodes/Actor/FlowNode_PlayLevelSequence.h | 11 +- Source/Flow/Public/Nodes/FlowNode.h | 47 +++- Source/Flow/Public/Nodes/FlowNodeBase.h | 3 - .../Public/Nodes/Graph/FlowNode_SubGraph.h | 9 +- .../Public/Policies/FlowPinConnectionPolicy.h | 85 ++++++++ .../FlowPinTypeMatchPolicy.h | 12 +- Source/Flow/Public/Policies/FlowPolicy.h | 19 ++ .../Flow/Public/Policies/FlowPreloadHelper.h | 105 +++++++++ .../Flow/Public/Policies/FlowPreloadPolicy.h | 29 +++ .../Public/Policies/FlowPreloadPolicyEnums.h | 80 +++++++ .../FlowStandardPinConnectionPolicies.h | 105 +++++++++ .../Policies/FlowStandardPreloadPolicies.h | 43 ++++ .../Public/Types/FlowPinTypeNamesStandard.h | 16 +- .../Private/Graph/FlowGraphSchema.cpp | 81 +++---- .../Private/Graph/FlowGraphSettings.cpp | 6 +- .../Private/Graph/Nodes/FlowGraphNode.cpp | 2 +- .../FlowEditor/Public/Graph/FlowGraphSchema.h | 19 +- 42 files changed, 1626 insertions(+), 433 deletions(-) create mode 100644 Source/Flow/Private/Interfaces/FlowPreloadableInterface.cpp create mode 100644 Source/Flow/Private/Policies/FlowPinConnectionPolicy.cpp create mode 100644 Source/Flow/Private/Policies/FlowPinTypeMatchPolicy.cpp create mode 100644 Source/Flow/Private/Policies/FlowPreloadHelper.cpp create mode 100644 Source/Flow/Private/Policies/FlowPreloadPolicy.cpp create mode 100644 Source/Flow/Private/Policies/FlowStandardPreloadPolicies.cpp create mode 100644 Source/Flow/Public/Interfaces/FlowPreloadableInterface.h create mode 100644 Source/Flow/Public/Policies/FlowPinConnectionPolicy.h rename Source/Flow/Public/{Asset => Policies}/FlowPinTypeMatchPolicy.h (62%) create mode 100644 Source/Flow/Public/Policies/FlowPolicy.h create mode 100644 Source/Flow/Public/Policies/FlowPreloadHelper.h create mode 100644 Source/Flow/Public/Policies/FlowPreloadPolicy.h create mode 100644 Source/Flow/Public/Policies/FlowPreloadPolicyEnums.h create mode 100644 Source/Flow/Public/Policies/FlowStandardPinConnectionPolicies.h create mode 100644 Source/Flow/Public/Policies/FlowStandardPreloadPolicies.h diff --git a/LICENSE b/LICENSE index 2ab42e29..7670746d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -MIT License +MIT License Copyright (c) https://github.com/MothCocoon/FlowGraph/graphs/contributors diff --git a/Source/Flow/Private/AddOns/FlowNodeAddOn.cpp b/Source/Flow/Private/AddOns/FlowNodeAddOn.cpp index 3a809ddf..b79ee7ad 100644 --- a/Source/Flow/Private/AddOns/FlowNodeAddOn.cpp +++ b/Source/Flow/Private/AddOns/FlowNodeAddOn.cpp @@ -63,6 +63,14 @@ EFlowAddOnAcceptResult UFlowNodeAddOn::AcceptFlowNodeAddOnParent_Implementation( return EFlowAddOnAcceptResult::Undetermined; } +void UFlowNodeAddOn::NotifyPreloadComplete() +{ + if (ensure(FlowNode)) + { + FlowNode->NotifyPreloadComplete(); + } +} + UFlowNode* UFlowNodeAddOn::GetFlowNode() const { // We are making the assumption that this would always be known during runtime diff --git a/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateCompareValues.cpp b/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateCompareValues.cpp index 513f2811..a0f1c8ae 100644 --- a/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateCompareValues.cpp +++ b/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateCompareValues.cpp @@ -6,6 +6,8 @@ #include "Types/FlowPinTypeNamesStandard.h" #include "Types/FlowPinTypesStandard.h" #include "Types/FlowDataPinValuesStandard.h" +#include "FlowAsset.h" +#include "Policies/FlowPinConnectionPolicy.h" #define LOCTEXT_NAMESPACE "FlowNodeAddOn_PredicateCompareValues" @@ -93,25 +95,27 @@ bool UFlowNodeAddOn_PredicateCompareValues::IsArithmeticOp() const return EFlowPredicateCompareOperatorType_Classifiers::IsArithmeticOperation(OperatorType); } -bool UFlowNodeAddOn_PredicateCompareValues::IsNumericTypeName(const FName& TypeName) +bool UFlowNodeAddOn_PredicateCompareValues::IsNumericTypeName( + const FFlowPinConnectionPolicy& PinConnectionPolicy, + const FName& TypeName) { - return - IsFloatingPointType(TypeName) || - IsIntegerType(TypeName); + return + PinConnectionPolicy.GetAllSupportedIntegerTypes().Contains(TypeName) || + PinConnectionPolicy.GetAllSupportedFloatTypes().Contains(TypeName); } -bool UFlowNodeAddOn_PredicateCompareValues::IsFloatingPointType(const FName& TypeName) +bool UFlowNodeAddOn_PredicateCompareValues::IsFloatingPointType( + const FFlowPinConnectionPolicy& PinConnectionPolicy, + const FName& TypeName) { - return - TypeName == FFlowPinTypeNamesStandard::PinTypeNameFloat || - TypeName == FFlowPinTypeNamesStandard::PinTypeNameDouble; + return PinConnectionPolicy.GetAllSupportedFloatTypes().Contains(TypeName); } -bool UFlowNodeAddOn_PredicateCompareValues::IsIntegerType(const FName& TypeName) +bool UFlowNodeAddOn_PredicateCompareValues::IsIntegerType( + const FFlowPinConnectionPolicy& PinConnectionPolicy, + const FName& TypeName) { - return - TypeName == FFlowPinTypeNamesStandard::PinTypeNameInt || - TypeName == FFlowPinTypeNamesStandard::PinTypeNameInt64; + return PinConnectionPolicy.GetAllSupportedIntegerTypes().Contains(TypeName); } bool UFlowNodeAddOn_PredicateCompareValues::IsTextType(const FName& TypeName) @@ -132,24 +136,21 @@ bool UFlowNodeAddOn_PredicateCompareValues::IsNameLikeType(const FName& TypeName TypeName == FFlowPinTypeNamesStandard::PinTypeNameEnum; } -bool UFlowNodeAddOn_PredicateCompareValues::IsEnumTypeName(const FName& TypeName) -{ - return TypeName == FFlowPinTypeNamesStandard::PinTypeNameEnum; -} - -bool UFlowNodeAddOn_PredicateCompareValues::IsAnyStringLikeTypeName(const FName& TypeName) +bool UFlowNodeAddOn_PredicateCompareValues::IsAnyStringLikeTypeName( + const FFlowPinConnectionPolicy& PinConnectionPolicy, + const FName& TypeName) { - return + // Special-casing NameLike, since the CompareValues predicate counts Enums as Names + return IsNameLikeType(TypeName) || - IsTextType(TypeName) || - IsStringType(TypeName); + PinConnectionPolicy.GetAllSupportedStringLikeTypes().Contains(TypeName); } -bool UFlowNodeAddOn_PredicateCompareValues::IsGameplayTagLikeTypeName(const FName& TypeName) +bool UFlowNodeAddOn_PredicateCompareValues::IsGameplayTagLikeTypeName( + const FFlowPinConnectionPolicy& PinConnectionPolicy, + const FName& TypeName) { - return - TypeName == FFlowPinTypeNamesStandard::PinTypeNameGameplayTag || - TypeName == FFlowPinTypeNamesStandard::PinTypeNameGameplayTagContainer; + return PinConnectionPolicy.GetAllSupportedGameplayTagTypes().Contains(TypeName); } bool UFlowNodeAddOn_PredicateCompareValues::IsBoolTypeName(const FName& TypeName) @@ -257,12 +258,17 @@ EDataValidationResult UFlowNodeAddOn_PredicateCompareValues::ValidateNode() } // Check type compatibility + + const UFlowAsset* FlowAsset = GetFlowAsset(); + check(IsValid(FlowAsset)); + const FFlowPinConnectionPolicy& PinConnectionPolicy = FlowAsset->GetPinConnectionPolicy(); + const FName LeftTypeName = LeftPinTypeName.Name; const FName RightTypeName = RightPinTypeName.Name; const bool bSameType = (LeftTypeName == RightTypeName); - if (!bSameType && !AreComparableStandardPinTypes(LeftTypeName, RightTypeName)) + if (!bSameType && !AreComparablePinTypes(PinConnectionPolicy, LeftTypeName, RightTypeName)) { LogValidationError(FString::Printf( TEXT("Pin types are not comparable: '%s' vs '%s'."), @@ -272,7 +278,8 @@ EDataValidationResult UFlowNodeAddOn_PredicateCompareValues::ValidateNode() } // Validate arithmetic operators are only used with numeric types - if (IsArithmeticOp() && !(IsNumericTypeName(LeftTypeName) && IsNumericTypeName(RightTypeName))) + if (IsArithmeticOp() && + !(IsNumericTypeName(PinConnectionPolicy, LeftTypeName) && IsNumericTypeName(PinConnectionPolicy, RightTypeName))) { LogValidationError(FString::Printf( TEXT("Arithmetic operator '%s' is only supported for numeric pin types (Int/Int64/Float/Double). Current types: '%s' vs '%s'."), @@ -321,41 +328,9 @@ FText UFlowNodeAddOn_PredicateCompareValues::K2_GetNodeTitle_Implementation() co #endif // WITH_EDITOR -bool UFlowNodeAddOn_PredicateCompareValues::AreComparableStandardPinTypes(const FName& LeftPinTypeName, const FName& RightPinTypeName) +bool UFlowNodeAddOn_PredicateCompareValues::AreComparablePinTypes(const FFlowPinConnectionPolicy& PinConnectionPolicy, const FName& LeftPinTypeName, const FName& RightPinTypeName) { - // TODO (gtaylor) We should update this function to respect the authored pin type compatibility settings. - // We can't at this time, because they are known only to the editor flow code (UFlowGraphSchema::ArePinTypesCompatible), - // but we can conceivably move that information to UFlowAsset (or similar) for runtime and editor-time code to use. - - if (LeftPinTypeName == RightPinTypeName) - { - return true; - } - - // Numeric: allow int/int64/float/double interchange - if (IsNumericTypeName(LeftPinTypeName) && IsNumericTypeName(RightPinTypeName)) - { - return true; - } - - // String-like: allow Name/String/Text/Enum interchange - // (we include Enums as they have FName values for the purposes of comparison) - if (IsAnyStringLikeTypeName(LeftPinTypeName) && IsAnyStringLikeTypeName(RightPinTypeName)) - { - return true; - } - - // GameplayTag / Container: allow interchange (type templates can upscale tag -> container) - if (IsGameplayTagLikeTypeName(LeftPinTypeName) && IsGameplayTagLikeTypeName(RightPinTypeName)) - { - return true; - } - - // Note: Bool, Vector, Rotator, Transform, Object, Class, InstancedStruct are all - // only comparable with themselves (handled by the LeftPinTypeName == RightPinTypeName check above). - // Unknown/user types also fall into same-type comparison via the fallback path in EvaluatePredicate. - - return false; + return PinConnectionPolicy.CanConnectPinTypeNames(LeftPinTypeName, RightPinTypeName); } bool UFlowNodeAddOn_PredicateCompareValues::CacheTypeNames(FCachedTypeNames& OutCache) const @@ -594,6 +569,10 @@ bool UFlowNodeAddOn_PredicateCompareValues::EvaluatePredicate_Implementation() c return false; } + const UFlowAsset* FlowAsset = GetFlowAsset(); + check(IsValid(FlowAsset)); + const FFlowPinConnectionPolicy& PinConnectionPolicy = FlowAsset->GetPinConnectionPolicy(); + const FName& LeftTypeName = Cache.LeftTypeName; const FName& RightTypeName = Cache.RightTypeName; @@ -601,7 +580,7 @@ bool UFlowNodeAddOn_PredicateCompareValues::EvaluatePredicate_Implementation() c // Type compatibility gate. // Same-type unknowns are allowed through for the fallback path at the bottom. - if (!bSameType && !AreComparableStandardPinTypes(LeftTypeName, RightTypeName)) + if (!bSameType && !AreComparablePinTypes(PinConnectionPolicy, LeftTypeName, RightTypeName)) { LogError(FString::Printf( TEXT("Compare Values pin types are not comparable: '%s' vs '%s'."), @@ -612,16 +591,16 @@ bool UFlowNodeAddOn_PredicateCompareValues::EvaluatePredicate_Implementation() c } // Arithmetic operators: numeric only (fast reject before the cascade) - if (IsArithmeticOp() && !(IsNumericTypeName(LeftTypeName) && IsNumericTypeName(RightTypeName))) + if (IsArithmeticOp() && !(IsNumericTypeName(PinConnectionPolicy, LeftTypeName) && IsNumericTypeName(PinConnectionPolicy, RightTypeName))) { LogError(TEXT("Arithmetic operators are only supported for numeric pin types (Int/Int64/Float/Double).")); return false; } // Numeric (full operator set) - if (IsNumericTypeName(LeftTypeName) && IsNumericTypeName(RightTypeName)) + if (IsNumericTypeName(PinConnectionPolicy, LeftTypeName) && IsNumericTypeName(PinConnectionPolicy, RightTypeName)) { - if (IsFloatingPointType(LeftTypeName) || IsFloatingPointType(RightTypeName)) + if (IsFloatingPointType(PinConnectionPolicy, LeftTypeName) || IsFloatingPointType(PinConnectionPolicy, RightTypeName)) { return TryCompareAsDouble(); } @@ -630,14 +609,14 @@ bool UFlowNodeAddOn_PredicateCompareValues::EvaluatePredicate_Implementation() c } // Gameplay tags: compare as container (superset). Equality ops only. - if (IsGameplayTagLikeTypeName(LeftTypeName) || IsGameplayTagLikeTypeName(RightTypeName)) + if (IsGameplayTagLikeTypeName(PinConnectionPolicy, LeftTypeName) || IsGameplayTagLikeTypeName(PinConnectionPolicy, RightTypeName)) { return EvaluateEqualityBlock(TEXT("Gameplay Tag"), [this](bool& bIsEqual) { return TryCheckGameplayTagsEqual(bIsEqual); }); } // String-like (including enums-as-names). Equality ops only. - if (IsAnyStringLikeTypeName(LeftTypeName) || IsAnyStringLikeTypeName(RightTypeName)) + if (IsAnyStringLikeTypeName(PinConnectionPolicy, LeftTypeName) || IsAnyStringLikeTypeName(PinConnectionPolicy, RightTypeName)) { // Dispatch order is significant: // 1) Name-like (Name OR Enum) => case-insensitive compare via FString diff --git a/Source/Flow/Private/FlowAsset.cpp b/Source/Flow/Private/FlowAsset.cpp index 0294a9a0..09b2f334 100644 --- a/Source/Flow/Private/FlowAsset.cpp +++ b/Source/Flow/Private/FlowAsset.cpp @@ -14,6 +14,8 @@ #include "Nodes/Graph/FlowNode_CustomOutput.h" #include "Nodes/Graph/FlowNode_Start.h" #include "Nodes/Graph/FlowNode_SubGraph.h" +#include "Policies/FlowPinConnectionPolicy.h" +#include "Policies/FlowPreloadPolicy.h" #include "Types/FlowAutoDataPinsWorkingData.h" #include "Types/FlowDataPinValue.h" #include "Types/FlowStructUtils.h" @@ -54,6 +56,8 @@ UFlowAsset::UFlowAsset(const FObjectInitializer& ObjectInitializer) , bStartNodePlacedAsGhostNode(false) , TemplateAsset(nullptr) , FinishPolicy(EFlowFinishPolicy::Keep) + , PinConnectionPolicy() + , PreloadPolicy() { if (!AssetGuid.IsValid()) { @@ -63,6 +67,16 @@ UFlowAsset::UFlowAsset(const FObjectInitializer& ObjectInitializer) ExpectedOwnerClass = GetDefault()->GetDefaultExpectedOwnerClass(); } +void UFlowAsset::PostInitProperties() +{ + Super::PostInitProperties(); + +#if WITH_EDITOR + InitializePinConnectionPolicy(); + InitializePreloadPolicy(); +#endif +} + #if WITH_EDITOR void UFlowAsset::AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector) { @@ -1002,13 +1016,6 @@ void UFlowAsset::FinishFlow(const EFlowFinishPolicy InFinishPolicy, const bool b } ActiveNodes.Empty(); - // flush preloaded content - for (UFlowNode* PreloadedNode : PreloadedNodes) - { - PreloadedNode->TriggerFlush(); - } - PreloadedNodes.Empty(); - // provides option to finish game-specific logic prior to removing asset instance if (bRemoveInstance) { @@ -1041,8 +1048,8 @@ void UFlowAsset::CancelAndWarnForUnflushedDeferredTriggers() 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()); + "This is usually unexpected and may indicate a bug or abnormal termination."), + *GetName(), DeferredTransitionScopes.Num()); } TotalDroppedTriggers += Triggers.Num(); @@ -1052,18 +1059,21 @@ void UFlowAsset::CancelAndWarnForUnflushedDeferredTriggers() const UFlowNode* ToNode = GetNode(Trigger.NodeGuid); const UFlowNode* FromNode = Trigger.FromPin.NodeGuid.IsValid() ? GetNode(Trigger.FromPin.NodeGuid) : nullptr; + const FString ToNodeName = ToNode ? ToNode->GetName() : TEXT(""); + const FString FromNodeName = FromNode ? FromNode->GetName() : TEXT(""); + 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() + 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"), + *ToNodeName, + *Trigger.NodeGuid.ToString(), + *Trigger.PinName.ToString(), + *FromNodeName, + *Trigger.FromPin.NodeGuid.ToString(), + *Trigger.FromPin.PinName.ToString() ); } } @@ -1079,10 +1089,22 @@ bool UFlowAsset::HasStartedFlow() const AActor* UFlowAsset::TryFindActorOwner() const { - const UActorComponent* OwnerAsComponent = Cast(GetOwner()); - if (IsValid(OwnerAsComponent)) + UObject* OwnerObject = GetOwner(); + if (!IsValid(OwnerObject)) + { + return nullptr; + } + + // If the owner is already an Actor, return it directly + if (AActor* OwnerAsActor = Cast(OwnerObject)) + { + return OwnerAsActor; + } + + // If the owner is a Component, return its owning Actor + if (const UActorComponent* OwnerAsComponent = Cast(OwnerObject)) { - return Cast(OwnerAsComponent->GetOwner()); + return OwnerAsComponent->GetOwner(); } return nullptr; @@ -1415,40 +1437,92 @@ bool UFlowAsset::IsBoundToWorld_Implementation() const return bWorldBound; } -#if WITH_EDITOR -void UFlowAsset::LogError(const FString& MessageToLog, const UFlowNodeBase* Node) const +const FFlowPinConnectionPolicy& UFlowAsset::GetPinConnectionPolicy() const { - // this is runtime log which is should be only called on runtime instances of asset - if (TemplateAsset) + // Runtime instances delegate to their template, which holds the serialized policy + if (!PinConnectionPolicy.IsValid() && IsValid(TemplateAsset)) { - UE_LOG(LogFlow, Log, TEXT("Attempted to use Runtime Log on asset instance %s"), *MessageToLog); + return TemplateAsset->GetPinConnectionPolicy(); } - if (RuntimeLog.Get()) + // Graceful fallback: if PinConnectionPolicy was never initialized (asset predates this feature, + // or was never opened in editor), read directly from project settings at runtime. + if (!PinConnectionPolicy.IsValid()) { - const TSharedRef TokenizedMessage = RuntimeLog.Get()->Error(*MessageToLog, Node); - BroadcastRuntimeMessageAdded(TokenizedMessage); + const FFlowPinConnectionPolicy* SettingsPolicy = GetDefault()->GetPinConnectionPolicy(); + ensureAlways(SettingsPolicy); + if (SettingsPolicy) + { + return *SettingsPolicy; + } } + + check(PinConnectionPolicy.IsValid()); + return PinConnectionPolicy.Get(); } -void UFlowAsset::LogWarning(const FString& MessageToLog, const UFlowNodeBase* Node) const +const FFlowPreloadPolicy& UFlowAsset::GetPreloadPolicy() const { - // this is runtime log which is should be only called on runtime instances of asset - if (TemplateAsset) + // Runtime instances delegate to their template, which holds the serialized policy. + if (!PreloadPolicy.IsValid() && IsValid(TemplateAsset)) { - UE_LOG(LogFlow, Log, TEXT("Attempted to use Runtime Log on asset instance %s"), *MessageToLog); + return TemplateAsset->GetPreloadPolicy(); } - if (RuntimeLog.Get()) + // Graceful fallback: if PreloadPolicy was never initialized (asset predates this feature, + // or was never opened in editor), read directly from project settings at runtime. + if (!PreloadPolicy.IsValid()) + { + const FFlowPreloadPolicy* SettingsPolicy = GetDefault()->GetPreloadPolicy(); + ensureAlways(SettingsPolicy); + if (SettingsPolicy) + { + return *SettingsPolicy; + } + } + + check(PreloadPolicy.IsValid()); + return PreloadPolicy.Get(); +} + +#if WITH_EDITOR + +void UFlowAsset::InitializePinConnectionPolicy() +{ + const FInstancedStruct& SourceStruct = GetDefault()->PinConnectionPolicy; + if (ensure(SourceStruct.IsValid())) + { + PinConnectionPolicy.InitializeAsScriptStruct(SourceStruct.GetScriptStruct(), SourceStruct.GetMemory()); + } +} + +void UFlowAsset::InitializePreloadPolicy() +{ + const FInstancedStruct& SourceStruct = GetDefault()->PreloadPolicy; + if (ensure(SourceStruct.IsValid())) { - const TSharedRef TokenizedMessage = RuntimeLog.Get()->Warning(*MessageToLog, Node); - BroadcastRuntimeMessageAdded(TokenizedMessage); + PreloadPolicy.InitializeAsScriptStruct(SourceStruct.GetScriptStruct(), SourceStruct.GetMemory()); } } +void UFlowAsset::LogError(const FString& MessageToLog, const UFlowNodeBase* Node) const +{ + LogRuntimeMessage(EMessageSeverity::Error, MessageToLog, Node); +} + +void UFlowAsset::LogWarning(const FString& MessageToLog, const UFlowNodeBase* Node) const +{ + LogRuntimeMessage(EMessageSeverity::Warning, MessageToLog, Node); +} + void UFlowAsset::LogNote(const FString& MessageToLog, const UFlowNodeBase* Node) const { - // this is runtime log which is should be only called on runtime instances of asset + LogRuntimeMessage(EMessageSeverity::Info, MessageToLog, Node); +} + +void UFlowAsset::LogRuntimeMessage(EMessageSeverity::Type Severity, const FString& MessageToLog, const UFlowNodeBase* Node) const +{ + // this is runtime log which should only be called on runtime instances of asset if (TemplateAsset) { UE_LOG(LogFlow, Log, TEXT("Attempted to use Runtime Log on asset instance %s"), *MessageToLog); @@ -1456,8 +1530,23 @@ void UFlowAsset::LogNote(const FString& MessageToLog, const UFlowNodeBase* Node) if (RuntimeLog.Get()) { - const TSharedRef TokenizedMessage = RuntimeLog.Get()->Note(*MessageToLog, Node); - BroadcastRuntimeMessageAdded(TokenizedMessage); + TSharedPtr TokenizedMessage = nullptr; + switch (Severity) + { + case EMessageSeverity::Error: + TokenizedMessage = RuntimeLog.Get()->Error(*MessageToLog, Node); + break; + + case EMessageSeverity::Warning: + TokenizedMessage = RuntimeLog.Get()->Warning(*MessageToLog, Node); + break; + + default: + TokenizedMessage = RuntimeLog.Get()->Note(*MessageToLog, Node); + break; + } + + BroadcastRuntimeMessageAdded(TokenizedMessage.ToSharedRef()); } } -#endif +#endif \ No newline at end of file diff --git a/Source/Flow/Private/FlowSettings.cpp b/Source/Flow/Private/FlowSettings.cpp index d51f7a14..b0045399 100644 --- a/Source/Flow/Private/FlowSettings.cpp +++ b/Source/Flow/Private/FlowSettings.cpp @@ -2,11 +2,17 @@ #include "FlowSettings.h" #include "FlowComponent.h" +#include "FlowLogChannels.h" +#include "Policies/FlowPreloadPolicy.h" +#include "Policies/FlowStandardPinConnectionPolicies.h" +#include "Policies/FlowStandardPreloadPolicies.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(FlowSettings) UFlowSettings::UFlowSettings(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) + , PinConnectionPolicy(FFlowPinConnectionPolicy_VeryRelaxed::StaticStruct()) + , PreloadPolicy(FFlowPreloadPolicy_Standard::StaticStruct()) , bDeferTriggeredOutputsWhileTriggering(true) , bLogOnSignalDisabled(true) , bLogOnSignalPassthrough(true) @@ -17,7 +23,18 @@ UFlowSettings::UFlowSettings(const FObjectInitializer& ObjectInitializer) { } +const FFlowPinConnectionPolicy* UFlowSettings::GetPinConnectionPolicy() const +{ + return PinConnectionPolicy.GetPtr(); +} + +const FFlowPreloadPolicy* UFlowSettings::GetPreloadPolicy() const +{ + return PreloadPolicy.GetPtr(); +} + #if WITH_EDITOR + void UFlowSettings::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) { Super::PostEditChangeProperty(PropertyChangedEvent); @@ -27,6 +44,7 @@ void UFlowSettings::PostEditChangeProperty(FPropertyChangedEvent& PropertyChange (void)OnAdaptiveNodeTitlesChanged.ExecuteIfBound(); } } + #endif UClass* UFlowSettings::GetDefaultExpectedOwnerClass() const diff --git a/Source/Flow/Private/FlowSubsystem.cpp b/Source/Flow/Private/FlowSubsystem.cpp index bd7da57c..ffa3ca55 100644 --- a/Source/Flow/Private/FlowSubsystem.cpp +++ b/Source/Flow/Private/FlowSubsystem.cpp @@ -174,11 +174,6 @@ UFlowAsset* UFlowSubsystem::CreateSubFlow(UFlowNode_SubGraph* SubGraphNode, cons if (NewInstance) { InstancedSubFlows.Add(SubGraphNode, NewInstance); - - if (bPreloading) - { - NewInstance->PreloadNodes(); - } } } diff --git a/Source/Flow/Private/Interfaces/FlowPreloadableInterface.cpp b/Source/Flow/Private/Interfaces/FlowPreloadableInterface.cpp new file mode 100644 index 00000000..d3f91eda --- /dev/null +++ b/Source/Flow/Private/Interfaces/FlowPreloadableInterface.cpp @@ -0,0 +1,10 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Interfaces/FlowPreloadableInterface.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowPreloadableInterface) + +bool IFlowPreloadableInterface::ImplementsInterfaceSafe(const UObject* Object) +{ + return IsValid(Object) && Object->GetClass()->ImplementsInterface(UFlowPreloadableInterface::StaticClass()); +} diff --git a/Source/Flow/Private/Nodes/Actor/FlowNode_ExecuteComponent.cpp b/Source/Flow/Private/Nodes/Actor/FlowNode_ExecuteComponent.cpp index fc0d8f61..b2faec55 100644 --- a/Source/Flow/Private/Nodes/Actor/FlowNode_ExecuteComponent.cpp +++ b/Source/Flow/Private/Nodes/Actor/FlowNode_ExecuteComponent.cpp @@ -2,6 +2,7 @@ #include "Nodes/Actor/FlowNode_ExecuteComponent.h" #include "Interfaces/FlowCoreExecutableInterface.h" +#include "Interfaces/FlowPreloadableInterface.h" #include "Interfaces/FlowExternalExecutableInterface.h" #include "Interfaces/FlowContextPinSupplierInterface.h" #include "FlowAsset.h" @@ -74,38 +75,40 @@ void UFlowNode_ExecuteComponent::DeinitializeInstance() Super::DeinitializeInstance(); } -void UFlowNode_ExecuteComponent::PreloadContent() +EFlowPreloadResult UFlowNode_ExecuteComponent::PreloadContent() { - Super::PreloadContent(); - if (UActorComponent* ResolvedComp = TryResolveComponent()) { - if (IFlowCoreExecutableInterface* ComponentAsCoreExecutable = Cast(ResolvedComp)) - { - ComponentAsCoreExecutable->PreloadContent(); - } - else if (ResolvedComp->Implements()) + if (IFlowPreloadableInterface* PreloadableComponent = Cast(ResolvedComp)) { - IFlowCoreExecutableInterface::Execute_K2_PreloadContent(ResolvedComp); + FLOW_ASSERT_ENUM_MAX(EFlowPreloadResult, 2); + + const EFlowPreloadResult PreloadableComponentResult = PreloadableComponent->PreloadContent(); + + // TODO (gtaylor) Consider adding a mechanism for components to do an async preload. + // Components have no back-reference to this node and cannot call NotifyPreloadComplete(). + // Async (PreloadInProgress) component preloads are therefore unsupported (For Now(tm)): + // if a component returns PreloadInProgress the PendingPreloadCount would never reach zero. + ensureAlwaysMsgf(PreloadableComponentResult == EFlowPreloadResult::Completed, + TEXT("Component '%s' returned PreloadInProgress from PreloadContent(), but UFlowNode_ExecuteComponent has no mechanism to receive the async completion callback. Treating as Completed."), + *ResolvedComp->GetName()); + + return EFlowPreloadResult::Completed; } } + + return EFlowPreloadResult::Completed; } void UFlowNode_ExecuteComponent::FlushContent() { if (UActorComponent* ResolvedComp = TryResolveComponent()) { - if (IFlowCoreExecutableInterface* ComponentAsCoreExecutable = Cast(ResolvedComp)) - { - ComponentAsCoreExecutable->FlushContent(); - } - else if (ResolvedComp->Implements()) + if (IFlowPreloadableInterface* Preloadable = Cast(ResolvedComp)) { - IFlowCoreExecutableInterface::Execute_K2_FlushContent(ResolvedComp); + Preloadable->FlushContent(); } } - - Super::FlushContent(); } void UFlowNode_ExecuteComponent::OnActivate() @@ -180,6 +183,13 @@ void UFlowNode_ExecuteComponent::ForceFinishNode() void UFlowNode_ExecuteComponent::ExecuteInput(const FName& PinName) { + // Since this node implements IFlowPreloadableInterface, + // we need to call this to allow the PreloadHelper to intercept preload-specific PinNames + if (DispatchExecuteInputToPreloadHelper(PinName)) + { + return; + } + Super::ExecuteInput(PinName); if (UActorComponent* ResolvedComp = TryResolveComponent()) diff --git a/Source/Flow/Private/Nodes/Actor/FlowNode_PlayLevelSequence.cpp b/Source/Flow/Private/Nodes/Actor/FlowNode_PlayLevelSequence.cpp index 5b2d0fcb..c7d6c324 100644 --- a/Source/Flow/Private/Nodes/Actor/FlowNode_PlayLevelSequence.cpp +++ b/Source/Flow/Private/Nodes/Actor/FlowNode_PlayLevelSequence.cpp @@ -100,24 +100,44 @@ void UFlowNode_PlayLevelSequence::PostEditChangeProperty(FPropertyChangedEvent& } #endif -void UFlowNode_PlayLevelSequence::PreloadContent() +EFlowPreloadResult UFlowNode_PlayLevelSequence::PreloadContent() { #if ENABLE_VISUAL_LOG - UE_VLOG(this, LogFlow, Log, TEXT("Preloading")); + UE_VLOG(this, LogFlow, Log, TEXT("Preloading Content")); #endif - if (!Sequence.IsNull()) + FLOW_ASSERT_ENUM_MAX(EFlowPreloadResult, 2); + + if (Sequence.IsNull()) { - StreamableManager.RequestAsyncLoad({Sequence.ToSoftObjectPath()}, FStreamableDelegate()); + return EFlowPreloadResult::Completed; } + + // Bind a weak delegate so NotifyPreloadComplete() is called when streaming finishes. + // If the asset is already cached, RequestAsyncLoad fires the delegate synchronously + // (safe — PendingPreloadCount is already set by TriggerPreload before this call). + PreloadHandle = StreamableManager.RequestAsyncLoad( + Sequence.ToSoftObjectPath(), + FStreamableDelegate::CreateWeakLambda(this, [this]() + { + NotifyPreloadComplete(); + })); + + return EFlowPreloadResult::PreloadInProgress; } void UFlowNode_PlayLevelSequence::FlushContent() { #if ENABLE_VISUAL_LOG - UE_VLOG(this, LogFlow, Log, TEXT("Flushing preload")); + UE_VLOG(this, LogFlow, Log, TEXT("Flushing Preloaded Content")); #endif + if (PreloadHandle.IsValid()) + { + PreloadHandle->CancelHandle(); + PreloadHandle.Reset(); + } + if (!Sequence.IsNull()) { StreamableManager.Unload(Sequence.ToSoftObjectPath()); @@ -166,6 +186,13 @@ void UFlowNode_PlayLevelSequence::CreatePlayer() void UFlowNode_PlayLevelSequence::ExecuteInput(const FName& PinName) { + // Since this node implements IFlowPreloadableInterface, + // we need to call this to allow the PreloadHelper to intercept preload-specific PinNames + if (DispatchExecuteInputToPreloadHelper(PinName)) + { + return; + } + if (PinName == TEXT("Start")) { LoadedSequence = Sequence.LoadSynchronous(); diff --git a/Source/Flow/Private/Nodes/FlowNode.cpp b/Source/Flow/Private/Nodes/FlowNode.cpp index a0a4fe91..d6f1a696 100644 --- a/Source/Flow/Private/Nodes/FlowNode.cpp +++ b/Source/Flow/Private/Nodes/FlowNode.cpp @@ -5,6 +5,9 @@ #include "FlowAsset.h" #include "FlowSettings.h" +#include "Interfaces/FlowPreloadableInterface.h" +#include "Policies/FlowPreloadHelper.h" +#include "Policies/FlowPreloadPolicy.h" #include "Interfaces/FlowNodeWithExternalDataPinSupplierInterface.h" #include "Types/FlowAutoDataPinsWorkingData.h" #include "Types/FlowDataPinValue.h" @@ -33,7 +36,6 @@ FString UFlowNode::NoActorsFound = TEXT("No actors found"); UFlowNode::UFlowNode() : AllowedSignalModes({EFlowSignalMode::Enabled, EFlowSignalMode::Disabled, EFlowSignalMode::PassThrough}) , SignalMode(EFlowSignalMode::Enabled) - , bPreloaded(false) , ActivationState(EFlowNodeState::NeverActivated) { #if WITH_EDITOR @@ -1183,16 +1185,161 @@ void UFlowNode::RecursiveFindNodesByClass(UFlowNode* Node, const TSubclassOfOnNodeActivate(*this); + } +} + +void UFlowNode::Cleanup() +{ + if (FFlowPreloadHelper* Helper = PreloadHelper.GetMutablePtr()) + { + Helper->OnNodeCleanup(*this); + } + + Super::Cleanup(); +} + +void UFlowNode::ExecuteInput(const FName& PinName) +{ + // Often ExecuteInput is replaced rather than extended in subclasses. + // So any subclasses that implement the preload interface will want to call this function + // in their ExecuteInput() override. + if (DispatchExecuteInputToPreloadHelper(PinName)) + { + return; + } + + Super::ExecuteInput(PinName); +} + +bool UFlowNode::DispatchExecuteInputToPreloadHelper(const FName& PinName) +{ + FLOW_ASSERT_ENUM_MAX(EFlowPreloadInputResult, 2); + + if (FFlowPreloadHelper* Helper = PreloadHelper.GetMutablePtr()) + { + return Helper->OnNodeExecuteInput(*this, PinName) == EFlowPreloadInputResult::Handled; + } + + return false; +} + +bool UFlowNode::IsContentPreloaded() const +{ + if (const FFlowPreloadHelper* Helper = PreloadHelper.GetPtr()) + { + return Helper->IsContentPreloaded(); + } + + return false; +} + +void UFlowNode::NotifyPreloadComplete() +{ + FLOW_ASSERT_ENUM_MAX(EFlowPreloadResult, 2); + + if (FFlowPreloadHelper* Helper = PreloadHelper.GetMutablePtr()) + { + if (Helper->OnPreloadComplete(*this) == EFlowPreloadResult::Completed) + { + TriggerOutput(FFlowPreloadHelper::OUTPIN_AllPreloadsComplete.PinName, false); + } + } +} + void UFlowNode::TriggerPreload() { - bPreloaded = true; - PreloadContent(); + if (!IsContentPreloaded()) + { + if (FFlowPreloadHelper* Helper = PreloadHelper.GetMutablePtr()) + { + Helper->TriggerPreload(*this); + } + } } void UFlowNode::TriggerFlush() { - bPreloaded = false; - FlushContent(); + if (FFlowPreloadHelper* Helper = PreloadHelper.GetMutablePtr()) + { + Helper->TriggerFlush(*this); + } +} + +bool UFlowNode::TryInitializePreloadHelper() +{ + // Allocate a helper if the node itself or any of its addons implements IFlowPreloadableInterface. + bool bIsPreloadable = IFlowPreloadableInterface::ImplementsInterfaceSafe(this); + + if (!bIsPreloadable) + { + ForEachAddOnForClass([&bIsPreloadable](UFlowNodeAddOn& /*AddOn*/) + { + bIsPreloadable = true; + return EFlowForEachAddOnFunctionReturnValue::BreakWithSuccess; + }); + } + + if (!bIsPreloadable) + { + return false; + } + + const UFlowAsset* FlowAsset = GetFlowAsset(); + if (!IsValid(FlowAsset)) + { + LogError(TEXT("IFlowPreloadableInterface node has no valid FlowAsset during InitializeInstance — PreloadHelper will not be created.")); + return false; + } + + const FFlowPreloadPolicy& PreloadPolicy = FlowAsset->GetPreloadPolicy(); + + UScriptStruct* HelperType = PreloadPolicy.GetPreloadHelperStructType(*this); + if (!IsValid(HelperType)) + { + LogError(TEXT("FFlowPreloadPolicy::GetPreloadHelperStructType returned null — PreloadHelper will not be created.")); + return false; + } + + PreloadHelper.InitializeAsScriptStruct(HelperType); + + if (FFlowPreloadHelper* Helper = PreloadHelper.GetMutablePtr()) + { + Helper->OnNodeInitializeInstance(*this); + return true; + } + + return false; +} + +void UFlowNode::DeinitializePreloadHelper() +{ + if (FFlowPreloadHelper* Helper = PreloadHelper.GetMutablePtr()) + { + Helper->OnNodeDeinitializeInstance(*this); + } + + PreloadHelper.Reset(); } void UFlowNode::TriggerInput(const FName& PinName, const EFlowPinActivationType ActivationType /*= Default*/) diff --git a/Source/Flow/Private/Nodes/FlowNodeBase.cpp b/Source/Flow/Private/Nodes/FlowNodeBase.cpp index 043fa4ba..1243618a 100644 --- a/Source/Flow/Private/Nodes/FlowNodeBase.cpp +++ b/Source/Flow/Private/Nodes/FlowNodeBase.cpp @@ -107,26 +107,6 @@ void UFlowNodeBase::DeinitializeInstance() IFlowCoreExecutableInterface::DeinitializeInstance(); } -void UFlowNodeBase::PreloadContent() -{ - IFlowCoreExecutableInterface::PreloadContent(); - - for (UFlowNodeAddOn* AddOn : AddOns) - { - AddOn->PreloadContent(); - } -} - -void UFlowNodeBase::FlushContent() -{ - for (UFlowNodeAddOn* AddOn : AddOns) - { - AddOn->FlushContent(); - } - - IFlowCoreExecutableInterface::FlushContent(); -} - void UFlowNodeBase::OnActivate() { IFlowCoreExecutableInterface::OnActivate(); diff --git a/Source/Flow/Private/Nodes/Graph/FlowNode_SubGraph.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_SubGraph.cpp index b502aa50..3c07698f 100644 --- a/Source/Flow/Private/Nodes/Graph/FlowNode_SubGraph.cpp +++ b/Source/Flow/Private/Nodes/Graph/FlowNode_SubGraph.cpp @@ -35,12 +35,19 @@ bool UFlowNode_SubGraph::CanBeAssetInstanced() const return !Asset.IsNull() && (bCanInstanceIdenticalAsset || Asset.ToString() != GetFlowAsset()->GetTemplateAsset()->GetPathName()); } -void UFlowNode_SubGraph::PreloadContent() +EFlowPreloadResult UFlowNode_SubGraph::PreloadContent() { if (CanBeAssetInstanced() && GetFlowSubsystem()) { GetFlowSubsystem()->CreateSubFlow(this, FString(), true); } + + FLOW_ASSERT_ENUM_MAX(EFlowPreloadResult, 2); + + // TODO (gtaylor) CreateSubFlow is currently synchronous-only, + // we could conceivably ADD ASYNC UFlowAsset load + // (which could do the call CreateSubFlow after the asset was loaded). + return EFlowPreloadResult::Completed; } void UFlowNode_SubGraph::FlushContent() @@ -53,6 +60,13 @@ void UFlowNode_SubGraph::FlushContent() void UFlowNode_SubGraph::ExecuteInput(const FName& PinName) { + // Since this node implements IFlowPreloadableInterface, + // we need to call this to allow the PreloadHelper to intercept preload-specific PinNames + if (DispatchExecuteInputToPreloadHelper(PinName)) + { + return; + } + if (CanBeAssetInstanced() == false) { if (Asset.IsNull()) diff --git a/Source/Flow/Private/Policies/FlowPinConnectionPolicy.cpp b/Source/Flow/Private/Policies/FlowPinConnectionPolicy.cpp new file mode 100644 index 00000000..02898885 --- /dev/null +++ b/Source/Flow/Private/Policies/FlowPinConnectionPolicy.cpp @@ -0,0 +1,167 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Policies/FlowPinConnectionPolicy.h" +#include "Nodes/FlowPin.h" +#include "Types/FlowPinTypeNamesStandard.h" +#include "FlowAsset.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowPinConnectionPolicy) + +FFlowPinConnectionPolicy::FFlowPinConnectionPolicy() +{ +} + +const FFlowPinTypeMatchPolicy* FFlowPinConnectionPolicy::TryFindPinTypeMatchPolicy(const FName& PinTypeName) const +{ + return PinTypeMatchPolicies.Find(PinTypeName); +} + +bool FFlowPinConnectionPolicy::CanConnectPinTypeNames(const FName& FromOutputPinTypeName, const FName& ToInputPinTypeName) const +{ + const bool bIsInputExecPin = FFlowPin::IsExecPinCategory(ToInputPinTypeName); + const bool bIsOutputExecPin = FFlowPin::IsExecPinCategory(FromOutputPinTypeName); + if (bIsInputExecPin || bIsOutputExecPin) + { + // Exec pins must match exactly (exec ↔ exec only). + return (bIsInputExecPin && bIsOutputExecPin); + } + + const FFlowPinTypeMatchPolicy* FoundPinTypeMatchPolicy = TryFindPinTypeMatchPolicy(ToInputPinTypeName); + if (!FoundPinTypeMatchPolicy) + { + // Could not find PinTypeMatchPolicy for ToInputPinTypeName. + return false; + } + + // PinCategories must match exactly or be in the map of compatible PinCategories for the input pin type + const bool bRequirePinCategoryMatch = + EnumHasAnyFlags(FoundPinTypeMatchPolicy->PinTypeMatchRules, EFlowPinTypeMatchRules::RequirePinCategoryMatch); + + if (bRequirePinCategoryMatch && + FromOutputPinTypeName != ToInputPinTypeName && + !FoundPinTypeMatchPolicy->PinCategories.Contains(FromOutputPinTypeName)) + { + // Pin type mismatch FromOutputPinTypeName != ToInputPinTypeName (and not in compatible categories list). + return false; + } + + return true; +} + +const TSet& FFlowPinConnectionPolicy::GetAllSupportedTypes() const +{ + return FFlowPinTypeNamesStandard::AllStandardTypeNames; +} + +const TSet& FFlowPinConnectionPolicy::GetAllSupportedIntegerTypes() const +{ + return FFlowPinTypeNamesStandard::AllStandardIntegerTypeNames; +} + +const TSet& FFlowPinConnectionPolicy::GetAllSupportedFloatTypes() const +{ + return FFlowPinTypeNamesStandard::AllStandardFloatTypeNames; +} + +const TSet& FFlowPinConnectionPolicy::GetAllSupportedGameplayTagTypes() const +{ + return FFlowPinTypeNamesStandard::AllStandardGameplayTagTypeNames; +} + +const TSet& FFlowPinConnectionPolicy::GetAllSupportedStringLikeTypes() const +{ + return FFlowPinTypeNamesStandard::AllStandardStringLikeTypeNames; +} + +const TSet& FFlowPinConnectionPolicy::GetAllSupportedSubCategoryObjectTypes() const +{ + return FFlowPinTypeNamesStandard::AllStandardSubCategoryObjectTypeNames; +} + +const TSet& FFlowPinConnectionPolicy::GetAllSupportedConvertibleToStringTypes() const +{ + // By default, all types are convertible to string + return GetAllSupportedTypes(); +} + +const TSet& FFlowPinConnectionPolicy::GetAllSupportedReceivingConvertToStringTypes() const +{ + // Only allowing to convert to String type specifically by default. + // Subclasses could choose different or additional type(s) for the ConvertibleToString conversion + static const TSet OnlyStringType = { FFlowPinTypeNamesStandard::PinTypeNameString }; + return OnlyStringType; +} + +EFlowPinTypeMatchRules FFlowPinConnectionPolicy::GetPinTypeMatchRulesForType(const FName& PinTypeName) const +{ + const TSet& SubCategoryObjectTypes = GetAllSupportedSubCategoryObjectTypes(); + if (SubCategoryObjectTypes.Contains(PinTypeName)) + { + return EFlowPinTypeMatchRules::SubCategoryObjectPinTypeMatchRulesMask; + } + else + { + return EFlowPinTypeMatchRules::StandardPinTypeMatchRulesMask; + } +} + +#if WITH_EDITOR + +void FFlowPinConnectionPolicy::ConfigurePolicy( + bool bAllowAllTypesConvertibleToString, + bool bAllowAllNumericsConvertible, + bool bAllowAllTypeFamiliesConvertible) +{ + PinTypeMatchPolicies.Reset(); + + const TSet& AllSupportedTypes = GetAllSupportedTypes(); + const TSet& AllGameplayTagTypes= GetAllSupportedGameplayTagTypes(); + const TSet& AllSubCategoryObjectTypes = GetAllSupportedSubCategoryObjectTypes(); + const TSet& AllStringLikeTypes = GetAllSupportedStringLikeTypes(); + const TSet& AllConvertibleToStringTypes = GetAllSupportedConvertibleToStringTypes(); + const TSet& AllReceivingConvertToStringTypes = GetAllSupportedReceivingConvertToStringTypes(); + const TSet& AllIntegerTypes = GetAllSupportedIntegerTypes(); + const TSet& AllFloatTypes = GetAllSupportedFloatTypes(); + TSet AllNumericTypes = AllIntegerTypes; + AllNumericTypes.Append(AllFloatTypes); + + TSet ConnectablePinCategories; + + for (const FName& PinTypeName : AllSupportedTypes) + { + const EFlowPinTypeMatchRules PinTypeMatchRules = GetPinTypeMatchRulesForType(PinTypeName); + + ConnectablePinCategories.Reset(); + + // Add support for AllowAllTypesConvertibleToString + if (bAllowAllTypesConvertibleToString && + AllReceivingConvertToStringTypes.Contains(PinTypeName)) + { + AddConnectablePinTypes(AllConvertibleToStringTypes, PinTypeName, ConnectablePinCategories); + } + + // Add support for numeric type conversion + if (bAllowAllNumericsConvertible) + { + AddConnectablePinTypesIfContains(AllNumericTypes, PinTypeName, ConnectablePinCategories); + } + + if (bAllowAllTypeFamiliesConvertible) + { + // The type families are: Integer, Float, GameplayTag and String-Like + AddConnectablePinTypesIfContains(AllIntegerTypes, PinTypeName, ConnectablePinCategories); + AddConnectablePinTypesIfContains(AllFloatTypes, PinTypeName, ConnectablePinCategories); + AddConnectablePinTypesIfContains(AllGameplayTagTypes, PinTypeName, ConnectablePinCategories); + AddConnectablePinTypesIfContains(AllStringLikeTypes, PinTypeName, ConnectablePinCategories); + } + + // Add the entry for this PinTypeName to the match policies map + PinTypeMatchPolicies.Add( + PinTypeName, + FFlowPinTypeMatchPolicy( + PinTypeMatchRules, + ConnectablePinCategories)); + } +} + +#endif \ No newline at end of file diff --git a/Source/Flow/Private/Policies/FlowPinTypeMatchPolicy.cpp b/Source/Flow/Private/Policies/FlowPinTypeMatchPolicy.cpp new file mode 100644 index 00000000..bd895121 --- /dev/null +++ b/Source/Flow/Private/Policies/FlowPinTypeMatchPolicy.cpp @@ -0,0 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Policies/FlowPinTypeMatchPolicy.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowPinTypeMatchPolicy) diff --git a/Source/Flow/Private/Policies/FlowPreloadHelper.cpp b/Source/Flow/Private/Policies/FlowPreloadHelper.cpp new file mode 100644 index 00000000..99aa5bd1 --- /dev/null +++ b/Source/Flow/Private/Policies/FlowPreloadHelper.cpp @@ -0,0 +1,200 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Policies/FlowPreloadHelper.h" +#include "Interfaces/FlowPreloadableInterface.h" +#include "AddOns/FlowNodeAddOn.h" +#include "Policies/FlowPreloadPolicy.h" +#include "FlowAsset.h" +#include "Nodes/FlowNode.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowPreloadHelper) + +const FFlowPin FFlowPreloadHelper::OUTPIN_AllPreloadsComplete(TEXT("All Preloads Complete")); + +const FFlowPin FFlowPreloadHelper_Standard::INPIN_PreloadContent(TEXT("Preload Content")); +const FFlowPin FFlowPreloadHelper_Standard::INPIN_FlushContent(TEXT("Flush Content")); + +void FFlowPreloadHelper_Standard::TriggerPreload(UFlowNode& Node) +{ + FLOW_ASSERT_ENUM_MAX(EFlowPreloadResult, 2); + + if (bContentPreloaded || PendingPreloadCount > 0) + { + return; + } + + // Count all preloadable participants (node + addons) before calling any PreloadContent. + // PendingPreloadCount must be fully set before the first call so that re-entrant + // NotifyPreloadComplete() (e.g. sync FStreamableManager) sees the correct total. + const bool bNodePreloadable = Cast(&Node) != nullptr; + if (bNodePreloadable) + { + ++PendingPreloadCount; + } + + Node.ForEachAddOnForClass([this](UFlowNodeAddOn& /*AddOn*/) + { + ++PendingPreloadCount; + return EFlowForEachAddOnFunctionReturnValue::Continue; + }); + + if (PendingPreloadCount == 0) + { + return; + } + + // Trigger the node itself. + if (bNodePreloadable) + { + if (Cast(&Node)->PreloadContent() == EFlowPreloadResult::Completed) + { + Node.NotifyPreloadComplete(); + } + } + + // Trigger each preloadable addon. + // PreloadInProgress addons must call NotifyPreloadComplete() on themselves when done. + Node.ForEachAddOnForClass([&Node](UFlowNodeAddOn& AddOn) + { + IFlowPreloadableInterface* Preloadable = CastChecked(&AddOn); + if (Preloadable->PreloadContent() == EFlowPreloadResult::Completed) + { + Node.NotifyPreloadComplete(); + } + return EFlowForEachAddOnFunctionReturnValue::Continue; + }); +} + +void FFlowPreloadHelper_Standard::TriggerFlush(UFlowNode& Node) +{ + // Reset pending count first. Any late-arriving PreloadInProgress NotifyPreloadComplete() + // will be rejected by the PendingPreloadCount <= 0 guard in OnPreloadComplete. + PendingPreloadCount = 0; + + if (bContentPreloaded) + { + bContentPreloaded = false; + + if (IFlowPreloadableInterface* Preloadable = Cast(&Node)) + { + Preloadable->FlushContent(); + } + + Node.ForEachAddOnForClass([](UFlowNodeAddOn& AddOn) + { + CastChecked(&AddOn)->FlushContent(); + return EFlowForEachAddOnFunctionReturnValue::Continue; + }); + } +} + +EFlowPreloadResult FFlowPreloadHelper_Standard::OnPreloadComplete(UFlowNode& /*Node*/) +{ + FLOW_ASSERT_ENUM_MAX(EFlowPreloadResult, 2); + + if (PendingPreloadCount <= 0) + { + // Guard: TriggerFlush was called, or this is a spurious/duplicate call. Discard. + return EFlowPreloadResult::PreloadInProgress; + } + + --PendingPreloadCount; + + if (PendingPreloadCount > 0) + { + // Still waiting on other participants (addons or the node itself). + return EFlowPreloadResult::PreloadInProgress; + } + + bContentPreloaded = true; + return EFlowPreloadResult::Completed; +} + +void FFlowPreloadHelper_Standard::OnNodeActivate(UFlowNode& Node) +{ + FLOW_ASSERT_ENUM_MAX(EFlowPreloadTiming, 3); + + if (const UFlowAsset* FlowAsset = Node.GetFlowAsset()) + { + const FFlowPreloadPolicy& Policy = FlowAsset->GetPreloadPolicy(); + if (Policy.GetPreloadTimingForNode(Node) == EFlowPreloadTiming::OnActivate) + { + TriggerPreload(Node); + } + } +} + +void FFlowPreloadHelper_Standard::OnNodeInitializeInstance(UFlowNode& Node) +{ + FLOW_ASSERT_ENUM_MAX(EFlowPreloadTiming, 3); + + if (const UFlowAsset* FlowAsset = Node.GetFlowAsset()) + { + const FFlowPreloadPolicy& Policy = FlowAsset->GetPreloadPolicy(); + if (Policy.GetPreloadTimingForNode(Node) == EFlowPreloadTiming::OnGraphInitialize) + { + TriggerPreload(Node); + } + } +} + +void FFlowPreloadHelper_Standard::OnNodeCleanup(UFlowNode& Node) +{ + FLOW_ASSERT_ENUM_MAX(EFlowFlushTiming, 3); + + if (const UFlowAsset* FlowAsset = Node.GetFlowAsset()) + { + const FFlowPreloadPolicy& Policy = FlowAsset->GetPreloadPolicy(); + if (Policy.GetFlushTimingForNode(Node) == EFlowFlushTiming::OnNodeFinish) + { + TriggerFlush(Node); + } + } +} + +void FFlowPreloadHelper_Standard::OnNodeDeinitializeInstance(UFlowNode& Node) +{ + FLOW_ASSERT_ENUM_MAX(EFlowFlushTiming, 3); + + if (const UFlowAsset* FlowAsset = Node.GetFlowAsset()) + { + const FFlowPreloadPolicy& Policy = FlowAsset->GetPreloadPolicy(); + if (Policy.GetFlushTimingForNode(Node) != EFlowFlushTiming::ManualOnly) + { + // Flush regardless of specific timing (safety net for OnNodeFinish + // where content may still be loaded at graph teardown). TriggerFlush is idempotent. + TriggerFlush(Node); + } + } +} + +EFlowPreloadInputResult FFlowPreloadHelper_Standard::OnNodeExecuteInput(UFlowNode& Node, const FName& PinName) +{ + FLOW_ASSERT_ENUM_MAX(EFlowPreloadInputResult, 2); + + if (PinName == INPIN_PreloadContent.PinName) + { + TriggerPreload(Node); + return EFlowPreloadInputResult::Handled; + } + else if (PinName == INPIN_FlushContent.PinName) + { + TriggerFlush(Node); + return EFlowPreloadInputResult::Handled; + } + + return EFlowPreloadInputResult::Unhandled; +} + +#if WITH_EDITOR +void FFlowPreloadHelper_Standard::GetContextInputs(TArray& OutInputPins) const +{ + OutInputPins.Add(INPIN_PreloadContent); + OutInputPins.Add(INPIN_FlushContent); +} + +void FFlowPreloadHelper_Standard::GetContextOutputs(TArray& OutOutputPins) const +{ + OutOutputPins.Add(OUTPIN_AllPreloadsComplete); +} +#endif diff --git a/Source/Flow/Private/Policies/FlowPreloadPolicy.cpp b/Source/Flow/Private/Policies/FlowPreloadPolicy.cpp new file mode 100644 index 00000000..18b8e61b --- /dev/null +++ b/Source/Flow/Private/Policies/FlowPreloadPolicy.cpp @@ -0,0 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Policies/FlowPreloadPolicy.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowPreloadPolicy) diff --git a/Source/Flow/Private/Policies/FlowStandardPreloadPolicies.cpp b/Source/Flow/Private/Policies/FlowStandardPreloadPolicies.cpp new file mode 100644 index 00000000..7fbc1579 --- /dev/null +++ b/Source/Flow/Private/Policies/FlowStandardPreloadPolicies.cpp @@ -0,0 +1,32 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Policies/FlowStandardPreloadPolicies.h" +#include "Policies/FlowPreloadHelper.h" +#include "Nodes/FlowNode.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowStandardPreloadPolicies) + +EFlowPreloadTiming FFlowPreloadPolicy_Standard::GetPreloadTimingForNode(const UFlowNode& Node) const +{ + if (const EFlowPreloadTiming* OverrideTiming = NodePreloadTimingOverrides.Find(Node.GetClass()->GetFName())) + { + return *OverrideTiming; + } + + return DefaultPreloadTiming; +} + +EFlowFlushTiming FFlowPreloadPolicy_Standard::GetFlushTimingForNode(const UFlowNode& Node) const +{ + if (const EFlowFlushTiming* OverrideTiming = NodeFlushTimingOverrides.Find(Node.GetClass()->GetFName())) + { + return *OverrideTiming; + } + + return DefaultFlushTiming; +} + +UScriptStruct* FFlowPreloadPolicy_Standard::GetPreloadHelperStructType(const UFlowNode& Node) const +{ + return FFlowPreloadHelper_Standard::StaticStruct(); +} diff --git a/Source/Flow/Private/Types/FlowPinTypeNamesStandard.cpp b/Source/Flow/Private/Types/FlowPinTypeNamesStandard.cpp index 28d1b0c9..775b025e 100644 --- a/Source/Flow/Private/Types/FlowPinTypeNamesStandard.cpp +++ b/Source/Flow/Private/Types/FlowPinTypeNamesStandard.cpp @@ -2,161 +2,50 @@ #include "Types/FlowPinTypeNamesStandard.h" -#if WITH_EDITOR - -// Cross-conversion rules: -// - Most* types → String (one-way) (*except InstancedStruct) -// - Numeric: full bidirectional conversion -// - Name/String/Text: full bidirectional -// - GameplayTag ↔ Container: bidirectional - -const TMap FFlowPinTypeNamesStandard::PinTypeMatchPolicies = -{ - { FFlowPinTypeNamesStandard::PinTypeNameBool, - { - EFlowPinTypeMatchRules::StandardPinTypeMatchRulesMask, - { }, - } - }, - { FFlowPinTypeNamesStandard::PinTypeNameInt, - { - EFlowPinTypeMatchRules::StandardPinTypeMatchRulesMask, - { - FFlowPinTypeNamesStandard::PinTypeNameInt64, - FFlowPinTypeNamesStandard::PinTypeNameFloat, - FFlowPinTypeNamesStandard::PinTypeNameDouble - }, - } - }, - { FFlowPinTypeNamesStandard::PinTypeNameInt64, - { - EFlowPinTypeMatchRules::StandardPinTypeMatchRulesMask, - { - FFlowPinTypeNamesStandard::PinTypeNameInt, - FFlowPinTypeNamesStandard::PinTypeNameFloat, - FFlowPinTypeNamesStandard::PinTypeNameDouble - }, - } - }, - { FFlowPinTypeNamesStandard::PinTypeNameFloat, - { - EFlowPinTypeMatchRules::StandardPinTypeMatchRulesMask, - { - FFlowPinTypeNamesStandard::PinTypeNameInt, - FFlowPinTypeNamesStandard::PinTypeNameInt64, - FFlowPinTypeNamesStandard::PinTypeNameDouble - }, - } - }, - { FFlowPinTypeNamesStandard::PinTypeNameDouble, - { - EFlowPinTypeMatchRules::StandardPinTypeMatchRulesMask, - { - FFlowPinTypeNamesStandard::PinTypeNameInt, - FFlowPinTypeNamesStandard::PinTypeNameInt64, - FFlowPinTypeNamesStandard::PinTypeNameFloat - }, - } - }, - { FFlowPinTypeNamesStandard::PinTypeNameEnum, - { - EFlowPinTypeMatchRules::StandardPinTypeMatchRulesMask, - { }, - } - }, - { FFlowPinTypeNamesStandard::PinTypeNameName, - { - EFlowPinTypeMatchRules::StandardPinTypeMatchRulesMask, - { - FFlowPinTypeNamesStandard::PinTypeNameString, - FFlowPinTypeNamesStandard::PinTypeNameText - }, - } - }, - { FFlowPinTypeNamesStandard::PinTypeNameString, - { - EFlowPinTypeMatchRules::StandardPinTypeMatchRulesMask, - { - FFlowPinTypeNamesStandard::PinTypeNameName, - FFlowPinTypeNamesStandard::PinTypeNameText, - - // All other types (except InstancedStruct) can cross-convert to string - FFlowPinTypeNamesStandard::PinTypeNameBool, - FFlowPinTypeNamesStandard::PinTypeNameInt, - FFlowPinTypeNamesStandard::PinTypeNameInt64, - FFlowPinTypeNamesStandard::PinTypeNameFloat, - FFlowPinTypeNamesStandard::PinTypeNameDouble, - FFlowPinTypeNamesStandard::PinTypeNameEnum, - FFlowPinTypeNamesStandard::PinTypeNameVector, - FFlowPinTypeNamesStandard::PinTypeNameRotator, - FFlowPinTypeNamesStandard::PinTypeNameTransform, - FFlowPinTypeNamesStandard::PinTypeNameGameplayTag, - FFlowPinTypeNamesStandard::PinTypeNameGameplayTagContainer, - FFlowPinTypeNamesStandard::PinTypeNameObject, - FFlowPinTypeNamesStandard::PinTypeNameClass - }, - } - }, - { FFlowPinTypeNamesStandard::PinTypeNameText, - { - EFlowPinTypeMatchRules::StandardPinTypeMatchRulesMask, - { - FFlowPinTypeNamesStandard::PinTypeNameString, - FFlowPinTypeNamesStandard::PinTypeNameName - }, - } - }, - { FFlowPinTypeNamesStandard::PinTypeNameVector, - { - EFlowPinTypeMatchRules::StandardPinTypeMatchRulesMask, - { }, - } - }, - { FFlowPinTypeNamesStandard::PinTypeNameRotator, - { - EFlowPinTypeMatchRules::StandardPinTypeMatchRulesMask, - { }, - } - }, - { FFlowPinTypeNamesStandard::PinTypeNameTransform, - { - EFlowPinTypeMatchRules::StandardPinTypeMatchRulesMask, - { }, - } - }, - { FFlowPinTypeNamesStandard::PinTypeNameGameplayTag, - { - EFlowPinTypeMatchRules::StandardPinTypeMatchRulesMask, - { - FFlowPinTypeNamesStandard::PinTypeNameGameplayTagContainer - }, - } - }, - { FFlowPinTypeNamesStandard::PinTypeNameGameplayTagContainer, - { - EFlowPinTypeMatchRules::StandardPinTypeMatchRulesMask, - { - FFlowPinTypeNamesStandard::PinTypeNameGameplayTag - }, - } - }, - { FFlowPinTypeNamesStandard::PinTypeNameInstancedStruct, - { - EFlowPinTypeMatchRules::SubCategoryObjectPinTypeMatchRulesMask, - { }, - } - }, - { FFlowPinTypeNamesStandard::PinTypeNameObject, - { - EFlowPinTypeMatchRules::SubCategoryObjectPinTypeMatchRulesMask, - { }, - } - }, - { FFlowPinTypeNamesStandard::PinTypeNameClass, - { - EFlowPinTypeMatchRules::SubCategoryObjectPinTypeMatchRulesMask, - { }, - } - }, -}; -#endif \ No newline at end of file +const TSet FFlowPinTypeNamesStandard::AllStandardTypeNames = + { + PinTypeNameBool, + PinTypeNameInt, + PinTypeNameInt64, + PinTypeNameFloat, + PinTypeNameDouble, + PinTypeNameEnum, + PinTypeNameName, + PinTypeNameString, + PinTypeNameText, + PinTypeNameVector, + PinTypeNameRotator, + PinTypeNameTransform, + PinTypeNameGameplayTag, + PinTypeNameGameplayTagContainer, + PinTypeNameInstancedStruct, + PinTypeNameObject, + PinTypeNameClass, + }; +const TSet FFlowPinTypeNamesStandard::AllStandardIntegerTypeNames = + { + PinTypeNameInt, + PinTypeNameInt64, + }; +const TSet FFlowPinTypeNamesStandard::AllStandardFloatTypeNames = + { + PinTypeNameFloat, + PinTypeNameDouble, + }; +const TSet FFlowPinTypeNamesStandard::AllStandardStringLikeTypeNames = + { + PinTypeNameName, + PinTypeNameString, + PinTypeNameText, + }; +const TSet FFlowPinTypeNamesStandard::AllStandardGameplayTagTypeNames = + { + PinTypeNameGameplayTag, + PinTypeNameGameplayTagContainer, + }; +const TSet FFlowPinTypeNamesStandard::AllStandardSubCategoryObjectTypeNames = + { + PinTypeNameInstancedStruct, + PinTypeNameObject, + PinTypeNameClass, + }; \ No newline at end of file diff --git a/Source/Flow/Public/AddOns/FlowNodeAddOn.h b/Source/Flow/Public/AddOns/FlowNodeAddOn.h index a56ad7c7..c1971fa8 100644 --- a/Source/Flow/Public/AddOns/FlowNodeAddOn.h +++ b/Source/Flow/Public/AddOns/FlowNodeAddOn.h @@ -77,6 +77,12 @@ class UFlowNodeAddOn : public UFlowNodeBase * By default, uses the seed for the Flow Node that this addon is attached to. */ FLOW_API virtual int32 GetRandomSeed() const override; + /* Called when this AddOn's async preloading finishes (i.e. PreloadContent returned PreloadInProgress). + * Async C++ addons call this from their completion delegate; async Blueprint addons call it on self. + * Delegates to the owning FlowNode's NotifyPreloadComplete(). */ + UFUNCTION(BlueprintCallable, Category = "Preload Content") + FLOW_API void NotifyPreloadComplete(); + #if WITH_EDITOR // IFlowContextPinSupplierInterface FLOW_API virtual bool SupportsContextPins() const override { return Super::SupportsContextPins() || (!InputPins.IsEmpty() || !OutputPins.IsEmpty()); } diff --git a/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateCompareValues.h b/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateCompareValues.h index b20b2329..bfd5fae0 100644 --- a/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateCompareValues.h +++ b/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateCompareValues.h @@ -10,6 +10,8 @@ #include "FlowNodeAddOn_PredicateCompareValues.generated.h" +struct FFlowPinConnectionPolicy; + UCLASS(MinimalApi, NotBlueprintable, meta = (DisplayName = "Compare Values")) class UFlowNodeAddOn_PredicateCompareValues : public UFlowNodeAddOn @@ -64,22 +66,22 @@ class UFlowNodeAddOn_PredicateCompareValues bool IsEqualityOp() const; bool IsArithmeticOp() const; - /* Compatibility check by standard pin type names. */ - static bool AreComparableStandardPinTypes(const FName& LeftPinTypeName, const FName& RightPinTypeName); + /* Compatibility check by pin type names. */ + static bool AreComparablePinTypes( + const FFlowPinConnectionPolicy& PinConnectionPolicy, + const FName& LeftPinTypeName, + const FName& RightPinTypeName); // Domain classifiers - static bool IsNumericTypeName(const FName& TypeName); - static bool IsFloatingPointType(const FName& TypeName); - static bool IsIntegerType(const FName& TypeName); + static bool IsNumericTypeName(const FFlowPinConnectionPolicy& PinConnectionPolicy, const FName& TypeName); + static bool IsFloatingPointType(const FFlowPinConnectionPolicy& PinConnectionPolicy, const FName& TypeName); + static bool IsIntegerType(const FFlowPinConnectionPolicy& PinConnectionPolicy, const FName& TypeName); + static bool IsAnyStringLikeTypeName(const FFlowPinConnectionPolicy& PinConnectionPolicy, const FName& TypeName); + static bool IsGameplayTagLikeTypeName(const FFlowPinConnectionPolicy& PinConnectionPolicy, const FName& TypeName); static bool IsTextType(const FName& TypeName); static bool IsStringType(const FName& TypeName); static bool IsNameLikeType(const FName& TypeName); - static bool IsEnumTypeName(const FName& TypeName); - - static bool IsAnyStringLikeTypeName(const FName& TypeName); - static bool IsGameplayTagLikeTypeName(const FName& TypeName); - static bool IsBoolTypeName(const FName& TypeName); static bool IsVectorTypeName(const FName& TypeName); static bool IsRotatorTypeName(const FName& TypeName); @@ -94,9 +96,9 @@ class UFlowNodeAddOn_PredicateCompareValues // ----------------------------------------------------------------------- /* Generic equality check: resolve both sides as TFlowPinType, compare with Comparator. - * Works for any pin type whose ValueType is supported by the comparator. - * ErrorLabel is used in LogError messages (e.g. "Bool", "Vector", "Object"). - * ComparatorFn defaults to std::equal_to<> (transparent), which uses operator==. */ + * Works for any pin type whose ValueType is supported by the comparator. + * ErrorLabel is used in LogError messages (e.g. "Bool", "Vector", "Object"). + * ComparatorFn defaults to std::equal_to<> (transparent), which uses operator==. */ template > bool TryCheckResolvedValuesEqual(bool& bOutIsEqual, const TCHAR* ErrorLabel, ComparatorFn Comparator = {}) const; @@ -104,7 +106,7 @@ class UFlowNodeAddOn_PredicateCompareValues bool TryCheckGameplayTagsEqual(bool& bOutIsEqual) const; /* Fallback: both sides convert to string via TryConvertValuesToString. - * This supports user-added pin types from other plugins, so long as they implement TryConvertValuesToString. */ + * This supports user-added pin types from other plugins, so long as they implement TryConvertValuesToString. */ bool TryCheckFallbackStringEqual(bool& bOutIsEqual) const; // Numeric comparisons support full operator set diff --git a/Source/Flow/Public/FlowAsset.h b/Source/Flow/Public/FlowAsset.h index dd0d5062..9c44d736 100644 --- a/Source/Flow/Public/FlowAsset.h +++ b/Source/Flow/Public/FlowAsset.h @@ -6,6 +6,7 @@ #include "Asset/FlowAssetParamsTypes.h" #include "Asset/FlowDeferredTransitionScope.h" #include "Nodes/FlowNode.h" +#include "StructUtils/InstancedStruct.h" #if WITH_EDITOR #include "FlowMessageLog.h" @@ -19,6 +20,8 @@ class UFlowNode_CustomOutput; class UFlowNode_CustomInput; class UFlowNode_SubGraph; class UFlowSubsystem; +struct FFlowPreloadPolicy; +struct FFlowPinConnectionPolicy; class UEdGraph; class UEdGraphNode; @@ -60,6 +63,9 @@ class FLOW_API UFlowAsset : public UObject ////////////////////////////////////////////////////////////////////////// // Graph (editor-only) +public: + virtual void PostInitProperties() override; + #if WITH_EDITOR public: friend class UFlowGraph; @@ -301,9 +307,6 @@ class FLOW_API UFlowAsset : public UObject UPROPERTY() TSet> CustomInputNodes; - UPROPERTY() - TSet> PreloadedNodes; - /* Nodes that have any work left, not marked as Finished yet. */ UPROPERTY() TArray> ActiveNodes; @@ -312,6 +315,7 @@ class FLOW_API UFlowAsset : public UObject UPROPERTY() TArray> RecordedNodes; + UPROPERTY(Transient) EFlowFinishPolicy FinishPolicy; public: @@ -336,9 +340,6 @@ class FLOW_API UFlowAsset : public UObject UFUNCTION(BlueprintPure, Category = "Flow") AActor* TryFindActorOwner() const; - /* Opportunity to preload content of project-specific nodes. */ - virtual void PreloadNodes() {} - virtual void PreStartFlow(); virtual void StartFlow(IFlowDataPinValueSupplierInterface* DataPinValueSupplier = nullptr); @@ -384,6 +385,31 @@ class FLOW_API UFlowAsset : public UObject UFUNCTION(BlueprintPure, Category = "Flow") const TArray& GetRecordedNodes() const { return RecordedNodes; } +////////////////////////////////////////////////////////////////////////// +// FFlowPolicy subclass access + +protected: + /* Policy for UFlowGraphSchema (and others) to use to enforce pin connectivity. + * Also used at runtime by predicates (e.g., CompareValues) for type classification queries. */ + UPROPERTY(VisibleAnywhere, AdvancedDisplay, Category = PinConnection) + TInstancedStruct PinConnectionPolicy; + + /* Policy controlling when nodes implementing IFlowPreloadableInterface preload and flush their content. + * Initialized from UFlowSettings defaults. Override InitializePreloadPolicy() in a subclass to set a unique policy. */ + UPROPERTY(VisibleAnywhere, AdvancedDisplay, Category = Preload) + TInstancedStruct PreloadPolicy; + +#if WITH_EDITOR + /* Override these functions to set up unique policy(ies) for a UFlowAsset subclass */ + virtual void InitializePinConnectionPolicy(); + virtual void InitializePreloadPolicy(); +#endif + +public: + /* FFlowPolicy accessors */ + const FFlowPinConnectionPolicy& GetPinConnectionPolicy() const; + const FFlowPreloadPolicy& GetPreloadPolicy() const; + ////////////////////////////////////////////////////////////////////////// // Deferred trigger support @@ -484,5 +510,9 @@ class FLOW_API UFlowAsset : public UObject void LogError(const FString& MessageToLog, const UFlowNodeBase* Node) const; void LogWarning(const FString& MessageToLog, const UFlowNodeBase* Node) const; void LogNote(const FString& MessageToLog, const UFlowNodeBase* Node) const; + +private: + /* Shared implementation for LogError/LogWarning/LogNote to avoid code duplication. */ + void LogRuntimeMessage(EMessageSeverity::Type Severity, const FString& MessageToLog, const UFlowNodeBase* Node) const; #endif -}; +}; \ No newline at end of file diff --git a/Source/Flow/Public/FlowSettings.h b/Source/Flow/Public/FlowSettings.h index 44284bc6..c86d3787 100644 --- a/Source/Flow/Public/FlowSettings.h +++ b/Source/Flow/Public/FlowSettings.h @@ -2,10 +2,14 @@ #pragma once #include "Engine/DeveloperSettings.h" +#include "StructUtils/InstancedStruct.h" #include "Templates/SubclassOf.h" #include "UObject/SoftObjectPath.h" #include "FlowSettings.generated.h" +struct FFlowPinConnectionPolicy; +struct FFlowPreloadPolicy; + /** * Mostly runtime settings of the Flow Graph. */ @@ -14,10 +18,23 @@ class FLOW_API UFlowSettings : public UDeveloperSettings { GENERATED_UCLASS_BODY() + // Returns a typed pointer to the current pin connection policy, or nullptr if unset/invalid. + const FFlowPinConnectionPolicy* GetPinConnectionPolicy() const; + + // Returns a typed pointer to the current preload policy, or nullptr if unset/invalid. + const FFlowPreloadPolicy* GetPreloadPolicy() const; + #if WITH_EDITOR virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; #endif + /* The policy for connecting pins in the Flow Graph Editor */ + UPROPERTY(EditAnywhere, config, Category = "Default Policies", DisplayName = "Pin Connection Policy", NoClear, meta = (ExcludeBaseStruct, BaseStruct = "/Script/Flow.FlowPinConnectionPolicy")) + FInstancedStruct PinConnectionPolicy; + + UPROPERTY(EditAnywhere, config, Category = "Default Policies", DisplayName = "Preload Policy", NoClear, meta = (ExcludeBaseStruct, BaseStruct = "/Script/Flow.FlowPreloadPolicy")) + FInstancedStruct PreloadPolicy; + /* If True, defer the Triggered Outputs for a FlowAsset while it is currently processing a TriggeredInput. * If False, use legacy behavior for backward compatability. */ UPROPERTY(Config, EditAnywhere, Category = "Flow") diff --git a/Source/Flow/Public/Interfaces/FlowCoreExecutableInterface.h b/Source/Flow/Public/Interfaces/FlowCoreExecutableInterface.h index a08ebfc5..28a0a8e0 100644 --- a/Source/Flow/Public/Interfaces/FlowCoreExecutableInterface.h +++ b/Source/Flow/Public/Interfaces/FlowCoreExecutableInterface.h @@ -31,16 +31,6 @@ class FLOW_API IFlowCoreExecutableInterface void K2_DeinitializeInstance(); virtual void DeinitializeInstance() { Execute_K2_DeinitializeInstance(Cast(this)); } - /* If preloading is enabled, will be called to preload content. */ - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", DisplayName = "Preload Content") - void K2_PreloadContent(); - virtual void PreloadContent() { Execute_K2_PreloadContent(Cast(this)); } - - /* If preloading is enabled, will be called to flush content. */ - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", DisplayName = "Flush Content") - void K2_FlushContent(); - virtual void FlushContent() { Execute_K2_FlushContent(Cast(this)); } - /* Called immediately before the first input is triggered. */ UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", DisplayName = "OnActivate") void K2_OnActivate(); diff --git a/Source/Flow/Public/Interfaces/FlowPreloadableInterface.h b/Source/Flow/Public/Interfaces/FlowPreloadableInterface.h new file mode 100644 index 00000000..0a8c2f70 --- /dev/null +++ b/Source/Flow/Public/Interfaces/FlowPreloadableInterface.h @@ -0,0 +1,51 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/Interface.h" +#include "Policies/FlowPreloadPolicyEnums.h" + +#include "FlowPreloadableInterface.generated.h" + +/** + * Implemented by Flow Nodes that have content which can be asynchronously preloaded. + * Implementing this interface opts the node into the preload system: the node will have + * a FFlowPreloadHelper allocated during InitializeInstance (as determined by the asset's + * FFlowPreloadPolicy), which drives when PreloadContent and FlushContent are called. + */ +UINTERFACE(MinimalAPI, Blueprintable, DisplayName = "Flow Preloadable Interface") +class UFlowPreloadableInterface : public UInterface +{ + GENERATED_BODY() +}; + +class FLOW_API IFlowPreloadableInterface +{ + GENERATED_BODY() + +public: + /* Called by the preload helper to start loading this node's content. + * + * Return EFlowPreloadResult::Completed if loading finished synchronously. + * Return EFlowPreloadResult::PreloadInProgress if loading started but is not yet done. + * - In the PreloadInProgress case you MUST call NotifyPreloadComplete() on this node + * (game thread) when loading finishes. AllPreloadsComplete fires at that point. + * - If NotifyPreloadComplete() is called from within PreloadContent() itself + * (e.g. FStreamableManager fires synchronously for an already-cached asset), + * that is safe — state guards prevent double-fire. + * + * The default implementation calls K2_PreloadContent (Blueprint event) and returns + * Completed, so Blueprint nodes and existing sync C++ overrides work unchanged. + * Async C++ nodes override PreloadContent(); async Blueprint nodes override + * K2_PreloadContent and return PreloadInProgress, then call NotifyPreloadComplete() when done. */ + UFUNCTION(BlueprintNativeEvent, Category = FlowPreloadableInterface, DisplayName = "Preload Content") + EFlowPreloadResult K2_PreloadContent(); + virtual EFlowPreloadResult K2_PreloadContent_Implementation() { return EFlowPreloadResult::Completed; } + virtual EFlowPreloadResult PreloadContent() { return Execute_K2_PreloadContent(Cast(this)); } + + /* Called by the preload helper to release this node's preloaded content. */ + UFUNCTION(BlueprintImplementableEvent, Category = FlowPreloadableInterface, DisplayName = "Flush Content") + void K2_FlushContent(); + virtual void FlushContent() { Execute_K2_FlushContent(Cast(this)); } + + static bool ImplementsInterfaceSafe(const UObject* Object); +}; diff --git a/Source/Flow/Public/Nodes/Actor/FlowNode_ExecuteComponent.h b/Source/Flow/Public/Nodes/Actor/FlowNode_ExecuteComponent.h index 867da90f..30773ba4 100644 --- a/Source/Flow/Public/Nodes/Actor/FlowNode_ExecuteComponent.h +++ b/Source/Flow/Public/Nodes/Actor/FlowNode_ExecuteComponent.h @@ -1,6 +1,7 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #pragma once +#include "Interfaces/FlowPreloadableInterface.h" #include "Nodes/FlowNode.h" #include "Types/FlowActorOwnerComponentRef.h" #include "Types/FlowEnumUtils.h" @@ -36,7 +37,9 @@ namespace EExecuteComponentSource_Classifiers * Execute a UActorComponent on the owning actor as if it was a flow subgraph. */ UCLASS(NotBlueprintable, meta = (DisplayName = "Execute Component")) -class FLOW_API UFlowNode_ExecuteComponent : public UFlowNode +class FLOW_API UFlowNode_ExecuteComponent + : public UFlowNode + , public IFlowPreloadableInterface { GENERATED_BODY() @@ -46,14 +49,17 @@ class FLOW_API UFlowNode_ExecuteComponent : public UFlowNode // IFlowCoreExecutableInterface virtual void InitializeInstance() override; virtual void DeinitializeInstance() override; - virtual void PreloadContent() override; - virtual void FlushContent() override; virtual void OnActivate() override; virtual void Cleanup() override; virtual void ForceFinishNode() override; virtual void ExecuteInput(const FName& PinName) override; // -- + // IFlowPreloadableInterface + virtual EFlowPreloadResult PreloadContent() override; + virtual void FlushContent() override; + // -- + // UFlowNodeBase virtual void UpdateNodeConfigText_Implementation() override; // -- diff --git a/Source/Flow/Public/Nodes/Actor/FlowNode_PlayLevelSequence.h b/Source/Flow/Public/Nodes/Actor/FlowNode_PlayLevelSequence.h index efeb5a73..02eaa1b5 100644 --- a/Source/Flow/Public/Nodes/Actor/FlowNode_PlayLevelSequence.h +++ b/Source/Flow/Public/Nodes/Actor/FlowNode_PlayLevelSequence.h @@ -6,6 +6,7 @@ #include "LevelSequencePlayer.h" #include "MovieSceneSequencePlayer.h" +#include "Interfaces/FlowPreloadableInterface.h" #include "Nodes/FlowNode.h" #include "FlowNode_PlayLevelSequence.generated.h" @@ -21,7 +22,9 @@ DECLARE_MULTICAST_DELEGATE(FFlowNodeLevelSequenceEvent); * - Completed */ UCLASS(NotBlueprintable, meta = (DisplayName = "Play Level Sequence")) -class FLOW_API UFlowNode_PlayLevelSequence : public UFlowNode +class FLOW_API UFlowNode_PlayLevelSequence + : public UFlowNode + , public IFlowPreloadableInterface { GENERATED_BODY() @@ -86,6 +89,8 @@ class FLOW_API UFlowNode_PlayLevelSequence : public UFlowNode FStreamableManager StreamableManager; + TSharedPtr PreloadHandle; + public: #if WITH_EDITOR // IFlowContextPinSupplierInterface @@ -96,8 +101,10 @@ class FLOW_API UFlowNode_PlayLevelSequence : public UFlowNode virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; #endif - virtual void PreloadContent() override; + // IFlowPreloadableInterface + virtual EFlowPreloadResult PreloadContent() override; virtual void FlushContent() override; + // -- virtual void InitializeInstance() override; void CreatePlayer(); diff --git a/Source/Flow/Public/Nodes/FlowNode.h b/Source/Flow/Public/Nodes/FlowNode.h index d2eb666a..39c1407f 100644 --- a/Source/Flow/Public/Nodes/FlowNode.h +++ b/Source/Flow/Public/Nodes/FlowNode.h @@ -3,6 +3,7 @@ #include "EdGraph/EdGraphNode.h" #include "GameplayTagContainer.h" +#include "StructUtils/InstancedStruct.h" #include "UObject/TextProperty.h" #include "VisualLogger/VisualLoggerDebugSnapshotInterface.h" @@ -15,6 +16,7 @@ #include "Types/FlowPinConnectionChange.h" #include "FlowNode.generated.h" +struct FFlowPreloadHelper; /** * A Flow Node is UObject-based node designed to handle entire gameplay feature within single node. @@ -336,7 +338,14 @@ class FLOW_API UFlowNode : public UFlowNodeBase // Executing node instance public: - bool bPreloaded; + // IFlowCoreExecutableInterface + virtual void InitializeInstance() override; + virtual void DeinitializeInstance() override; + + virtual void OnActivate() override; + virtual void Cleanup() override; + virtual void ExecuteInput(const FName& PinName) override; + // -- protected: UPROPERTY(SaveGame) @@ -353,10 +362,6 @@ class FLOW_API UFlowNode : public UFlowNodeBase TMap> OutputRecords; #endif -public: - void TriggerPreload(); - void TriggerFlush(); - protected: /* Trigger execution of input pin. */ void TriggerInput(const FName& PinName, const EFlowPinActivationType ActivationType = EFlowPinActivationType::Default); @@ -372,6 +377,36 @@ class FLOW_API UFlowNode : public UFlowNodeBase private: void ResetRecords(); +////////////////////////////////////////////////////////////////////////// +// Preload Content (subclasses must implement IFlowPreloadableInterface to use this code) + +public: + // Called by FFlowPreloadHelper at policy-determined lifecycle points, and directly by callers for ManualOnly timing. + void TriggerPreload(); + void TriggerFlush(); + + // Returns true if this node's content is currently preloaded. + bool IsContentPreloaded() const; + + // Called when async preloading finishes (i.e. PreloadContent returned PreloadInProgress). Updates helper state and fires OUTPIN_AllPreloadsComplete. + // Async C++ nodes call this from their completion delegate; async Blueprint nodes call it on self. + // Safe to call from within PreloadContent() (e.g. if FStreamableManager fires synchronously). + // Must be called on the game thread. No-op if called after TriggerFlush (cancellation guard). + UFUNCTION(BlueprintCallable, Category = "Preload Content") + void NotifyPreloadComplete(); + +protected: + // Instanced preload helper allocated at InitializeInstance for nodes implementing IFlowPreloadableInterface. + // Remains uninitialized (invalid) for non-preloadable nodes. + UPROPERTY(Transient) + TInstancedStruct PreloadHelper; + + bool TryInitializePreloadHelper(); + void DeinitializePreloadHelper(); + + // Forwards PinName to the PreloadHelper if one exists. Returns true if the helper consumed the pin. + bool DispatchExecuteInputToPreloadHelper(const FName& PinName); + ////////////////////////////////////////////////////////////////////////// // SaveGame support @@ -394,7 +429,7 @@ class FLOW_API UFlowNode : public UFlowNodeBase UFUNCTION(BlueprintNativeEvent, Category = "FlowNode") bool ShouldSave(); - + ////////////////////////////////////////////////////////////////////////// // Utils diff --git a/Source/Flow/Public/Nodes/FlowNodeBase.h b/Source/Flow/Public/Nodes/FlowNodeBase.h index 8600d61d..38199dff 100644 --- a/Source/Flow/Public/Nodes/FlowNodeBase.h +++ b/Source/Flow/Public/Nodes/FlowNodeBase.h @@ -104,9 +104,6 @@ class FLOW_API UFlowNodeBase virtual void InitializeInstance() override; virtual void DeinitializeInstance() override; - virtual void PreloadContent() override; - virtual void FlushContent() override; - virtual void OnActivate() override; virtual void ExecuteInput(const FName& PinName) override; diff --git a/Source/Flow/Public/Nodes/Graph/FlowNode_SubGraph.h b/Source/Flow/Public/Nodes/Graph/FlowNode_SubGraph.h index 7b346c54..5d7dfaf6 100644 --- a/Source/Flow/Public/Nodes/Graph/FlowNode_SubGraph.h +++ b/Source/Flow/Public/Nodes/Graph/FlowNode_SubGraph.h @@ -1,6 +1,7 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #pragma once +#include "Interfaces/FlowPreloadableInterface.h" #include "Nodes/FlowNode.h" #include "FlowNode_SubGraph.generated.h" @@ -10,7 +11,9 @@ class UFlowAssetParams; * Creates instance of provided Flow Asset and starts its execution. */ UCLASS(NotBlueprintable, meta = (DisplayName = "Sub Graph")) -class FLOW_API UFlowNode_SubGraph : public UFlowNode +class FLOW_API UFlowNode_SubGraph + : public UFlowNode + , public IFlowPreloadableInterface { GENERATED_BODY() @@ -43,8 +46,10 @@ class FLOW_API UFlowNode_SubGraph : public UFlowNode protected: virtual bool CanBeAssetInstanced() const; - virtual void PreloadContent() override; + // IFlowPreloadableInterface + virtual EFlowPreloadResult PreloadContent() override; virtual void FlushContent() override; + // -- virtual void ExecuteInput(const FName& PinName) override; virtual void Cleanup() override; diff --git a/Source/Flow/Public/Policies/FlowPinConnectionPolicy.h b/Source/Flow/Public/Policies/FlowPinConnectionPolicy.h new file mode 100644 index 00000000..40701f24 --- /dev/null +++ b/Source/Flow/Public/Policies/FlowPinConnectionPolicy.h @@ -0,0 +1,85 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Policies/FlowPolicy.h" +#include "FlowPinTypeMatchPolicy.h" + +#include "FlowPinConnectionPolicy.generated.h" + +// Policy for Flow Pin type relationships. +// +// This struct serves as the domain's type system definition, consumed by: +// 1. The FlowGraphSchema — for pin connection compatibility in the editor +// 2. Runtime predicates (e.g., CompareValues) — for type classification and comparison dispatch +// +// Both consumers access the policy through UFlowAsset::GetFlowPinConnectionPolicy(), +// which allows per-asset-subclass customization of the type system. +USTRUCT() +struct FFlowPinConnectionPolicy : public FFlowPolicy +{ + GENERATED_BODY() + +protected: + /* These are the policies for matching data pin types. */ + UPROPERTY(EditAnywhere, Category = PinConnection, meta = (ShowOnlyInnerProperties)) + TMap PinTypeMatchPolicies; + +public: + FFlowPinConnectionPolicy(); + +////////////////////////////////////////////////////////////////////////// +// Runtime-available queries (used by CompareValues predicate and others) + + FLOW_API const FFlowPinTypeMatchPolicy* TryFindPinTypeMatchPolicy(const FName& PinTypeName) const; + + // Simple connection test using only pin type names + // (more checks will be needed for actual pin connection testing in the Schema) + FLOW_API bool CanConnectPinTypeNames(const FName& FromOutputPinTypeName, const FName& ToInputPinTypeName) const; + + FLOW_API virtual const TSet& GetAllSupportedTypes() const; + FLOW_API virtual const TSet& GetAllSupportedIntegerTypes() const; + FLOW_API virtual const TSet& GetAllSupportedFloatTypes() const; + FLOW_API virtual const TSet& GetAllSupportedGameplayTagTypes() const; + FLOW_API virtual const TSet& GetAllSupportedStringLikeTypes() const; + FLOW_API virtual const TSet& GetAllSupportedSubCategoryObjectTypes() const; + FLOW_API virtual const TSet& GetAllSupportedConvertibleToStringTypes() const; + FLOW_API virtual const TSet& GetAllSupportedReceivingConvertToStringTypes() const; + FLOW_API virtual EFlowPinTypeMatchRules GetPinTypeMatchRulesForType(const FName& PinTypeName) const; + +////////////////////////////////////////////////////////////////////////// +// Policy configuration (editor-only, used to build PinTypeMatchPolicies) + +#if WITH_EDITOR + FLOW_API void ConfigurePolicy( + bool bAllowAllTypesConvertibleToString, + bool bAllowAllNumericsConvertible, + bool bAllowAllTypeFamiliesConvertible); + + FLOW_API FORCEINLINE static void AddConnectablePinTypes(const TSet& PinTypeNames, const FName& PinTypeName, TSet& ConnectablePinCategories); + FLOW_API FORCEINLINE static void AddConnectablePinTypesIfContains(const TSet& PinTypeNames, const FName& PinTypeName, TSet& ConnectablePinCategories); + FLOW_API FORCEINLINE static TSet BuildSetExcludingName(const TSet& NamesSet, const FName& NameToExclude); +#endif +}; + +#if WITH_EDITOR +// Inline implementations +void FFlowPinConnectionPolicy::AddConnectablePinTypes(const TSet& PinTypeNames, const FName& PinTypeName, TSet& ConnectablePinCategories) +{ + ConnectablePinCategories.Append(BuildSetExcludingName(PinTypeNames, PinTypeName)); +} + +void FFlowPinConnectionPolicy::AddConnectablePinTypesIfContains(const TSet& PinTypeNames, const FName& PinTypeName, TSet& ConnectablePinCategories) +{ + if (PinTypeNames.Contains(PinTypeName)) + { + AddConnectablePinTypes(PinTypeNames, PinTypeName, ConnectablePinCategories); + } +} + +TSet FFlowPinConnectionPolicy::BuildSetExcludingName(const TSet& NamesSet, const FName& NameToExclude) +{ + TSet NewSet = NamesSet; + NewSet.Remove(NameToExclude); + return MoveTemp(NewSet); +} +#endif \ No newline at end of file diff --git a/Source/Flow/Public/Asset/FlowPinTypeMatchPolicy.h b/Source/Flow/Public/Policies/FlowPinTypeMatchPolicy.h similarity index 62% rename from Source/Flow/Public/Asset/FlowPinTypeMatchPolicy.h rename to Source/Flow/Public/Policies/FlowPinTypeMatchPolicy.h index 41e2f15b..ce1e9177 100644 --- a/Source/Flow/Public/Asset/FlowPinTypeMatchPolicy.h +++ b/Source/Flow/Public/Policies/FlowPinTypeMatchPolicy.h @@ -16,16 +16,18 @@ enum class EFlowPinTypeMatchRules : uint32 AllowSubCategoryObjectSameLayout = 1 << 5, SameLayoutMustMatchPropertyNames = 1 << 6, - // Masks for convenience + // The "Standard" PinType matching rules (applies to most types) StandardPinTypeMatchRulesMask = RequirePinCategoryMatch | RequirePinCategoryMemberReferenceMatch | AllowSubCategoryObjectSubclasses | - AllowSubCategoryObjectSameLayout UMETA(Hidden), + AllowSubCategoryObjectSameLayout UMETA(DisplayName = "Standard PinType Match Rules (mask)"), + // For types like Object, Class, InstancedStruct, + // which use the SubCategoryObject field to customize the pin type SubCategoryObjectPinTypeMatchRulesMask = StandardPinTypeMatchRulesMask | - RequirePinSubCategoryObjectMatch UMETA(Hidden), + RequirePinSubCategoryObjectMatch UMETA(DisplayName = "SubCategory Object PinType Match Rules (mask)"), }; USTRUCT() @@ -33,10 +35,10 @@ struct FFlowPinTypeMatchPolicy { GENERATED_BODY() - UPROPERTY() + UPROPERTY(EditAnywhere, Category = PinConnection, meta = (Bitmask, BitmaskEnum = "/Script/Flow.EFlowPinTypeMatchRules")) EFlowPinTypeMatchRules PinTypeMatchRules = EFlowPinTypeMatchRules::StandardPinTypeMatchRulesMask; /* Pin categories to allow beyond an exact match. */ - UPROPERTY() + UPROPERTY(EditAnywhere, Category = PinConnection, DisplayName = "Allow Conversion From PinTypes") TSet PinCategories; }; diff --git a/Source/Flow/Public/Policies/FlowPolicy.h b/Source/Flow/Public/Policies/FlowPolicy.h new file mode 100644 index 00000000..e50080af --- /dev/null +++ b/Source/Flow/Public/Policies/FlowPolicy.h @@ -0,0 +1,19 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/Class.h" + +#include "FlowPolicy.generated.h" + +// Flow Policy base-class, for policy structs to inherit from +USTRUCT() +struct FFlowPolicy +{ + GENERATED_BODY() + +public: + + virtual ~FFlowPolicy() = default; + + // Nothing of interest here, yet, but defining a class for it, just-in-case +}; diff --git a/Source/Flow/Public/Policies/FlowPreloadHelper.h b/Source/Flow/Public/Policies/FlowPreloadHelper.h new file mode 100644 index 00000000..65a38286 --- /dev/null +++ b/Source/Flow/Public/Policies/FlowPreloadHelper.h @@ -0,0 +1,105 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Nodes/FlowPin.h" +#include "Policies/FlowPreloadPolicyEnums.h" + +#include "FlowPreloadHelper.generated.h" + +class UFlowNode; + +/** + * Base preload helper struct, which establishes the interface for preload helpers. + * + * - Nodes and/or Nodes with AddOns that implement IFlowPreloadableInterface allocate SUBCLASS of this struct. + * - Non-preloadable nodes (with no preloadable addons) leave PreloadHelper uninitialized (invalid). + * - The base implementation is a pure virtual. * + * - The concrete instance type is determined by FFlowPreloadPolicy::GetPreloadHelperStructType(), + * typically FFlowPreloadHelper_Standard. Projects may supply their own subclass via a custom + * FFlowPreloadPolicy subclass. + */ +USTRUCT() +struct FLOW_API FFlowPreloadHelper +{ + GENERATED_BODY() + + virtual ~FFlowPreloadHelper() = default; + + // IFlowCoreExecutableInterface pass-thrus + virtual void OnNodeInitializeInstance(UFlowNode& Node) PURE_VIRTUAL(OnNodeInitializeInstance); + virtual void OnNodeActivate(UFlowNode& Node) PURE_VIRTUAL(OnNodeActivate); + virtual void OnNodeCleanup(UFlowNode& Node) PURE_VIRTUAL(OnNodeCleanup); + virtual void OnNodeDeinitializeInstance(UFlowNode& Node) PURE_VIRTUAL(OnNodeDeinitializeInstance); + virtual EFlowPreloadInputResult OnNodeExecuteInput(UFlowNode& Node, const FName& PinName) PURE_VIRTUAL(OnNodeExecuteInput, return EFlowPreloadInputResult::Invalid; ); + + // Returns true if this node's content is fully preloaded (if async, the async load(s) must be complete). + virtual bool IsContentPreloaded() const PURE_VIRTUAL(IsContentPreloaded, return false; ); + + // These Trigger functions are safe to be called when already preloaded, or already flushed. + virtual void TriggerPreload(UFlowNode& Node) PURE_VIRTUAL(TriggerPreload); + virtual void TriggerFlush(UFlowNode& Node) PURE_VIRTUAL(TriggerFlush); + + // Called by UFlowNode::NotifyPreloadComplete() when async preloading finishes. + // Returns: + // - Completed - all participants finished; AllPreloadsComplete should fire. + // - PreloadInProgress - call arrived after flush/cancel, or other participants are still in progress. + virtual EFlowPreloadResult OnPreloadComplete(UFlowNode& Node) PURE_VIRTUAL(OnPreloadComplete, return EFlowPreloadResult::Invalid; ); + + // Exec output pin fired when all preloads for this node are complete. + static const FFlowPin OUTPIN_AllPreloadsComplete; + +#if WITH_EDITOR + // Provide Preload-specific pins to the FlowNode + virtual void GetContextInputs(TArray& OutInputPins) const {} + virtual void GetContextOutputs(TArray& OutOutputPins) const {} +#endif +}; + +/** + * Standard preload helper. + * + * Calls TriggerPreload/TriggerFlush on the owning node at the + * timing specified by the asset's FFlowPreloadPolicy. + * + * Also adds the Preload and Flush exec input pins for manual triggering. + */ +USTRUCT() +struct FLOW_API FFlowPreloadHelper_Standard : public FFlowPreloadHelper +{ + GENERATED_BODY() + + // IFlowCoreExecutableInterface pass-thrus + virtual void OnNodeInitializeInstance(UFlowNode& Node) override; + virtual void OnNodeActivate(UFlowNode& Node) override; + virtual void OnNodeCleanup(UFlowNode& Node) override; + virtual void OnNodeDeinitializeInstance(UFlowNode& Node) override; + virtual EFlowPreloadInputResult OnNodeExecuteInput(UFlowNode& Node, const FName& PinName) override; + + virtual bool IsContentPreloaded() const override { return bContentPreloaded; } + + virtual void TriggerPreload(UFlowNode& Node) override; + virtual void TriggerFlush(UFlowNode& Node) override; + + // Called by UFlowNode::NotifyPreloadComplete() to update async state before the output pin fires. + virtual EFlowPreloadResult OnPreloadComplete(UFlowNode& Node) override; + +protected: + // true if the content completed its preload (and hasn't been flushed) + bool bContentPreloaded = false; + + // Number of outstanding async completions (node + addons) between TriggerPreload and full completion. + // Counts up before any PreloadContent calls so re-entrant NotifyPreloadComplete() is safe. + // TriggerFlush resets to 0; OnPreloadComplete decrements; AllPreloadsComplete fires when it reaches 0. + int32 PendingPreloadCount = 0; + +#if WITH_EDITOR + virtual void GetContextInputs(TArray& OutInputPins) const override; + virtual void GetContextOutputs(TArray& OutOutputPins) const override; +#endif + + // Exec input pin triggered to manually preload this node's content. + static const FFlowPin INPIN_PreloadContent; + + // Exec input pin triggered to manually flush this node's content. + static const FFlowPin INPIN_FlushContent; +}; diff --git a/Source/Flow/Public/Policies/FlowPreloadPolicy.h b/Source/Flow/Public/Policies/FlowPreloadPolicy.h new file mode 100644 index 00000000..f14247a6 --- /dev/null +++ b/Source/Flow/Public/Policies/FlowPreloadPolicy.h @@ -0,0 +1,29 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Policies/FlowPolicy.h" +#include "Policies/FlowPreloadPolicyEnums.h" + +#include "FlowPreloadPolicy.generated.h" + +class UFlowNode; + +// Policy governing how preloading and flushing of node content is managed for a Flow Asset. +// Configure the default policy project-wide via UFlowSettings, and override per-domain via UFlowAsset subclasses. +USTRUCT(BlueprintType) +struct FLOW_API FFlowPreloadPolicy : public FFlowPolicy +{ + GENERATED_BODY() + + // Returns the resolved preload timing for the given node, checking per-class overrides first. + // Override in subclasses for code-driven per-node logic. + virtual EFlowPreloadTiming GetPreloadTimingForNode(const UFlowNode& Node) const PURE_VIRTUAL(FFlowPreloadPolicy::GetPreloadTimingForNode, return EFlowPreloadTiming::Invalid;); + + // Returns the resolved flush timing for the given node, checking per-class overrides first. + // Override in subclasses for code-driven per-node logic. + virtual EFlowFlushTiming GetFlushTimingForNode(const UFlowNode& Node) const PURE_VIRTUAL(FFlowPreloadPolicy::GetFlushTimingForNode, return EFlowFlushTiming::Invalid;); + + // Returns the UScriptStruct type to instantiate as the FFlowPreloadHelper for a given preloadable node. + // Default returns FFlowPreloadHelper_Standard. Override to supply project-specific helper types. + virtual UScriptStruct* GetPreloadHelperStructType(const UFlowNode& Node) const PURE_VIRTUAL(FFlowPreloadPolicy::GetPreloadHelperStructType, return nullptr;); +}; diff --git a/Source/Flow/Public/Policies/FlowPreloadPolicyEnums.h b/Source/Flow/Public/Policies/FlowPreloadPolicyEnums.h new file mode 100644 index 00000000..1f422957 --- /dev/null +++ b/Source/Flow/Public/Policies/FlowPreloadPolicyEnums.h @@ -0,0 +1,80 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Types/FlowEnumUtils.h" + +#include "FlowPreloadPolicyEnums.generated.h" + +// Timing for when a preloadable node's content should be preloaded. +UENUM() +enum class EFlowPreloadTiming : uint8 +{ + // Preload content when the graph instance is initialized. + OnGraphInitialize, + + // Preload content when the node activates (just-in-time before execution). + OnActivate, + + // Do not automatically preload; content is ONLY preloaded when the Preload exec pin is triggered. + ManualOnly, + + Max UMETA(Hidden), + Invalid UMETA(Hidden), + Min = 0 UMETA(Hidden), +}; +FLOW_ENUM_RANGE_VALUES(EFlowPreloadTiming); + +// Timing for when a preloadable node's content should be flushed. +UENUM() +enum class EFlowFlushTiming : uint8 +{ + // Flush content when the graph instance is deinitialized. + OnGraphDeinitialize, + + // Flush content when the node finishes execution. + OnNodeFinish, + + // Do not automatically flush; content is ONLY flushed when the Flush exec pin is triggered. + ManualOnly, + + Max UMETA(Hidden), + Invalid UMETA(Hidden), + Min = 0 UMETA(Hidden), +}; +FLOW_ENUM_RANGE_VALUES(EFlowFlushTiming); + +// Return value of IFlowPreloadableInterface::PreloadContent(). +// Tells the preload helper whether the node finished synchronously or deferred completion. +UENUM() +enum class EFlowPreloadResult : uint8 +{ + // Preloading completed synchronously. The helper fires AllPreloadsComplete immediately. + Completed, + + // Preloading started but is not yet finished (e.g. async asset streaming). + // The node MUST call NotifyPreloadComplete() on itself (game thread) when loading finishes. + // The helper fires AllPreloadsComplete only when that call arrives. + PreloadInProgress, + + Max UMETA(Hidden), + Invalid UMETA(Hidden), + Min = 0 UMETA(Hidden), +}; +FLOW_ENUM_RANGE_VALUES(EFlowPreloadResult); + +// Return value of FFlowPreloadHelper::OnNodeExecuteInput(). +// Indicates whether the helper consumed the input pin or it should pass through to the node. +UENUM() +enum class EFlowPreloadInputResult : uint8 +{ + // The helper handled this pin (e.g. Preload or Flush exec). Do not pass it to the node. + Handled, + + // This pin is not a preload pin; pass through to the node's ExecuteInput. + Unhandled, + + Max UMETA(Hidden), + Invalid UMETA(Hidden), + Min = 0 UMETA(Hidden), +}; +FLOW_ENUM_RANGE_VALUES(EFlowPreloadInputResult); diff --git a/Source/Flow/Public/Policies/FlowStandardPinConnectionPolicies.h b/Source/Flow/Public/Policies/FlowStandardPinConnectionPolicies.h new file mode 100644 index 00000000..31efd09f --- /dev/null +++ b/Source/Flow/Public/Policies/FlowStandardPinConnectionPolicies.h @@ -0,0 +1,105 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Policies/FlowPinConnectionPolicy.h" + +#include "FlowStandardPinConnectionPolicies.generated.h" + +/* A very relaxed policy that allows maximum data-pin connectivity lenience */ +USTRUCT() +struct FFlowPinConnectionPolicy_VeryRelaxed : public FFlowPinConnectionPolicy +{ + GENERATED_BODY() + +public: +#if WITH_EDITOR + FFlowPinConnectionPolicy_VeryRelaxed() + { + // Cross-conversion rules: + // - Most* types → String (one-way) (*except InstancedStruct) + // - Numeric: full bidirectional conversion + // - Name/String/Text: full bidirectional + // - GameplayTag ↔ Container: bidirectional + constexpr bool bAllowAllTypesConvertibleToString = true; + constexpr bool bAllowAllNumericsConvertible = true; + constexpr bool bAllowAllTypeFamiliesConvertible = true; + + ConfigurePolicy( + bAllowAllTypesConvertibleToString, + bAllowAllNumericsConvertible, + bAllowAllTypeFamiliesConvertible); + } +#endif +}; + +/* A moderately relaxed policy that allows reasonable data-pin connectivity lenience */ +USTRUCT() +struct FFlowPinConnectionPolicy_Relaxed : public FFlowPinConnectionPolicy +{ + GENERATED_BODY() + +public: +#if WITH_EDITOR + FFlowPinConnectionPolicy_Relaxed() + { + // Cross-conversion rules: + // - Most* types → String (one-way) (*except InstancedStruct) + // - Int/Float/Name/String/Text: full bidirectional + // - GameplayTag ↔ Container: bidirectional + constexpr bool bAllowAllTypesConvertibleToString = true; + constexpr bool bAllowAllNumericsConvertible = false; + constexpr bool bAllowAllTypeFamiliesConvertible = true; + + ConfigurePolicy( + bAllowAllTypesConvertibleToString, + bAllowAllNumericsConvertible, + bAllowAllTypeFamiliesConvertible); + } +#endif +}; + +/* A strict policy that allows no cross-type connectivity (except to string, for dev purposes) */ +USTRUCT() +struct FFlowPinConnectionPolicy_Strict : public FFlowPinConnectionPolicy +{ + GENERATED_BODY() + +public: +#if WITH_EDITOR + FFlowPinConnectionPolicy_Strict() + { + // Cross-conversion rules: + // - Most* types → String (one-way) (*except InstancedStruct) + constexpr bool bAllowAllTypesConvertibleToString = true; + constexpr bool bAllowAllNumericsConvertible = false; + constexpr bool bAllowAllTypeFamiliesConvertible = false; + + ConfigurePolicy( + bAllowAllTypesConvertibleToString, + bAllowAllNumericsConvertible, + bAllowAllTypeFamiliesConvertible); + } +#endif +}; + +/* A strict policy that allows no cross-type connectivity at all */ +USTRUCT() +struct FFlowPinConnectionPolicy_VeryStrict : public FFlowPinConnectionPolicy +{ + GENERATED_BODY() + +public: +#if WITH_EDITOR + FFlowPinConnectionPolicy_VeryStrict() + { + constexpr bool bAllowAllTypesConvertibleToString = false; + constexpr bool bAllowAllNumericsConvertible = false; + constexpr bool bAllowAllTypeFamiliesConvertible = false; + + ConfigurePolicy( + bAllowAllTypesConvertibleToString, + bAllowAllNumericsConvertible, + bAllowAllTypeFamiliesConvertible); + } +#endif +}; diff --git a/Source/Flow/Public/Policies/FlowStandardPreloadPolicies.h b/Source/Flow/Public/Policies/FlowStandardPreloadPolicies.h new file mode 100644 index 00000000..cb19086a --- /dev/null +++ b/Source/Flow/Public/Policies/FlowStandardPreloadPolicies.h @@ -0,0 +1,43 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Policies/FlowPreloadPolicy.h" + +#include "FlowStandardPreloadPolicies.generated.h" + +// The "standard" preload implementation, this may be updated in subclasses of this class or of FFlowPreloadPolicy directly +USTRUCT(BlueprintType) +struct FLOW_API FFlowPreloadPolicy_Standard : public FFlowPreloadPolicy +{ + GENERATED_BODY() + +public: + // Default preload timing applied to all preloadable nodes in the graph. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Preload") + EFlowPreloadTiming DefaultPreloadTiming = EFlowPreloadTiming::OnGraphInitialize; + + // Default flush timing applied to all preloadable nodes in the graph. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Preload") + EFlowFlushTiming DefaultFlushTiming = EFlowFlushTiming::OnGraphDeinitialize; + + // Per-node-class preload timing overrides (key = GetFName(), e.g. "FlowNode_SubGraph"). + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Preload") + TMap NodePreloadTimingOverrides; + + // Per-node-class flush timing overrides (key = GetFName(), e.g. "FlowNode_SubGraph"). + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Preload") + TMap NodeFlushTimingOverrides; + +public: + // Returns the resolved preload timing for the given node, checking per-class overrides first. + // Override in subclasses for code-driven per-node logic. + virtual EFlowPreloadTiming GetPreloadTimingForNode(const UFlowNode& Node) const override; + + // Returns the resolved flush timing for the given node, checking per-class overrides first. + // Override in subclasses for code-driven per-node logic. + virtual EFlowFlushTiming GetFlushTimingForNode(const UFlowNode& Node) const override; + + // Returns the UScriptStruct type to instantiate as the FFlowPreloadHelper for a given preloadable node. + // Default returns FFlowPreloadHelper_Standard. Override to supply project-specific helper types. + virtual UScriptStruct* GetPreloadHelperStructType(const UFlowNode& Node) const override; +}; \ No newline at end of file diff --git a/Source/Flow/Public/Types/FlowPinTypeNamesStandard.h b/Source/Flow/Public/Types/FlowPinTypeNamesStandard.h index ecdd41d4..fd163736 100644 --- a/Source/Flow/Public/Types/FlowPinTypeNamesStandard.h +++ b/Source/Flow/Public/Types/FlowPinTypeNamesStandard.h @@ -1,8 +1,8 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #pragma once +#include "Containers/Set.h" #include "UObject/NameTypes.h" -#include "Asset/FlowPinTypeMatchPolicy.h" struct FFlowPinTypeNamesStandard { @@ -29,9 +29,11 @@ struct FFlowPinTypeNamesStandard FLOW_API static constexpr const TCHAR* PinTypeNameObject = TEXT("Object"); FLOW_API static constexpr const TCHAR* PinTypeNameClass = TEXT("Class"); -#if WITH_EDITOR - /* These are the default pin match policies for input pin connections in the UFlowGraphSchema. - * Schema subclasses can modify this map. . */ - FLOW_API static const TMap PinTypeMatchPolicies; -#endif -}; + // Sets of PinTypeNames that will be used in the FFlowPinConnectionPolicy functions + FLOW_API static const TSet AllStandardTypeNames; + FLOW_API static const TSet AllStandardIntegerTypeNames; + FLOW_API static const TSet AllStandardFloatTypeNames; + FLOW_API static const TSet AllStandardStringLikeTypeNames; + FLOW_API static const TSet AllStandardGameplayTagTypeNames; + FLOW_API static const TSet AllStandardSubCategoryObjectTypeNames; +}; \ No newline at end of file diff --git a/Source/FlowEditor/Private/Graph/FlowGraphSchema.cpp b/Source/FlowEditor/Private/Graph/FlowGraphSchema.cpp index e6e0d3ae..296d3444 100644 --- a/Source/FlowEditor/Private/Graph/FlowGraphSchema.cpp +++ b/Source/FlowEditor/Private/Graph/FlowGraphSchema.cpp @@ -22,6 +22,7 @@ #include "Nodes/Graph/FlowNode_CustomInput.h" #include "Nodes/Graph/FlowNode_Start.h" #include "Nodes/Route/FlowNode_Reroute.h" +#include "Policies/FlowPinConnectionPolicy.h" #include "Types/FlowPinType.h" #include "AssetRegistry/AssetRegistryModule.h" @@ -199,19 +200,6 @@ UFlowGraphSchema::UFlowGraphSchema(const FObjectInitializer& ObjectInitializer) } } -void UFlowGraphSchema::EnsurePinTypesInitialized() -{ - if (PinTypeMatchPolicies.IsEmpty()) - { - InitializedPinTypes(); - } -} - -void UFlowGraphSchema::InitializedPinTypes() -{ - PinTypeMatchPolicies = FFlowPinTypeNamesStandard::PinTypeMatchPolicies; -} - void UFlowGraphSchema::SubscribeToAssetChanges() { const FAssetRegistryModule& AssetRegistry = FModuleManager::LoadModuleChecked(AssetRegistryConstants::ModuleName); @@ -318,15 +306,17 @@ bool UFlowGraphSchema::ArePinsCompatible(const UEdGraphPin* PinA, const UEdGraph } } - return ArePinTypesCompatible(OutputPin->PinType, InputPin->PinType, CallingContext, bIgnoreArray); + return ArePinTypesCompatible(*OutputPin, *InputPin, CallingContext, bIgnoreArray); } bool UFlowGraphSchema::ArePinTypesCompatible( - const FEdGraphPinType& OutputPinType, - const FEdGraphPinType& InputPinType, + const UEdGraphPin& OutputPin, + const UEdGraphPin& InputPin, const UClass* CallingContext, bool bIgnoreArray) const { + const FEdGraphPinType& InputPinType = InputPin.PinType; + const FEdGraphPinType& OutputPinType = OutputPin.PinType; const bool bIsInputExecPin = FFlowPin::IsExecPinCategory(InputPinType.PinCategory); const bool bIsOutputExecPin = FFlowPin::IsExecPinCategory(OutputPinType.PinCategory); if (bIsInputExecPin || bIsOutputExecPin) @@ -335,28 +325,24 @@ bool UFlowGraphSchema::ArePinTypesCompatible( return (bIsInputExecPin && bIsOutputExecPin); } - UFlowGraphSchema* MutableThis = const_cast(this); - MutableThis->EnsurePinTypesInitialized(); - - const FFlowPinTypeMatchPolicy* FoundPinTypeMatchPolicy = PinTypeMatchPolicies.Find(InputPinType.PinCategory); - if (!FoundPinTypeMatchPolicy) + const UFlowAsset* FlowAsset = GetFlowAssetForPin(OutputPin); + if (!IsValid(FlowAsset)) { - // Could not find PinTypeMatchPolicy for InputPinType.PinCategory. + UE_LOG(LogFlowEditor, Error, TEXT("Could not find the FlowAsset when trying to check ArePinTypesCompatible!")); return false; } - // PinCategories must match exactly or be in the map of compatible PinCategories for the input pin type - const bool bRequirePinCategoryMatch = - EnumHasAnyFlags(FoundPinTypeMatchPolicy->PinTypeMatchRules, EFlowPinTypeMatchRules::RequirePinCategoryMatch); - - if (bRequirePinCategoryMatch && - OutputPinType.PinCategory != InputPinType.PinCategory && - !FoundPinTypeMatchPolicy->PinCategories.Contains(OutputPinType.PinCategory)) + // Get the PinConnectionPolicy from the FlowAsset + const FFlowPinConnectionPolicy& PinConnectionPolicy = FlowAsset->GetPinConnectionPolicy(); + if (!PinConnectionPolicy.CanConnectPinTypeNames(OutputPinType.PinCategory, InputPinType.PinCategory)) { - // Pin type mismatch OutputPinType.PinCategory != InputPinType.PinCategory (and not in compatible categories list). + // Type-name based check failed return false; } + const FFlowPinTypeMatchPolicy* FoundPinTypeMatchPolicy = PinConnectionPolicy.TryFindPinTypeMatchPolicy(InputPinType.PinCategory); + checkf(FoundPinTypeMatchPolicy, TEXT("Should fail CanConnectPinTypeNames, if no MatchPolicy")); + // RequirePinCategoryMemberReference const bool bRequirePinCategoryMemberReferenceMatch = EnumHasAnyFlags(FoundPinTypeMatchPolicy->PinTypeMatchRules, EFlowPinTypeMatchRules::RequirePinCategoryMemberReferenceMatch); @@ -627,6 +613,29 @@ bool UFlowGraphSchema::IsPIESimulating() return GEditor->bIsSimulatingInEditor || (GEditor->PlayWorld != nullptr); } +const UFlowNodeBase* UFlowGraphSchema::GetFlowNodeBaseForPin(const UEdGraphPin& EdGraphPin) +{ + if (const UFlowGraphNode* OwningFlowGraphNode = CastChecked(EdGraphPin.GetOwningNode(), ECastCheckedType::NullAllowed)) + { + return OwningFlowGraphNode->GetFlowNodeBase(); + } + + return nullptr; +} + +const UFlowAsset* UFlowGraphSchema::GetFlowAssetForPin(const UEdGraphPin& EdGraphPin) +{ + if (const UEdGraphNode* OwningEdGraphNode = EdGraphPin.GetOwningNode()) + { + if (const UFlowGraph* FlowGraph = CastChecked(OwningEdGraphNode->GetGraph(), ECastCheckedType::NullAllowed)) + { + return FlowGraph->GetFlowAsset(); + } + } + + return nullptr; +} + const FPinConnectionResponse UFlowGraphSchema::CanMergeNodes(const UEdGraphNode* NodeA, const UEdGraphNode* NodeB) const { if (IsPIESimulating()) @@ -689,13 +698,11 @@ bool UFlowGraphSchema::TryCreateConnection(UEdGraphPin* PinA, UEdGraphPin* PinB) RerouteNode->ApplyTypeFromConnectedPin(*OtherPin); - const FEdGraphPinType NewType = OtherPin->PinType; - constexpr bool bForInputPins = true; - BreakIncompatibleConnections(RerouteNode, RerouteNode->InputPins, NewType); + BreakIncompatibleConnections(RerouteNode, RerouteNode->InputPins, *OtherPin); constexpr bool bForOutputPins = false; - BreakIncompatibleConnections(RerouteNode, RerouteNode->OutputPins, NewType); + BreakIncompatibleConnections(RerouteNode, RerouteNode->OutputPins, *OtherPin); } if (EdGraph) @@ -708,7 +715,7 @@ bool UFlowGraphSchema::TryCreateConnection(UEdGraphPin* PinA, UEdGraphPin* PinB) } template -void UFlowGraphSchema::BreakIncompatibleConnections(UFlowGraphNode_Reroute* RerouteNode, const TArray& Pins, FEdGraphPinType NewType) const +void UFlowGraphSchema::BreakIncompatibleConnections(UFlowGraphNode_Reroute* RerouteNode, const TArray& Pins, const UEdGraphPin& TypeFromPin) const { // Helper function to break incompatible connections on a set of pins for (UEdGraphPin* Pin : Pins) @@ -721,12 +728,12 @@ void UFlowGraphSchema::BreakIncompatibleConnections(UFlowGraphNode_Reroute* Rero if constexpr (bIsInputPins) { // LinkedPin (output) to NewType (input) - bIsCompatible = ArePinTypesCompatible(LinkedPin->PinType, NewType, nullptr); + bIsCompatible = ArePinTypesCompatible(*LinkedPin, TypeFromPin, nullptr); } else { // NewType (output) to LinkedPin (input) - bIsCompatible = ArePinTypesCompatible(NewType, LinkedPin->PinType, nullptr); + bIsCompatible = ArePinTypesCompatible(TypeFromPin, *LinkedPin, nullptr); } if (!bIsCompatible) diff --git a/Source/FlowEditor/Private/Graph/FlowGraphSettings.cpp b/Source/FlowEditor/Private/Graph/FlowGraphSettings.cpp index 05393725..ac7e439d 100644 --- a/Source/FlowEditor/Private/Graph/FlowGraphSettings.cpp +++ b/Source/FlowEditor/Private/Graph/FlowGraphSettings.cpp @@ -57,8 +57,10 @@ void UFlowGraphSettings::PostInitProperties() void UFlowGraphSettings::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) { Super::PostEditChangeProperty(PropertyChangedEvent); + + const FName MemberPropertyName = PropertyChangedEvent.GetMemberPropertyName(); - if (PropertyChangedEvent.GetMemberPropertyName() == GET_MEMBER_NAME_CHECKED( UFlowGraphSettings, NodePrefixesToRemove )) + if (MemberPropertyName == GET_MEMBER_NAME_CHECKED( UFlowGraphSettings, NodePrefixesToRemove )) { // // We need to sort items in array, because unsorted array can cause only partial prefix removal. @@ -86,7 +88,7 @@ void UFlowGraphSettings::PostEditChangeProperty(FPropertyChangedEvent& PropertyC UFlowGraphSchema::UpdateGeneratedDisplayNames(); } } - else if (PropertyChangedEvent.GetMemberPropertyName() == GET_MEMBER_NAME_CHECKED(UFlowGraphSettings, NodeDisplayStyles)) + else if (MemberPropertyName == GET_MEMBER_NAME_CHECKED(UFlowGraphSettings, NodeDisplayStyles)) { if (FlowArray::TrySortAndRemoveDuplicatesFromArrayInPlace(NodeDisplayStyles)) { diff --git a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp index 8e3e2326..be77f910 100644 --- a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp +++ b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp @@ -813,7 +813,7 @@ bool UFlowGraphNode::IsContentPreloaded() const { if (const UFlowNode* InspectedInstance = FlowNode->GetInspectedInstance()) { - return InspectedInstance->bPreloaded; + return InspectedInstance->IsContentPreloaded(); } } diff --git a/Source/FlowEditor/Public/Graph/FlowGraphSchema.h b/Source/FlowEditor/Public/Graph/FlowGraphSchema.h index 5b4ab457..93c82bfc 100644 --- a/Source/FlowEditor/Public/Graph/FlowGraphSchema.h +++ b/Source/FlowEditor/Public/Graph/FlowGraphSchema.h @@ -4,7 +4,7 @@ #include "EdGraph/EdGraphSchema.h" #include "Templates/SubclassOf.h" -#include "Asset/FlowPinTypeMatchPolicy.h" +#include "Policies/FlowPinTypeMatchPolicy.h" #include "FlowGraphSchema.generated.h" class UFlowAsset; @@ -68,8 +68,6 @@ class FLOWEDITOR_API UFlowGraphSchema : public UEdGraphSchema static const FFlowPinType* LookupDataPinTypeForPinCategory(const FName& PinCategory); - void EnsurePinTypesInitialized(); - bool ArePinSubCategoryObjectsCompatible( const UStruct* OutputStruct, const UStruct* InputStruct, @@ -87,7 +85,7 @@ class FLOWEDITOR_API UFlowGraphSchema : public UEdGraphSchema * * @return true if the pin types are compatible. */ - virtual bool ArePinTypesCompatible(const FEdGraphPinType& Output, const FEdGraphPinType& Input, const UClass* CallingContext = NULL, bool bIgnoreArray = false) const; + virtual bool ArePinTypesCompatible(const UEdGraphPin& OutputPin, const UEdGraphPin& InputPin, const UClass* CallingContext = NULL, bool bIgnoreArray = false) const; /** * Returns the connection response for connecting PinA to PinB, which have already been determined to be compatible @@ -118,21 +116,16 @@ class FLOWEDITOR_API UFlowGraphSchema : public UEdGraphSchema static bool IsPIESimulating(); -protected: - - /* These are the policies for matching data pin types. */ - UPROPERTY(Transient) - TMap PinTypeMatchPolicies; + static const UFlowNodeBase* GetFlowNodeBaseForPin(const UEdGraphPin& EdGraphPin); + static const UFlowAsset* GetFlowAssetForPin(const UEdGraphPin& EdGraphPin); - /* TODO (gtaylor) The mechanism for customizing PinTypeMatchPolicies will need some revision. - * I am going with a simple virtual method on schema For Now(tm) but expect a revision in how this is done, in the future. */ - virtual void InitializedPinTypes(); +protected: static UFlowGraphNode* CreateDefaultNode(UEdGraph& Graph, const TSubclassOf& NodeClass, const FVector2f& Offset, bool bPlacedAsGhostNode); /* Helper to break incompatible connections on a set of pins. */ template - void BreakIncompatibleConnections(UFlowGraphNode_Reroute* RerouteNode, const TArray& Pins, FEdGraphPinType NewType) const; + void BreakIncompatibleConnections(UFlowGraphNode_Reroute* RerouteNode, const TArray& Pins, const UEdGraphPin& TypeFromPin) const; /* Handles post-connection notifications for affected nodes. */ void NotifyNodesChanged(UFlowGraphNode* NodeA, UFlowGraphNode* NodeB, UEdGraph* Graph) const; From 7599db2965d4da19dd5bfb6650a311ba34581c3d Mon Sep 17 00:00:00 2001 From: LindyHopperGT <91915878+LindyHopperGT@users.noreply.github.com> Date: Wed, 27 May 2026 15:21:27 -0700 Subject: [PATCH 2/7] Preload & play level sequence bugfixes - Level sequence actor instance data is not set to replicate to the clients, so the clients will still play the level sequence using the world origin as the sequence origin. Instead of replicating the data, just assume the spawn transform of the level sequence actor is the sequence origin. The Flow Level Sequence Player either spawned the level sequence actor at the world origin or at the transform of the provided TransformOriginActor. Either way, the actor is at the correct origin transform. - The editor previously assumed only Level Sequence Actors would be used to preview Level Sequences. However, any IMovieScenePlaybackClient can do this. Added a new interface, ILevelSequenceEditorPlaybackInterface, to minimize the engine changes needed to support finding UObjects that implement this interface. - Fixed preload helper not adding its pins to preloadable nodes - --- Source/Flow/Private/FlowAsset.cpp | 14 -------------- .../LevelSequence/FlowLevelSequenceActor.cpp | 10 ++++++++++ .../LevelSequence/FlowLevelSequencePlayer.cpp | 14 +++++++------- .../Actor/FlowNode_PlayLevelSequence.cpp | 6 +++--- Source/Flow/Private/Nodes/FlowNode.cpp | 19 ++++++++++++++++--- .../Private/Policies/FlowPreloadHelper.cpp | 8 ++++---- .../Flow/Public/Policies/FlowPreloadHelper.h | 3 +-- 7 files changed, 41 insertions(+), 33 deletions(-) diff --git a/Source/Flow/Private/FlowAsset.cpp b/Source/Flow/Private/FlowAsset.cpp index 7225988b..f7345f92 100644 --- a/Source/Flow/Private/FlowAsset.cpp +++ b/Source/Flow/Private/FlowAsset.cpp @@ -1102,23 +1102,10 @@ AActor* UFlowAsset::TryFindActorOwner() const // If the owner is a Component, return its owning Actor if (const UActorComponent* OwnerAsComponent = Cast(OwnerObject)) - { { return nullptr; } - // If the owner is already an Actor, return it directly - if (AActor* OwnerAsActor = Cast(OwnerObject)) - { - return OwnerAsActor; - } - - // If the owner is a Component, return its owning Actor - if (const UActorComponent* OwnerAsComponent = Cast(OwnerObject)) - { - return OwnerAsComponent->GetOwner(); - } - return nullptr; } @@ -1481,7 +1468,6 @@ const FFlowPreloadPolicy& UFlowAsset::GetPreloadPolicy() const { return TemplateAsset->GetPreloadPolicy(); } -} // Graceful fallback: if PreloadPolicy was never initialized (asset predates this feature, // or was never opened in editor), read directly from project settings at runtime. diff --git a/Source/Flow/Private/LevelSequence/FlowLevelSequenceActor.cpp b/Source/Flow/Private/LevelSequence/FlowLevelSequenceActor.cpp index a750b42d..88b0f35c 100644 --- a/Source/Flow/Private/LevelSequence/FlowLevelSequenceActor.cpp +++ b/Source/Flow/Private/LevelSequence/FlowLevelSequenceActor.cpp @@ -5,6 +5,8 @@ #include "Net/UnrealNetwork.h" #include "Runtime/Launch/Resources/Version.h" +#include "DefaultLevelSequenceInstanceData.h" + #include UE_INLINE_GENERATED_CPP_BY_NAME(FlowLevelSequenceActor) AFlowLevelSequenceActor::AFlowLevelSequenceActor(const FObjectInitializer& ObjectInitializer) @@ -39,6 +41,14 @@ void AFlowLevelSequenceActor::OnRep_ReplicatedLevelSequenceAsset() { LevelSequenceAsset = ReplicatedLevelSequenceAsset; ReplicatedLevelSequenceAsset = nullptr; + + // InstanceData is not replicated to the client. + // However, it can be assumed that the spawn transform of the level sequence actor is the transform origin for the sequence. + if (UDefaultLevelSequenceInstanceData* InstanceData = Cast(DefaultInstanceData)) + { + bOverrideInstanceData = true; + InstanceData->TransformOriginActor = this; + } InitializePlayer(); } diff --git a/Source/Flow/Private/LevelSequence/FlowLevelSequencePlayer.cpp b/Source/Flow/Private/LevelSequence/FlowLevelSequencePlayer.cpp index c38aa9e7..acd9e98d 100644 --- a/Source/Flow/Private/LevelSequence/FlowLevelSequencePlayer.cpp +++ b/Source/Flow/Private/LevelSequence/FlowLevelSequencePlayer.cpp @@ -57,14 +57,14 @@ UFlowLevelSequencePlayer* UFlowLevelSequencePlayer::CreateFlowLevelSequencePlaye Actor->SetPlaybackSettings(Settings); Actor->CameraSettings = CameraSettings; - // apply Transform Origin to spawned actor - if (TransformOriginActor) + // InstanceData is not set to replicate to the clients, + // so the clients will still play the level sequence using the world origin as the sequence origin. + // Instead of replicating the data, just assume the spawn transform of the level sequence actor is the sequence origin. + // The level sequence actor was either spawned at the world origin or at the transform of TransformOriginActor. + if (UDefaultLevelSequenceInstanceData* InstanceData = Cast(Actor->DefaultInstanceData)) { - if (UDefaultLevelSequenceInstanceData* InstanceData = Cast(Actor->DefaultInstanceData)) - { - Actor->bOverrideInstanceData = true; - InstanceData->TransformOriginActor = TransformOriginActor; - } + Actor->bOverrideInstanceData = true; + InstanceData->TransformOriginActor = Actor; } // support networking diff --git a/Source/Flow/Private/Nodes/Actor/FlowNode_PlayLevelSequence.cpp b/Source/Flow/Private/Nodes/Actor/FlowNode_PlayLevelSequence.cpp index c7d6c324..3cd94789 100644 --- a/Source/Flow/Private/Nodes/Actor/FlowNode_PlayLevelSequence.cpp +++ b/Source/Flow/Private/Nodes/Actor/FlowNode_PlayLevelSequence.cpp @@ -54,13 +54,13 @@ UFlowNode_PlayLevelSequence::UFlowNode_PlayLevelSequence() #if WITH_EDITOR TArray UFlowNode_PlayLevelSequence::GetContextOutputs() const { + TArray Pins = Super::GetContextOutputs(); + if (Sequence.IsNull()) { - return TArray(); + return Pins; } - TArray Pins = {}; - Sequence.LoadSynchronous(); if (Sequence && Sequence->GetMovieScene()) { diff --git a/Source/Flow/Private/Nodes/FlowNode.cpp b/Source/Flow/Private/Nodes/FlowNode.cpp index d6f1a696..e7b24874 100644 --- a/Source/Flow/Private/Nodes/FlowNode.cpp +++ b/Source/Flow/Private/Nodes/FlowNode.cpp @@ -165,6 +165,9 @@ void UFlowNode::SetupForEditing(UEdGraphNode& EdGraphNode) // Ensure AddOn editor pointers are correct as soon as we're prepared for editing. EnsureAddOnFlowNodePointersForEditor(); + + // Initialize the preload helper in editor + TryInitializePreloadHelper(); } bool UFlowNode::RebuildPinArray(const TArray& NewPinNames, TArray& InOutPins, const FFlowPin& DefaultPin) @@ -336,15 +339,20 @@ bool UFlowNode::SupportsContextPins() const TArray UFlowNode::GetContextInputs() const { - TArray ContextOutputs = Super::GetContextInputs(); + TArray ContextInputs = Super::GetContextInputs(); + + if (PreloadHelper.IsValid()) + { + PreloadHelper.Get().GetContextInputs(ContextInputs); + } // Add the Auto-Generated DataPins as GetContextInputs for (const FFlowPin& AutoGeneratedDataPin : AutoInputDataPins) { - ContextOutputs.AddUnique(AutoGeneratedDataPin); + ContextInputs.AddUnique(AutoGeneratedDataPin); } - return ContextOutputs; + return ContextInputs; } TArray UFlowNode::GetContextOutputs() const @@ -357,6 +365,11 @@ TArray UFlowNode::GetContextOutputs() const ContextOutputs.AddUnique(AutoGeneratedDataPin); } + if (PreloadHelper.IsValid()) + { + PreloadHelper.Get().GetContextOutputs(ContextOutputs); + } + return ContextOutputs; } diff --git a/Source/Flow/Private/Policies/FlowPreloadHelper.cpp b/Source/Flow/Private/Policies/FlowPreloadHelper.cpp index 99aa5bd1..047998ef 100644 --- a/Source/Flow/Private/Policies/FlowPreloadHelper.cpp +++ b/Source/Flow/Private/Policies/FlowPreloadHelper.cpp @@ -189,12 +189,12 @@ EFlowPreloadInputResult FFlowPreloadHelper_Standard::OnNodeExecuteInput(UFlowNod #if WITH_EDITOR void FFlowPreloadHelper_Standard::GetContextInputs(TArray& OutInputPins) const { - OutInputPins.Add(INPIN_PreloadContent); - OutInputPins.Add(INPIN_FlushContent); + OutInputPins.AddUnique(INPIN_PreloadContent); + OutInputPins.AddUnique(INPIN_FlushContent); } -void FFlowPreloadHelper_Standard::GetContextOutputs(TArray& OutOutputPins) const +void FFlowPreloadHelper::GetContextOutputs(TArray& OutOutputPins) const { - OutOutputPins.Add(OUTPIN_AllPreloadsComplete); + OutOutputPins.AddUnique(OUTPIN_AllPreloadsComplete); } #endif diff --git a/Source/Flow/Public/Policies/FlowPreloadHelper.h b/Source/Flow/Public/Policies/FlowPreloadHelper.h index 65a38286..965a21f1 100644 --- a/Source/Flow/Public/Policies/FlowPreloadHelper.h +++ b/Source/Flow/Public/Policies/FlowPreloadHelper.h @@ -51,7 +51,7 @@ struct FLOW_API FFlowPreloadHelper #if WITH_EDITOR // Provide Preload-specific pins to the FlowNode virtual void GetContextInputs(TArray& OutInputPins) const {} - virtual void GetContextOutputs(TArray& OutOutputPins) const {} + virtual void GetContextOutputs(TArray& OutOutputPins) const; #endif }; @@ -94,7 +94,6 @@ struct FLOW_API FFlowPreloadHelper_Standard : public FFlowPreloadHelper #if WITH_EDITOR virtual void GetContextInputs(TArray& OutInputPins) const override; - virtual void GetContextOutputs(TArray& OutOutputPins) const override; #endif // Exec input pin triggered to manually preload this node's content. From 7e44f83e4802ed41d6d98371d8914c1036499a1d Mon Sep 17 00:00:00 2001 From: LindyHopperGT <91915878+LindyHopperGT@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:32:13 -0700 Subject: [PATCH 3/7] Subgraph and sequencer latest from P4 --- Config/DefaultFlow.ini | 2 + Source/Flow/Private/FlowAsset.cpp | 100 +++++++++++++--- Source/Flow/Private/FlowComponent.cpp | 4 +- Source/Flow/Private/FlowSubsystem.cpp | 47 ++++++-- .../LevelSequence/FlowLevelSequenceActor.cpp | 52 ++++++++ .../LevelSequence/FlowLevelSequencePlayer.cpp | 2 +- .../IFlowPlayLevelSequenceAddOnInterface.cpp | 12 ++ .../Actor/FlowNode_PlayLevelSequence.cpp | 37 +++++- Source/Flow/Private/Nodes/FlowNodeBase.cpp | 40 ++++++- .../Private/Nodes/Graph/FlowNode_Finish.cpp | 10 +- .../Nodes/Graph/FlowNode_SetGraphOutput.cpp | 113 ++++++++++++++++++ .../Private/Nodes/Graph/FlowNode_SubGraph.cpp | 52 +++++++- Source/Flow/Public/FlowAsset.h | 41 ++++++- Source/Flow/Public/FlowSubsystem.h | 9 +- .../FlowGraphOutputDataReceiverInterface.h | 27 +++++ .../LevelSequence/FlowLevelSequenceActor.h | 25 ++++ .../LevelSequence/FlowLevelSequencePlayer.h | 3 +- .../IFlowPlayLevelSequenceAddOnInterface.h | 40 +++++++ .../Nodes/Actor/FlowNode_PlayLevelSequence.h | 8 ++ Source/Flow/Public/Nodes/FlowNodeBase.h | 17 ++- .../Flow/Public/Nodes/Graph/FlowNode_Finish.h | 4 +- .../Nodes/Graph/FlowNode_SetGraphOutput.h | 35 ++++++ .../Public/Nodes/Graph/FlowNode_SubGraph.h | 20 +++- .../Public/Types/FlowOutputDataPinValues.h | 25 ++++ 24 files changed, 673 insertions(+), 52 deletions(-) create mode 100644 Source/Flow/Private/LevelSequence/IFlowPlayLevelSequenceAddOnInterface.cpp create mode 100644 Source/Flow/Private/Nodes/Graph/FlowNode_SetGraphOutput.cpp create mode 100644 Source/Flow/Public/Interfaces/FlowGraphOutputDataReceiverInterface.h create mode 100644 Source/Flow/Public/LevelSequence/IFlowPlayLevelSequenceAddOnInterface.h create mode 100644 Source/Flow/Public/Nodes/Graph/FlowNode_SetGraphOutput.h create mode 100644 Source/Flow/Public/Types/FlowOutputDataPinValues.h diff --git a/Config/DefaultFlow.ini b/Config/DefaultFlow.ini index fe550471..703e8278 100644 --- a/Config/DefaultFlow.ini +++ b/Config/DefaultFlow.ini @@ -4,3 +4,5 @@ +PropertyRedirects=(OldName="FlowGraphNode.FlowNode",NewName="FlowGraphNode.NodeInstance") +StructRedirects=(OldName="/Script/Flow.FlowNamedDataPinOutputProperty",NewName="/Script/Flow.FlowNamedDataPinProperty") +PropertyRedirects=(OldName="FlowNode_DefineProperties.OutputProperties",NewName="NamedProperties") ++FunctionRedirects=(OldName="/Script/Flow.FlowSubsystem.FinishRootFlow",NewName="/Script/Flow.FlowSubsystem.FinishAndDeinitializeRootFlow") ++FunctionRedirects=(OldName="/Script/Flow.FlowSubsystem.FinishAllRootFlows",NewName="/Script/Flow.FlowSubsystem.FinishAndDeinitializeAllRootFlows") \ No newline at end of file diff --git a/Source/Flow/Private/FlowAsset.cpp b/Source/Flow/Private/FlowAsset.cpp index f7345f92..75fe1e14 100644 --- a/Source/Flow/Private/FlowAsset.cpp +++ b/Source/Flow/Private/FlowAsset.cpp @@ -9,6 +9,8 @@ #include "Asset/FlowAssetParams.h" #include "Asset/FlowAssetParamsUtils.h" #include "Interfaces/FlowExecutionGate.h" +#include "Interfaces/FlowGraphOutputDataReceiverInterface.h" +#include "Types/FlowNamedDataPinProperty.h" #include "Nodes/FlowNodeBase.h" #include "Nodes/Graph/FlowNode_CustomInput.h" #include "Nodes/Graph/FlowNode_CustomOutput.h" @@ -16,6 +18,7 @@ #include "Nodes/Graph/FlowNode_SubGraph.h" #include "Policies/FlowPinConnectionPolicy.h" #include "Policies/FlowPreloadPolicy.h" +#include "Types/FlowAutoDataPinsWorkingData.h" #include "Types/FlowDataPinValue.h" #include "Types/FlowStructUtils.h" @@ -25,6 +28,7 @@ #include "Algo/AnyOf.h" #if WITH_EDITOR +#include "Nodes/Graph/FlowNode_SetGraphOutput.h" #include "AssetRegistry/AssetRegistryModule.h" #include "AssetToolsModule.h" #include "ContentBrowserModule.h" @@ -89,11 +93,26 @@ void UFlowAsset::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEv { Super::PostEditChangeProperty(PropertyChangedEvent); - if (PropertyChangedEvent.Property && (PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(UFlowAsset, CustomInputs) - || PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(UFlowAsset, CustomOutputs))) + const FName ChangedPropertyName = PropertyChangedEvent.GetPropertyName(); + const FName ChangedMemberPropertyName = PropertyChangedEvent.GetMemberPropertyName(); + if (PropertyChangedEvent.Property && (ChangedPropertyName == GET_MEMBER_NAME_CHECKED(UFlowAsset, CustomInputs) + || ChangedPropertyName == GET_MEMBER_NAME_CHECKED(UFlowAsset, CustomOutputs) + || ChangedMemberPropertyName == GET_MEMBER_NAME_CHECKED(UFlowAsset, OutputDataPinDeclarations))) { OnSubGraphReconstructionRequested.ExecuteIfBound(); } + + if (PropertyChangedEvent.Property && ChangedMemberPropertyName == GET_MEMBER_NAME_CHECKED(UFlowAsset, OutputDataPinDeclarations)) + { + for (const TPair& NodePair : GetNodes()) + { + UFlowNode_SetGraphOutput* SetOutputNode = Cast(NodePair.Value); + if (IsValid(SetOutputNode) && SetOutputNode->TryUpdateAutoDataPins()) + { + SetOutputNode->OnReconstructionRequested.ExecuteIfBound(); + } + } + } } void UFlowAsset::PostDuplicate(bool bDuplicateForPIE) @@ -872,7 +891,7 @@ void UFlowAsset::ClearInstances() { if (ActiveInstances.IsValidIndex(i) && ActiveInstances[i]) { - ActiveInstances[i]->FinishFlow(EFlowFinishPolicy::Keep); + ActiveInstances[i]->FinishFlowAndDeinitializeInstance(EFlowFinishPolicy::Keep); } } @@ -965,6 +984,12 @@ void UFlowAsset::DeinitializeInstance() } } +void UFlowAsset::FinishFlowAndDeinitializeInstance(const EFlowFinishPolicy InFinishPolicy) +{ + FinishFlow(InFinishPolicy); + DeinitializeInstance(); +} + void UFlowAsset::PreStartFlow() { ResetNodes(); @@ -985,8 +1010,10 @@ void UFlowAsset::PreStartFlow() #endif } -void UFlowAsset::StartFlow(IFlowDataPinValueSupplierInterface* DataPinValueSupplier) +void UFlowAsset::StartFlow(IFlowDataPinValueSupplierInterface* DataPinValueSupplier, IFlowGraphOutputDataReceiverInterface* InOutputDataReceiver) { + InitializeOutputDataReceiverAndValues(InOutputDataReceiver); + PreStartFlow(); if (UFlowNode* ConnectedEntryNode = GetDefaultEntryNode()) @@ -1002,7 +1029,51 @@ void UFlowAsset::StartFlow(IFlowDataPinValueSupplierInterface* DataPinValueSuppl } } -void UFlowAsset::FinishFlow(const EFlowFinishPolicy InFinishPolicy, const bool bRemoveInstance /*= true*/) +void UFlowAsset::InitializeOutputDataReceiverAndValues(IFlowGraphOutputDataReceiverInterface* InOutputDataReceiver) +{ + OutputDataReceiver = Cast(InOutputDataReceiver); + + // Initialize the live output store from the template asset's declarations + OutputDataPinValues.Values.Reset(); + + if (const UFlowAsset* Template = TemplateAsset.Get()) + { + for (const FFlowNamedDataPinProperty& Declaration : Template->OutputDataPinDeclarations) + { + if (Declaration.IsValid()) + { + OutputDataPinValues.Values.Add(Declaration.Name, Declaration.DataPinValue); + } + else + { + UE_LOG(LogFlow, Warning, TEXT("Invalid OutputDataPin %s"), *Declaration.Name.ToString()); + } + } + } +} + +void UFlowAsset::WriteOutputDataPinValue(const FName& PinName, const TInstancedStruct& Value) +{ + if (OutputDataPinValues.Values.Contains(PinName)) + { + OutputDataPinValues.Values[PinName] = Value; + } + else + { + UE_LOG(LogFlow, Warning, TEXT("Could not find pin named %s in WriteOutputDataPinValue"), *PinName.ToString()); + } +} + +void UFlowAsset::FlushOutputDataPinValuesToReceiver() +{ + if (IFlowGraphOutputDataReceiverInterface* Receiver = Cast(OutputDataReceiver.Get())) + { + // Do an immediate push to the receiver + Receiver->ReceiveOutputDataSnapshot(OutputDataPinValues); + } +} + +void UFlowAsset::FinishFlow(const EFlowFinishPolicy InFinishPolicy) { FinishPolicy = InFinishPolicy; @@ -1014,12 +1085,6 @@ void UFlowAsset::FinishFlow(const EFlowFinishPolicy InFinishPolicy, const bool b Node->Deactivate(); } ActiveNodes.Empty(); - - // provides option to finish game-specific logic prior to removing asset instance - if (bRemoveInstance) - { - DeinitializeInstance(); - } } void UFlowAsset::CancelAndWarnForUnflushedDeferredTriggers() @@ -1103,7 +1168,7 @@ AActor* UFlowAsset::TryFindActorOwner() const // If the owner is a Component, return its owning Actor if (const UActorComponent* OwnerAsComponent = Cast(OwnerObject)) { - return nullptr; + return OwnerAsComponent->GetOwner(); } return nullptr; @@ -1290,6 +1355,11 @@ void UFlowAsset::FinishNode(UFlowNode* Node) // if graph reached Finish and this asset instance was created by SubGraph node if (Node->CanFinishGraph()) { + if (IFlowGraphOutputDataReceiverInterface* Receiver = Cast(OutputDataReceiver.Get())) + { + Receiver->ReceiveOutputDataSnapshot(OutputDataPinValues); + } + if (NodeOwningThisAssetInstance.IsValid()) { NodeOwningThisAssetInstance.Get()->TriggerFirstOutput(true); @@ -1297,13 +1367,14 @@ void UFlowAsset::FinishNode(UFlowNode* Node) return; } - // if this instance is a Root Flow, we need to deregister it from the subsystem first + // if this instance is a Root Flow, we need to deregister it from the subsystem first. This will + // finalize and deinitialize the root flow. if (Owner.IsValid()) { const TSet& RootFlowInstances = GetFlowSubsystem()->GetRootInstancesByOwner(Owner.Get()); if (RootFlowInstances.Contains(this)) { - GetFlowSubsystem()->FinishRootFlow(Owner.Get(), TemplateAsset, EFlowFinishPolicy::Keep); + GetFlowSubsystem()->FinishAndDeinitializeRootFlow(Owner.Get(), TemplateAsset, EFlowFinishPolicy::Keep); return; } @@ -1446,7 +1517,6 @@ const FFlowPinConnectionPolicy& UFlowAsset::GetPinConnectionPolicy() const // Graceful fallback: if PinConnectionPolicy was never initialized (asset predates this feature, // or was never opened in editor), read directly from project settings at runtime. - // or was never opened in editor), read directly from Project Settings at runtime. if (!PinConnectionPolicy.IsValid()) { const FFlowPinConnectionPolicy* SettingsPolicy = GetDefault()->GetPinConnectionPolicy(); diff --git a/Source/Flow/Private/FlowComponent.cpp b/Source/Flow/Private/FlowComponent.cpp index da1dbe1f..62ca1dbc 100644 --- a/Source/Flow/Private/FlowComponent.cpp +++ b/Source/Flow/Private/FlowComponent.cpp @@ -99,7 +99,7 @@ void UFlowComponent::UnregisterWithFlowSubsystem() { if (UFlowSubsystem* FlowSubsystem = GetFlowSubsystem()) { - FlowSubsystem->FinishAllRootFlows(this, EFlowFinishPolicy::Keep); + FlowSubsystem->FinishAndDeinitializeAllRootFlows(this, EFlowFinishPolicy::Keep); FlowSubsystem->UnregisterComponent(this); } } @@ -461,7 +461,7 @@ void UFlowComponent::FinishRootFlow(UFlowAsset* TemplateAsset, const EFlowFinish { if (UFlowSubsystem* FlowSubsystem = GetFlowSubsystem()) { - FlowSubsystem->FinishRootFlow(this, TemplateAsset, FinishPolicy); + FlowSubsystem->FinishAndDeinitializeRootFlow(this, TemplateAsset, FinishPolicy); } } diff --git a/Source/Flow/Private/FlowSubsystem.cpp b/Source/Flow/Private/FlowSubsystem.cpp index 65c49e9b..186aba16 100644 --- a/Source/Flow/Private/FlowSubsystem.cpp +++ b/Source/Flow/Private/FlowSubsystem.cpp @@ -85,7 +85,11 @@ void UFlowSubsystem::StartRootFlow(UObject* Owner, UFlowAsset* FlowAsset, const { if (UFlowAsset* NewFlow = CreateRootFlow(Owner, FlowAsset, bAllowMultipleInstances)) { - NewFlow->StartFlow(DataPinValueSupplier.GetInterface()); + // TODO (gtaylor) Not implementing output parameters "yet", + // see Subgraph node for the pioneer implementation. + constexpr IFlowGraphOutputDataReceiverInterface* OutputDataReceiverInterface = nullptr; + + NewFlow->StartFlow(DataPinValueSupplier.GetInterface(), OutputDataReceiverInterface); } } #if WITH_EDITOR @@ -123,7 +127,7 @@ UFlowAsset* UFlowSubsystem::CreateRootFlow(UObject* Owner, UFlowAsset* FlowAsset return NewFlow; } -void UFlowSubsystem::FinishRootFlow(UObject* Owner, UFlowAsset* TemplateAsset, const EFlowFinishPolicy FinishPolicy) +void UFlowSubsystem::FinishAndDeinitializeRootFlow(UObject* Owner, UFlowAsset* TemplateAsset, const EFlowFinishPolicy FinishPolicy) { UFlowAsset* InstanceToFinish = nullptr; @@ -139,11 +143,11 @@ void UFlowSubsystem::FinishRootFlow(UObject* Owner, UFlowAsset* TemplateAsset, c if (InstanceToFinish) { RootInstances.Remove(InstanceToFinish); - InstanceToFinish->FinishFlow(FinishPolicy); + InstanceToFinish->FinishFlowAndDeinitializeInstance(FinishPolicy); } } -void UFlowSubsystem::FinishAllRootFlows(UObject* Owner, const EFlowFinishPolicy FinishPolicy) +void UFlowSubsystem::FinishAndDeinitializeAllRootFlows(UObject* Owner, const EFlowFinishPolicy FinishPolicy) { TArray InstancesToFinish; @@ -158,7 +162,7 @@ void UFlowSubsystem::FinishAllRootFlows(UObject* Owner, const EFlowFinishPolicy for (UFlowAsset* InstanceToFinish : InstancesToFinish) { RootInstances.Remove(InstanceToFinish); - InstanceToFinish->FinishFlow(FinishPolicy); + InstanceToFinish->FinishFlowAndDeinitializeInstance(FinishPolicy); } } @@ -182,19 +186,41 @@ UFlowAsset* UFlowSubsystem::CreateSubFlow(UFlowNode_SubGraph* SubGraphNode, cons // get instanced asset from map - in case it was already instanced by calling CreateSubFlow() with bPreloading == true UFlowAsset* AssetInstance = InstancedSubFlows[SubGraphNode]; - AssetInstance->NodeOwningThisAssetInstance = SubGraphNode; + if (!AssetInstance->NodeOwningThisAssetInstance.IsValid()) + { + AssetInstance->NodeOwningThisAssetInstance = SubGraphNode; + } + check(AssetInstance->NodeOwningThisAssetInstance == SubGraphNode); + SubGraphNode->GetFlowAsset()->ActiveSubGraphs.Add(SubGraphNode, AssetInstance); // don't activate Start Node if we're loading Sub Graph from SaveGame if (SavedInstanceName.IsEmpty()) { - AssetInstance->StartFlow(SubGraphNode); + AssetInstance->StartFlow(SubGraphNode, SubGraphNode); } } return NewInstance; } +void UFlowSubsystem::FinishSubFlow(UFlowNode_SubGraph* SubGraphNode, const EFlowFinishPolicy FinishPolicy) +{ + if (InstancedSubFlows.Contains(SubGraphNode)) + { + // The flow asset running on the subgraph node. + UFlowAsset* SubgraphFlowAsset = InstancedSubFlows[SubGraphNode]; + + // This is the flow asset that has the subgraph node. Do not confuse with the flow asset that the node is running. + // Remove the subgraph flow from the owning flow active subgraph list. + UFlowAsset* SubgraphNodeParentFlow = SubGraphNode->GetFlowAsset(); + SubgraphNodeParentFlow->ActiveSubGraphs.Remove(SubGraphNode); + + // Finish the flow but do not remove the instance. + SubgraphFlowAsset->FinishFlow(FinishPolicy); + } +} + void UFlowSubsystem::RemoveSubFlow(UFlowNode_SubGraph* SubGraphNode, const EFlowFinishPolicy FinishPolicy) { if (InstancedSubFlows.Contains(SubGraphNode)) @@ -204,7 +230,12 @@ void UFlowSubsystem::RemoveSubFlow(UFlowNode_SubGraph* SubGraphNode, const EFlow SubGraphNode->GetFlowAsset()->ActiveSubGraphs.Remove(SubGraphNode); InstancedSubFlows.Remove(SubGraphNode); - AssetInstance->FinishFlow(FinishPolicy); + if (AssetInstance->IsActive()) + { + AssetInstance->FinishFlow(FinishPolicy); + } + + AssetInstance->DeinitializeInstance(); // Make sure to set the NodeOwningThisAssetInstance after the FinishFlow call, as it may be needed in the FinishFlow method AssetInstance->NodeOwningThisAssetInstance = nullptr; diff --git a/Source/Flow/Private/LevelSequence/FlowLevelSequenceActor.cpp b/Source/Flow/Private/LevelSequence/FlowLevelSequenceActor.cpp index 88b0f35c..a51da700 100644 --- a/Source/Flow/Private/LevelSequence/FlowLevelSequenceActor.cpp +++ b/Source/Flow/Private/LevelSequence/FlowLevelSequenceActor.cpp @@ -1,11 +1,14 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #include "LevelSequence/FlowLevelSequenceActor.h" +#include "FlowLogChannels.h" #include "LevelSequence/FlowLevelSequencePlayer.h" #include "Net/UnrealNetwork.h" #include "Runtime/Launch/Resources/Version.h" +// #PlayLevelSequenceAtSpawnTransform #include "DefaultLevelSequenceInstanceData.h" +// #include UE_INLINE_GENERATED_CPP_BY_NAME(FlowLevelSequenceActor) @@ -20,6 +23,7 @@ void AFlowLevelSequenceActor::GetLifetimeReplicatedProps(TArray& OutActor ) { if (LevelSequence == nullptr) diff --git a/Source/Flow/Private/LevelSequence/IFlowPlayLevelSequenceAddOnInterface.cpp b/Source/Flow/Private/LevelSequence/IFlowPlayLevelSequenceAddOnInterface.cpp new file mode 100644 index 00000000..8942d1dc --- /dev/null +++ b/Source/Flow/Private/LevelSequence/IFlowPlayLevelSequenceAddOnInterface.cpp @@ -0,0 +1,12 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "LevelSequence/IFlowPlayLevelSequenceAddOnInterface.h" + +#include "AddOns/FlowNodeAddOn.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(IFlowPlayLevelSequenceAddOnInterface) + +bool IFlowPlayLevelSequenceAddOnInterface::ImplementsInterfaceSafe(const UFlowNodeAddOn* AddOnTemplate) +{ + return IsValid(AddOnTemplate) && AddOnTemplate->Implements(); +} diff --git a/Source/Flow/Private/Nodes/Actor/FlowNode_PlayLevelSequence.cpp b/Source/Flow/Private/Nodes/Actor/FlowNode_PlayLevelSequence.cpp index 3cd94789..e07b68a3 100644 --- a/Source/Flow/Private/Nodes/Actor/FlowNode_PlayLevelSequence.cpp +++ b/Source/Flow/Private/Nodes/Actor/FlowNode_PlayLevelSequence.cpp @@ -5,7 +5,10 @@ #include "FlowAsset.h" #include "FlowLogChannels.h" #include "FlowSubsystem.h" +#include "AddOns/FlowNodeAddOn.h" +#include "LevelSequence/FlowLevelSequenceActor.h" #include "LevelSequence/FlowLevelSequencePlayer.h" +#include "LevelSequence/IFlowPlayLevelSequenceAddOnInterface.h" #if WITH_EDITOR #include "MovieScene/MovieSceneFlowTrack.h" @@ -13,7 +16,6 @@ #endif #include "LevelSequence.h" -#include "LevelSequenceActor.h" #include "VisualLogger/VisualLogger.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_PlayLevelSequence) @@ -148,17 +150,29 @@ void UFlowNode_PlayLevelSequence::InitializeInstance() { Super::InitializeInstance(); + SequenceActor = nullptr; + // Cache Play Rate set by user CachedPlayRate = PlaybackSettings.PlayRate; } +EFlowAddOnAcceptResult UFlowNode_PlayLevelSequence::AcceptFlowNodeAddOnChild_Implementation( + const UFlowNodeAddOn* AddOnTemplate, + const TArray& AdditionalAddOnsToAssumeAreChildren) const +{ + if (IFlowPlayLevelSequenceAddOnInterface::ImplementsInterfaceSafe(AddOnTemplate)) + { + return EFlowAddOnAcceptResult::TentativeAccept; + } + + return Super::AcceptFlowNodeAddOnChild_Implementation(AddOnTemplate, AdditionalAddOnsToAssumeAreChildren); +} + void UFlowNode_PlayLevelSequence::CreatePlayer() { LoadedSequence = Sequence.LoadSynchronous(); if (LoadedSequence) { - ALevelSequenceActor* SequenceActor; - AActor* OwningActor = TryGetRootFlowActorOwner(); // Apply AActor::CustomTimeDilation from owner of the Root Flow @@ -178,6 +192,17 @@ void UFlowNode_PlayLevelSequence::CreatePlayer() SequencePlayer->SetFlowEventReceiver(this); } + // Notify add-ons so they can apply binding overrides before Play() is called + if (AFlowLevelSequenceActor* FlowSequenceActor = SequenceActor.Get()) + { + ForEachAddOnForClass([this, FlowSequenceActor, OwningActor](UFlowNodeAddOn& AddOn) + { + IFlowPlayLevelSequenceAddOnInterface* Interface = CastChecked(&AddOn); + Interface->OnSequencePlayerCreated(*FlowSequenceActor, OwningActor); + return EFlowForEachAddOnFunctionReturnValue::Continue; + }); + } + const FFrameRate FrameRate = LoadedSequence->GetMovieScene()->GetTickResolution(); const FFrameNumber PlaybackStartFrame = LoadedSequence->GetMovieScene()->GetPlaybackRange().GetLowerBoundValue(); StartTime = FQualifiedFrameTime(FFrameTime(PlaybackStartFrame, 0.0f), FrameRate).AsSeconds(); @@ -319,6 +344,12 @@ void UFlowNode_PlayLevelSequence::Cleanup() SequencePlayer = nullptr; } + if (IsValid(SequenceActor) && SequenceActor->HasAuthority()) + { + SequenceActor->Destroy(); + } + SequenceActor = nullptr; + LoadedSequence = nullptr; StartTime = 0.0f; ElapsedTime = 0.0f; diff --git a/Source/Flow/Private/Nodes/FlowNodeBase.cpp b/Source/Flow/Private/Nodes/FlowNodeBase.cpp index 04aa5364..b45faad8 100644 --- a/Source/Flow/Private/Nodes/FlowNodeBase.cpp +++ b/Source/Flow/Private/Nodes/FlowNodeBase.cpp @@ -621,7 +621,10 @@ FString UFlowNodeBase::GetNodeCategory() const } } - return Category; + // #ASIntegration #NodeCategory + return K2_GetNodeCategory(); + //return Category; + // } bool UFlowNodeBase::GetDynamicTitleColor(FLinearColor& OutColor) const @@ -636,6 +639,32 @@ bool UFlowNodeBase::GetDynamicTitleColor(FLinearColor& OutColor) const return false; } +FText UFlowNodeBase::GetNodeTitle() const +{ + if (HasAnyFlags(RF_ClassDefaultObject | RF_ArchetypeObject)) + { + // For the archetype of the node (e.g. in the node selection UI), only use the default value + return UFlowNodeBase::K2_GetNodeTitle_Implementation(); + } + else + { + return K2_GetNodeTitle(); + } +} + +FText UFlowNodeBase::GetNodeToolTip() const +{ + if (HasAnyFlags(RF_ClassDefaultObject | RF_ArchetypeObject)) + { + // For the archetype of the node (e.g. in the node selection UI), only use the default value + return UFlowNodeBase::K2_GetNodeToolTip_Implementation(); + } + else + { + return K2_GetNodeToolTip(); + } +} + FText UFlowNodeBase::GetGeneratedDisplayName() const { static const FName NAME_GeneratedDisplayName(TEXT("GeneratedDisplayName")); @@ -820,6 +849,15 @@ FText UFlowNodeBase::K2_GetNodeToolTip_Implementation() const #endif } +FString UFlowNodeBase::K2_GetNodeCategory_Implementation() const +{ +#if WITH_EDITORONLY_DATA + return Category; +#else + return ""; +#endif +} + FText UFlowNodeBase::GetNodeConfigText() const { #if WITH_EDITORONLY_DATA diff --git a/Source/Flow/Private/Nodes/Graph/FlowNode_Finish.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_Finish.cpp index ea34286d..1afd1297 100644 --- a/Source/Flow/Private/Nodes/Graph/FlowNode_Finish.cpp +++ b/Source/Flow/Private/Nodes/Graph/FlowNode_Finish.cpp @@ -2,21 +2,19 @@ #include "Nodes/Graph/FlowNode_Finish.h" +#include "FlowAsset.h" + #include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_Finish) UFlowNode_Finish::UFlowNode_Finish() { -#if WITH_EDITOR - Category = TEXT("Graph"); - NodeDisplayStyle = FlowNodeStyle::InOut; -#endif - OutputPins = {}; - AllowedSignalModes = {EFlowSignalMode::Enabled, EFlowSignalMode::Disabled}; } void UFlowNode_Finish::ExecuteInput(const FName& PinName) { + CommitOutputDataPinValues(); + // this will call FinishFlow() Finish(); } diff --git a/Source/Flow/Private/Nodes/Graph/FlowNode_SetGraphOutput.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_SetGraphOutput.cpp new file mode 100644 index 00000000..0753dd86 --- /dev/null +++ b/Source/Flow/Private/Nodes/Graph/FlowNode_SetGraphOutput.cpp @@ -0,0 +1,113 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Nodes/Graph/FlowNode_SetGraphOutput.h" + +#include "FlowAsset.h" +#include "Types/FlowDataPinResults.h" +#include "Types/FlowNamedDataPinProperty.h" + +#if WITH_EDITOR +#include "Types/FlowAutoDataPinsWorkingData.h" +#endif + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_SetGraphOutput) + +UFlowNode_SetGraphOutput::UFlowNode_SetGraphOutput() +{ +#if WITH_EDITOR + Category = TEXT("Graph"); + NodeDisplayStyle = FlowNodeStyle::InOut; +#endif + + AllowedSignalModes = {EFlowSignalMode::Enabled, EFlowSignalMode::Disabled}; +} + +void UFlowNode_SetGraphOutput::ExecuteInput(const FName& PinName) +{ + CommitOutputDataPinValues(); + + TriggerFirstOutput(true); +} + +void UFlowNode_SetGraphOutput::CommitOutputDataPinValues() +{ + UFlowAsset* FlowAsset = GetFlowAsset(); + if (!IsValid(FlowAsset)) + { + return; + } + + const UFlowAsset* TemplateAsset = FlowAsset->GetTemplateAsset(); + if (!IsValid(TemplateAsset)) + { + return; + } + + bool bNeedsFlush = false; + + for (const FFlowNamedDataPinProperty& Declaration : TemplateAsset->GetOutputDataPinDeclarations()) + { + if (!Declaration.IsValid()) + { + continue; + } + + if (!IsInputConnected(Declaration.Name)) + { + continue; + } + + const FFlowDataPinResult PinResult = TryResolveDataPin_SetGraphOutputAccess(Declaration.Name); + if (PinResult.Result == EFlowDataPinResolveResult::Success) + { + bNeedsFlush = true; + + FlowAsset->WriteOutputDataPinValue(Declaration.Name, PinResult.ResultValue); + } + } + + if (bNeedsFlush) + { + FlowAsset->FlushOutputDataPinValuesToReceiver(); + } +} + +#if WITH_EDITOR +bool UFlowNode_SetGraphOutput::SupportsContextPins() const +{ + const UFlowAsset* FlowAsset = GetFlowAsset(); + if (IsValid(FlowAsset) && !FlowAsset->GetOutputDataPinDeclarations().IsEmpty()) + { + return true; + } + + return Super::SupportsContextPins(); +} + +void UFlowNode_SetGraphOutput::AutoGenerateDataPins(FFlowDataPinValueOwner& ValueOwner, FFlowAutoDataPinsWorkingData& InOutWorkingData) +{ + Super::AutoGenerateDataPins(ValueOwner, InOutWorkingData); + + const UFlowAsset* FlowAsset = GetFlowAsset(); + if (!IsValid(FlowAsset)) + { + return; + } + + for (FFlowNamedDataPinProperty DeclarationCopy : FlowAsset->GetOutputDataPinDeclarations()) + { + if (!DeclarationCopy.IsValid()) + { + continue; + } + + if (DeclarationCopy.DataPinValue.IsValid()) + { + FFlowDataPinValue& Value = DeclarationCopy.DataPinValue.GetMutable(); + Value.bIsInputPin = true; + } + + DeclarationCopy.AutoGenerateDataPinForProperty(ValueOwner, InOutWorkingData); + } +} +#endif diff --git a/Source/Flow/Private/Nodes/Graph/FlowNode_SubGraph.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_SubGraph.cpp index 3c07698f..57d66c54 100644 --- a/Source/Flow/Private/Nodes/Graph/FlowNode_SubGraph.cpp +++ b/Source/Flow/Private/Nodes/Graph/FlowNode_SubGraph.cpp @@ -97,14 +97,26 @@ void UFlowNode_SubGraph::ExecuteInput(const FName& PinName) void UFlowNode_SubGraph::Cleanup() { - if (CanBeAssetInstanced() && GetFlowSubsystem()) + UFlowSubsystem* FlowSubsystem = GetFlowSubsystem(); + if (CanBeAssetInstanced() && FlowSubsystem) { - GetFlowSubsystem()->RemoveSubFlow(this, EFlowFinishPolicy::Keep); + FlowSubsystem->FinishSubFlow(this, EFlowFinishPolicy::Keep); } Super::Cleanup(); } +void UFlowNode_SubGraph::DeinitializeInstance() +{ + UFlowSubsystem* FlowSubsystem = GetFlowSubsystem(); + if (CanBeAssetInstanced() && FlowSubsystem) + { + FlowSubsystem->RemoveSubFlow(this, EFlowFinishPolicy::Keep); + } + + Super::DeinitializeInstance(); +} + void UFlowNode_SubGraph::ForceFinishNode() { TriggerFirstOutput(true); @@ -119,6 +131,11 @@ void UFlowNode_SubGraph::OnLoad_Implementation() } } +void UFlowNode_SubGraph::ReceiveOutputDataSnapshot(const FFlowOutputDataPinValues& Snapshot) +{ + CachedOutputDataPinValues = Snapshot; +} + #if WITH_EDITOR FText UFlowNode_SubGraph::K2_GetNodeTitle_Implementation() const @@ -236,6 +253,22 @@ void UFlowNode_SubGraph::AutoGenerateDataPins(FFlowDataPinValueOwner& ValueOwner } } } + + for (FFlowNamedDataPinProperty Declaration : Asset->GetOutputDataPinDeclarations()) + { + if (!Declaration.IsValid()) + { + continue; + } + + if (Declaration.DataPinValue.IsValid()) + { + FFlowDataPinValue& Value = Declaration.DataPinValue.GetMutable(); + Value.bIsInputPin = false; + } + + Declaration.AutoGenerateDataPinForProperty(ValueOwner, InOutWorkingData); + } } FFlowDataPinResult UFlowNode_SubGraph::TrySupplyDataPin(FName PinName) const @@ -248,6 +281,19 @@ FFlowDataPinResult UFlowNode_SubGraph::TrySupplyDataPin(FName PinName) const return Super::TrySupplyDataPin(PinName); } + // Check cached output data pin values first — output pins are never "input connected", + // so this must come before IsInputConnected to avoid a spurious "unknown input pin" error. + if (const TInstancedStruct* CachedValue = CachedOutputDataPinValues.Values.Find(PinName)) + { + if (CachedValue->IsValid()) + { + FFlowDataPinResult Result; + Result.Result = EFlowDataPinResolveResult::Success; + Result.ResultValue = *CachedValue; + return Result; + } + } + if (!IsInputConnected(PinName)) { const bool bHasAssetParams = IsInputConnected(AssetParams_MemberName) || !AssetParams.IsNull(); @@ -271,7 +317,7 @@ FFlowDataPinResult UFlowNode_SubGraph::TrySupplyDataPin(FName PinName) const } } } - + // Prefer the standard lookup if the pin is connected // (or if there is no FlowAssetParams to ask) return Super::TrySupplyDataPin(PinName); diff --git a/Source/Flow/Public/FlowAsset.h b/Source/Flow/Public/FlowAsset.h index 9c44d736..d84ad80f 100644 --- a/Source/Flow/Public/FlowAsset.h +++ b/Source/Flow/Public/FlowAsset.h @@ -5,8 +5,12 @@ #include "FlowTypes.h" #include "Asset/FlowAssetParamsTypes.h" #include "Asset/FlowDeferredTransitionScope.h" +#include "Interfaces/FlowGraphOutputDataReceiverInterface.h" #include "Nodes/FlowNode.h" #include "StructUtils/InstancedStruct.h" +#include "Types/FlowDataPinValue.h" +#include "Types/FlowNamedDataPinProperty.h" +#include "Types/FlowOutputDataPinValues.h" #if WITH_EDITOR #include "FlowMessageLog.h" @@ -125,13 +129,22 @@ class FLOW_API UFlowAsset : public UObject TArray> AllowedInSubgraphNodeClasses; TArray> DeniedInSubgraphNodeClasses; - + bool bStartNodePlacedAsGhostNode; private: UPROPERTY() TMap> Nodes; +public: + const TArray& GetOutputDataPinDeclarations() const { return OutputDataPinDeclarations; } + +protected: + /* Output Data Pins define typed data values that this graph produces when it finishes. + * Sub Graph node using this Flow Asset will generate a context Output Data Pin for every entry on this list. */ + UPROPERTY(EditAnywhere, Category = "Sub Graph") + TArray OutputDataPinDeclarations; + #if WITH_EDITORONLY_DATA protected: /* Custom Inputs define custom entry points in graph, it's similar to blueprint Custom Events. @@ -318,10 +331,22 @@ class FLOW_API UFlowAsset : public UObject UPROPERTY(Transient) EFlowFinishPolicy FinishPolicy; + /* Receiver that will be given a snapshot of OutputDataPinValues when this graph finishes. + * Typically the SubGraph node that created this instance. */ + UPROPERTY(Transient) + TWeakObjectPtr OutputDataReceiver; + + /* Live output data pin values for this running instance. + * Initialized from OutputDataPinDeclarations defaults at StartFlow; updated by SetGraphOutput/Finish nodes. */ + UPROPERTY(Transient) + FFlowOutputDataPinValues OutputDataPinValues; + public: virtual void InitializeInstance(const TWeakObjectPtr InOwner, UFlowAsset& InTemplateAsset); virtual void DeinitializeInstance(); bool IsInstanceInitialized() const { return IsValid(TemplateAsset); } + + void FinishFlowAndDeinitializeInstance(const EFlowFinishPolicy InFinishPolicy); UFlowAsset* GetTemplateAsset() const { return TemplateAsset; } @@ -341,9 +366,17 @@ class FLOW_API UFlowAsset : public UObject AActor* TryFindActorOwner() const; virtual void PreStartFlow(); - virtual void StartFlow(IFlowDataPinValueSupplierInterface* DataPinValueSupplier = nullptr); + virtual void StartFlow(IFlowDataPinValueSupplierInterface* DataPinValueSupplier = nullptr, IFlowGraphOutputDataReceiverInterface* InOutputDataReceiver = nullptr); - virtual void FinishFlow(const EFlowFinishPolicy InFinishPolicy, const bool bRemoveInstance = true); + /* Write a single output data pin value into the live store for this running instance. + * Called by SetGraphOutput and Finish nodes for each connected output pin. */ + void WriteOutputDataPinValue(const FName& PinName, const TInstancedStruct& Value); + + /* Flush all of the OutputDataPinValues to the receiver (if set) */ + void FlushOutputDataPinValuesToReceiver(); + + virtual void FinishFlow(const EFlowFinishPolicy InFinishPolicy); + bool HasStartedFlow() const; void TriggerCustomInput(const FName& EventName, IFlowDataPinValueSupplierInterface* DataPinValueSupplier = nullptr); @@ -361,6 +394,8 @@ class FLOW_API UFlowAsset : public UObject virtual void FinishNode(UFlowNode* Node); void ResetNodes(); + void InitializeOutputDataReceiverAndValues(IFlowGraphOutputDataReceiverInterface* InOutputDataReceiver); + #if !UE_BUILD_SHIPPING public: FFlowSignalEvent OnPinTriggered; diff --git a/Source/Flow/Public/FlowSubsystem.h b/Source/Flow/Public/FlowSubsystem.h index ea68d892..5cf33058 100644 --- a/Source/Flow/Public/FlowSubsystem.h +++ b/Source/Flow/Public/FlowSubsystem.h @@ -78,16 +78,21 @@ class FLOW_API UFlowSubsystem : public UGameInstanceSubsystem * Nodes have opportunity to terminate themselves differently if Flow Graph has been aborted * Example: Spawn node might despawn all actors if Flow Graph is aborted, not completed */ UFUNCTION(BlueprintCallable, Category = "FlowSubsystem", meta = (DefaultToSelf = "Owner")) - virtual void FinishRootFlow(UObject* Owner, UFlowAsset* TemplateAsset, const EFlowFinishPolicy FinishPolicy); + virtual void FinishAndDeinitializeRootFlow(UObject* Owner, UFlowAsset* TemplateAsset, const EFlowFinishPolicy FinishPolicy); /* Finish Policy value is read by Flow Node * Nodes have opportunity to terminate themselves differently if Flow Graph has been aborted * Example: Spawn node might despawn all actors if Flow Graph is aborted, not completed */ UFUNCTION(BlueprintCallable, Category = "FlowSubsystem", meta = (DefaultToSelf = "Owner")) - virtual void FinishAllRootFlows(UObject* Owner, const EFlowFinishPolicy FinishPolicy); + virtual void FinishAndDeinitializeAllRootFlows(UObject* Owner, const EFlowFinishPolicy FinishPolicy); protected: UFlowAsset* CreateSubFlow(UFlowNode_SubGraph* SubGraphNode, const FString& SavedInstanceName = FString(), const bool bPreloading = false); + + /* Finishes the SubFlow running in the SubGraphNode. It does not deinitialize or removes from the internal InstancedSubFlows list */ + void FinishSubFlow(UFlowNode_SubGraph* SubGraphNode, const EFlowFinishPolicy FinishPolicy); + + /* Removes the Subflow from the InstancedSubFlows list; and Finishes and Deinitializes it. */ void RemoveSubFlow(UFlowNode_SubGraph* SubGraphNode, const EFlowFinishPolicy FinishPolicy); public: diff --git a/Source/Flow/Public/Interfaces/FlowGraphOutputDataReceiverInterface.h b/Source/Flow/Public/Interfaces/FlowGraphOutputDataReceiverInterface.h new file mode 100644 index 00000000..d58e05d9 --- /dev/null +++ b/Source/Flow/Public/Interfaces/FlowGraphOutputDataReceiverInterface.h @@ -0,0 +1,27 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/Interface.h" + +#include "FlowGraphOutputDataReceiverInterface.generated.h" + +struct FFlowOutputDataPinValues; + +/** + * Interface for objects that receive a snapshot of a Flow Asset's output data pin values + * when the asset's graph finishes execution. + * Example: UFlowNode_SubGraph receives the snapshot and caches it for downstream pin resolution. + */ +UINTERFACE(MinimalAPI, NotBlueprintable, DisplayName = "Flow Graph Output Data Receiver Interface") +class UFlowGraphOutputDataReceiverInterface : public UInterface +{ + GENERATED_BODY() +}; + +class FLOW_API IFlowGraphOutputDataReceiverInterface +{ + GENERATED_BODY() + +public: + virtual void ReceiveOutputDataSnapshot(const FFlowOutputDataPinValues& OutputDataPinValues) { } +}; diff --git a/Source/Flow/Public/LevelSequence/FlowLevelSequenceActor.h b/Source/Flow/Public/LevelSequence/FlowLevelSequenceActor.h index 99c6d5cf..6144b52a 100644 --- a/Source/Flow/Public/LevelSequence/FlowLevelSequenceActor.h +++ b/Source/Flow/Public/LevelSequence/FlowLevelSequenceActor.h @@ -6,6 +6,19 @@ class ULevelSequence; +/** Single actor binding override entry, replicated from server to clients. */ +USTRUCT() +struct FFlowSequenceBindingEntry +{ + GENERATED_BODY() + + UPROPERTY() + FName BindingTag; + + UPROPERTY() + TObjectPtr BoundActor; +}; + /** * Custom ALevelSequenceActor is needed to override ULevelSequencePlayer class. */ @@ -24,7 +37,19 @@ class FLOW_API AFlowLevelSequenceActor : public ALevelSequenceActor void SetPlaybackSettings(FMovieSceneSequencePlaybackSettings NewPlaybackSettings); void SetReplicatedLevelSequenceAsset(ULevelSequence* Asset); + /** Server only. Adds a binding override that replicates to clients via OnRep_BindingEntries. */ + void AddBinding(FName Tag, AActor* Actor); + + /** Server only. Clears all binding overrides and replicates the cleared state to clients. */ + void ClearAllBindings(); + protected: UFUNCTION() void OnRep_ReplicatedLevelSequenceAsset(); + + UPROPERTY(ReplicatedUsing = OnRep_BindingEntries) + TArray BindingEntries; + + UFUNCTION() + void OnRep_BindingEntries(); }; diff --git a/Source/Flow/Public/LevelSequence/FlowLevelSequencePlayer.h b/Source/Flow/Public/LevelSequence/FlowLevelSequencePlayer.h index 038125d1..525b6754 100644 --- a/Source/Flow/Public/LevelSequence/FlowLevelSequencePlayer.h +++ b/Source/Flow/Public/LevelSequence/FlowLevelSequencePlayer.h @@ -4,6 +4,7 @@ #include "LevelSequencePlayer.h" #include "FlowLevelSequencePlayer.generated.h" +class AFlowLevelSequenceActor; class UFlowNode; /** @@ -29,7 +30,7 @@ class FLOW_API UFlowLevelSequencePlayer : public ULevelSequencePlayer AActor* TransformOriginActor, const bool bReplicates, const bool bAlwaysRelevant, - ALevelSequenceActor*& OutActor); + TObjectPtr& OutActor); void SetFlowEventReceiver(UFlowNode* FlowNode) { FlowEventReceiver = FlowNode; } diff --git a/Source/Flow/Public/LevelSequence/IFlowPlayLevelSequenceAddOnInterface.h b/Source/Flow/Public/LevelSequence/IFlowPlayLevelSequenceAddOnInterface.h new file mode 100644 index 00000000..d98c8272 --- /dev/null +++ b/Source/Flow/Public/LevelSequence/IFlowPlayLevelSequenceAddOnInterface.h @@ -0,0 +1,40 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "UObject/Interface.h" + +#include "IFlowPlayLevelSequenceAddOnInterface.generated.h" + +class AActor; +class AFlowLevelSequenceActor; +class UFlowNodeAddOn; + +/** + * Interface for add-ons that want to apply setup (e.g. actor binding overrides) to a + * level sequence actor immediately after the sequence player is created, before Play() is called. + * + * Attach to any flow node that spawns an AFlowLevelSequenceActor and supports this interface. + */ +UINTERFACE(MinimalAPI) +class UFlowPlayLevelSequenceAddOnInterface : public UInterface +{ + GENERATED_BODY() +}; + +class FLOW_API IFlowPlayLevelSequenceAddOnInterface +{ + GENERATED_BODY() + +public: + + static bool ImplementsInterfaceSafe(const UFlowNodeAddOn* AddOnTemplate); + + /** + * Called after the sequence player is created, before Play() is invoked. + * + * @param SequenceActor The spawned AFlowLevelSequenceActor. Never null when called. + * @param FlowOwner The actor that owns the flow graph. May be null. + */ + virtual void OnSequencePlayerCreated(AFlowLevelSequenceActor& SequenceActor, AActor* FlowOwner) {} +}; diff --git a/Source/Flow/Public/Nodes/Actor/FlowNode_PlayLevelSequence.h b/Source/Flow/Public/Nodes/Actor/FlowNode_PlayLevelSequence.h index 02eaa1b5..19914c08 100644 --- a/Source/Flow/Public/Nodes/Actor/FlowNode_PlayLevelSequence.h +++ b/Source/Flow/Public/Nodes/Actor/FlowNode_PlayLevelSequence.h @@ -10,6 +10,7 @@ #include "Nodes/FlowNode.h" #include "FlowNode_PlayLevelSequence.generated.h" +class AFlowLevelSequenceActor; class UFlowLevelSequencePlayer; DECLARE_MULTICAST_DELEGATE(FFlowNodeLevelSequenceEvent); @@ -75,6 +76,9 @@ class FLOW_API UFlowNode_PlayLevelSequence UPROPERTY() TObjectPtr SequencePlayer; + UPROPERTY() + TObjectPtr SequenceActor; + /* Play Rate set by the user in PlaybackSettings. */ float CachedPlayRate; @@ -106,6 +110,10 @@ class FLOW_API UFlowNode_PlayLevelSequence virtual void FlushContent() override; // -- + // UFlowNodeBase + virtual EFlowAddOnAcceptResult AcceptFlowNodeAddOnChild_Implementation(const UFlowNodeAddOn* AddOnTemplate, const TArray& AdditionalAddOnsToAssumeAreChildren) const override; + // -- + virtual void InitializeInstance() override; void CreatePlayer(); diff --git a/Source/Flow/Public/Nodes/FlowNodeBase.h b/Source/Flow/Public/Nodes/FlowNodeBase.h index c3c1a4d0..c9cf262a 100644 --- a/Source/Flow/Public/Nodes/FlowNodeBase.h +++ b/Source/Flow/Public/Nodes/FlowNodeBase.h @@ -288,6 +288,12 @@ class FLOW_API UFlowNodeBase UFUNCTION(BlueprintPure, Category = DataPins, DisplayName = "Resolve DataPin By Name") FFlowDataPinResult TryResolveDataPin(FName PinName) const; +protected: + /* Protected accessor for TryResolveDataPin()'s use + * (we still want "most" flow nodes to not use TryResolveDataPin directly, + * they should be using the template versions below.) */ + FFlowDataPinResult TryResolveDataPin_SetGraphOutputAccess(FName PinName) const { return TryResolveDataPin(PinName); } + public: /* Generic single-value resolve & extractor. */ template @@ -456,8 +462,8 @@ class FLOW_API UFlowNodeBase /* This method allows to have different for every node instance, i.e. Red if node represents enemy, Green if node represents a friend. */ virtual bool GetDynamicTitleColor(FLinearColor& OutColor) const; - virtual FText GetNodeTitle() const { return K2_GetNodeTitle(); } - virtual FText GetNodeToolTip() const { return K2_GetNodeToolTip(); } + virtual FText GetNodeTitle() const; + virtual FText GetNodeToolTip() const; FText GetGeneratedDisplayName() const; @@ -488,7 +494,12 @@ class FLOW_API UFlowNodeBase UFUNCTION(BlueprintNativeEvent, Category = "FlowNode") FText K2_GetNodeToolTip() const; - + + // #ASIntegration #NodeCategory add overridable function for AS, no BP gen class to edit default category for, cant edit category member variable either + UFUNCTION(BlueprintNativeEvent, Category = "FlowNode") + FString K2_GetNodeCategory() const; + // + UFUNCTION(BlueprintPure, Category = "FlowNode") virtual FText GetNodeConfigText() const; diff --git a/Source/Flow/Public/Nodes/Graph/FlowNode_Finish.h b/Source/Flow/Public/Nodes/Graph/FlowNode_Finish.h index b4458eba..1717aa3c 100644 --- a/Source/Flow/Public/Nodes/Graph/FlowNode_Finish.h +++ b/Source/Flow/Public/Nodes/Graph/FlowNode_Finish.h @@ -1,7 +1,7 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #pragma once -#include "Nodes/FlowNode.h" +#include "Nodes/Graph/FlowNode_SetGraphOutput.h" #include "FlowNode_Finish.generated.h" /** @@ -9,7 +9,7 @@ * All active nodes and sub graphs will be deactivated. */ UCLASS(NotBlueprintable, meta = (DisplayName = "Finish")) -class FLOW_API UFlowNode_Finish : public UFlowNode +class FLOW_API UFlowNode_Finish : public UFlowNode_SetGraphOutput { GENERATED_BODY() diff --git a/Source/Flow/Public/Nodes/Graph/FlowNode_SetGraphOutput.h b/Source/Flow/Public/Nodes/Graph/FlowNode_SetGraphOutput.h new file mode 100644 index 00000000..f3b89ef6 --- /dev/null +++ b/Source/Flow/Public/Nodes/Graph/FlowNode_SetGraphOutput.h @@ -0,0 +1,35 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Nodes/FlowNode.h" +#include "FlowNode_SetGraphOutput.generated.h" + +/** + * Resolves connected input data pins from the Flow Asset's OutputDataPinDeclarations and + * writes their values to the asset's output data store. Functions as a pass-through exec node. + * UFlowNode_Finish derives from this class and additionally finishes the graph. + */ +UCLASS(NotBlueprintable, meta = (DisplayName = "Set Graph Output")) +class FLOW_API UFlowNode_SetGraphOutput : public UFlowNode +{ + GENERATED_BODY() + +public: + UFlowNode_SetGraphOutput(); + +protected: + virtual void ExecuteInput(const FName& PinName) override; + + /* Resolve all connected input data pins and write them to the Flow Asset's output store. */ + void CommitOutputDataPinValues(); + +#if WITH_EDITOR + // IFlowContextPinSupplierInterface + virtual bool SupportsContextPins() const override; + // -- + + // IFlowDataPinValueOwnerInterface + virtual void AutoGenerateDataPins(FFlowDataPinValueOwner& ValueOwner, FFlowAutoDataPinsWorkingData& InOutWorkingData) override; + // -- +#endif +}; diff --git a/Source/Flow/Public/Nodes/Graph/FlowNode_SubGraph.h b/Source/Flow/Public/Nodes/Graph/FlowNode_SubGraph.h index 5d7dfaf6..cf434b59 100644 --- a/Source/Flow/Public/Nodes/Graph/FlowNode_SubGraph.h +++ b/Source/Flow/Public/Nodes/Graph/FlowNode_SubGraph.h @@ -1,8 +1,12 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #pragma once +#include "Interfaces/FlowGraphOutputDataReceiverInterface.h" #include "Interfaces/FlowPreloadableInterface.h" #include "Nodes/FlowNode.h" +#include "Types/FlowDataPinValue.h" +#include "Types/FlowOutputDataPinValues.h" +#include "StructUtils/InstancedStruct.h" #include "FlowNode_SubGraph.generated.h" class UFlowAssetParams; @@ -14,11 +18,12 @@ UCLASS(NotBlueprintable, meta = (DisplayName = "Sub Graph")) class FLOW_API UFlowNode_SubGraph : public UFlowNode , public IFlowPreloadableInterface + , public IFlowGraphOutputDataReceiverInterface { GENERATED_BODY() public: - UFlowNode_SubGraph(); + UFlowNode_SubGraph(); friend class UFlowAsset; friend class FFlowNode_SubGraphDetails; @@ -27,7 +32,7 @@ class FLOW_API UFlowNode_SubGraph static FFlowPin StartPin; static FFlowPin FinishPin; -private: +protected: UPROPERTY(EditAnywhere, Category = "Graph") TSoftObjectPtr Asset; @@ -43,7 +48,17 @@ class FLOW_API UFlowNode_SubGraph UPROPERTY(SaveGame) FString SavedAssetInstanceName; + /* Cached output data pin values received from the inner Flow Asset when it finishes. + * Note - Not saved "yet", but should decide if we should include the + * cached output values in the save state. */ + UPROPERTY(Transient) + FFlowOutputDataPinValues CachedOutputDataPinValues; + protected: + // IFlowGraphOutputDataReceiverInterface + virtual void ReceiveOutputDataSnapshot(const FFlowOutputDataPinValues& Snapshot) override; + // -- + virtual bool CanBeAssetInstanced() const; // IFlowPreloadableInterface @@ -53,6 +68,7 @@ class FLOW_API UFlowNode_SubGraph virtual void ExecuteInput(const FName& PinName) override; virtual void Cleanup() override; + virtual void DeinitializeInstance() override; public: virtual void ForceFinishNode() override; diff --git a/Source/Flow/Public/Types/FlowOutputDataPinValues.h b/Source/Flow/Public/Types/FlowOutputDataPinValues.h new file mode 100644 index 00000000..6d587ac2 --- /dev/null +++ b/Source/Flow/Public/Types/FlowOutputDataPinValues.h @@ -0,0 +1,25 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/NameTypes.h" +#include "StructUtils/InstancedStruct.h" +#include "Containers/Map.h" + +#include "FlowOutputDataPinValues.generated.h" + +struct FFlowDataPinValue; + +/** + * Container for output data pin values from a Flow Asset execution. + * Holds a map of pin names to their resolved values. + */ +USTRUCT() +struct FFlowOutputDataPinValues +{ + GENERATED_BODY() + +public: + /* Map of output pin names to their resolved values. */ + UPROPERTY() + TMap> Values; +}; From aec50122a888c626fb7fb00231b7d915416466b7 Mon Sep 17 00:00:00 2001 From: LindyHopperGT <91915878+LindyHopperGT@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:16:54 -0700 Subject: [PATCH 4/7] Updates from the integrate with the Preload PR --- Source/Flow/Private/FlowAsset.cpp | 878 +++++++++--------- Source/Flow/Public/FlowAsset.h | 61 +- Source/FlowEditor/Private/Graph/FlowGraph.cpp | 6 + 3 files changed, 458 insertions(+), 487 deletions(-) diff --git a/Source/Flow/Private/FlowAsset.cpp b/Source/Flow/Private/FlowAsset.cpp index e38ba377..a7ed41c5 100644 --- a/Source/Flow/Private/FlowAsset.cpp +++ b/Source/Flow/Private/FlowAsset.cpp @@ -57,9 +57,9 @@ UFlowAsset::UFlowAsset(const FObjectInitializer& ObjectInitializer) , AllowedNodeClasses({UFlowNodeBase::StaticClass()}) , AllowedInSubgraphNodeClasses({UFlowNode_SubGraph::StaticClass()}) , bStartNodePlacedAsGhostNode(false) + , PinConnectionPolicy() , TemplateAsset(nullptr) , FinishPolicy(EFlowFinishPolicy::Keep) -, PinConnectionPolicy() , PreloadPolicy() { if (!AssetGuid.IsValid()) @@ -70,16 +70,6 @@ UFlowAsset::UFlowAsset(const FObjectInitializer& ObjectInitializer) ExpectedOwnerClass = GetDefault()->GetDefaultExpectedOwnerClass(); } -void UFlowAsset::PostInitProperties() -{ - Super::PostInitProperties(); - -#if WITH_EDITOR - InitializePinConnectionPolicy(); - InitializePreloadPolicy(); -#endif -} - #if WITH_EDITOR void UFlowAsset::AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector) { @@ -133,7 +123,7 @@ void UFlowAsset::PostLoad() const UPackage* Package = GetPackage(); if (IsValid(Package) && !FPackageName::IsTempPackage(Package->GetPathName())) { - // If we removed or moved a flow node blueprint (and there is no redirector) we might lose the reference to it resulting + // If we removed or moved a flow node blueprint (and there is no redirector) we might loose the reference to it resulting // in null pointers in the Nodes FGUID->UFlowNode* Map. So here we iterate over all the Nodes and remove all pairs that // are nulled out. @@ -161,6 +151,120 @@ void UFlowAsset::PreSaveRoot(FObjectPreSaveRootContext ObjectSaveContext) ReconcileBaseAssetParams(FDateTime::Now()); } +void UFlowAsset::ReconcileBaseAssetParams(const FDateTime& AssetLastSavedTimestamp) +{ + if (BaseAssetParams.AssetPtr.IsNull()) + { + return; + } + + UFlowAssetParams* BaseAssetParamsPtr = BaseAssetParams.AssetPtr.LoadSynchronous(); + if (!IsValid(BaseAssetParamsPtr)) + { + UE_LOG(LogFlow, Error, TEXT("Failed to load BaseAssetParams: %s"), *BaseAssetParams.AssetPtr.ToString()); + return; + } + + IFlowNamedPropertiesSupplierInterface* NamedPropertiesSupplier = Cast(GetDefaultEntryNode()); + if (!NamedPropertiesSupplier) + { + UE_LOG(LogFlow, Error, TEXT("No NamedPropertiesSupplier (e.g., Start node) found in FlowAsset: %s"), *GetPathName()); + return; + } + + TArray& MutableStartNodeProperties = NamedPropertiesSupplier->GetMutableNamedProperties(); + const EFlowReconcilePropertiesResult ReconcileResult = + BaseAssetParamsPtr->ReconcilePropertiesWithStartNode(AssetLastSavedTimestamp, this, MutableStartNodeProperties); + + if (EFlowReconcilePropertiesResult_Classifiers::IsErrorResult(ReconcileResult)) + { + UE_LOG(LogFlow, Error, TEXT("Failed to reconcile BaseAssetParams for %s: %s"), + *BaseAssetParamsPtr->GetPathName(), *UEnum::GetDisplayValueAsText(ReconcileResult).ToString()); + } +} + +UFlowAssetParams* UFlowAsset::GenerateParamsFromStartNode() +{ + if (BaseAssetParams.AssetPtr.IsValid()) + { + UE_LOG(LogFlow, Warning, TEXT("BaseAssetParams already exists for %s: %s"), *GetPathName(), *BaseAssetParams.AssetPtr.ToString()); + return BaseAssetParams.AssetPtr.LoadSynchronous(); + } + + // Get the Start node + IFlowNamedPropertiesSupplierInterface* NamedPropertiesSupplier = Cast(GetDefaultEntryNode()); + if (!NamedPropertiesSupplier) + { + UE_LOG(LogFlow, Error, TEXT("No valid Start node found for generating params in %s"), *GetPathName()); + return nullptr; + } + + // Determine the params asset name + const FString ParamsAssetName = GenerateParamsAssetName(); + if (ParamsAssetName.IsEmpty()) + { + UE_LOG(LogFlow, Error, TEXT("Generated empty params asset name for %s"), *GetPathName()); + return nullptr; + } + + // Create the params asset + FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); + const FString PackagePath = FPackageName::GetLongPackagePath(GetPackage()->GetPathName()); + FString UniquePackageName, UniqueAssetName; + AssetToolsModule.Get().CreateUniqueAssetName(PackagePath + TEXT("/") + ParamsAssetName, TEXT(""), UniquePackageName, UniqueAssetName); + + UFlowAssetParams* NewParams = Cast( + AssetToolsModule.Get().CreateAsset(UniqueAssetName, PackagePath, UFlowAssetParams::StaticClass(), nullptr)); + if (!IsValid(NewParams)) + { + UE_LOG(LogFlow, Error, TEXT("Failed to create Flow Asset Params: %s"), *UniqueAssetName); + return nullptr; + } + + // Reconfigure with the new properties + NewParams->ConfigureFlowAssetParams(this, nullptr, NamedPropertiesSupplier->GetMutableNamedProperties()); + + // Source control integration + 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()); + } + } + + // Assign to BaseAssetParams and sync Content Browser + BaseAssetParams.AssetPtr = NewParams; + + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + AssetRegistryModule.Get().AssetCreated(NewParams); + + FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked("ContentBrowser"); + TArray AssetsToSync = {NewParams}; + ContentBrowserModule.Get().SyncBrowserToAssets(AssetsToSync, true); + + return NewParams; +} + +FString UFlowAsset::GenerateParamsAssetName() const +{ + const FString FlowAssetName = GetName(); + + const int32 UnderscoreIndex = FlowAssetName.Find(TEXT("_"), ESearchCase::CaseSensitive); + + if (UnderscoreIndex != INDEX_NONE) + { + const FString Prefix = FlowAssetName.Left(UnderscoreIndex); + const FString Suffix = FlowAssetName.Mid(UnderscoreIndex + 1); + return FString::Printf(TEXT("%sParams_%s"), *Prefix, *Suffix); + } + else + { + return FlowAssetName + TEXT("Params"); + } +} + EDataValidationResult UFlowAsset::ValidateAsset(FFlowMessageLog& MessageLog) { // validate nodes @@ -204,7 +308,7 @@ EDataValidationResult UFlowAsset::ValidateAsset(FFlowMessageLog& MessageLog) } } - // if at least one error has been logged : mark the asset as invalid + // if at least one error has been has been logged : mark the asset as invalid for (const TSharedRef& Msg : MessageLog.Messages) { if (Msg->GetSeverity() == EMessageSeverity::Error) @@ -245,7 +349,7 @@ bool UFlowAsset::IsNodeOrAddOnClassAllowed(const UClass* FlowNodeOrAddOnClass, F bool UFlowAsset::CanFlowNodeClassBeUsedByFlowAsset(const UClass& FlowNodeClass) const { - const UFlowNode* NodeDefaults = Cast(FlowNodeClass.GetDefaultObject()); + UFlowNode* NodeDefaults = Cast(FlowNodeClass.GetDefaultObject()); if (!NodeDefaults) { check(FlowNodeClass.IsChildOf()); @@ -302,54 +406,6 @@ bool UFlowAsset::CanFlowAssetUseFlowNodeClass(const UClass& FlowNodeClass) const return true; } -bool UFlowAsset::CanFlowAssetReferenceFlowNode(const UClass& FlowNodeClass, FText* OutOptionalFailureReason) const -{ - if (!GEditor || !IsValid(&FlowNodeClass)) - { - return false; - } - - // Confirm plugin reference restrictions are being respected - FAssetReferenceFilterContext AssetReferenceFilterContext; - AssetReferenceFilterContext.AddReferencingAsset(FAssetData(this)); - const TSharedPtr FlowAssetReferenceFilter = GEditor->MakeAssetReferenceFilter(AssetReferenceFilterContext); - if (FlowAssetReferenceFilter.IsValid()) - { - const FAssetData FlowNodeAssetData(&FlowNodeClass); - if (!FlowAssetReferenceFilter->PassesFilter(FlowNodeAssetData, OutOptionalFailureReason)) - { - return false; - } - } - - return true; -} - -bool UFlowAsset::IsFlowNodeClassInAllowedClasses(const UClass& FlowNodeClass, const TSubclassOf& RequiredAncestor) const -{ - if (AllowedNodeClasses.Num() > 0) - { - bool bAllowedInAsset = false; - for (const TSubclassOf& AllowedNodeClass : AllowedNodeClasses) - { - // If a RequiredAncestor is provided, the AllowedNodeClass must be a subclass of the RequiredAncestor - if (AllowedNodeClass && FlowNodeClass.IsChildOf(AllowedNodeClass) && (!RequiredAncestor || AllowedNodeClass->IsChildOf(RequiredAncestor))) - { - bAllowedInAsset = true; - - break; - } - } - - if (!bAllowedInAsset) - { - return false; - } - } - - return true; -} - bool UFlowAsset::IsFlowNodeClassInDeniedClasses(const UClass& FlowNodeClass) const { for (const TSubclassOf& DeniedNodeClass : DeniedNodeClasses) @@ -396,6 +452,55 @@ void UFlowAsset::ValidateAddOnTree(UFlowNodeAddOn& AddOn, FFlowMessageLog& Messa } } +bool UFlowAsset::IsFlowNodeClassInAllowedClasses(const UClass& FlowNodeClass, + const TSubclassOf& RequiredAncestor) const +{ + if (AllowedNodeClasses.Num() > 0) + { + bool bAllowedInAsset = false; + for (const TSubclassOf& AllowedNodeClass : AllowedNodeClasses) + { + // If a RequiredAncestor is provided, the AllowedNodeClass must be a subclass of the RequiredAncestor + if (AllowedNodeClass && FlowNodeClass.IsChildOf(AllowedNodeClass) && (!RequiredAncestor || AllowedNodeClass->IsChildOf(RequiredAncestor))) + { + bAllowedInAsset = true; + + break; + } + } + + if (!bAllowedInAsset) + { + return false; + } + } + + return true; +} + +bool UFlowAsset::CanFlowAssetReferenceFlowNode(const UClass& FlowNodeClass, FText* OutOptionalFailureReason) const +{ + if (!GEditor || !IsValid(&FlowNodeClass)) + { + return false; + } + + // Confirm plugin reference restrictions are being respected + FAssetReferenceFilterContext AssetReferenceFilterContext; + AssetReferenceFilterContext.AddReferencingAsset(FAssetData(this)); + const TSharedPtr FlowAssetReferenceFilter = GEditor->MakeAssetReferenceFilter(AssetReferenceFilterContext); + if (FlowAssetReferenceFilter.IsValid()) + { + const FAssetData FlowNodeAssetData(&FlowNodeClass); + if (!FlowAssetReferenceFilter->PassesFilter(FlowNodeAssetData, OutOptionalFailureReason)) + { + return false; + } + } + + return true; +} + UFlowNode* UFlowAsset::CreateNode(const UClass* NodeClass, UEdGraphNode* GraphNode) { UFlowNode* NewNode = NewObject(this, NodeClass, NAME_None, RF_Transactional); @@ -425,7 +530,7 @@ void UFlowAsset::UnregisterNode(const FGuid& NodeGuid) HarvestNodeConnections(); - (void)MarkPackageDirty(); + MarkPackageDirty(); } void UFlowAsset::HarvestNodeConnections(UFlowNode* TargetNode) @@ -557,33 +662,6 @@ bool UFlowAsset::TryGetDefaultForInputPinName(const FStructProperty& StructPrope #endif -TArray UFlowAsset::GetAllNodes() const -{ - TArray> AllNodes; - AllNodes.Reserve(Nodes.Num()); - Nodes.GenerateValueArray(AllNodes); - - return ObjectPtrDecay(AllNodes); -} - -TArray UFlowAsset::GetNodesInExecutionOrder(UFlowNode* FirstIteratedNode, const TSubclassOf FlowNodeClass) const -{ - TArray FoundNodes; - GetNodesInExecutionOrder(FirstIteratedNode, FoundNodes); - - // filter out nodes by class - for (int32 i = FoundNodes.Num() - 1; i >= 0; i--) - { - if (!FoundNodes[i]->GetClass()->IsChildOf(FlowNodeClass)) - { - FoundNodes.RemoveAt(i); - } - } - FoundNodes.Shrink(); - - return FoundNodes; -} - UFlowNode* UFlowAsset::GetDefaultEntryNode() const { UFlowNode* FirstStartNode = nullptr; @@ -607,26 +685,39 @@ UFlowNode* UFlowAsset::GetDefaultEntryNode() const return FirstStartNode; } -TArray UFlowAsset::GatherNodesConnectedToAllInputs() const +#if WITH_EDITOR +void UFlowAsset::AddCustomInput(const FName& EventName) { - TSet> IteratedNodes; - TArray ConnectedNodes; - - // Nodes connected to the Start node - UFlowNode* DefaultEntryNode = GetDefaultEntryNode(); - GetNodesInExecutionOrder_Recursive(DefaultEntryNode, IteratedNodes, ConnectedNodes); - - // Nodes connected to Custom Input node(s) - for (const TPair& Node : ObjectPtrDecay(Nodes)) + if (!CustomInputs.Contains(EventName)) { - if (UFlowNode_CustomInput* CustomInput = Cast(Node.Value)) - { - GetNodesInExecutionOrder_Recursive(CustomInput, IteratedNodes, ConnectedNodes); - } + CustomInputs.Add(EventName); } +} - return ConnectedNodes; +void UFlowAsset::RemoveCustomInput(const FName& EventName) +{ + if (CustomInputs.Contains(EventName)) + { + CustomInputs.Remove(EventName); + } +} + +void UFlowAsset::AddCustomOutput(const FName& EventName) +{ + if (!CustomOutputs.Contains(EventName)) + { + CustomOutputs.Add(EventName); + } +} + +void UFlowAsset::RemoveCustomOutput(const FName& EventName) +{ + if (CustomOutputs.Contains(EventName)) + { + CustomOutputs.Remove(EventName); + } } +#endif // WITH_EDITOR UFlowNode_CustomInput* UFlowAsset::TryFindCustomInputNodeByEventName(const FName& EventName) const { @@ -694,73 +785,43 @@ TArray UFlowAsset::GatherCustomOutputNodeEventNames() const return Results; } -#if WITH_EDITOR -void UFlowAsset::AddCustomInput(const FName& EventName) -{ - if (!CustomInputs.Contains(EventName)) - { - CustomInputs.Add(EventName); - } -} - -void UFlowAsset::RemoveCustomInput(const FName& EventName) +TArray UFlowAsset::GetNodesInExecutionOrder(UFlowNode* FirstIteratedNode, const TSubclassOf FlowNodeClass) const { - if (CustomInputs.Contains(EventName)) - { - CustomInputs.Remove(EventName); - } -} + TArray FoundNodes; + GetNodesInExecutionOrder(FirstIteratedNode, FoundNodes); -void UFlowAsset::AddCustomOutput(const FName& EventName) -{ - if (!CustomOutputs.Contains(EventName)) + // filter out nodes by class + for (int32 i = FoundNodes.Num() - 1; i >= 0; i--) { - CustomOutputs.Add(EventName); + if (!FoundNodes[i]->GetClass()->IsChildOf(FlowNodeClass)) + { + FoundNodes.RemoveAt(i); + } } -} + FoundNodes.Shrink(); -void UFlowAsset::RemoveCustomOutput(const FName& EventName) -{ - if (CustomOutputs.Contains(EventName)) - { - CustomOutputs.Remove(EventName); - } + return FoundNodes; } -#endif // WITH_EDITOR -#if WITH_EDITOR -void UFlowAsset::InitializePinConnectionPolicy() +TArray UFlowAsset::GatherNodesConnectedToAllInputs() const { - const FInstancedStruct& SourceStruct = GetDefault()->PinConnectionPolicy; - if (ensure(SourceStruct.IsValid())) - { - PinConnectionPolicy.InitializeAsScriptStruct(SourceStruct.GetScriptStruct(), SourceStruct.GetMemory()); - } -} -#endif + TSet> IteratedNodes; + TArray ConnectedNodes; -const FFlowPinConnectionPolicy& UFlowAsset::GetPinConnectionPolicy() const -{ - // Runtime instances delegate to their template, which holds the serialized policy - if (!PinConnectionPolicy.IsValid() && IsValid(TemplateAsset)) - { - return TemplateAsset->GetPinConnectionPolicy(); - } + // Nodes connected to the Start node + UFlowNode* DefaultEntryNode = GetDefaultEntryNode(); + GetNodesInExecutionOrder_Recursive(DefaultEntryNode, IteratedNodes, ConnectedNodes); - // Graceful fallback: if PinConnectionPolicy was never initialized (asset predates this feature, - // or was never opened in editor), read directly from Project Settings at runtime. - if (!PinConnectionPolicy.IsValid()) + // Nodes connected to Custom Input node(s) + for (const TPair& Node : ObjectPtrDecay(Nodes)) { - const FFlowPinConnectionPolicy* SettingsPolicy = GetDefault()->GetPinConnectionPolicy(); - ensureAlways(SettingsPolicy); - if (SettingsPolicy) + if (UFlowNode_CustomInput* CustomInput = Cast(Node.Value)) { - return *SettingsPolicy; + GetNodesInExecutionOrder_Recursive(CustomInput, IteratedNodes, ConnectedNodes); } } - check(PinConnectionPolicy.IsValid()); - return PinConnectionPolicy.Get(); + return ConnectedNodes; } TArray UFlowAsset::GatherPinsConnectedToPin(const FConnectedPin& Pin) const @@ -780,121 +841,14 @@ TArray UFlowAsset::GatherPinsConnectedToPin(const FConnectedPin& return ConnectedPins; } -#if WITH_EDITOR -UFlowAssetParams* UFlowAsset::GenerateParamsFromStartNode() -{ - if (BaseAssetParams.AssetPtr.IsValid()) - { - UE_LOG(LogFlow, Warning, TEXT("BaseAssetParams already exists for %s: %s"), *GetPathName(), *BaseAssetParams.AssetPtr.ToString()); - return BaseAssetParams.AssetPtr.LoadSynchronous(); - } - - // Get the Start node - IFlowNamedPropertiesSupplierInterface* NamedPropertiesSupplier = Cast(GetDefaultEntryNode()); - if (!NamedPropertiesSupplier) - { - UE_LOG(LogFlow, Error, TEXT("No valid Start node found for generating params in %s"), *GetPathName()); - return nullptr; - } - - // Determine the params asset name - const FString ParamsAssetName = GenerateParamsAssetName(); - if (ParamsAssetName.IsEmpty()) - { - UE_LOG(LogFlow, Error, TEXT("Generated empty params asset name for %s"), *GetPathName()); - return nullptr; - } - - // Create the params asset - const FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); - const FString PackagePath = FPackageName::GetLongPackagePath(GetPackage()->GetPathName()); - FString UniquePackageName, UniqueAssetName; - AssetToolsModule.Get().CreateUniqueAssetName(PackagePath + TEXT("/") + ParamsAssetName, TEXT(""), UniquePackageName, UniqueAssetName); - - UFlowAssetParams* NewParams = Cast( - AssetToolsModule.Get().CreateAsset(UniqueAssetName, PackagePath, UFlowAssetParams::StaticClass(), nullptr)); - if (!IsValid(NewParams)) - { - UE_LOG(LogFlow, Error, TEXT("Failed to create Flow Asset Params: %s"), *UniqueAssetName); - return nullptr; - } - - // Reconfigure with the new properties - NewParams->ConfigureFlowAssetParams(this, nullptr, NamedPropertiesSupplier->GetMutableNamedProperties()); - - // Source control integration - 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()); - } - } - - // Assign to BaseAssetParams and sync Content Browser - BaseAssetParams.AssetPtr = NewParams; - - 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; -} - -FString UFlowAsset::GenerateParamsAssetName() const -{ - const FString FlowAssetName = GetName(); - - const int32 UnderscoreIndex = FlowAssetName.Find(TEXT("_"), ESearchCase::CaseSensitive); - - if (UnderscoreIndex != INDEX_NONE) - { - const FString Prefix = FlowAssetName.Left(UnderscoreIndex); - const FString Suffix = FlowAssetName.Mid(UnderscoreIndex + 1); - return FString::Printf(TEXT("%sParams_%s"), *Prefix, *Suffix); - } - else - { - return FlowAssetName + TEXT("Params"); - } -} - -void UFlowAsset::ReconcileBaseAssetParams(const FDateTime& AssetLastSavedTimestamp) +TArray UFlowAsset::GetAllNodes() const { - if (BaseAssetParams.AssetPtr.IsNull()) - { - return; - } - - UFlowAssetParams* BaseAssetParamsPtr = BaseAssetParams.AssetPtr.LoadSynchronous(); - if (!IsValid(BaseAssetParamsPtr)) - { - UE_LOG(LogFlow, Error, TEXT("Failed to load BaseAssetParams: %s"), *BaseAssetParams.AssetPtr.ToString()); - return; - } - - IFlowNamedPropertiesSupplierInterface* NamedPropertiesSupplier = Cast(GetDefaultEntryNode()); - if (!NamedPropertiesSupplier) - { - UE_LOG(LogFlow, Error, TEXT("No NamedPropertiesSupplier (e.g., Start node) found in FlowAsset: %s"), *GetPathName()); - return; - } - - TArray& MutableStartNodeProperties = NamedPropertiesSupplier->GetMutableNamedProperties(); - const EFlowReconcilePropertiesResult ReconcileResult = - BaseAssetParamsPtr->ReconcilePropertiesWithStartNode(AssetLastSavedTimestamp, this, MutableStartNodeProperties); + TArray> AllNodes; + AllNodes.Reserve(Nodes.Num()); + Nodes.GenerateValueArray(AllNodes); - if (EFlowReconcilePropertiesResult_Classifiers::IsErrorResult(ReconcileResult)) - { - UE_LOG(LogFlow, Error, TEXT("Failed to reconcile BaseAssetParams for %s: %s"), - *BaseAssetParamsPtr->GetPathName(), *UEnum::GetDisplayValueAsText(ReconcileResult).ToString()); - } + return ObjectPtrDecay(AllNodes); } -#endif void UFlowAsset::AddInstance(UFlowAsset* Instance) { @@ -969,6 +923,14 @@ void UFlowAsset::BroadcastRuntimeMessageAdded(const TSharedRef InOwner, UFlowAsset& InTemplateAsset) @@ -978,6 +940,9 @@ void UFlowAsset::InitializeInstance(const TWeakObjectPtr InOwner, UFlow Owner = InOwner; TemplateAsset = &InTemplateAsset; + // Initialize any customizable Policies before we instantiate nodes + InitializePreloadPolicy(); + for (TPair>& Node : Nodes) { UFlowNode* NewNodeInstance = NewObject(this, Node.Value->GetClass(), NAME_None, RF_Transient, Node.Value, false, nullptr); @@ -1026,29 +991,6 @@ void UFlowAsset::FinishFlowAndDeinitializeInstance(const EFlowFinishPolicy InFin DeinitializeInstance(); } -AActor* UFlowAsset::TryFindActorOwner() const -{ - UObject* OwnerObject = GetOwner(); - if (!IsValid(OwnerObject)) - { - return nullptr; - } - - // If the owner is already an Actor, return it directly - if (AActor* OwnerAsActor = Cast(OwnerObject)) - { - return OwnerAsActor; - } - - // If the owner is a Component, return its owning Actor - if (const UActorComponent* OwnerAsComponent = Cast(OwnerObject)) - { - return OwnerAsComponent->GetOwner(); - } - - return nullptr; -} - void UFlowAsset::PreStartFlow() { ResetNodes(); @@ -1111,112 +1053,144 @@ void UFlowAsset::InitializeOutputDataReceiverAndValues(IFlowGraphOutputDataRecei } } -bool UFlowAsset::HasStartedFlow() const +void UFlowAsset::WriteOutputDataPinValue(const FName& PinName, const TInstancedStruct& Value) { - return RecordedNodes.Num() > 0; + if (OutputDataPinValues.Values.Contains(PinName)) + { + OutputDataPinValues.Values[PinName] = Value; + } + else + { + UE_LOG(LogFlow, Warning, TEXT("Could not find pin named %s in WriteOutputDataPinValue"), *PinName.ToString()); + } } -void UFlowAsset::FinishNode(UFlowNode* Node) +void UFlowAsset::FlushOutputDataPinValuesToReceiver() { - if (ActiveNodes.Contains(Node)) + if (IFlowGraphOutputDataReceiverInterface* Receiver = Cast(OutputDataReceiver.Get())) { - ActiveNodes.Remove(Node); + // Do an immediate push to the receiver + Receiver->ReceiveOutputDataSnapshot(OutputDataPinValues); + } +} - // if graph reached Finish and this asset instance was created by SubGraph node - if (Node->CanFinishGraph()) +void UFlowAsset::FinishFlow(const EFlowFinishPolicy InFinishPolicy) +{ + FinishPolicy = InFinishPolicy; + + CancelAndWarnForUnflushedDeferredTriggers(); + + // end execution of this asset and all of its nodes + for (UFlowNode* Node : ActiveNodes) + { + Node->Deactivate(); + } + ActiveNodes.Empty(); +} + +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 (NodeOwningThisAssetInstance.IsValid()) + if (!ScopePtr.IsValid()) { - NodeOwningThisAssetInstance.Get()->TriggerFirstOutput(true); + continue; + } - return; + 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()); } - // if this instance is a Root Flow, we need to deregister it from the subsystem first - if (Owner.IsValid()) + TotalDroppedTriggers += Triggers.Num(); + + for (const FFlowDeferredTriggerInput& Trigger : Triggers) { - const TSet& RootFlowInstances = GetFlowSubsystem()->GetRootInstancesByOwner(Owner.Get()); - if (RootFlowInstances.Contains(this)) - { - GetFlowSubsystem()->FinishRootFlow(Owner.Get(), TemplateAsset, EFlowFinishPolicy::Keep); + const UFlowNode* ToNode = GetNode(Trigger.NodeGuid); + const UFlowNode* FromNode = Trigger.FromPin.NodeGuid.IsValid() ? GetNode(Trigger.FromPin.NodeGuid) : nullptr; - return; - } - } + const FString ToNodeName = ToNode ? ToNode->GetName() : TEXT(""); + const FString FromNodeName = FromNode ? FromNode->GetName() : TEXT(""); - FinishFlow(EFlowFinishPolicy::Keep); + 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"), + *ToNodeName, + *Trigger.NodeGuid.ToString(), + *Trigger.PinName.ToString(), + *FromNodeName, + *Trigger.FromPin.NodeGuid.ToString(), + *Trigger.FromPin.PinName.ToString() + ); + } } + + ClearAllDeferredTriggerScopes(); } } -void UFlowAsset::WriteOutputDataPinValue(const FName& PinName, const TInstancedStruct& Value) +bool UFlowAsset::HasStartedFlow() const { - if (OutputDataPinValues.Values.Contains(PinName)) - { - OutputDataPinValues.Values[PinName] = Value; - } - else - { - UE_LOG(LogFlow, Warning, TEXT("Could not find pin named %s in WriteOutputDataPinValue"), *PinName.ToString()); - } + return RecordedNodes.Num() > 0; } -void UFlowAsset::FlushOutputDataPinValuesToReceiver() +AActor* UFlowAsset::TryFindActorOwner() const { - if (IFlowGraphOutputDataReceiverInterface* Receiver = Cast(OutputDataReceiver.Get())) + UObject* OwnerObject = GetOwner(); + if (!IsValid(OwnerObject)) { - // Do an immediate push to the receiver - Receiver->ReceiveOutputDataSnapshot(OutputDataPinValues); + return nullptr; } -} - -void UFlowAsset::FinishFlow(const EFlowFinishPolicy InFinishPolicy) -{ - FinishFlow(InFinishPolicy, true); -} -void UFlowAsset::ResetNodes() -{ - for (UFlowNode* Node : RecordedNodes) + // If the owner is already an Actor, return it directly + if (AActor* OwnerAsActor = Cast(OwnerObject)) { - Node->ResetRecords(); + return OwnerAsActor; } - RecordedNodes.Empty(); -} - -void UFlowAsset::FinishFlow(const EFlowFinishPolicy InFinishPolicy, const bool bRemoveInstance /*= true*/) -{ - FinishPolicy = InFinishPolicy; - - CancelAndWarnForUnflushedDeferredTriggers(); - - // end execution of this asset and all of its nodes - for (UFlowNode* Node : ActiveNodes) + // If the owner is a Component, return its owning Actor + if (const UActorComponent* OwnerAsComponent = Cast(OwnerObject)) { - Node->Deactivate(); + return OwnerAsComponent->GetOwner(); } - ActiveNodes.Empty(); -} -UFlowSubsystem* UFlowAsset::GetFlowSubsystem() const -{ - return Cast(GetOuter()); + return nullptr; } -UFlowNode_SubGraph* UFlowAsset::GetNodeOwningThisAssetInstance() const +TWeakObjectPtr UFlowAsset::GetFlowInstance(UFlowNode_SubGraph* SubGraphNode) const { - return NodeOwningThisAssetInstance.Get(); + return ActiveSubGraphs.FindRef(SubGraphNode); } -UFlowAsset* UFlowAsset::GetParentInstance() const +void UFlowAsset::TriggerCustomInput_FromSubGraph(UFlowNode_SubGraph* SubGraphNode, const FName& EventName) const { - return NodeOwningThisAssetInstance.IsValid() ? NodeOwningThisAssetInstance.Get()->GetFlowAsset() : nullptr; -} + // 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). -TWeakObjectPtr UFlowAsset::GetFlowInstance(UFlowNode_SubGraph* SubGraphNode) const -{ - return ActiveSubGraphs.FindRef(SubGraphNode); + const TWeakObjectPtr FlowInstance = ActiveSubGraphs.FindRef(SubGraphNode); + if (FlowInstance.IsValid()) + { + FlowInstance->TriggerCustomInput(EventName, SubGraphNode); + } } void UFlowAsset::TriggerCustomInput(const FName& EventName, IFlowDataPinValueSupplierInterface* DataPinValueSupplier) @@ -1241,19 +1215,6 @@ void UFlowAsset::TriggerCustomInput(const FName& EventName, IFlowDataPinValueSup } } -void UFlowAsset::TriggerCustomInput_FromSubGraph(UFlowNode_SubGraph* SubGraphNode, const FName& EventName) const -{ - // 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). - - const TWeakObjectPtr FlowInstance = ActiveSubGraphs.FindRef(SubGraphNode); - if (FlowInstance.IsValid()) - { - FlowInstance->TriggerCustomInput(EventName, SubGraphNode); - } -} - void UFlowAsset::TriggerCustomOutput(const FName& EventName) { if (NodeOwningThisAssetInstance.IsValid()) @@ -1317,19 +1278,6 @@ bool UFlowAsset::ShouldDeferTriggers() const return GetDefault()->bDeferTriggeredOutputsWhileTriggering; } -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}); -} - TSharedPtr UFlowAsset::PushDeferredTransitionScope() { // Close the former top scope (if any) @@ -1343,11 +1291,6 @@ TSharedPtr UFlowAsset::PushDeferredTransitionScope return DeferredTransitionScopes.Add_GetRef(MakeShared()); } -void UFlowAsset::PopDeferredTransitionScope(const TSharedPtr& Scope) -{ - TryFlushAndRemoveDeferredTransitionScope(Scope); -} - bool UFlowAsset::TryFlushAndRemoveDeferredTransitionScope(const TSharedPtr& ScopeToFlush) { if (ScopeToFlush->TryFlushDeferredTriggers(*this)) @@ -1364,6 +1307,19 @@ bool UFlowAsset::TryFlushAndRemoveDeferredTransitionScope(const TSharedPtrIsOpen()) + { + // 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()) @@ -1373,7 +1329,7 @@ bool UFlowAsset::TryFlushAllDeferredTriggerScopes() break; } - // Keep flushing until stack is empty, or we hit an ExecutionGate halt + // Keep flushing until stack is empty or we hit an ExecutionGate halt } check(DeferredTransitionScopes.IsEmpty() || FFlowExecutionGate::IsHalted()); @@ -1386,68 +1342,78 @@ void UFlowAsset::ClearAllDeferredTriggerScopes() DeferredTransitionScopes.Reset(); } -void UFlowAsset::CancelAndWarnForUnflushedDeferredTriggers() +TSharedPtr UFlowAsset::GetTopDeferredTransitionScope() const { - // 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()) + return !DeferredTransitionScopes.IsEmpty() ? DeferredTransitionScopes.Top() : nullptr; +} + +void UFlowAsset::FinishNode(UFlowNode* Node) +{ + if (ActiveNodes.Contains(Node)) { - int32 TotalDroppedTriggers = 0; + ActiveNodes.Remove(Node); - for (const TSharedPtr& ScopePtr : DeferredTransitionScopes) + // if graph reached Finish and this asset instance was created by SubGraph node + if (Node->CanFinishGraph()) { - if (!ScopePtr.IsValid()) + if (IFlowGraphOutputDataReceiverInterface* Receiver = Cast(OutputDataReceiver.Get())) { - continue; + Receiver->ReceiveOutputDataSnapshot(OutputDataPinValues); } - const TArray& Triggers = ScopePtr->GetDeferredTriggers(); - - if (TotalDroppedTriggers == 0 && !Triggers.IsEmpty()) + if (NodeOwningThisAssetInstance.IsValid()) { - 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()); - } + NodeOwningThisAssetInstance.Get()->TriggerFirstOutput(true); - TotalDroppedTriggers += Triggers.Num(); + return; + } - for (const FFlowDeferredTriggerInput& Trigger : Triggers) + // if this instance is a Root Flow, we need to deregister it from the subsystem first. This will + // finalize and deinitialize the root flow. + if (Owner.IsValid()) { - const UFlowNode* ToNode = GetNode(Trigger.NodeGuid); - const UFlowNode* FromNode = Trigger.FromPin.NodeGuid.IsValid() ? GetNode(Trigger.FromPin.NodeGuid) : nullptr; - - const FString ToNodeName = ToNode ? ToNode->GetName() : TEXT(""); - const FString FromNodeName = FromNode ? FromNode->GetName() : TEXT(""); + const TSet& RootFlowInstances = GetFlowSubsystem()->GetRootInstancesByOwner(Owner.Get()); + if (RootFlowInstances.Contains(this)) + { + GetFlowSubsystem()->FinishAndDeinitializeRootFlow(Owner.Get(), TemplateAsset, EFlowFinishPolicy::Keep); - 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"), - *ToNodeName, - *Trigger.NodeGuid.ToString(), - *Trigger.PinName.ToString(), - *FromNodeName, - *Trigger.FromPin.NodeGuid.ToString(), - *Trigger.FromPin.PinName.ToString() - ); + return; + } } + + FinishFlow(EFlowFinishPolicy::Keep); } + } +} - ClearAllDeferredTriggerScopes(); +void UFlowAsset::ResetNodes() +{ + for (UFlowNode* Node : RecordedNodes) + { + Node->ResetRecords(); } + + RecordedNodes.Empty(); } -TSharedPtr UFlowAsset::GetTopDeferredTransitionScope() const +UFlowSubsystem* UFlowAsset::GetFlowSubsystem() const { - return !DeferredTransitionScopes.IsEmpty() ? DeferredTransitionScopes.Top() : nullptr; + return Cast(GetOuter()); +} + +FName UFlowAsset::GetDisplayName() const +{ + return GetFName(); +} + +UFlowNode_SubGraph* UFlowAsset::GetNodeOwningThisAssetInstance() const +{ + return NodeOwningThisAssetInstance.Get(); +} + +UFlowAsset* UFlowAsset::GetParentInstance() const +{ + return NodeOwningThisAssetInstance.IsValid() ? NodeOwningThisAssetInstance.Get()->GetFlowAsset() : nullptr; } FFlowAssetSaveData UFlowAsset::SaveInstance(TArray& SavedFlowInstances) @@ -1568,25 +1534,7 @@ const FFlowPinConnectionPolicy& UFlowAsset::GetPinConnectionPolicy() const const FFlowPreloadPolicy& UFlowAsset::GetPreloadPolicy() const { - // Runtime instances delegate to their template, which holds the serialized policy. - if (!PreloadPolicy.IsValid() && IsValid(TemplateAsset)) - { - return TemplateAsset->GetPreloadPolicy(); - } - - // Graceful fallback: if PreloadPolicy was never initialized (asset predates this feature, - // or was never opened in editor), read directly from project settings at runtime. - if (!PreloadPolicy.IsValid()) - { - const FFlowPreloadPolicy* SettingsPolicy = GetDefault()->GetPreloadPolicy(); - ensureAlways(SettingsPolicy); - if (SettingsPolicy) - { - return *SettingsPolicy; - } - } - - check(PreloadPolicy.IsValid()); + checkf(PreloadPolicy.IsValid(), TEXT("PreloadPolicy must be initialized prior to calling GetPreloadPolicy()")); return PreloadPolicy.Get(); } @@ -1601,16 +1549,30 @@ void UFlowAsset::InitializePinConnectionPolicy() } } +#endif + void UFlowAsset::InitializePreloadPolicy() { - const FInstancedStruct& SourceStruct = GetDefault()->PreloadPolicy; - if (ensure(SourceStruct.IsValid())) + if (PreloadPolicy.IsValid()) + { + // use per-class policy + PreloadPolicy.InitializeAsScriptStruct(PreloadPolicy.GetScriptStruct(), PreloadPolicy.GetMemory()); + } + else { - PreloadPolicy.InitializeAsScriptStruct(SourceStruct.GetScriptStruct(), SourceStruct.GetMemory()); + // fallback to project's default policy + const FInstancedStruct& DefaultPolicy = GetDefault()->PreloadPolicy; + if (ensure(DefaultPolicy.IsValid())) + { + PreloadPolicy.InitializeAsScriptStruct(DefaultPolicy.GetScriptStruct(), DefaultPolicy.GetMemory()); + } } + + ensureAlwaysMsgf(PreloadPolicy.IsValid(), TEXT("There's no valid Preload Policy set in the project!")); } -#endif +#if WITH_EDITOR + void UFlowAsset::LogError(const FString& MessageToLog, const UFlowNodeBase* Node) const { LogRuntimeMessage(EMessageSeverity::Error, MessageToLog, Node); diff --git a/Source/Flow/Public/FlowAsset.h b/Source/Flow/Public/FlowAsset.h index 209f52ba..4649e7b4 100644 --- a/Source/Flow/Public/FlowAsset.h +++ b/Source/Flow/Public/FlowAsset.h @@ -7,7 +7,6 @@ #include "Asset/FlowDeferredTransitionScope.h" #include "Interfaces/FlowGraphOutputDataReceiverInterface.h" #include "Nodes/FlowNode.h" -#include "StructUtils/InstancedStruct.h" #include "Types/FlowDataPinValue.h" #include "Types/FlowNamedDataPinProperty.h" #include "Types/FlowOutputDataPinValues.h" @@ -16,6 +15,7 @@ #include "FlowMessageLog.h" #endif +#include "StructUtils/InstancedStruct.h" #include "Templates/SharedPointer.h" #include "UObject/ObjectKey.h" @@ -29,6 +29,8 @@ struct FFlowPreloadPolicy; struct FFlowPinConnectionPolicy; class UEdGraph; +class UFlowAsset; +class UFlowAssetParams; class UEdGraphNode; #if !UE_BUILD_SHIPPING @@ -53,6 +55,7 @@ class FLOW_API UFlowAsset : public UObject friend class FFlowAssetDetails; friend class FFlowNode_SubGraphDetails; friend class UFlowGraphSchema; + friend struct FFlowDeferredTransitionScope; UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Flow Asset") FGuid AssetGuid; @@ -66,8 +69,6 @@ class FLOW_API UFlowAsset : public UObject // Graph (editor-only) public: - virtual void PostInitProperties() override; - #if WITH_EDITOR public: friend class UFlowGraph; @@ -97,6 +98,8 @@ class FLOW_API UFlowAsset : public UObject #if WITH_EDITOR public: + void SetupForEditing(); + UEdGraph* GetGraph() const { return FlowGraph; } virtual EDataValidationResult ValidateAsset(FFlowMessageLog& MessageLog); @@ -128,7 +131,7 @@ class FLOW_API UFlowAsset : public UObject TArray> AllowedInSubgraphNodeClasses; TArray> DeniedInSubgraphNodeClasses; - + bool bStartNodePlacedAsGhostNode; private: @@ -144,19 +147,6 @@ class FLOW_API UFlowAsset : public UObject UPROPERTY(EditAnywhere, Category = "Sub Graph") TArray OutputDataPinDeclarations; -#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. */ - 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. */ - UPROPERTY(EditAnywhere, Category = "Sub Graph") - TArray CustomOutputs; -#endif // WITH_EDITORONLY_DATA - public: #if WITH_EDITOR FFlowGraphEvent OnSubGraphReconstructionRequested; @@ -397,7 +387,7 @@ class FLOW_API UFlowAsset : public UObject virtual void InitializeInstance(const TWeakObjectPtr InOwner, UFlowAsset& InTemplateAsset); virtual void DeinitializeInstance(); bool IsInstanceInitialized() const { return IsValid(TemplateAsset); } - + void FinishFlowAndDeinitializeInstance(const EFlowFinishPolicy InFinishPolicy); UFlowAsset* GetTemplateAsset() const { return TemplateAsset; } @@ -418,7 +408,7 @@ class FLOW_API UFlowAsset : public UObject AActor* TryFindActorOwner() const; virtual void PreStartFlow(); -virtual void StartFlow(IFlowDataPinValueSupplierInterface* DataPinValueSupplier = nullptr, IFlowGraphOutputDataReceiverInterface* InOutputDataReceiver = nullptr); + virtual void StartFlow(IFlowDataPinValueSupplierInterface* DataPinValueSupplier = nullptr, IFlowGraphOutputDataReceiverInterface* InOutputDataReceiver = nullptr); /* Write a single output data pin value into the live store for this running instance. * Called by SetGraphOutput and Finish nodes for each connected output pin. */ @@ -426,11 +416,8 @@ virtual void StartFlow(IFlowDataPinValueSupplierInterface* DataPinValueSupplier /* Flush all of the OutputDataPinValues to the receiver (if set) */ void FlushOutputDataPinValuesToReceiver(); - - virtual void FinishFlow(const EFlowFinishPolicy InFinishPolicy); -public: - virtual void FinishFlow(const EFlowFinishPolicy InFinishPolicy, const bool bRemoveInstance = true); + virtual void FinishFlow(const EFlowFinishPolicy InFinishPolicy); bool HasStartedFlow() const; @@ -440,13 +427,9 @@ virtual void StartFlow(IFlowDataPinValueSupplierInterface* DataPinValueSupplier void InitializeOutputDataReceiverAndValues(IFlowGraphOutputDataReceiverInterface* InOutputDataReceiver); -#if !UE_BUILD_SHIPPING -public: - FFlowSignalEvent OnPinTriggered; -#endif - public: UFlowSubsystem* GetFlowSubsystem() const; + FName GetDisplayName() const; UFlowNode_SubGraph* GetNodeOwningThisAssetInstance() const; UFlowAsset* GetParentInstance() const; @@ -466,8 +449,28 @@ virtual void StartFlow(IFlowDataPinValueSupplierInterface* DataPinValueSupplier UFUNCTION(BlueprintPure, Category = "Flow") const TArray& GetRecordedNodes() const { return RecordedNodes; } +////////////////////////////////////////////////////////////////////////// +// Preload policy + +protected: + /* Policy controlling when nodes implementing IFlowPreloadableInterface preload and flush their content. + * Initialized from UFlowSettings defaults. Override InitializePreloadPolicy() in a subclass to set a unique policy. */ + UPROPERTY(VisibleAnywhere, AdvancedDisplay, Category = Preload) + TInstancedStruct PreloadPolicy; + + /* Override these functions to set up unique policy(ies) for a UFlowAsset subclass. */ + virtual void InitializePreloadPolicy(); + +public: + const FFlowPreloadPolicy& GetPreloadPolicy() const; + ////////////////////////////////////////////////////////////////////////// // Trigger Input + +#if !UE_BUILD_SHIPPING +public: + FFlowSignalEvent OnPinTriggered; +#endif protected: /* Stack of active deferred transition scopes (innermost = top). @@ -494,7 +497,7 @@ virtual void StartFlow(IFlowDataPinValueSupplierInterface* DataPinValueSupplier protected: void EnqueueDeferredTrigger(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin); TSharedPtr PushDeferredTransitionScope(); - void PopDeferredTransitionScope(const TSharedPtr& Scope); + void PopDeferredTransitionScope(const TSharedPtr& Scope) { TryFlushAndRemoveDeferredTransitionScope(Scope); } bool TryFlushAndRemoveDeferredTransitionScope(const TSharedPtr& Scope); diff --git a/Source/FlowEditor/Private/Graph/FlowGraph.cpp b/Source/FlowEditor/Private/Graph/FlowGraph.cpp index c458129f..89884346 100644 --- a/Source/FlowEditor/Private/Graph/FlowGraph.cpp +++ b/Source/FlowEditor/Private/Graph/FlowGraph.cpp @@ -180,6 +180,12 @@ void UFlowGraph::OnLoaded() UpdateVersion(); + UFlowAsset* FlowAsset = GetFlowAsset(); + if (IsValid(FlowAsset)) + { + FlowAsset->SetupForEditing(); + } + // Setup all the Nodes in the graph for editing for (UEdGraphNode* Node : Nodes) { From 50c1d014ed79b62afac90c9bafc2634da7974f94 Mon Sep 17 00:00:00 2001 From: LindyHopperGT <91915878+LindyHopperGT@users.noreply.github.com> Date: Wed, 3 Jun 2026 09:33:31 -0700 Subject: [PATCH 5/7] Tweaks from P4 --- Source/Flow/Public/FlowAsset.h | 4 +- Source/FlowAsset.cpp.ourLatest | 1620 +++++++++++++++++ Source/FlowAsset.h.ourLatest | 567 ++++++ .../Private/Asset/FlowAssetToolbar.cpp | 4 +- 4 files changed, 2191 insertions(+), 4 deletions(-) create mode 100644 Source/FlowAsset.cpp.ourLatest create mode 100644 Source/FlowAsset.h.ourLatest diff --git a/Source/Flow/Public/FlowAsset.h b/Source/Flow/Public/FlowAsset.h index 4649e7b4..e1f0984b 100644 --- a/Source/Flow/Public/FlowAsset.h +++ b/Source/Flow/Public/FlowAsset.h @@ -29,9 +29,9 @@ struct FFlowPreloadPolicy; struct FFlowPinConnectionPolicy; class UEdGraph; +class UEdGraphNode; class UFlowAsset; class UFlowAssetParams; -class UEdGraphNode; #if !UE_BUILD_SHIPPING DECLARE_DELEGATE(FFlowGraphEvent); @@ -131,7 +131,7 @@ class FLOW_API UFlowAsset : public UObject TArray> AllowedInSubgraphNodeClasses; TArray> DeniedInSubgraphNodeClasses; - + bool bStartNodePlacedAsGhostNode; private: diff --git a/Source/FlowAsset.cpp.ourLatest b/Source/FlowAsset.cpp.ourLatest new file mode 100644 index 00000000..a7ed41c5 --- /dev/null +++ b/Source/FlowAsset.cpp.ourLatest @@ -0,0 +1,1620 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "FlowAsset.h" + +#include "FlowLogChannels.h" +#include "FlowSettings.h" +#include "FlowSubsystem.h" +#include "AddOns/FlowNodeAddOn.h" +#include "Asset/FlowAssetParams.h" +#include "Asset/FlowAssetParamsUtils.h" +#include "Interfaces/FlowExecutionGate.h" +#include "Interfaces/FlowGraphOutputDataReceiverInterface.h" +#include "Types/FlowNamedDataPinProperty.h" +#include "Nodes/FlowNodeBase.h" +#include "Nodes/Graph/FlowNode_CustomInput.h" +#include "Nodes/Graph/FlowNode_CustomOutput.h" +#include "Nodes/Graph/FlowNode_Start.h" +#include "Nodes/Graph/FlowNode_SubGraph.h" +#include "Policies/FlowPinConnectionPolicy.h" +#include "Policies/FlowPreloadPolicy.h" +#include "Types/FlowAutoDataPinsWorkingData.h" +#include "Types/FlowDataPinValue.h" +#include "Types/FlowStructUtils.h" + +#include "Engine/World.h" +#include "Serialization/MemoryReader.h" +#include "Serialization/MemoryWriter.h" +#include "Algo/AnyOf.h" + +#if WITH_EDITOR +#include "Nodes/Graph/FlowNode_SetGraphOutput.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetToolsModule.h" +#include "ContentBrowserModule.h" +#include "IContentBrowserSingleton.h" +#include "Editor.h" +#include "Editor/EditorEngine.h" +#include "Modules/ModuleManager.h" +#include "SourceControlHelpers.h" +#include "UObject/ObjectSaveContext.h" +#include "UObject/Package.h" + +FString UFlowAsset::ValidationError_NodeClassNotAllowed = TEXT("Node class {0} is not allowed in this asset."); +FString UFlowAsset::ValidationError_AddOnNodeClassNotAllowed = TEXT("AddOn Node class {0} is not allowed in this asset."); +FString UFlowAsset::ValidationError_NullNodeInstance = TEXT("Node with GUID {0} is NULL"); +FString UFlowAsset::ValidationError_NullAddOnNodeInstance = TEXT("Node with GUID {0} has NULL AddOn(s)"); +#endif + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowAsset) + +UFlowAsset::UFlowAsset(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , bWorldBound(true) +#if WITH_EDITORONLY_DATA + , FlowGraph(nullptr) +#endif + , AllowedNodeClasses({UFlowNodeBase::StaticClass()}) + , AllowedInSubgraphNodeClasses({UFlowNode_SubGraph::StaticClass()}) + , bStartNodePlacedAsGhostNode(false) + , PinConnectionPolicy() + , TemplateAsset(nullptr) + , FinishPolicy(EFlowFinishPolicy::Keep) + , PreloadPolicy() +{ + if (!AssetGuid.IsValid()) + { + AssetGuid = FGuid::NewGuid(); + } + + ExpectedOwnerClass = GetDefault()->GetDefaultExpectedOwnerClass(); +} + +#if WITH_EDITOR +void UFlowAsset::AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector) +{ + UFlowAsset* This = CastChecked(InThis); + Collector.AddReferencedObject(This->FlowGraph, This); + + Super::AddReferencedObjects(InThis, Collector); +} + +void UFlowAsset::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + const FName ChangedPropertyName = PropertyChangedEvent.GetPropertyName(); + const FName ChangedMemberPropertyName = PropertyChangedEvent.GetMemberPropertyName(); + if (PropertyChangedEvent.Property && (ChangedPropertyName == GET_MEMBER_NAME_CHECKED(UFlowAsset, CustomInputs) + || ChangedPropertyName == GET_MEMBER_NAME_CHECKED(UFlowAsset, CustomOutputs) + || ChangedMemberPropertyName == GET_MEMBER_NAME_CHECKED(UFlowAsset, OutputDataPinDeclarations))) + { + OnSubGraphReconstructionRequested.ExecuteIfBound(); + } + + if (PropertyChangedEvent.Property && ChangedMemberPropertyName == GET_MEMBER_NAME_CHECKED(UFlowAsset, OutputDataPinDeclarations)) + { + for (const TPair& NodePair : GetNodes()) + { + UFlowNode_SetGraphOutput* SetOutputNode = Cast(NodePair.Value); + if (IsValid(SetOutputNode) && SetOutputNode->TryUpdateAutoDataPins()) + { + SetOutputNode->OnReconstructionRequested.ExecuteIfBound(); + } + } + } +} + +void UFlowAsset::PostDuplicate(bool bDuplicateForPIE) +{ + Super::PostDuplicate(bDuplicateForPIE); + + if (!bDuplicateForPIE) + { + AssetGuid = FGuid::NewGuid(); + Nodes.Empty(); + } +} + +void UFlowAsset::PostLoad() +{ + Super::PostLoad(); + + const UPackage* Package = GetPackage(); + if (IsValid(Package) && !FPackageName::IsTempPackage(Package->GetPathName())) + { + // If we removed or moved a flow node blueprint (and there is no redirector) we might loose the reference to it resulting + // in null pointers in the Nodes FGUID->UFlowNode* Map. So here we iterate over all the Nodes and remove all pairs that + // are nulled out. + + TSet NodesToRemoveGUID; + + for (const TPair& Node : GetNodes()) + { + if (!IsValid(Node.Value)) + { + NodesToRemoveGUID.Emplace(Node.Key); + } + } + + for (const FGuid& Guid : NodesToRemoveGUID) + { + UnregisterNode(Guid); + } + + ReconcileBaseAssetParams(FFlowAssetParamsUtils::GetLastSavedTimestampForObject(this)); + } +} + +void UFlowAsset::PreSaveRoot(FObjectPreSaveRootContext ObjectSaveContext) +{ + ReconcileBaseAssetParams(FDateTime::Now()); +} + +void UFlowAsset::ReconcileBaseAssetParams(const FDateTime& AssetLastSavedTimestamp) +{ + if (BaseAssetParams.AssetPtr.IsNull()) + { + return; + } + + UFlowAssetParams* BaseAssetParamsPtr = BaseAssetParams.AssetPtr.LoadSynchronous(); + if (!IsValid(BaseAssetParamsPtr)) + { + UE_LOG(LogFlow, Error, TEXT("Failed to load BaseAssetParams: %s"), *BaseAssetParams.AssetPtr.ToString()); + return; + } + + IFlowNamedPropertiesSupplierInterface* NamedPropertiesSupplier = Cast(GetDefaultEntryNode()); + if (!NamedPropertiesSupplier) + { + UE_LOG(LogFlow, Error, TEXT("No NamedPropertiesSupplier (e.g., Start node) found in FlowAsset: %s"), *GetPathName()); + return; + } + + TArray& MutableStartNodeProperties = NamedPropertiesSupplier->GetMutableNamedProperties(); + const EFlowReconcilePropertiesResult ReconcileResult = + BaseAssetParamsPtr->ReconcilePropertiesWithStartNode(AssetLastSavedTimestamp, this, MutableStartNodeProperties); + + if (EFlowReconcilePropertiesResult_Classifiers::IsErrorResult(ReconcileResult)) + { + UE_LOG(LogFlow, Error, TEXT("Failed to reconcile BaseAssetParams for %s: %s"), + *BaseAssetParamsPtr->GetPathName(), *UEnum::GetDisplayValueAsText(ReconcileResult).ToString()); + } +} + +UFlowAssetParams* UFlowAsset::GenerateParamsFromStartNode() +{ + if (BaseAssetParams.AssetPtr.IsValid()) + { + UE_LOG(LogFlow, Warning, TEXT("BaseAssetParams already exists for %s: %s"), *GetPathName(), *BaseAssetParams.AssetPtr.ToString()); + return BaseAssetParams.AssetPtr.LoadSynchronous(); + } + + // Get the Start node + IFlowNamedPropertiesSupplierInterface* NamedPropertiesSupplier = Cast(GetDefaultEntryNode()); + if (!NamedPropertiesSupplier) + { + UE_LOG(LogFlow, Error, TEXT("No valid Start node found for generating params in %s"), *GetPathName()); + return nullptr; + } + + // Determine the params asset name + const FString ParamsAssetName = GenerateParamsAssetName(); + if (ParamsAssetName.IsEmpty()) + { + UE_LOG(LogFlow, Error, TEXT("Generated empty params asset name for %s"), *GetPathName()); + return nullptr; + } + + // Create the params asset + FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); + const FString PackagePath = FPackageName::GetLongPackagePath(GetPackage()->GetPathName()); + FString UniquePackageName, UniqueAssetName; + AssetToolsModule.Get().CreateUniqueAssetName(PackagePath + TEXT("/") + ParamsAssetName, TEXT(""), UniquePackageName, UniqueAssetName); + + UFlowAssetParams* NewParams = Cast( + AssetToolsModule.Get().CreateAsset(UniqueAssetName, PackagePath, UFlowAssetParams::StaticClass(), nullptr)); + if (!IsValid(NewParams)) + { + UE_LOG(LogFlow, Error, TEXT("Failed to create Flow Asset Params: %s"), *UniqueAssetName); + return nullptr; + } + + // Reconfigure with the new properties + NewParams->ConfigureFlowAssetParams(this, nullptr, NamedPropertiesSupplier->GetMutableNamedProperties()); + + // Source control integration + 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()); + } + } + + // Assign to BaseAssetParams and sync Content Browser + BaseAssetParams.AssetPtr = NewParams; + + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + AssetRegistryModule.Get().AssetCreated(NewParams); + + FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked("ContentBrowser"); + TArray AssetsToSync = {NewParams}; + ContentBrowserModule.Get().SyncBrowserToAssets(AssetsToSync, true); + + return NewParams; +} + +FString UFlowAsset::GenerateParamsAssetName() const +{ + const FString FlowAssetName = GetName(); + + const int32 UnderscoreIndex = FlowAssetName.Find(TEXT("_"), ESearchCase::CaseSensitive); + + if (UnderscoreIndex != INDEX_NONE) + { + const FString Prefix = FlowAssetName.Left(UnderscoreIndex); + const FString Suffix = FlowAssetName.Mid(UnderscoreIndex + 1); + return FString::Printf(TEXT("%sParams_%s"), *Prefix, *Suffix); + } + else + { + return FlowAssetName + TEXT("Params"); + } +} + +EDataValidationResult UFlowAsset::ValidateAsset(FFlowMessageLog& MessageLog) +{ + // validate nodes + for (const TPair& Node : ObjectPtrDecay(Nodes)) + { + if (IsValid(Node.Value)) + { + FText FailureReason; + if (!IsNodeOrAddOnClassAllowed(Node.Value->GetClass(), &FailureReason)) + { + const FString ErrorMsg = + FailureReason.IsEmpty() + ? FString::Format(*ValidationError_NodeClassNotAllowed, {*Node.Value->GetClass()->GetName()}) + : FailureReason.ToString(); + + MessageLog.Error(*ErrorMsg, Node.Value); + } + + Node.Value->ValidationLog.Messages.Empty(); + Node.Value->ValidateNode(); + MessageLog.Messages.Append(Node.Value->ValidationLog.Messages); + + // Validate AddOns + for (UFlowNodeAddOn* AddOn : Node.Value->GetFlowNodeAddOnChildren()) + { + if (IsValid(AddOn)) + { + ValidateAddOnTree(*AddOn, MessageLog); + } + else + { + const FString ErrorMsg = FString::Format(*ValidationError_NullAddOnNodeInstance, {*Node.Key.ToString()}); + MessageLog.Error(*ErrorMsg, this); + } + } + } + else + { + const FString ErrorMsg = FString::Format(*ValidationError_NullNodeInstance, {*Node.Key.ToString()}); + MessageLog.Error(*ErrorMsg, this); + } + } + + // if at least one error has been has been logged : mark the asset as invalid + for (const TSharedRef& Msg : MessageLog.Messages) + { + if (Msg->GetSeverity() == EMessageSeverity::Error) + { + return EDataValidationResult::Invalid; + } + } + + // otherwise, the asset is considered valid (even with warnings or notes) + return EDataValidationResult::Valid; +} + +bool UFlowAsset::IsNodeOrAddOnClassAllowed(const UClass* FlowNodeOrAddOnClass, FText* OutOptionalFailureReason) const +{ + if (!IsValid(FlowNodeOrAddOnClass)) + { + return false; + } + + if (!CanFlowNodeClassBeUsedByFlowAsset(*FlowNodeOrAddOnClass)) + { + return false; + } + + if (!CanFlowAssetUseFlowNodeClass(*FlowNodeOrAddOnClass)) + { + return false; + } + + // Confirm plugin reference restrictions are being respected + if (!CanFlowAssetReferenceFlowNode(*FlowNodeOrAddOnClass, OutOptionalFailureReason)) + { + return false; + } + + return true; +} + +bool UFlowAsset::CanFlowNodeClassBeUsedByFlowAsset(const UClass& FlowNodeClass) const +{ + UFlowNode* NodeDefaults = Cast(FlowNodeClass.GetDefaultObject()); + if (!NodeDefaults) + { + check(FlowNodeClass.IsChildOf()); + + // AddOns don't have the AllowedAssetClasses/DeniedAssetClasses + // (yet? maybe we move it up to the base?) + return true; + } + + // UFlowNode class limits which UFlowAsset class can use it + const TArray>& DeniedAssetClasses = NodeDefaults->DeniedAssetClasses; + for (const UClass* DeniedAssetClass : DeniedAssetClasses) + { + if (DeniedAssetClass && GetClass()->IsChildOf(DeniedAssetClass)) + { + return false; + } + } + + const TArray>& AllowedAssetClasses = NodeDefaults->AllowedAssetClasses; + if (AllowedAssetClasses.Num() > 0) + { + bool bAllowedInAsset = false; + for (const UClass* AllowedAssetClass : AllowedAssetClasses) + { + if (AllowedAssetClass && GetClass()->IsChildOf(AllowedAssetClass)) + { + bAllowedInAsset = true; + break; + } + } + if (!bAllowedInAsset) + { + return false; + } + } + + return true; +} + +bool UFlowAsset::CanFlowAssetUseFlowNodeClass(const UClass& FlowNodeClass) const +{ + // UFlowAsset class can limit which UFlowNodeBase classes can be used + if (IsFlowNodeClassInDeniedClasses(FlowNodeClass)) + { + return false; + } + + if (!IsFlowNodeClassInAllowedClasses(FlowNodeClass)) + { + return false; + } + + return true; +} + +bool UFlowAsset::IsFlowNodeClassInDeniedClasses(const UClass& FlowNodeClass) const +{ + for (const TSubclassOf& DeniedNodeClass : DeniedNodeClasses) + { + if (DeniedNodeClass && FlowNodeClass.IsChildOf(DeniedNodeClass)) + { + // Subclasses of a DeniedNodeClass can opt back in to being allowed + if (!IsFlowNodeClassInAllowedClasses(FlowNodeClass, DeniedNodeClass)) + { + return true; + } + } + } + + return false; +} + +void UFlowAsset::ValidateAddOnTree(UFlowNodeAddOn& AddOn, FFlowMessageLog& MessageLog) +{ + // Filter unauthorized addon nodes + FText FailureReason; + if (!IsNodeOrAddOnClassAllowed(AddOn.GetClass(), &FailureReason)) + { + const FString ErrorMsg = + FailureReason.IsEmpty() + ? FString::Format(*ValidationError_AddOnNodeClassNotAllowed, {*AddOn.GetClass()->GetName()}) + : FailureReason.ToString(); + + MessageLog.Error(*ErrorMsg, AddOn.GetFlowNodeSelfOrOwner()); + } + + // Validate AddOn + AddOn.ValidationLog.Messages.Empty(); + AddOn.ValidateNode(); + MessageLog.Messages.Append(AddOn.ValidationLog.Messages); + + // Validate Children + for (UFlowNodeAddOn* Child : AddOn.GetFlowNodeAddOnChildren()) + { + if (IsValid(Child)) + { + ValidateAddOnTree(*Child, MessageLog); + } + } +} + +bool UFlowAsset::IsFlowNodeClassInAllowedClasses(const UClass& FlowNodeClass, + const TSubclassOf& RequiredAncestor) const +{ + if (AllowedNodeClasses.Num() > 0) + { + bool bAllowedInAsset = false; + for (const TSubclassOf& AllowedNodeClass : AllowedNodeClasses) + { + // If a RequiredAncestor is provided, the AllowedNodeClass must be a subclass of the RequiredAncestor + if (AllowedNodeClass && FlowNodeClass.IsChildOf(AllowedNodeClass) && (!RequiredAncestor || AllowedNodeClass->IsChildOf(RequiredAncestor))) + { + bAllowedInAsset = true; + + break; + } + } + + if (!bAllowedInAsset) + { + return false; + } + } + + return true; +} + +bool UFlowAsset::CanFlowAssetReferenceFlowNode(const UClass& FlowNodeClass, FText* OutOptionalFailureReason) const +{ + if (!GEditor || !IsValid(&FlowNodeClass)) + { + return false; + } + + // Confirm plugin reference restrictions are being respected + FAssetReferenceFilterContext AssetReferenceFilterContext; + AssetReferenceFilterContext.AddReferencingAsset(FAssetData(this)); + const TSharedPtr FlowAssetReferenceFilter = GEditor->MakeAssetReferenceFilter(AssetReferenceFilterContext); + if (FlowAssetReferenceFilter.IsValid()) + { + const FAssetData FlowNodeAssetData(&FlowNodeClass); + if (!FlowAssetReferenceFilter->PassesFilter(FlowNodeAssetData, OutOptionalFailureReason)) + { + return false; + } + } + + return true; +} + +UFlowNode* UFlowAsset::CreateNode(const UClass* NodeClass, UEdGraphNode* GraphNode) +{ + UFlowNode* NewNode = NewObject(this, NodeClass, NAME_None, RF_Transactional); + NewNode->SetGraphNode(GraphNode); + + RegisterNode(GraphNode->NodeGuid, NewNode); + return NewNode; +} + +void UFlowAsset::RegisterNode(const FGuid& NewGuid, UFlowNode* NewNode) +{ + NewNode->SetGuid(NewGuid); + Nodes.Emplace(NewGuid, NewNode); + + HarvestNodeConnections(); + + if (NewNode->TryUpdateAutoDataPins()) + { + (void)NewNode->OnReconstructionRequested.ExecuteIfBound(); + } +} + +void UFlowAsset::UnregisterNode(const FGuid& NodeGuid) +{ + Nodes.Remove(NodeGuid); + Nodes.Compact(); + + HarvestNodeConnections(); + + MarkPackageDirty(); +} + +void UFlowAsset::HarvestNodeConnections(UFlowNode* TargetNode) +{ + TArray TargetNodes; + + if (IsValid(TargetNode)) + { + TargetNodes.Reserve(1); + TargetNodes.Add(TargetNode); + } + else + { + TargetNodes.Reserve(Nodes.Num()); + for (const TPair& Pair : ObjectPtrDecay(Nodes)) + { + TargetNodes.Add(Pair.Value); + } + } + + // Remove any invalid nodes + for (auto NodeIt = TargetNodes.CreateIterator(); NodeIt; ++NodeIt) + { + if (*NodeIt == nullptr) + { + NodeIt.RemoveCurrent(); + Modify(); + } + } + + for (UFlowNode* FlowNode : TargetNodes) + { + bool bNodeDirty = false; + + TMap FoundConnections; + const TArray& GraphNodePins = FlowNode->GetGraphNode()->Pins; + + for (const UEdGraphPin* ThisPin : GraphNodePins) + { + const bool bIsExecPin = FFlowPin::IsExecPinCategory(ThisPin->PinType.PinCategory); + const bool bIsDataPin = !bIsExecPin; + const bool bIsOutputPin = (ThisPin->Direction == EGPD_Output); + const bool bIsInputPin = (ThisPin->Direction == EGPD_Input); + const bool bHasAtLeastOneConnection = ThisPin->LinkedTo.Num() > 0; + + if (bIsExecPin && bIsOutputPin && bHasAtLeastOneConnection) + { + // For Exec Pins, harvest the 0th connection (we should have only 1 connection, because of schema rules) + if (const UEdGraphPin* LinkedPin = ThisPin->LinkedTo[0]) + { + const UEdGraphNode* LinkedNode = LinkedPin->GetOwningNode(); + FoundConnections.Add(ThisPin->PinName, FConnectedPin(LinkedNode->NodeGuid, LinkedPin->PinName)); + } + } + else if (bIsDataPin && bIsInputPin && bHasAtLeastOneConnection) + { + // For Data Pins, harvest the 0th connection (we should have only 1 connection, because of schema rules) + if (const UEdGraphPin* LinkedPin = ThisPin->LinkedTo[0]) + { + const UEdGraphNode* LinkedNode = LinkedPin->GetOwningNode(); + FoundConnections.Add(ThisPin->PinName, FConnectedPin(LinkedNode->NodeGuid, LinkedPin->PinName)); + } + } + } + + // This check exists to ensure that we don't mark graph dirty, if none of connections changed + { + const TMap& OldConnections = FlowNode->Connections; + if (FoundConnections.Num() != OldConnections.Num()) + { + bNodeDirty = true; + } + else + { + for (const TPair& FoundConnection : FoundConnections) + { + if (const FConnectedPin* OldConnection = OldConnections.Find(FoundConnection.Key)) + { + if (FoundConnection.Value != *OldConnection) + { + bNodeDirty = true; + break; + } + } + else + { + bNodeDirty = true; + break; + } + } + } + } + + if (bNodeDirty) + { + FlowNode->SetFlags(RF_Transactional); + FlowNode->Modify(); + + FlowNode->SetConnections(FoundConnections); + FlowNode->PostEditChange(); + } + } +} + +bool UFlowAsset::TryGetDefaultForInputPinName(const FStructProperty& StructProperty, const void* Container, FString& OutString) +{ + // We also look in the USTRUCT for DefaultForInputFlowPin + const FString* DefaultForInputFlowPinName = StructProperty.Struct->FindMetaData(FFlowPin::MetadataKey_DefaultForInputFlowPin); + + if (DefaultForInputFlowPinName) + { + OutString = *DefaultForInputFlowPinName; + + return true; + } + + // For blueprint use, we allow the Value structs to set input pins via editor-only data + + const FFlowDataPinValue* DataPinValue = FlowStructUtils::CastStructValue(StructProperty, Container); + if (DataPinValue && DataPinValue->IsInputPin()) + { + OutString.Empty(); + + return true; + } + + return false; +} + +#endif + +UFlowNode* UFlowAsset::GetDefaultEntryNode() const +{ + UFlowNode* FirstStartNode = nullptr; + + for (const TPair& Node : ObjectPtrDecay(Nodes)) + { + if (UFlowNode_Start* StartNode = Cast(Node.Value)) + { + if (StartNode->GatherConnectedNodes().Num() > 0) + { + return StartNode; + } + else if (FirstStartNode == nullptr) + { + FirstStartNode = StartNode; + } + } + } + + // If none of the found start nodes have connections, fallback to the first start node we found + return FirstStartNode; +} + +#if WITH_EDITOR +void UFlowAsset::AddCustomInput(const FName& EventName) +{ + if (!CustomInputs.Contains(EventName)) + { + CustomInputs.Add(EventName); + } +} + +void UFlowAsset::RemoveCustomInput(const FName& EventName) +{ + if (CustomInputs.Contains(EventName)) + { + CustomInputs.Remove(EventName); + } +} + +void UFlowAsset::AddCustomOutput(const FName& EventName) +{ + if (!CustomOutputs.Contains(EventName)) + { + CustomOutputs.Add(EventName); + } +} + +void UFlowAsset::RemoveCustomOutput(const FName& EventName) +{ + if (CustomOutputs.Contains(EventName)) + { + CustomOutputs.Remove(EventName); + } +} +#endif // WITH_EDITOR + +UFlowNode_CustomInput* UFlowAsset::TryFindCustomInputNodeByEventName(const FName& EventName) const +{ + for (const TPair& Node : ObjectPtrDecay(Nodes)) + { + if (UFlowNode_CustomInput* CustomInput = Cast(Node.Value)) + { + if (CustomInput->GetEventName() == EventName) + { + return CustomInput; + } + } + } + + return nullptr; +} + +UFlowNode_CustomOutput* UFlowAsset::TryFindCustomOutputNodeByEventName(const FName& EventName) const +{ + for (const TPair& Node : ObjectPtrDecay(Nodes)) + { + if (UFlowNode_CustomOutput* CustomOutput = Cast(Node.Value)) + { + if (CustomOutput->GetEventName() == EventName) + { + return CustomOutput; + } + } + } + + return nullptr; +} + +TArray UFlowAsset::GatherCustomInputNodeEventNames() const +{ + // Runtime-safe gathering of the CustomInputs (which is editor-only data) + // from the actual flow nodes + TArray Results; + + for (const TPair& Node : ObjectPtrDecay(Nodes)) + { + if (UFlowNode_CustomInput* CustomInput = Cast(Node.Value)) + { + Results.Add(CustomInput->GetEventName()); + } + } + + return Results; +} + +TArray UFlowAsset::GatherCustomOutputNodeEventNames() const +{ + // Runtime-safe gathering of the CustomOutputs (which is editor-only data) + // from the actual flow nodes + TArray Results; + + for (const TPair& Node : ObjectPtrDecay(Nodes)) + { + if (UFlowNode_CustomOutput* CustomOutput = Cast(Node.Value)) + { + Results.Add(CustomOutput->GetEventName()); + } + } + + return Results; +} + +TArray UFlowAsset::GetNodesInExecutionOrder(UFlowNode* FirstIteratedNode, const TSubclassOf FlowNodeClass) const +{ + TArray FoundNodes; + GetNodesInExecutionOrder(FirstIteratedNode, FoundNodes); + + // filter out nodes by class + for (int32 i = FoundNodes.Num() - 1; i >= 0; i--) + { + if (!FoundNodes[i]->GetClass()->IsChildOf(FlowNodeClass)) + { + FoundNodes.RemoveAt(i); + } + } + FoundNodes.Shrink(); + + return FoundNodes; +} + +TArray UFlowAsset::GatherNodesConnectedToAllInputs() const +{ + TSet> IteratedNodes; + TArray ConnectedNodes; + + // Nodes connected to the Start node + UFlowNode* DefaultEntryNode = GetDefaultEntryNode(); + GetNodesInExecutionOrder_Recursive(DefaultEntryNode, IteratedNodes, ConnectedNodes); + + // Nodes connected to Custom Input node(s) + for (const TPair& Node : ObjectPtrDecay(Nodes)) + { + if (UFlowNode_CustomInput* CustomInput = Cast(Node.Value)) + { + GetNodesInExecutionOrder_Recursive(CustomInput, IteratedNodes, ConnectedNodes); + } + } + + return ConnectedNodes; +} + +TArray UFlowAsset::GatherPinsConnectedToPin(const FConnectedPin& Pin) const +{ + TArray ConnectedPins; + + // Connections are only stored on one of the Nodes they connect depending on pin type. + // As such, we need to iterate all Nodes to find all possible Connections for the Pin. + for (const auto& GuidNodePair : Nodes) + { + if (IsValid(GuidNodePair.Value)) + { + ConnectedPins.Append(GuidNodePair.Value->GetKnownConnectionsToPin(Pin)); + } + } + + return ConnectedPins; +} + +TArray UFlowAsset::GetAllNodes() const +{ + TArray> AllNodes; + AllNodes.Reserve(Nodes.Num()); + Nodes.GenerateValueArray(AllNodes); + + return ObjectPtrDecay(AllNodes); +} + +void UFlowAsset::AddInstance(UFlowAsset* Instance) +{ + ActiveInstances.Add(Instance); +} + +int32 UFlowAsset::RemoveInstance(UFlowAsset* Instance) +{ +#if WITH_EDITOR + if (InspectedInstance.IsValid() && InspectedInstance.Get() == Instance) + { + SetInspectedInstance(nullptr); + } +#endif + + ActiveInstances.Remove(Instance); + return ActiveInstances.Num(); +} + +void UFlowAsset::ClearInstances() +{ +#if WITH_EDITOR + if (InspectedInstance.IsValid()) + { + SetInspectedInstance(nullptr); + } +#endif + + for (int32 i = ActiveInstances.Num() - 1; i >= 0; i--) + { + if (ActiveInstances.IsValidIndex(i) && ActiveInstances[i]) + { + ActiveInstances[i]->FinishFlowAndDeinitializeInstance(EFlowFinishPolicy::Keep); + } + } + + ActiveInstances.Empty(); +} + +#if WITH_EDITOR +void UFlowAsset::SetInspectedInstance(TWeakObjectPtr NewInspectedInstance) +{ + if (NewInspectedInstance.IsValid()) + { + if (InspectedInstance == NewInspectedInstance) + { + // Nothing changed + return; + } + + bool bIsNewInstancePresent = Algo::AnyOf(ActiveInstances, [NewInspectedInstance](const UFlowAsset* ActiveInstance) + { + return ActiveInstance && ActiveInstance == NewInspectedInstance; + }); + + if (!ensureMsgf(bIsNewInstancePresent, TEXT("Trying to set %s as InspectedInstance, but it is not one of the ActiveInstances"), *NewInspectedInstance->GetName())) + { + NewInspectedInstance = nullptr; + } + } + + InspectedInstance = NewInspectedInstance; + BroadcastDebuggerRefresh(); +} + +void UFlowAsset::BroadcastDebuggerRefresh() const +{ + RefreshDebuggerEvent.Broadcast(); +} + +void UFlowAsset::BroadcastRuntimeMessageAdded(const TSharedRef& Message) const +{ + RuntimeMessageEvent.Broadcast(this, Message); +} + +void UFlowAsset::SetupForEditing() +{ + InitializePinConnectionPolicy(); + + // Initialize any customizable Policies before we instantiate nodes + InitializePreloadPolicy(); +} +#endif // WITH_EDITOR + +void UFlowAsset::InitializeInstance(const TWeakObjectPtr InOwner, UFlowAsset& InTemplateAsset) +{ + check(!IsInstanceInitialized()); + + Owner = InOwner; + TemplateAsset = &InTemplateAsset; + + // Initialize any customizable Policies before we instantiate nodes + InitializePreloadPolicy(); + + for (TPair>& Node : Nodes) + { + UFlowNode* NewNodeInstance = NewObject(this, Node.Value->GetClass(), NAME_None, RF_Transient, Node.Value, false, nullptr); + Node.Value = NewNodeInstance; + + if (UFlowNode_CustomInput* CustomInput = Cast(NewNodeInstance)) + { + if (!CustomInput->EventName.IsNone()) + { + CustomInputNodes.Emplace(CustomInput); + } + } + + NewNodeInstance->InitializeInstance(); + } +} + +void UFlowAsset::DeinitializeInstance() +{ + // These should have been flushed in FinishFlow() + check(DeferredTransitionScopes.IsEmpty()); + + if (IsInstanceInitialized()) + { + for (const TPair& Node : ObjectPtrDecay(Nodes)) + { + if (IsValid(Node.Value)) + { + Node.Value->DeinitializeInstance(); + } + } + + const int32 ActiveInstancesLeft = TemplateAsset->RemoveInstance(this); + if (ActiveInstancesLeft == 0 && GetFlowSubsystem()) + { + GetFlowSubsystem()->RemoveInstancedTemplate(TemplateAsset); + } + + TemplateAsset = nullptr; + } +} + +void UFlowAsset::FinishFlowAndDeinitializeInstance(const EFlowFinishPolicy InFinishPolicy) +{ + FinishFlow(InFinishPolicy); + DeinitializeInstance(); +} + +void UFlowAsset::PreStartFlow() +{ + ResetNodes(); + +#if WITH_EDITOR + check(IsInstanceInitialized()); + + if (TemplateAsset->ActiveInstances.Num() == 1) + { + // this instance is the only active one, set it directly as Inspected Instance + TemplateAsset->SetInspectedInstance(this); + } + else + { + // request to refresh list to show newly created instance + TemplateAsset->BroadcastDebuggerRefresh(); + } +#endif +} + +void UFlowAsset::StartFlow(IFlowDataPinValueSupplierInterface* DataPinValueSupplier, IFlowGraphOutputDataReceiverInterface* InOutputDataReceiver) +{ + InitializeOutputDataReceiverAndValues(InOutputDataReceiver); + + PreStartFlow(); + + if (UFlowNode* ConnectedEntryNode = GetDefaultEntryNode()) + { + RecordedNodes.Add(ConnectedEntryNode); + + if (IFlowNodeWithExternalDataPinSupplierInterface* ExternalPinSuppliedNode = Cast(ConnectedEntryNode)) + { + ExternalPinSuppliedNode->SetDataPinValueSupplier(DataPinValueSupplier); + } + + ConnectedEntryNode->TriggerFirstOutput(true); + } +} + +void UFlowAsset::InitializeOutputDataReceiverAndValues(IFlowGraphOutputDataReceiverInterface* InOutputDataReceiver) +{ + OutputDataReceiver = Cast(InOutputDataReceiver); + + // Initialize the live output store from the template asset's declarations + OutputDataPinValues.Values.Reset(); + + if (const UFlowAsset* Template = TemplateAsset.Get()) + { + for (const FFlowNamedDataPinProperty& Declaration : Template->OutputDataPinDeclarations) + { + if (Declaration.IsValid()) + { + OutputDataPinValues.Values.Add(Declaration.Name, Declaration.DataPinValue); + } + else + { + UE_LOG(LogFlow, Warning, TEXT("Invalid OutputDataPin %s"), *Declaration.Name.ToString()); + } + } + } +} + +void UFlowAsset::WriteOutputDataPinValue(const FName& PinName, const TInstancedStruct& Value) +{ + if (OutputDataPinValues.Values.Contains(PinName)) + { + OutputDataPinValues.Values[PinName] = Value; + } + else + { + UE_LOG(LogFlow, Warning, TEXT("Could not find pin named %s in WriteOutputDataPinValue"), *PinName.ToString()); + } +} + +void UFlowAsset::FlushOutputDataPinValuesToReceiver() +{ + if (IFlowGraphOutputDataReceiverInterface* Receiver = Cast(OutputDataReceiver.Get())) + { + // Do an immediate push to the receiver + Receiver->ReceiveOutputDataSnapshot(OutputDataPinValues); + } +} + +void UFlowAsset::FinishFlow(const EFlowFinishPolicy InFinishPolicy) +{ + FinishPolicy = InFinishPolicy; + + CancelAndWarnForUnflushedDeferredTriggers(); + + // end execution of this asset and all of its nodes + for (UFlowNode* Node : ActiveNodes) + { + Node->Deactivate(); + } + ActiveNodes.Empty(); +} + +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; + + const FString ToNodeName = ToNode ? ToNode->GetName() : TEXT(""); + const FString FromNodeName = FromNode ? FromNode->GetName() : TEXT(""); + + 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"), + *ToNodeName, + *Trigger.NodeGuid.ToString(), + *Trigger.PinName.ToString(), + *FromNodeName, + *Trigger.FromPin.NodeGuid.ToString(), + *Trigger.FromPin.PinName.ToString() + ); + } + } + + ClearAllDeferredTriggerScopes(); + } +} + +bool UFlowAsset::HasStartedFlow() const +{ + return RecordedNodes.Num() > 0; +} + +AActor* UFlowAsset::TryFindActorOwner() const +{ + UObject* OwnerObject = GetOwner(); + if (!IsValid(OwnerObject)) + { + return nullptr; + } + + // If the owner is already an Actor, return it directly + if (AActor* OwnerAsActor = Cast(OwnerObject)) + { + return OwnerAsActor; + } + + // If the owner is a Component, return its owning Actor + if (const UActorComponent* OwnerAsComponent = Cast(OwnerObject)) + { + return OwnerAsComponent->GetOwner(); + } + + return nullptr; +} + +TWeakObjectPtr UFlowAsset::GetFlowInstance(UFlowNode_SubGraph* SubGraphNode) const +{ + return ActiveSubGraphs.FindRef(SubGraphNode); +} + +void UFlowAsset::TriggerCustomInput_FromSubGraph(UFlowNode_SubGraph* SubGraphNode, const FName& EventName) const +{ + // 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). + + const TWeakObjectPtr FlowInstance = ActiveSubGraphs.FindRef(SubGraphNode); + if (FlowInstance.IsValid()) + { + FlowInstance->TriggerCustomInput(EventName, SubGraphNode); + } +} + +void UFlowAsset::TriggerCustomInput(const FName& EventName, IFlowDataPinValueSupplierInterface* DataPinValueSupplier) +{ + for (UFlowNode_CustomInput* CustomInputNode : CustomInputNodes) + { + if (CustomInputNode->EventName == EventName) + { + RecordedNodes.Add(CustomInputNode); + + // 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). + + if (IFlowNodeWithExternalDataPinSupplierInterface* ExternalPinSuppliedNode = Cast(CustomInputNode)) + { + ExternalPinSuppliedNode->SetDataPinValueSupplier(DataPinValueSupplier); + } + + CustomInputNode->ExecuteInput(EventName); + } + } +} + +void UFlowAsset::TriggerCustomOutput(const FName& EventName) +{ + if (NodeOwningThisAssetInstance.IsValid()) + { + // it's a SubGraph + NodeOwningThisAssetInstance->TriggerOutput(EventName); + } + else + { + // it's a Root Flow, so the intention here might be to call event on the Flow Component + if (UFlowComponent* FlowComponent = Cast(GetOwner())) + { + FlowComponent->DispatchRootFlowCustomEvent(this, EventName); + } + } +} + +void UFlowAsset::TriggerInput(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin) +{ + if (FFlowExecutionGate::IsHalted()) + { + // Halt always takes precedence for debugger correctness + EnqueueDeferredTrigger(NodeGuid, PinName, FromPin); + } + else if (ShouldDeferTriggers()) + { + // Defer only if we have an open the top scope + if (!DeferredTransitionScopes.IsEmpty() && DeferredTransitionScopes.Top()->IsOpen()) + { + EnqueueDeferredTrigger(NodeGuid, PinName, FromPin); + } + else + { + const TSharedPtr CurrentScope = PushDeferredTransitionScope(); + TriggerInputDirect(NodeGuid, PinName, FromPin); + PopDeferredTransitionScope(CurrentScope); + } + } + 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)) + { + ActiveNodes.Add(Node); + RecordedNodes.Add(Node); + } + + Node->TriggerInput(PinName); + } +} + +bool UFlowAsset::ShouldDeferTriggers() 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)) + { + ActiveNodes.Remove(Node); + + // if graph reached Finish and this asset instance was created by SubGraph node + if (Node->CanFinishGraph()) + { + if (IFlowGraphOutputDataReceiverInterface* Receiver = Cast(OutputDataReceiver.Get())) + { + Receiver->ReceiveOutputDataSnapshot(OutputDataPinValues); + } + + if (NodeOwningThisAssetInstance.IsValid()) + { + NodeOwningThisAssetInstance.Get()->TriggerFirstOutput(true); + + return; + } + + // if this instance is a Root Flow, we need to deregister it from the subsystem first. This will + // finalize and deinitialize the root flow. + if (Owner.IsValid()) + { + const TSet& RootFlowInstances = GetFlowSubsystem()->GetRootInstancesByOwner(Owner.Get()); + if (RootFlowInstances.Contains(this)) + { + GetFlowSubsystem()->FinishAndDeinitializeRootFlow(Owner.Get(), TemplateAsset, EFlowFinishPolicy::Keep); + + return; + } + } + + FinishFlow(EFlowFinishPolicy::Keep); + } + } +} + +void UFlowAsset::ResetNodes() +{ + for (UFlowNode* Node : RecordedNodes) + { + Node->ResetRecords(); + } + + RecordedNodes.Empty(); +} + +UFlowSubsystem* UFlowAsset::GetFlowSubsystem() const +{ + return Cast(GetOuter()); +} + +FName UFlowAsset::GetDisplayName() const +{ + return GetFName(); +} + +UFlowNode_SubGraph* UFlowAsset::GetNodeOwningThisAssetInstance() const +{ + return NodeOwningThisAssetInstance.Get(); +} + +UFlowAsset* UFlowAsset::GetParentInstance() const +{ + return NodeOwningThisAssetInstance.IsValid() ? NodeOwningThisAssetInstance.Get()->GetFlowAsset() : nullptr; +} + +FFlowAssetSaveData UFlowAsset::SaveInstance(TArray& SavedFlowInstances) +{ + FFlowAssetSaveData AssetRecord; + AssetRecord.WorldName = IsBoundToWorld() ? GetWorld()->GetName() : FString(); + AssetRecord.InstanceName = GetName(); + + // opportunity to collect data before serializing asset + OnSave(); + + // iterate nodes + TArray NodesInExecutionOrder; + GetNodesInExecutionOrder(GetDefaultEntryNode(), NodesInExecutionOrder); + for (UFlowNode* Node : NodesInExecutionOrder) + { + if (Node && Node->ShouldSave()) + { + // iterate SubGraphs + if (UFlowNode_SubGraph* SubGraphNode = Cast(Node)) + { + const TWeakObjectPtr SubFlowInstance = GetFlowInstance(SubGraphNode); + if (SubFlowInstance.IsValid()) + { + const FFlowAssetSaveData SubAssetRecord = SubFlowInstance->SaveInstance(SavedFlowInstances); + SubGraphNode->SavedAssetInstanceName = SubAssetRecord.InstanceName; + } + } + + FFlowNodeSaveData NodeRecord; + Node->SaveInstance(NodeRecord); + + AssetRecord.NodeRecords.Emplace(NodeRecord); + } + } + + // serialize asset + FMemoryWriter MemoryWriter(AssetRecord.AssetData, true); + FFlowArchive Ar(MemoryWriter); + Serialize(Ar); + + // write archive to SaveGame + SavedFlowInstances.Emplace(AssetRecord); + + return AssetRecord; +} + +void UFlowAsset::LoadInstance(const FFlowAssetSaveData& AssetRecord) +{ + FMemoryReader MemoryReader(AssetRecord.AssetData, true); + FFlowArchive Ar(MemoryReader); + Serialize(Ar); + + PreStartFlow(); + + // iterate graph "from the end", backward to execution order + // prevents issue when the preceding node would instantly fire output to a not-yet-loaded node + for (int32 i = AssetRecord.NodeRecords.Num() - 1; i >= 0; i--) + { + if (UFlowNode* Node = Nodes.FindRef(AssetRecord.NodeRecords[i].NodeGuid)) + { + Node->LoadInstance(AssetRecord.NodeRecords[i]); + } + } + + OnLoad(); +} + +void UFlowAsset::OnActivationStateLoaded(UFlowNode* Node) +{ + if (Node->ActivationState != EFlowNodeState::NeverActivated) + { + RecordedNodes.Emplace(Node); + } + + if (Node->ActivationState == EFlowNodeState::Active) + { + ActiveNodes.Emplace(Node); + } +} + +void UFlowAsset::OnSave_Implementation() +{ +} + +void UFlowAsset::OnLoad_Implementation() +{ +} + +bool UFlowAsset::IsBoundToWorld_Implementation() const +{ + return bWorldBound; +} + +const FFlowPinConnectionPolicy& UFlowAsset::GetPinConnectionPolicy() const +{ + // Runtime instances delegate to their template, which holds the serialized policy + if (!PinConnectionPolicy.IsValid() && IsValid(TemplateAsset)) + { + return TemplateAsset->GetPinConnectionPolicy(); + } + + // Graceful fallback: if PinConnectionPolicy was never initialized (asset predates this feature, + // or was never opened in editor), read directly from project settings at runtime. + if (!PinConnectionPolicy.IsValid()) + { + const FFlowPinConnectionPolicy* SettingsPolicy = GetDefault()->GetPinConnectionPolicy(); + ensureAlways(SettingsPolicy); + if (SettingsPolicy) + { + return *SettingsPolicy; + } + } + + check(PinConnectionPolicy.IsValid()); + return PinConnectionPolicy.Get(); +} + +const FFlowPreloadPolicy& UFlowAsset::GetPreloadPolicy() const +{ + checkf(PreloadPolicy.IsValid(), TEXT("PreloadPolicy must be initialized prior to calling GetPreloadPolicy()")); + return PreloadPolicy.Get(); +} + +#if WITH_EDITOR + +void UFlowAsset::InitializePinConnectionPolicy() +{ + const FInstancedStruct& SourceStruct = GetDefault()->PinConnectionPolicy; + if (ensure(SourceStruct.IsValid())) + { + PinConnectionPolicy.InitializeAsScriptStruct(SourceStruct.GetScriptStruct(), SourceStruct.GetMemory()); + } +} + +#endif + +void UFlowAsset::InitializePreloadPolicy() +{ + if (PreloadPolicy.IsValid()) + { + // use per-class policy + PreloadPolicy.InitializeAsScriptStruct(PreloadPolicy.GetScriptStruct(), PreloadPolicy.GetMemory()); + } + else + { + // fallback to project's default policy + const FInstancedStruct& DefaultPolicy = GetDefault()->PreloadPolicy; + if (ensure(DefaultPolicy.IsValid())) + { + PreloadPolicy.InitializeAsScriptStruct(DefaultPolicy.GetScriptStruct(), DefaultPolicy.GetMemory()); + } + } + + ensureAlwaysMsgf(PreloadPolicy.IsValid(), TEXT("There's no valid Preload Policy set in the project!")); +} + +#if WITH_EDITOR + +void UFlowAsset::LogError(const FString& MessageToLog, const UFlowNodeBase* Node) const +{ + LogRuntimeMessage(EMessageSeverity::Error, MessageToLog, Node); +} + +void UFlowAsset::LogWarning(const FString& MessageToLog, const UFlowNodeBase* Node) const +{ + LogRuntimeMessage(EMessageSeverity::Warning, MessageToLog, Node); +} + +void UFlowAsset::LogNote(const FString& MessageToLog, const UFlowNodeBase* Node) const +{ + LogRuntimeMessage(EMessageSeverity::Info, MessageToLog, Node); +} + +void UFlowAsset::LogRuntimeMessage(EMessageSeverity::Type Severity, const FString& MessageToLog, const UFlowNodeBase* Node) const +{ + // this is runtime log which should only be called on runtime instances of asset + if (TemplateAsset) + { + UE_LOG(LogFlow, Log, TEXT("Attempted to use Runtime Log on asset instance %s"), *MessageToLog); + } + + if (RuntimeLog.Get()) + { + TSharedPtr TokenizedMessage = nullptr; + switch (Severity) + { + case EMessageSeverity::Error: + TokenizedMessage = RuntimeLog.Get()->Error(*MessageToLog, Node); + break; + + case EMessageSeverity::Warning: + TokenizedMessage = RuntimeLog.Get()->Warning(*MessageToLog, Node); + break; + + default: + TokenizedMessage = RuntimeLog.Get()->Note(*MessageToLog, Node); + break; + } + + BroadcastRuntimeMessageAdded(TokenizedMessage.ToSharedRef()); + } +} +#endif \ No newline at end of file diff --git a/Source/FlowAsset.h.ourLatest b/Source/FlowAsset.h.ourLatest new file mode 100644 index 00000000..e1f0984b --- /dev/null +++ b/Source/FlowAsset.h.ourLatest @@ -0,0 +1,567 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "FlowSave.h" +#include "FlowTypes.h" +#include "Asset/FlowAssetParamsTypes.h" +#include "Asset/FlowDeferredTransitionScope.h" +#include "Interfaces/FlowGraphOutputDataReceiverInterface.h" +#include "Nodes/FlowNode.h" +#include "Types/FlowDataPinValue.h" +#include "Types/FlowNamedDataPinProperty.h" +#include "Types/FlowOutputDataPinValues.h" + +#if WITH_EDITOR +#include "FlowMessageLog.h" +#endif + +#include "StructUtils/InstancedStruct.h" +#include "Templates/SharedPointer.h" +#include "UObject/ObjectKey.h" + +#include "FlowAsset.generated.h" + +class UFlowNode_CustomOutput; +class UFlowNode_CustomInput; +class UFlowNode_SubGraph; +class UFlowSubsystem; +struct FFlowPreloadPolicy; +struct FFlowPinConnectionPolicy; + +class UEdGraph; +class UEdGraphNode; +class UFlowAsset; +class UFlowAssetParams; + +#if !UE_BUILD_SHIPPING +DECLARE_DELEGATE(FFlowGraphEvent); +DECLARE_DELEGATE_TwoParams(FFlowSignalEvent, UFlowNode* /*FlowNode*/, const FName& /*PinName*/); +#endif + +/** + * Asset containing Flow nodes organized as non-linear graph. + */ +UCLASS(BlueprintType, hideCategories = Object) +class FLOW_API UFlowAsset : public UObject +{ + GENERATED_UCLASS_BODY() + +public: + friend class UFlowNode; + friend class UFlowNode_CustomOutput; + friend class UFlowNode_SubGraph; + friend class UFlowSubsystem; + + 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. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Flow Asset") + bool bWorldBound; + +////////////////////////////////////////////////////////////////////////// +// Graph (editor-only) + +public: +#if WITH_EDITOR +public: + friend class UFlowGraph; + + // UObject + static void AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector); + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; + virtual void PostDuplicate(bool bDuplicateForPIE) override; + virtual void PostLoad() override; + virtual void PreSaveRoot(FObjectPreSaveRootContext ObjectSaveContext) override; + // -- +#endif + +#if WITH_EDITORONLY_DATA +public: + FSimpleDelegate OnDetailsRefreshRequested; + + static FString ValidationError_NodeClassNotAllowed; + static FString ValidationError_AddOnNodeClassNotAllowed; + static FString ValidationError_NullNodeInstance; + static FString ValidationError_NullAddOnNodeInstance; + +private: + UPROPERTY() + TObjectPtr FlowGraph; +#endif + +#if WITH_EDITOR +public: + void SetupForEditing(); + + UEdGraph* GetGraph() const { return FlowGraph; } + + virtual EDataValidationResult ValidateAsset(FFlowMessageLog& MessageLog); + + /* 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(); } + +protected: + bool CanFlowNodeClassBeUsedByFlowAsset(const UClass& FlowNodeClass) const; + bool CanFlowAssetUseFlowNodeClass(const UClass& FlowNodeClass) const; + bool CanFlowAssetReferenceFlowNode(const UClass& FlowNodeClass, FText* OutOptionalFailureReason = nullptr) const; + + bool IsFlowNodeClassInAllowedClasses(const UClass& FlowNodeClass, const TSubclassOf& RequiredAncestor = nullptr) const; + bool IsFlowNodeClassInDeniedClasses(const UClass& FlowNodeClass) const; + +private: + /* Recursively validates the given addon and its children. */ + void ValidateAddOnTree(UFlowNodeAddOn& AddOn, FFlowMessageLog& MessageLog); +#endif + +////////////////////////////////////////////////////////////////////////// +// Nodes + +protected: + TArray> AllowedNodeClasses; + TArray> DeniedNodeClasses; + + TArray> AllowedInSubgraphNodeClasses; + TArray> DeniedInSubgraphNodeClasses; + + bool bStartNodePlacedAsGhostNode; + +private: + UPROPERTY() + TMap> Nodes; + +public: + const TArray& GetOutputDataPinDeclarations() const { return OutputDataPinDeclarations; } + +protected: + /* Output Data Pins define typed data values that this graph produces when it finishes. + * Sub Graph node using this Flow Asset will generate a context Output Data Pin for every entry on this list. */ + UPROPERTY(EditAnywhere, Category = "Sub Graph") + TArray OutputDataPinDeclarations; + +public: +#if WITH_EDITOR + FFlowGraphEvent OnSubGraphReconstructionRequested; + + UFlowNode* CreateNode(const UClass* NodeClass, UEdGraphNode* GraphNode); + + 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). */ + void HarvestNodeConnections(UFlowNode* TargetNode = nullptr); + + static bool TryGetDefaultForInputPinName(const FStructProperty& StructProperty, const void* Container, FString& OutString); +#endif + +public: + const TMap& GetNodes() const { return ObjectPtrDecay(Nodes); } + TArray GetAllNodes() const; + + UFlowNode* GetNode(const FGuid& Guid) const { return Nodes.FindRef(Guid); } + + template + T* GetNode(const FGuid& Guid) const + { + static_assert(TPointerIsConvertibleFromTo::Value, "'T' template parameter to GetNode must be derived from UFlowNode"); + + if (UFlowNode* Node = Nodes.FindRef(Guid)) + { + return Cast(Node); + } + + return nullptr; + } + + UFUNCTION(BlueprintPure, Category = "FlowAsset", meta = (DeterminesOutputType = "FlowNodeClass")) + TArray GetNodesInExecutionOrder(UFlowNode* FirstIteratedNode, const TSubclassOf FlowNodeClass) const; + + template + void GetNodesInExecutionOrder(UFlowNode* FirstIteratedNode, TArray& OutNodes) const + { + static_assert(TPointerIsConvertibleFromTo::Value, "'T' template parameter to GetNodesInExecutionOrder must be derived from UFlowNode"); + + if (FirstIteratedNode) + { + TSet> IteratedNodes; + GetNodesInExecutionOrder_Recursive(FirstIteratedNode, IteratedNodes, OutNodes); + } + } + +protected: + template + void GetNodesInExecutionOrder_Recursive(UFlowNode* Node, TSet>& IteratedNodes, TArray& OutNodes) const + { + IteratedNodes.Add(Node); + + if (T* NodeOfRequiredType = Cast(Node)) + { + OutNodes.Emplace(NodeOfRequiredType); + } + + for (UFlowNode* ConnectedNode : Node->GatherConnectedNodes()) + { + if (ConnectedNode && !IteratedNodes.Contains(ConnectedNode)) + { + GetNodesInExecutionOrder_Recursive(ConnectedNode, IteratedNodes, OutNodes); + } + } + } + +public: + UFUNCTION(BlueprintPure, Category = "FlowAsset") + virtual UFlowNode* GetDefaultEntryNode() const; + +////////////////////////////////////////////////////////////////////////// +// Custom Inputs/Outputs + +#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. */ + 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. */ + UPROPERTY(EditAnywhere, Category = "Sub Graph") + TArray CustomOutputs; +#endif + +public: + /* Gathers all the nodes that are connected to the Start & Custom Inputs of the flow graph. */ + TArray GatherNodesConnectedToAllInputs() const; + + UFlowNode_CustomInput* TryFindCustomInputNodeByEventName(const FName& EventName) const; + UFlowNode_CustomOutput* TryFindCustomOutputNodeByEventName(const FName& EventName) const; + + TArray GatherCustomInputNodeEventNames() const; + TArray GatherCustomOutputNodeEventNames() const; + +#if WITH_EDITOR + const TArray& GetCustomInputs() const { return CustomInputs; } + const TArray& GetCustomOutputs() const { return CustomOutputs; } + +protected: + void AddCustomInput(const FName& EventName); + void RemoveCustomInput(const FName& EventName); + + void AddCustomOutput(const FName& EventName); + void RemoveCustomOutput(const FName& EventName); +#endif + +////////////////////////////////////////////////////////////////////////// +// Pin connections + +protected: + /* Policy for UFlowGraphSchema (and others) to use to enforce pin connectivity. + * Also used at runtime by predicates (e.g., CompareValues) for type classification queries. */ + UPROPERTY(VisibleAnywhere, AdvancedDisplay, Category = PinConnection) + TInstancedStruct PinConnectionPolicy; + +public: +#if WITH_EDITOR + /* Override these functions to set up unique policy(ies) for a UFlowAsset subclass */ + virtual void InitializePinConnectionPolicy(); +#endif + + const FFlowPinConnectionPolicy& GetPinConnectionPolicy() const; + + /* Return all other Pins connected to the passed Pin. */ + TArray GatherPinsConnectedToPin(const FConnectedPin& Pin) const; + +////////////////////////////////////////////////////////////////////////// +// FlowAssetParams support (Start node params for a Flow graph) + + /* Default parameters asset for this Flow Asset (optional). */ + UPROPERTY(EditAnywhere, Category = FlowAssetParams, meta = (ShowCreateNew, HideChildParams)) + FFlowAssetParamsPtr BaseAssetParams; + +#if WITH_EDITOR + /* 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. */ + virtual FString GenerateParamsAssetName() const; + +protected: + + void ReconcileBaseAssetParams(const FDateTime& AssetLastSavedTimestamp); +#endif + +////////////////////////////////////////////////////////////////////////// +// Instances of the template asset + +private: + /* 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. */ + TSharedPtr RuntimeLog; +#endif + +public: + void AddInstance(UFlowAsset* Instance); + int32 RemoveInstance(UFlowAsset* Instance); + TConstArrayView> GetActiveInstances() const { return ActiveInstances; } + + void ClearInstances(); + int32 GetInstancesNum() const { return ActiveInstances.Num(); } + +#if WITH_EDITOR + void SetInspectedInstance(TWeakObjectPtr NewInspectedInstance); + const UFlowAsset* GetInspectedInstance() const { return InspectedInstance.IsValid() ? InspectedInstance.Get() : nullptr; } + + DECLARE_EVENT(UFlowAsset, FRefreshDebuggerEvent); + + FRefreshDebuggerEvent& OnDebuggerRefresh() { return RefreshDebuggerEvent; } + FRefreshDebuggerEvent RefreshDebuggerEvent; + + DECLARE_EVENT_TwoParams(UFlowAsset, FRuntimeMessageEvent, const UFlowAsset*, const TSharedRef&); + + FRuntimeMessageEvent& OnRuntimeMessageAdded() { return RuntimeMessageEvent; } + FRuntimeMessageEvent RuntimeMessageEvent; + +private: + void BroadcastDebuggerRefresh() const; + void BroadcastRuntimeMessageAdded(const TSharedRef& Message) const; +#endif + +////////////////////////////////////////////////////////////////////////// +// Executing asset instance + +protected: + 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. */ + TWeakObjectPtr Owner; + + /* SubGraph node that created this Flow Asset instance. */ + TWeakObjectPtr NodeOwningThisAssetInstance; + + /* 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. */ + UPROPERTY() + TSet> CustomInputNodes; + + /* Nodes that have any work left, not marked as Finished yet. */ + UPROPERTY() + TArray> ActiveNodes; + + /* All nodes active in the past, done their work. */ + UPROPERTY() + TArray> RecordedNodes; + + UPROPERTY(Transient) + EFlowFinishPolicy FinishPolicy; + + /* Receiver that will be given a snapshot of OutputDataPinValues when this graph finishes. + * Typically the SubGraph node that created this instance. */ + UPROPERTY(Transient) + TWeakObjectPtr OutputDataReceiver; + + /* Live output data pin values for this running instance. + * Initialized from OutputDataPinDeclarations defaults at StartFlow; updated by SetGraphOutput/Finish nodes. */ + UPROPERTY(Transient) + FFlowOutputDataPinValues OutputDataPinValues; + +public: + virtual void InitializeInstance(const TWeakObjectPtr InOwner, UFlowAsset& InTemplateAsset); + virtual void DeinitializeInstance(); + bool IsInstanceInitialized() const { return IsValid(TemplateAsset); } + + void FinishFlowAndDeinitializeInstance(const EFlowFinishPolicy InFinishPolicy); + + 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. */ + UFUNCTION(BlueprintPure, Category = "Flow") + UObject* GetOwner() const { return Owner.Get(); } + + template + TWeakObjectPtr GetOwner() const + { + return Owner.IsValid() ? Cast(Owner) : nullptr; + } + + /* 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; + + virtual void PreStartFlow(); + virtual void StartFlow(IFlowDataPinValueSupplierInterface* DataPinValueSupplier = nullptr, IFlowGraphOutputDataReceiverInterface* InOutputDataReceiver = nullptr); + + /* Write a single output data pin value into the live store for this running instance. + * Called by SetGraphOutput and Finish nodes for each connected output pin. */ + void WriteOutputDataPinValue(const FName& PinName, const TInstancedStruct& Value); + + /* Flush all of the OutputDataPinValues to the receiver (if set) */ + void FlushOutputDataPinValuesToReceiver(); + + virtual void FinishFlow(const EFlowFinishPolicy InFinishPolicy); + + bool HasStartedFlow() const; + +protected: + virtual void FinishNode(UFlowNode* Node); + void ResetNodes(); + + void InitializeOutputDataReceiverAndValues(IFlowGraphOutputDataReceiverInterface* InOutputDataReceiver); + +public: + UFlowSubsystem* GetFlowSubsystem() const; + FName GetDisplayName() const; + + UFlowNode_SubGraph* GetNodeOwningThisAssetInstance() const; + UFlowAsset* GetParentInstance() const; + + /* Get Flow Asset instance created by the given SubGraph node. */ + TWeakObjectPtr GetFlowInstance(UFlowNode_SubGraph* SubGraphNode) const; + + /* 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. */ + UFUNCTION(BlueprintPure, Category = "Flow") + const TArray& GetActiveNodes() const { return ActiveNodes; } + + /* Returns nodes active in the past, done their work. */ + UFUNCTION(BlueprintPure, Category = "Flow") + const TArray& GetRecordedNodes() const { return RecordedNodes; } + +////////////////////////////////////////////////////////////////////////// +// Preload policy + +protected: + /* Policy controlling when nodes implementing IFlowPreloadableInterface preload and flush their content. + * Initialized from UFlowSettings defaults. Override InitializePreloadPolicy() in a subclass to set a unique policy. */ + UPROPERTY(VisibleAnywhere, AdvancedDisplay, Category = Preload) + TInstancedStruct PreloadPolicy; + + /* Override these functions to set up unique policy(ies) for a UFlowAsset subclass. */ + virtual void InitializePreloadPolicy(); + +public: + const FFlowPreloadPolicy& GetPreloadPolicy() const; + +////////////////////////////////////////////////////////////////////////// +// Trigger Input + +#if !UE_BUILD_SHIPPING +public: + FFlowSignalEvent OnPinTriggered; +#endif + +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; + +public: + void TriggerCustomInput(const FName& EventName, IFlowDataPinValueSupplierInterface* DataPinValueSupplier = nullptr); + + void TriggerCustomInput_FromSubGraph(UFlowNode_SubGraph* Node, const FName& EventName) const; + void TriggerCustomOutput(const FName& EventName); + + /* todo: Extend FromPin through to Node level Trigger functions. */ + virtual void TriggerInput(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin); + +protected: + /* Trigger the node directly (no deferral, no new scope). */ + void TriggerInputDirect(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin); + + /* Allow subclasses to disable the standard defer trigger mechanism */ + virtual bool ShouldDeferTriggers() const; + +protected: + void EnqueueDeferredTrigger(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin); + TSharedPtr PushDeferredTransitionScope(); + void PopDeferredTransitionScope(const TSharedPtr& Scope) { TryFlushAndRemoveDeferredTransitionScope(Scope); } + + bool TryFlushAndRemoveDeferredTransitionScope(const TSharedPtr& Scope); + +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: + 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; + +////////////////////////////////////////////////////////////////////////// +// Expected Owner Class support + +protected: + /* 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; + +public: + UClass* GetExpectedOwnerClass() const { return ExpectedOwnerClass; } + +////////////////////////////////////////////////////////////////////////// +// SaveGame support + +public: + UFUNCTION(BlueprintCallable, Category = "SaveGame") + FFlowAssetSaveData SaveInstance(TArray& SavedFlowInstances); + + UFUNCTION(BlueprintCallable, Category = "SaveGame") + void LoadInstance(const FFlowAssetSaveData& AssetRecord); + +protected: + virtual void OnActivationStateLoaded(UFlowNode* Node); + + UFUNCTION(BlueprintNativeEvent, Category = "SaveGame") + void OnSave(); + + UFUNCTION(BlueprintNativeEvent, Category = "SaveGame") + void OnLoad(); + +public: + UFUNCTION(BlueprintNativeEvent, Category = "SaveGame") + bool IsBoundToWorld() const; + +////////////////////////////////////////////////////////////////////////// +// Utils + +#if WITH_EDITOR +public: + void LogError(const FString& MessageToLog, const UFlowNodeBase* Node) const; + void LogWarning(const FString& MessageToLog, const UFlowNodeBase* Node) const; + void LogNote(const FString& MessageToLog, const UFlowNodeBase* Node) const; + +private: + /* Shared implementation for LogError/LogWarning/LogNote to avoid code duplication. */ + void LogRuntimeMessage(EMessageSeverity::Type Severity, const FString& MessageToLog, const UFlowNodeBase* Node) const; +#endif +}; \ No newline at end of file diff --git a/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp b/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp index af262a4e..860aeabe 100644 --- a/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp +++ b/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp @@ -242,7 +242,7 @@ FText SFlowAssetInstanceList::JoinInstanceAndContextTexts(const FObjectKey& Asse { if (const UFlowAsset* Instance = Cast(AssetInstance.ResolveObjectPtr())) { - FText Result = FText::FromName(Instance->GetFName()); + FText Result = FText::FromName(Instance->GetDisplayName()); // add context name if there are multiple contexts present if (InstancesPerContext.Num() > 1) @@ -321,7 +321,7 @@ void SFlowAssetBreadcrumb::FillBreadcrumb() const TWeakObjectPtr Instance = InstancesFromRoot[Index]; TWeakObjectPtr ChildInstance = Index < InstancesFromRoot.Num() - 1 ? InstancesFromRoot[Index + 1] : nullptr; - BreadcrumbTrail->PushCrumb(FText::FromName(Instance->GetFName()), FFlowBreadcrumb(Instance, ChildInstance)); + BreadcrumbTrail->PushCrumb(FText::FromName(Instance->GetDisplayName()), FFlowBreadcrumb(Instance, ChildInstance)); } } } From cc420a868bed22bb4db79e87007d96afde427656 Mon Sep 17 00:00:00 2001 From: LindyHopperGT <91915878+LindyHopperGT@users.noreply.github.com> Date: Wed, 3 Jun 2026 10:04:01 -0700 Subject: [PATCH 6/7] Update FlowPreloadHelper.cpp --- Source/Flow/Private/Policies/FlowPreloadHelper.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Flow/Private/Policies/FlowPreloadHelper.cpp b/Source/Flow/Private/Policies/FlowPreloadHelper.cpp index 82ba4970..c06298e3 100644 --- a/Source/Flow/Private/Policies/FlowPreloadHelper.cpp +++ b/Source/Flow/Private/Policies/FlowPreloadHelper.cpp @@ -3,8 +3,8 @@ #include "Policies/FlowPreloadHelper.h" #include "AddOns/FlowNodeAddOn.h" -#include "Interfaces/FlowPreloadableInterface.h" #include "FlowAsset.h" +#include "Interfaces/FlowPreloadableInterface.h" #include "Policies/FlowPreloadPolicy.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(FlowPreloadHelper) From 38cb0d8a2c0d0c01d9d704ad2bbbd4118521e3b7 Mon Sep 17 00:00:00 2001 From: LindyHopperGT <91915878+LindyHopperGT@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:43:34 -0700 Subject: [PATCH 7/7] reordered functions from flow mainline reordered functions from flow mainline --- Source/Flow/Private/FlowAsset.cpp | 923 +++++----- Source/Flow/Private/Nodes/FlowNodeBase.cpp | 3 - .../Private/Nodes/Graph/FlowNode_SubGraph.cpp | 2 +- .../Private/Policies/FlowPreloadHelper.cpp | 3 +- Source/Flow/Public/FlowAsset.h | 2 +- Source/Flow/Public/Nodes/FlowNodeBase.h | 2 - .../Flow/Public/Policies/FlowPreloadPolicy.h | 1 - Source/FlowAsset.cpp.ourLatest | 1620 ----------------- Source/FlowAsset.h.ourLatest | 567 ------ docs/.gitattributes | 2 - docs/.gitignore | 4 - 11 files changed, 466 insertions(+), 2663 deletions(-) delete mode 100644 Source/FlowAsset.cpp.ourLatest delete mode 100644 Source/FlowAsset.h.ourLatest delete mode 100644 docs/.gitattributes delete mode 100644 docs/.gitignore diff --git a/Source/Flow/Private/FlowAsset.cpp b/Source/Flow/Private/FlowAsset.cpp index a7ed41c5..4258682f 100644 --- a/Source/Flow/Private/FlowAsset.cpp +++ b/Source/Flow/Private/FlowAsset.cpp @@ -57,10 +57,8 @@ UFlowAsset::UFlowAsset(const FObjectInitializer& ObjectInitializer) , AllowedNodeClasses({UFlowNodeBase::StaticClass()}) , AllowedInSubgraphNodeClasses({UFlowNode_SubGraph::StaticClass()}) , bStartNodePlacedAsGhostNode(false) - , PinConnectionPolicy() , TemplateAsset(nullptr) , FinishPolicy(EFlowFinishPolicy::Keep) - , PreloadPolicy() { if (!AssetGuid.IsValid()) { @@ -123,7 +121,7 @@ void UFlowAsset::PostLoad() const UPackage* Package = GetPackage(); if (IsValid(Package) && !FPackageName::IsTempPackage(Package->GetPathName())) { - // If we removed or moved a flow node blueprint (and there is no redirector) we might loose the reference to it resulting + // If we removed or moved a flow node blueprint (and there is no redirector) we might lose the reference to it resulting // in null pointers in the Nodes FGUID->UFlowNode* Map. So here we iterate over all the Nodes and remove all pairs that // are nulled out. @@ -151,120 +149,6 @@ void UFlowAsset::PreSaveRoot(FObjectPreSaveRootContext ObjectSaveContext) ReconcileBaseAssetParams(FDateTime::Now()); } -void UFlowAsset::ReconcileBaseAssetParams(const FDateTime& AssetLastSavedTimestamp) -{ - if (BaseAssetParams.AssetPtr.IsNull()) - { - return; - } - - UFlowAssetParams* BaseAssetParamsPtr = BaseAssetParams.AssetPtr.LoadSynchronous(); - if (!IsValid(BaseAssetParamsPtr)) - { - UE_LOG(LogFlow, Error, TEXT("Failed to load BaseAssetParams: %s"), *BaseAssetParams.AssetPtr.ToString()); - return; - } - - IFlowNamedPropertiesSupplierInterface* NamedPropertiesSupplier = Cast(GetDefaultEntryNode()); - if (!NamedPropertiesSupplier) - { - UE_LOG(LogFlow, Error, TEXT("No NamedPropertiesSupplier (e.g., Start node) found in FlowAsset: %s"), *GetPathName()); - return; - } - - TArray& MutableStartNodeProperties = NamedPropertiesSupplier->GetMutableNamedProperties(); - const EFlowReconcilePropertiesResult ReconcileResult = - BaseAssetParamsPtr->ReconcilePropertiesWithStartNode(AssetLastSavedTimestamp, this, MutableStartNodeProperties); - - if (EFlowReconcilePropertiesResult_Classifiers::IsErrorResult(ReconcileResult)) - { - UE_LOG(LogFlow, Error, TEXT("Failed to reconcile BaseAssetParams for %s: %s"), - *BaseAssetParamsPtr->GetPathName(), *UEnum::GetDisplayValueAsText(ReconcileResult).ToString()); - } -} - -UFlowAssetParams* UFlowAsset::GenerateParamsFromStartNode() -{ - if (BaseAssetParams.AssetPtr.IsValid()) - { - UE_LOG(LogFlow, Warning, TEXT("BaseAssetParams already exists for %s: %s"), *GetPathName(), *BaseAssetParams.AssetPtr.ToString()); - return BaseAssetParams.AssetPtr.LoadSynchronous(); - } - - // Get the Start node - IFlowNamedPropertiesSupplierInterface* NamedPropertiesSupplier = Cast(GetDefaultEntryNode()); - if (!NamedPropertiesSupplier) - { - UE_LOG(LogFlow, Error, TEXT("No valid Start node found for generating params in %s"), *GetPathName()); - return nullptr; - } - - // Determine the params asset name - const FString ParamsAssetName = GenerateParamsAssetName(); - if (ParamsAssetName.IsEmpty()) - { - UE_LOG(LogFlow, Error, TEXT("Generated empty params asset name for %s"), *GetPathName()); - return nullptr; - } - - // Create the params asset - FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); - const FString PackagePath = FPackageName::GetLongPackagePath(GetPackage()->GetPathName()); - FString UniquePackageName, UniqueAssetName; - AssetToolsModule.Get().CreateUniqueAssetName(PackagePath + TEXT("/") + ParamsAssetName, TEXT(""), UniquePackageName, UniqueAssetName); - - UFlowAssetParams* NewParams = Cast( - AssetToolsModule.Get().CreateAsset(UniqueAssetName, PackagePath, UFlowAssetParams::StaticClass(), nullptr)); - if (!IsValid(NewParams)) - { - UE_LOG(LogFlow, Error, TEXT("Failed to create Flow Asset Params: %s"), *UniqueAssetName); - return nullptr; - } - - // Reconfigure with the new properties - NewParams->ConfigureFlowAssetParams(this, nullptr, NamedPropertiesSupplier->GetMutableNamedProperties()); - - // Source control integration - 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()); - } - } - - // Assign to BaseAssetParams and sync Content Browser - BaseAssetParams.AssetPtr = NewParams; - - FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); - AssetRegistryModule.Get().AssetCreated(NewParams); - - FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked("ContentBrowser"); - TArray AssetsToSync = {NewParams}; - ContentBrowserModule.Get().SyncBrowserToAssets(AssetsToSync, true); - - return NewParams; -} - -FString UFlowAsset::GenerateParamsAssetName() const -{ - const FString FlowAssetName = GetName(); - - const int32 UnderscoreIndex = FlowAssetName.Find(TEXT("_"), ESearchCase::CaseSensitive); - - if (UnderscoreIndex != INDEX_NONE) - { - const FString Prefix = FlowAssetName.Left(UnderscoreIndex); - const FString Suffix = FlowAssetName.Mid(UnderscoreIndex + 1); - return FString::Printf(TEXT("%sParams_%s"), *Prefix, *Suffix); - } - else - { - return FlowAssetName + TEXT("Params"); - } -} - EDataValidationResult UFlowAsset::ValidateAsset(FFlowMessageLog& MessageLog) { // validate nodes @@ -308,7 +192,7 @@ EDataValidationResult UFlowAsset::ValidateAsset(FFlowMessageLog& MessageLog) } } - // if at least one error has been has been logged : mark the asset as invalid + // if at least one error has been logged : mark the asset as invalid for (const TSharedRef& Msg : MessageLog.Messages) { if (Msg->GetSeverity() == EMessageSeverity::Error) @@ -349,7 +233,7 @@ bool UFlowAsset::IsNodeOrAddOnClassAllowed(const UClass* FlowNodeOrAddOnClass, F bool UFlowAsset::CanFlowNodeClassBeUsedByFlowAsset(const UClass& FlowNodeClass) const { - UFlowNode* NodeDefaults = Cast(FlowNodeClass.GetDefaultObject()); + const UFlowNode* NodeDefaults = Cast(FlowNodeClass.GetDefaultObject()); if (!NodeDefaults) { check(FlowNodeClass.IsChildOf()); @@ -406,6 +290,54 @@ bool UFlowAsset::CanFlowAssetUseFlowNodeClass(const UClass& FlowNodeClass) const return true; } +bool UFlowAsset::CanFlowAssetReferenceFlowNode(const UClass& FlowNodeClass, FText* OutOptionalFailureReason) const +{ + if (!GEditor || !IsValid(&FlowNodeClass)) + { + return false; + } + + // Confirm plugin reference restrictions are being respected + FAssetReferenceFilterContext AssetReferenceFilterContext; + AssetReferenceFilterContext.AddReferencingAsset(FAssetData(this)); + const TSharedPtr FlowAssetReferenceFilter = GEditor->MakeAssetReferenceFilter(AssetReferenceFilterContext); + if (FlowAssetReferenceFilter.IsValid()) + { + const FAssetData FlowNodeAssetData(&FlowNodeClass); + if (!FlowAssetReferenceFilter->PassesFilter(FlowNodeAssetData, OutOptionalFailureReason)) + { + return false; + } + } + + return true; +} + +bool UFlowAsset::IsFlowNodeClassInAllowedClasses(const UClass& FlowNodeClass, const TSubclassOf& RequiredAncestor) const +{ + if (AllowedNodeClasses.Num() > 0) + { + bool bAllowedInAsset = false; + for (const TSubclassOf& AllowedNodeClass : AllowedNodeClasses) + { + // If a RequiredAncestor is provided, the AllowedNodeClass must be a subclass of the RequiredAncestor + if (AllowedNodeClass && FlowNodeClass.IsChildOf(AllowedNodeClass) && (!RequiredAncestor || AllowedNodeClass->IsChildOf(RequiredAncestor))) + { + bAllowedInAsset = true; + + break; + } + } + + if (!bAllowedInAsset) + { + return false; + } + } + + return true; +} + bool UFlowAsset::IsFlowNodeClassInDeniedClasses(const UClass& FlowNodeClass) const { for (const TSubclassOf& DeniedNodeClass : DeniedNodeClasses) @@ -452,55 +384,6 @@ void UFlowAsset::ValidateAddOnTree(UFlowNodeAddOn& AddOn, FFlowMessageLog& Messa } } -bool UFlowAsset::IsFlowNodeClassInAllowedClasses(const UClass& FlowNodeClass, - const TSubclassOf& RequiredAncestor) const -{ - if (AllowedNodeClasses.Num() > 0) - { - bool bAllowedInAsset = false; - for (const TSubclassOf& AllowedNodeClass : AllowedNodeClasses) - { - // If a RequiredAncestor is provided, the AllowedNodeClass must be a subclass of the RequiredAncestor - if (AllowedNodeClass && FlowNodeClass.IsChildOf(AllowedNodeClass) && (!RequiredAncestor || AllowedNodeClass->IsChildOf(RequiredAncestor))) - { - bAllowedInAsset = true; - - break; - } - } - - if (!bAllowedInAsset) - { - return false; - } - } - - return true; -} - -bool UFlowAsset::CanFlowAssetReferenceFlowNode(const UClass& FlowNodeClass, FText* OutOptionalFailureReason) const -{ - if (!GEditor || !IsValid(&FlowNodeClass)) - { - return false; - } - - // Confirm plugin reference restrictions are being respected - FAssetReferenceFilterContext AssetReferenceFilterContext; - AssetReferenceFilterContext.AddReferencingAsset(FAssetData(this)); - const TSharedPtr FlowAssetReferenceFilter = GEditor->MakeAssetReferenceFilter(AssetReferenceFilterContext); - if (FlowAssetReferenceFilter.IsValid()) - { - const FAssetData FlowNodeAssetData(&FlowNodeClass); - if (!FlowAssetReferenceFilter->PassesFilter(FlowNodeAssetData, OutOptionalFailureReason)) - { - return false; - } - } - - return true; -} - UFlowNode* UFlowAsset::CreateNode(const UClass* NodeClass, UEdGraphNode* GraphNode) { UFlowNode* NewNode = NewObject(this, NodeClass, NAME_None, RF_Transactional); @@ -530,7 +413,7 @@ void UFlowAsset::UnregisterNode(const FGuid& NodeGuid) HarvestNodeConnections(); - MarkPackageDirty(); + (void)MarkPackageDirty(); } void UFlowAsset::HarvestNodeConnections(UFlowNode* TargetNode) @@ -662,6 +545,33 @@ bool UFlowAsset::TryGetDefaultForInputPinName(const FStructProperty& StructPrope #endif +TArray UFlowAsset::GetAllNodes() const +{ + TArray> AllNodes; + AllNodes.Reserve(Nodes.Num()); + Nodes.GenerateValueArray(AllNodes); + + return ObjectPtrDecay(AllNodes); +} + +TArray UFlowAsset::GetNodesInExecutionOrder(UFlowNode* FirstIteratedNode, const TSubclassOf FlowNodeClass) const +{ + TArray FoundNodes; + GetNodesInExecutionOrder(FirstIteratedNode, FoundNodes); + + // filter out nodes by class + for (int32 i = FoundNodes.Num() - 1; i >= 0; i--) + { + if (!FoundNodes[i]->GetClass()->IsChildOf(FlowNodeClass)) + { + FoundNodes.RemoveAt(i); + } + } + FoundNodes.Shrink(); + + return FoundNodes; +} + UFlowNode* UFlowAsset::GetDefaultEntryNode() const { UFlowNode* FirstStartNode = nullptr; @@ -685,43 +595,30 @@ UFlowNode* UFlowAsset::GetDefaultEntryNode() const return FirstStartNode; } -#if WITH_EDITOR -void UFlowAsset::AddCustomInput(const FName& EventName) +TArray UFlowAsset::GatherNodesConnectedToAllInputs() const { - if (!CustomInputs.Contains(EventName)) + TSet> IteratedNodes; + TArray ConnectedNodes; + + // Nodes connected to the Start node + UFlowNode* DefaultEntryNode = GetDefaultEntryNode(); + GetNodesInExecutionOrder_Recursive(DefaultEntryNode, IteratedNodes, ConnectedNodes); + + // Nodes connected to Custom Input node(s) + for (const TPair& Node : ObjectPtrDecay(Nodes)) { - CustomInputs.Add(EventName); + if (UFlowNode_CustomInput* CustomInput = Cast(Node.Value)) + { + GetNodesInExecutionOrder_Recursive(CustomInput, IteratedNodes, ConnectedNodes); + } } + + return ConnectedNodes; } -void UFlowAsset::RemoveCustomInput(const FName& EventName) +UFlowNode_CustomInput* UFlowAsset::TryFindCustomInputNodeByEventName(const FName& EventName) const { - if (CustomInputs.Contains(EventName)) - { - CustomInputs.Remove(EventName); - } -} - -void UFlowAsset::AddCustomOutput(const FName& EventName) -{ - if (!CustomOutputs.Contains(EventName)) - { - CustomOutputs.Add(EventName); - } -} - -void UFlowAsset::RemoveCustomOutput(const FName& EventName) -{ - if (CustomOutputs.Contains(EventName)) - { - CustomOutputs.Remove(EventName); - } -} -#endif // WITH_EDITOR - -UFlowNode_CustomInput* UFlowAsset::TryFindCustomInputNodeByEventName(const FName& EventName) const -{ - for (const TPair& Node : ObjectPtrDecay(Nodes)) + for (const TPair& Node : ObjectPtrDecay(Nodes)) { if (UFlowNode_CustomInput* CustomInput = Cast(Node.Value)) { @@ -785,43 +682,73 @@ TArray UFlowAsset::GatherCustomOutputNodeEventNames() const return Results; } -TArray UFlowAsset::GetNodesInExecutionOrder(UFlowNode* FirstIteratedNode, const TSubclassOf FlowNodeClass) const +#if WITH_EDITOR +void UFlowAsset::AddCustomInput(const FName& EventName) { - TArray FoundNodes; - GetNodesInExecutionOrder(FirstIteratedNode, FoundNodes); + if (!CustomInputs.Contains(EventName)) + { + CustomInputs.Add(EventName); + } +} - // filter out nodes by class - for (int32 i = FoundNodes.Num() - 1; i >= 0; i--) +void UFlowAsset::RemoveCustomInput(const FName& EventName) +{ + if (CustomInputs.Contains(EventName)) { - if (!FoundNodes[i]->GetClass()->IsChildOf(FlowNodeClass)) - { - FoundNodes.RemoveAt(i); - } + CustomInputs.Remove(EventName); } - FoundNodes.Shrink(); +} - return FoundNodes; +void UFlowAsset::AddCustomOutput(const FName& EventName) +{ + if (!CustomOutputs.Contains(EventName)) + { + CustomOutputs.Add(EventName); + } } -TArray UFlowAsset::GatherNodesConnectedToAllInputs() const +void UFlowAsset::RemoveCustomOutput(const FName& EventName) { - TSet> IteratedNodes; - TArray ConnectedNodes; + if (CustomOutputs.Contains(EventName)) + { + CustomOutputs.Remove(EventName); + } +} +#endif // WITH_EDITOR - // Nodes connected to the Start node - UFlowNode* DefaultEntryNode = GetDefaultEntryNode(); - GetNodesInExecutionOrder_Recursive(DefaultEntryNode, IteratedNodes, ConnectedNodes); +#if WITH_EDITOR +void UFlowAsset::InitializePinConnectionPolicy() +{ + const FInstancedStruct& SourceStruct = GetDefault()->PinConnectionPolicy; + if (ensure(SourceStruct.IsValid())) + { + PinConnectionPolicy.InitializeAsScriptStruct(SourceStruct.GetScriptStruct(), SourceStruct.GetMemory()); + } +} +#endif - // Nodes connected to Custom Input node(s) - for (const TPair& Node : ObjectPtrDecay(Nodes)) +const FFlowPinConnectionPolicy& UFlowAsset::GetPinConnectionPolicy() const +{ + // Runtime instances delegate to their template, which holds the serialized policy + if (!PinConnectionPolicy.IsValid() && IsValid(TemplateAsset)) { - if (UFlowNode_CustomInput* CustomInput = Cast(Node.Value)) + return TemplateAsset->GetPinConnectionPolicy(); + } + + // Graceful fallback: if PinConnectionPolicy was never initialized (asset predates this feature, + // or was never opened in editor), read directly from Project Settings at runtime. + if (!PinConnectionPolicy.IsValid()) + { + const FFlowPinConnectionPolicy* SettingsPolicy = GetDefault()->GetPinConnectionPolicy(); + ensureAlways(SettingsPolicy); + if (SettingsPolicy) { - GetNodesInExecutionOrder_Recursive(CustomInput, IteratedNodes, ConnectedNodes); + return *SettingsPolicy; } } - return ConnectedNodes; + check(PinConnectionPolicy.IsValid()); + return PinConnectionPolicy.Get(); } TArray UFlowAsset::GatherPinsConnectedToPin(const FConnectedPin& Pin) const @@ -841,14 +768,121 @@ TArray UFlowAsset::GatherPinsConnectedToPin(const FConnectedPin& return ConnectedPins; } -TArray UFlowAsset::GetAllNodes() const +#if WITH_EDITOR +UFlowAssetParams* UFlowAsset::GenerateParamsFromStartNode() { - TArray> AllNodes; - AllNodes.Reserve(Nodes.Num()); - Nodes.GenerateValueArray(AllNodes); + if (BaseAssetParams.AssetPtr.IsValid()) + { + UE_LOG(LogFlow, Warning, TEXT("BaseAssetParams already exists for %s: %s"), *GetPathName(), *BaseAssetParams.AssetPtr.ToString()); + return BaseAssetParams.AssetPtr.LoadSynchronous(); + } - return ObjectPtrDecay(AllNodes); + // Get the Start node + IFlowNamedPropertiesSupplierInterface* NamedPropertiesSupplier = Cast(GetDefaultEntryNode()); + if (!NamedPropertiesSupplier) + { + UE_LOG(LogFlow, Error, TEXT("No valid Start node found for generating params in %s"), *GetPathName()); + return nullptr; + } + + // Determine the params asset name + const FString ParamsAssetName = GenerateParamsAssetName(); + if (ParamsAssetName.IsEmpty()) + { + UE_LOG(LogFlow, Error, TEXT("Generated empty params asset name for %s"), *GetPathName()); + return nullptr; + } + + // Create the params asset + const FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); + const FString PackagePath = FPackageName::GetLongPackagePath(GetPackage()->GetPathName()); + FString UniquePackageName, UniqueAssetName; + AssetToolsModule.Get().CreateUniqueAssetName(PackagePath + TEXT("/") + ParamsAssetName, TEXT(""), UniquePackageName, UniqueAssetName); + + UFlowAssetParams* NewParams = Cast( + AssetToolsModule.Get().CreateAsset(UniqueAssetName, PackagePath, UFlowAssetParams::StaticClass(), nullptr)); + if (!IsValid(NewParams)) + { + UE_LOG(LogFlow, Error, TEXT("Failed to create Flow Asset Params: %s"), *UniqueAssetName); + return nullptr; + } + + // Reconfigure with the new properties + NewParams->ConfigureFlowAssetParams(this, nullptr, NamedPropertiesSupplier->GetMutableNamedProperties()); + + // Source control integration + 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()); + } + } + + // Assign to BaseAssetParams and sync Content Browser + BaseAssetParams.AssetPtr = NewParams; + + 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; +} + +FString UFlowAsset::GenerateParamsAssetName() const +{ + const FString FlowAssetName = GetName(); + + const int32 UnderscoreIndex = FlowAssetName.Find(TEXT("_"), ESearchCase::CaseSensitive); + + if (UnderscoreIndex != INDEX_NONE) + { + const FString Prefix = FlowAssetName.Left(UnderscoreIndex); + const FString Suffix = FlowAssetName.Mid(UnderscoreIndex + 1); + return FString::Printf(TEXT("%sParams_%s"), *Prefix, *Suffix); + } + else + { + return FlowAssetName + TEXT("Params"); + } +} + +void UFlowAsset::ReconcileBaseAssetParams(const FDateTime& AssetLastSavedTimestamp) +{ + if (BaseAssetParams.AssetPtr.IsNull()) + { + return; + } + + UFlowAssetParams* BaseAssetParamsPtr = BaseAssetParams.AssetPtr.LoadSynchronous(); + if (!IsValid(BaseAssetParamsPtr)) + { + UE_LOG(LogFlow, Error, TEXT("Failed to load BaseAssetParams: %s"), *BaseAssetParams.AssetPtr.ToString()); + return; + } + + IFlowNamedPropertiesSupplierInterface* NamedPropertiesSupplier = Cast(GetDefaultEntryNode()); + if (!NamedPropertiesSupplier) + { + UE_LOG(LogFlow, Error, TEXT("No NamedPropertiesSupplier (e.g., Start node) found in FlowAsset: %s"), *GetPathName()); + return; + } + + TArray& MutableStartNodeProperties = NamedPropertiesSupplier->GetMutableNamedProperties(); + const EFlowReconcilePropertiesResult ReconcileResult = + BaseAssetParamsPtr->ReconcilePropertiesWithStartNode(AssetLastSavedTimestamp, this, MutableStartNodeProperties); + + if (EFlowReconcilePropertiesResult_Classifiers::IsErrorResult(ReconcileResult)) + { + UE_LOG(LogFlow, Error, TEXT("Failed to reconcile BaseAssetParams for %s: %s"), + *BaseAssetParamsPtr->GetPathName(), *UEnum::GetDisplayValueAsText(ReconcileResult).ToString()); + } } +#endif void UFlowAsset::AddInstance(UFlowAsset* Instance) { @@ -985,6 +1019,29 @@ void UFlowAsset::DeinitializeInstance() } } +AActor* UFlowAsset::TryFindActorOwner() const +{ + UObject* OwnerObject = GetOwner(); + if (!IsValid(OwnerObject)) + { + return nullptr; + } + + // If the owner is already an Actor, return it directly + if (AActor* OwnerAsActor = Cast(OwnerObject)) + { + return OwnerAsActor; + } + + // If the owner is a Component, return its owning Actor + if (const UActorComponent* OwnerAsComponent = Cast(OwnerObject)) + { + return OwnerAsComponent->GetOwner(); + } + + return nullptr; +} + void UFlowAsset::FinishFlowAndDeinitializeInstance(const EFlowFinishPolicy InFinishPolicy) { FinishFlow(InFinishPolicy); @@ -1030,48 +1087,58 @@ void UFlowAsset::StartFlow(IFlowDataPinValueSupplierInterface* DataPinValueSuppl } } -void UFlowAsset::InitializeOutputDataReceiverAndValues(IFlowGraphOutputDataReceiverInterface* InOutputDataReceiver) +bool UFlowAsset::HasStartedFlow() const { - OutputDataReceiver = Cast(InOutputDataReceiver); - - // Initialize the live output store from the template asset's declarations - OutputDataPinValues.Values.Reset(); + return RecordedNodes.Num() > 0; +} - if (const UFlowAsset* Template = TemplateAsset.Get()) +void UFlowAsset::FinishNode(UFlowNode* Node) +{ + if (ActiveNodes.Contains(Node)) { - for (const FFlowNamedDataPinProperty& Declaration : Template->OutputDataPinDeclarations) + ActiveNodes.Remove(Node); + + // if graph reached Finish and this asset instance was created by SubGraph node + if (Node->CanFinishGraph()) { - if (Declaration.IsValid()) + if (IFlowGraphOutputDataReceiverInterface* Receiver = Cast(OutputDataReceiver.Get())) { - OutputDataPinValues.Values.Add(Declaration.Name, Declaration.DataPinValue); + Receiver->ReceiveOutputDataSnapshot(OutputDataPinValues); } - else + + if (NodeOwningThisAssetInstance.IsValid()) { - UE_LOG(LogFlow, Warning, TEXT("Invalid OutputDataPin %s"), *Declaration.Name.ToString()); + NodeOwningThisAssetInstance.Get()->TriggerFirstOutput(true); + + return; + } + + // if this instance is a Root Flow, we need to deregister it from the subsystem first. This will + // finalize and deinitialize the root flow. + if (Owner.IsValid()) + { + const TSet& RootFlowInstances = GetFlowSubsystem()->GetRootInstancesByOwner(Owner.Get()); + if (RootFlowInstances.Contains(this)) + { + GetFlowSubsystem()->FinishAndDeinitializeRootFlow(Owner.Get(), TemplateAsset, EFlowFinishPolicy::Keep); + + return; + } } + + FinishFlow(EFlowFinishPolicy::Keep); } } } -void UFlowAsset::WriteOutputDataPinValue(const FName& PinName, const TInstancedStruct& Value) +void UFlowAsset::ResetNodes() { - if (OutputDataPinValues.Values.Contains(PinName)) - { - OutputDataPinValues.Values[PinName] = Value; - } - else + for (UFlowNode* Node : RecordedNodes) { - UE_LOG(LogFlow, Warning, TEXT("Could not find pin named %s in WriteOutputDataPinValue"), *PinName.ToString()); + Node->ResetRecords(); } -} -void UFlowAsset::FlushOutputDataPinValuesToReceiver() -{ - if (IFlowGraphOutputDataReceiverInterface* Receiver = Cast(OutputDataReceiver.Get())) - { - // Do an immediate push to the receiver - Receiver->ReceiveOutputDataSnapshot(OutputDataPinValues); - } + RecordedNodes.Empty(); } void UFlowAsset::FinishFlow(const EFlowFinishPolicy InFinishPolicy) @@ -1085,111 +1152,102 @@ void UFlowAsset::FinishFlow(const EFlowFinishPolicy InFinishPolicy) { Node->Deactivate(); } + ActiveNodes.Empty(); } -void UFlowAsset::CancelAndWarnForUnflushedDeferredTriggers() +UFlowSubsystem* UFlowAsset::GetFlowSubsystem() const { - // 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(); + return Cast(GetOuter()); +} - 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()); - } +UFlowNode_SubGraph* UFlowAsset::GetNodeOwningThisAssetInstance() const +{ + return NodeOwningThisAssetInstance.Get(); +} - TotalDroppedTriggers += Triggers.Num(); +UFlowAsset* UFlowAsset::GetParentInstance() const +{ + return NodeOwningThisAssetInstance.IsValid() ? NodeOwningThisAssetInstance.Get()->GetFlowAsset() : nullptr; +} - for (const FFlowDeferredTriggerInput& Trigger : Triggers) - { - const UFlowNode* ToNode = GetNode(Trigger.NodeGuid); - const UFlowNode* FromNode = Trigger.FromPin.NodeGuid.IsValid() ? GetNode(Trigger.FromPin.NodeGuid) : nullptr; +TWeakObjectPtr UFlowAsset::GetFlowInstance(UFlowNode_SubGraph* SubGraphNode) const +{ + return ActiveSubGraphs.FindRef(SubGraphNode); +} - const FString ToNodeName = ToNode ? ToNode->GetName() : TEXT(""); - const FString FromNodeName = FromNode ? FromNode->GetName() : TEXT(""); +FName UFlowAsset::GetDisplayName() const +{ + return GetFName(); +} - 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"), - *ToNodeName, - *Trigger.NodeGuid.ToString(), - *Trigger.PinName.ToString(), - *FromNodeName, - *Trigger.FromPin.NodeGuid.ToString(), - *Trigger.FromPin.PinName.ToString() - ); - } +void UFlowAsset::InitializePreloadPolicy() +{ + if (PreloadPolicy.IsValid()) + { + // use per-class policy + PreloadPolicy.InitializeAsScriptStruct(PreloadPolicy.GetScriptStruct(), PreloadPolicy.GetMemory()); + } + else + { + // fallback to project's default policy + const FInstancedStruct& DefaultPolicy = GetDefault()->PreloadPolicy; + if (ensure(DefaultPolicy.IsValid())) + { + PreloadPolicy.InitializeAsScriptStruct(DefaultPolicy.GetScriptStruct(), DefaultPolicy.GetMemory()); } - - ClearAllDeferredTriggerScopes(); } + + ensureAlwaysMsgf(PreloadPolicy.IsValid(), TEXT("There's no valid Preload Policy set in the project!")); } -bool UFlowAsset::HasStartedFlow() const +const FFlowPreloadPolicy& UFlowAsset::GetPreloadPolicy() const { - return RecordedNodes.Num() > 0; + checkf(PreloadPolicy.IsValid(), TEXT("PreloadPolicy must be initialized prior to calling GetPreloadPolicy()")); + return PreloadPolicy.Get(); } -AActor* UFlowAsset::TryFindActorOwner() const +void UFlowAsset::InitializeOutputDataReceiverAndValues(IFlowGraphOutputDataReceiverInterface* InOutputDataReceiver) { - UObject* OwnerObject = GetOwner(); - if (!IsValid(OwnerObject)) - { - return nullptr; - } + OutputDataReceiver = Cast(InOutputDataReceiver); - // If the owner is already an Actor, return it directly - if (AActor* OwnerAsActor = Cast(OwnerObject)) - { - return OwnerAsActor; - } + // Initialize the live output store from the template asset's declarations + OutputDataPinValues.Values.Reset(); - // If the owner is a Component, return its owning Actor - if (const UActorComponent* OwnerAsComponent = Cast(OwnerObject)) + if (const UFlowAsset* Template = TemplateAsset.Get()) { - return OwnerAsComponent->GetOwner(); + for (const FFlowNamedDataPinProperty& Declaration : Template->OutputDataPinDeclarations) + { + if (Declaration.IsValid()) + { + OutputDataPinValues.Values.Add(Declaration.Name, Declaration.DataPinValue); + } + else + { + UE_LOG(LogFlow, Warning, TEXT("Invalid OutputDataPin %s"), *Declaration.Name.ToString()); + } + } } - - return nullptr; } -TWeakObjectPtr UFlowAsset::GetFlowInstance(UFlowNode_SubGraph* SubGraphNode) const +void UFlowAsset::WriteOutputDataPinValue(const FName& PinName, const TInstancedStruct& Value) { - return ActiveSubGraphs.FindRef(SubGraphNode); + if (OutputDataPinValues.Values.Contains(PinName)) + { + OutputDataPinValues.Values[PinName] = Value; + } + else + { + UE_LOG(LogFlow, Warning, TEXT("Could not find pin named %s in WriteOutputDataPinValue"), *PinName.ToString()); + } } -void UFlowAsset::TriggerCustomInput_FromSubGraph(UFlowNode_SubGraph* SubGraphNode, const FName& EventName) const +void UFlowAsset::FlushOutputDataPinValuesToReceiver() { - // 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). - - const TWeakObjectPtr FlowInstance = ActiveSubGraphs.FindRef(SubGraphNode); - if (FlowInstance.IsValid()) + if (IFlowGraphOutputDataReceiverInterface* Receiver = Cast(OutputDataReceiver.Get())) { - FlowInstance->TriggerCustomInput(EventName, SubGraphNode); + // Do an immediate push to the receiver + Receiver->ReceiveOutputDataSnapshot(OutputDataPinValues); } } @@ -1215,6 +1273,19 @@ void UFlowAsset::TriggerCustomInput(const FName& EventName, IFlowDataPinValueSup } } +void UFlowAsset::TriggerCustomInput_FromSubGraph(UFlowNode_SubGraph* SubGraphNode, const FName& EventName) const +{ + // 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). + + const TWeakObjectPtr FlowInstance = ActiveSubGraphs.FindRef(SubGraphNode); + if (FlowInstance.IsValid()) + { + FlowInstance->TriggerCustomInput(EventName, SubGraphNode); + } +} + void UFlowAsset::TriggerCustomOutput(const FName& EventName) { if (NodeOwningThisAssetInstance.IsValid()) @@ -1278,6 +1349,19 @@ bool UFlowAsset::ShouldDeferTriggers() const return GetDefault()->bDeferTriggeredOutputsWhileTriggering; } +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}); +} + TSharedPtr UFlowAsset::PushDeferredTransitionScope() { // Close the former top scope (if any) @@ -1291,6 +1375,11 @@ TSharedPtr UFlowAsset::PushDeferredTransitionScope return DeferredTransitionScopes.Add_GetRef(MakeShared()); } +void UFlowAsset::PopDeferredTransitionScope(const TSharedPtr& Scope) +{ + TryFlushAndRemoveDeferredTransitionScope(Scope); +} + bool UFlowAsset::TryFlushAndRemoveDeferredTransitionScope(const TSharedPtr& ScopeToFlush) { if (ScopeToFlush->TryFlushDeferredTriggers(*this)) @@ -1307,19 +1396,6 @@ bool UFlowAsset::TryFlushAndRemoveDeferredTransitionScope(const TSharedPtrIsOpen()) - { - // 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()) @@ -1329,7 +1405,7 @@ bool UFlowAsset::TryFlushAllDeferredTriggerScopes() break; } - // Keep flushing until stack is empty or we hit an ExecutionGate halt + // Keep flushing until stack is empty, or we hit an ExecutionGate halt } check(DeferredTransitionScopes.IsEmpty() || FFlowExecutionGate::IsHalted()); @@ -1342,78 +1418,68 @@ void UFlowAsset::ClearAllDeferredTriggerScopes() DeferredTransitionScopes.Reset(); } -TSharedPtr UFlowAsset::GetTopDeferredTransitionScope() const -{ - return !DeferredTransitionScopes.IsEmpty() ? DeferredTransitionScopes.Top() : nullptr; -} - -void UFlowAsset::FinishNode(UFlowNode* Node) +void UFlowAsset::CancelAndWarnForUnflushedDeferredTriggers() { - if (ActiveNodes.Contains(Node)) + // 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()) { - ActiveNodes.Remove(Node); + int32 TotalDroppedTriggers = 0; - // if graph reached Finish and this asset instance was created by SubGraph node - if (Node->CanFinishGraph()) + for (const TSharedPtr& ScopePtr : DeferredTransitionScopes) { - if (IFlowGraphOutputDataReceiverInterface* Receiver = Cast(OutputDataReceiver.Get())) + if (!ScopePtr.IsValid()) { - Receiver->ReceiveOutputDataSnapshot(OutputDataPinValues); + continue; } - if (NodeOwningThisAssetInstance.IsValid()) - { - NodeOwningThisAssetInstance.Get()->TriggerFirstOutput(true); + const TArray& Triggers = ScopePtr->GetDeferredTriggers(); - return; + 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()); } - // if this instance is a Root Flow, we need to deregister it from the subsystem first. This will - // finalize and deinitialize the root flow. - if (Owner.IsValid()) + TotalDroppedTriggers += Triggers.Num(); + + for (const FFlowDeferredTriggerInput& Trigger : Triggers) { - const TSet& RootFlowInstances = GetFlowSubsystem()->GetRootInstancesByOwner(Owner.Get()); - if (RootFlowInstances.Contains(this)) - { - GetFlowSubsystem()->FinishAndDeinitializeRootFlow(Owner.Get(), TemplateAsset, EFlowFinishPolicy::Keep); + const UFlowNode* ToNode = GetNode(Trigger.NodeGuid); + const UFlowNode* FromNode = Trigger.FromPin.NodeGuid.IsValid() ? GetNode(Trigger.FromPin.NodeGuid) : nullptr; - return; - } - } + const FString ToNodeName = ToNode ? ToNode->GetName() : TEXT(""); + const FString FromNodeName = FromNode ? FromNode->GetName() : TEXT(""); - FinishFlow(EFlowFinishPolicy::Keep); + 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"), + *ToNodeName, + *Trigger.NodeGuid.ToString(), + *Trigger.PinName.ToString(), + *FromNodeName, + *Trigger.FromPin.NodeGuid.ToString(), + *Trigger.FromPin.PinName.ToString() + ); + } } - } -} -void UFlowAsset::ResetNodes() -{ - for (UFlowNode* Node : RecordedNodes) - { - Node->ResetRecords(); + ClearAllDeferredTriggerScopes(); } - - RecordedNodes.Empty(); -} - -UFlowSubsystem* UFlowAsset::GetFlowSubsystem() const -{ - return Cast(GetOuter()); -} - -FName UFlowAsset::GetDisplayName() const -{ - return GetFName(); } -UFlowNode_SubGraph* UFlowAsset::GetNodeOwningThisAssetInstance() const -{ - return NodeOwningThisAssetInstance.Get(); -} - -UFlowAsset* UFlowAsset::GetParentInstance() const +TSharedPtr UFlowAsset::GetTopDeferredTransitionScope() const { - return NodeOwningThisAssetInstance.IsValid() ? NodeOwningThisAssetInstance.Get()->GetFlowAsset() : nullptr; + return !DeferredTransitionScopes.IsEmpty() ? DeferredTransitionScopes.Top() : nullptr; } FFlowAssetSaveData UFlowAsset::SaveInstance(TArray& SavedFlowInstances) @@ -1508,69 +1574,6 @@ bool UFlowAsset::IsBoundToWorld_Implementation() const return bWorldBound; } -const FFlowPinConnectionPolicy& UFlowAsset::GetPinConnectionPolicy() const -{ - // Runtime instances delegate to their template, which holds the serialized policy - if (!PinConnectionPolicy.IsValid() && IsValid(TemplateAsset)) - { - return TemplateAsset->GetPinConnectionPolicy(); - } - - // Graceful fallback: if PinConnectionPolicy was never initialized (asset predates this feature, - // or was never opened in editor), read directly from project settings at runtime. - if (!PinConnectionPolicy.IsValid()) - { - const FFlowPinConnectionPolicy* SettingsPolicy = GetDefault()->GetPinConnectionPolicy(); - ensureAlways(SettingsPolicy); - if (SettingsPolicy) - { - return *SettingsPolicy; - } - } - - check(PinConnectionPolicy.IsValid()); - return PinConnectionPolicy.Get(); -} - -const FFlowPreloadPolicy& UFlowAsset::GetPreloadPolicy() const -{ - checkf(PreloadPolicy.IsValid(), TEXT("PreloadPolicy must be initialized prior to calling GetPreloadPolicy()")); - return PreloadPolicy.Get(); -} - -#if WITH_EDITOR - -void UFlowAsset::InitializePinConnectionPolicy() -{ - const FInstancedStruct& SourceStruct = GetDefault()->PinConnectionPolicy; - if (ensure(SourceStruct.IsValid())) - { - PinConnectionPolicy.InitializeAsScriptStruct(SourceStruct.GetScriptStruct(), SourceStruct.GetMemory()); - } -} - -#endif - -void UFlowAsset::InitializePreloadPolicy() -{ - if (PreloadPolicy.IsValid()) - { - // use per-class policy - PreloadPolicy.InitializeAsScriptStruct(PreloadPolicy.GetScriptStruct(), PreloadPolicy.GetMemory()); - } - else - { - // fallback to project's default policy - const FInstancedStruct& DefaultPolicy = GetDefault()->PreloadPolicy; - if (ensure(DefaultPolicy.IsValid())) - { - PreloadPolicy.InitializeAsScriptStruct(DefaultPolicy.GetScriptStruct(), DefaultPolicy.GetMemory()); - } - } - - ensureAlwaysMsgf(PreloadPolicy.IsValid(), TEXT("There's no valid Preload Policy set in the project!")); -} - #if WITH_EDITOR void UFlowAsset::LogError(const FString& MessageToLog, const UFlowNodeBase* Node) const diff --git a/Source/Flow/Private/Nodes/FlowNodeBase.cpp b/Source/Flow/Private/Nodes/FlowNodeBase.cpp index b45faad8..f9d0de3a 100644 --- a/Source/Flow/Private/Nodes/FlowNodeBase.cpp +++ b/Source/Flow/Private/Nodes/FlowNodeBase.cpp @@ -621,10 +621,7 @@ FString UFlowNodeBase::GetNodeCategory() const } } - // #ASIntegration #NodeCategory return K2_GetNodeCategory(); - //return Category; - // } bool UFlowNodeBase::GetDynamicTitleColor(FLinearColor& OutColor) const diff --git a/Source/Flow/Private/Nodes/Graph/FlowNode_SubGraph.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_SubGraph.cpp index 57d66c54..ac268cce 100644 --- a/Source/Flow/Private/Nodes/Graph/FlowNode_SubGraph.cpp +++ b/Source/Flow/Private/Nodes/Graph/FlowNode_SubGraph.cpp @@ -317,7 +317,7 @@ FFlowDataPinResult UFlowNode_SubGraph::TrySupplyDataPin(FName PinName) const } } } - + // Prefer the standard lookup if the pin is connected // (or if there is no FlowAssetParams to ask) return Super::TrySupplyDataPin(PinName); diff --git a/Source/Flow/Private/Policies/FlowPreloadHelper.cpp b/Source/Flow/Private/Policies/FlowPreloadHelper.cpp index c06298e3..0d25e563 100644 --- a/Source/Flow/Private/Policies/FlowPreloadHelper.cpp +++ b/Source/Flow/Private/Policies/FlowPreloadHelper.cpp @@ -1,10 +1,9 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #include "Policies/FlowPreloadHelper.h" #include "AddOns/FlowNodeAddOn.h" -#include "FlowAsset.h" #include "Interfaces/FlowPreloadableInterface.h" +#include "FlowAsset.h" #include "Policies/FlowPreloadPolicy.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(FlowPreloadHelper) diff --git a/Source/Flow/Public/FlowAsset.h b/Source/Flow/Public/FlowAsset.h index e1f0984b..adec0a40 100644 --- a/Source/Flow/Public/FlowAsset.h +++ b/Source/Flow/Public/FlowAsset.h @@ -497,7 +497,7 @@ class FLOW_API UFlowAsset : public UObject protected: void EnqueueDeferredTrigger(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin); TSharedPtr PushDeferredTransitionScope(); - void PopDeferredTransitionScope(const TSharedPtr& Scope) { TryFlushAndRemoveDeferredTransitionScope(Scope); } + void PopDeferredTransitionScope(const TSharedPtr& Scope); bool TryFlushAndRemoveDeferredTransitionScope(const TSharedPtr& Scope); diff --git a/Source/Flow/Public/Nodes/FlowNodeBase.h b/Source/Flow/Public/Nodes/FlowNodeBase.h index c9cf262a..2981fefc 100644 --- a/Source/Flow/Public/Nodes/FlowNodeBase.h +++ b/Source/Flow/Public/Nodes/FlowNodeBase.h @@ -495,10 +495,8 @@ class FLOW_API UFlowNodeBase UFUNCTION(BlueprintNativeEvent, Category = "FlowNode") FText K2_GetNodeToolTip() const; - // #ASIntegration #NodeCategory add overridable function for AS, no BP gen class to edit default category for, cant edit category member variable either UFUNCTION(BlueprintNativeEvent, Category = "FlowNode") FString K2_GetNodeCategory() const; - // UFUNCTION(BlueprintPure, Category = "FlowNode") virtual FText GetNodeConfigText() const; diff --git a/Source/Flow/Public/Policies/FlowPreloadPolicy.h b/Source/Flow/Public/Policies/FlowPreloadPolicy.h index d66afcfc..2d4f45c5 100644 --- a/Source/Flow/Public/Policies/FlowPreloadPolicy.h +++ b/Source/Flow/Public/Policies/FlowPreloadPolicy.h @@ -3,7 +3,6 @@ #include "Policies/FlowPolicy.h" #include "Policies/FlowPreloadPolicyEnums.h" - #include "FlowPreloadPolicy.generated.h" class UFlowNode; diff --git a/Source/FlowAsset.cpp.ourLatest b/Source/FlowAsset.cpp.ourLatest deleted file mode 100644 index a7ed41c5..00000000 --- a/Source/FlowAsset.cpp.ourLatest +++ /dev/null @@ -1,1620 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - -#include "FlowAsset.h" - -#include "FlowLogChannels.h" -#include "FlowSettings.h" -#include "FlowSubsystem.h" -#include "AddOns/FlowNodeAddOn.h" -#include "Asset/FlowAssetParams.h" -#include "Asset/FlowAssetParamsUtils.h" -#include "Interfaces/FlowExecutionGate.h" -#include "Interfaces/FlowGraphOutputDataReceiverInterface.h" -#include "Types/FlowNamedDataPinProperty.h" -#include "Nodes/FlowNodeBase.h" -#include "Nodes/Graph/FlowNode_CustomInput.h" -#include "Nodes/Graph/FlowNode_CustomOutput.h" -#include "Nodes/Graph/FlowNode_Start.h" -#include "Nodes/Graph/FlowNode_SubGraph.h" -#include "Policies/FlowPinConnectionPolicy.h" -#include "Policies/FlowPreloadPolicy.h" -#include "Types/FlowAutoDataPinsWorkingData.h" -#include "Types/FlowDataPinValue.h" -#include "Types/FlowStructUtils.h" - -#include "Engine/World.h" -#include "Serialization/MemoryReader.h" -#include "Serialization/MemoryWriter.h" -#include "Algo/AnyOf.h" - -#if WITH_EDITOR -#include "Nodes/Graph/FlowNode_SetGraphOutput.h" -#include "AssetRegistry/AssetRegistryModule.h" -#include "AssetToolsModule.h" -#include "ContentBrowserModule.h" -#include "IContentBrowserSingleton.h" -#include "Editor.h" -#include "Editor/EditorEngine.h" -#include "Modules/ModuleManager.h" -#include "SourceControlHelpers.h" -#include "UObject/ObjectSaveContext.h" -#include "UObject/Package.h" - -FString UFlowAsset::ValidationError_NodeClassNotAllowed = TEXT("Node class {0} is not allowed in this asset."); -FString UFlowAsset::ValidationError_AddOnNodeClassNotAllowed = TEXT("AddOn Node class {0} is not allowed in this asset."); -FString UFlowAsset::ValidationError_NullNodeInstance = TEXT("Node with GUID {0} is NULL"); -FString UFlowAsset::ValidationError_NullAddOnNodeInstance = TEXT("Node with GUID {0} has NULL AddOn(s)"); -#endif - -#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowAsset) - -UFlowAsset::UFlowAsset(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) - , bWorldBound(true) -#if WITH_EDITORONLY_DATA - , FlowGraph(nullptr) -#endif - , AllowedNodeClasses({UFlowNodeBase::StaticClass()}) - , AllowedInSubgraphNodeClasses({UFlowNode_SubGraph::StaticClass()}) - , bStartNodePlacedAsGhostNode(false) - , PinConnectionPolicy() - , TemplateAsset(nullptr) - , FinishPolicy(EFlowFinishPolicy::Keep) - , PreloadPolicy() -{ - if (!AssetGuid.IsValid()) - { - AssetGuid = FGuid::NewGuid(); - } - - ExpectedOwnerClass = GetDefault()->GetDefaultExpectedOwnerClass(); -} - -#if WITH_EDITOR -void UFlowAsset::AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector) -{ - UFlowAsset* This = CastChecked(InThis); - Collector.AddReferencedObject(This->FlowGraph, This); - - Super::AddReferencedObjects(InThis, Collector); -} - -void UFlowAsset::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) -{ - Super::PostEditChangeProperty(PropertyChangedEvent); - - const FName ChangedPropertyName = PropertyChangedEvent.GetPropertyName(); - const FName ChangedMemberPropertyName = PropertyChangedEvent.GetMemberPropertyName(); - if (PropertyChangedEvent.Property && (ChangedPropertyName == GET_MEMBER_NAME_CHECKED(UFlowAsset, CustomInputs) - || ChangedPropertyName == GET_MEMBER_NAME_CHECKED(UFlowAsset, CustomOutputs) - || ChangedMemberPropertyName == GET_MEMBER_NAME_CHECKED(UFlowAsset, OutputDataPinDeclarations))) - { - OnSubGraphReconstructionRequested.ExecuteIfBound(); - } - - if (PropertyChangedEvent.Property && ChangedMemberPropertyName == GET_MEMBER_NAME_CHECKED(UFlowAsset, OutputDataPinDeclarations)) - { - for (const TPair& NodePair : GetNodes()) - { - UFlowNode_SetGraphOutput* SetOutputNode = Cast(NodePair.Value); - if (IsValid(SetOutputNode) && SetOutputNode->TryUpdateAutoDataPins()) - { - SetOutputNode->OnReconstructionRequested.ExecuteIfBound(); - } - } - } -} - -void UFlowAsset::PostDuplicate(bool bDuplicateForPIE) -{ - Super::PostDuplicate(bDuplicateForPIE); - - if (!bDuplicateForPIE) - { - AssetGuid = FGuid::NewGuid(); - Nodes.Empty(); - } -} - -void UFlowAsset::PostLoad() -{ - Super::PostLoad(); - - const UPackage* Package = GetPackage(); - if (IsValid(Package) && !FPackageName::IsTempPackage(Package->GetPathName())) - { - // If we removed or moved a flow node blueprint (and there is no redirector) we might loose the reference to it resulting - // in null pointers in the Nodes FGUID->UFlowNode* Map. So here we iterate over all the Nodes and remove all pairs that - // are nulled out. - - TSet NodesToRemoveGUID; - - for (const TPair& Node : GetNodes()) - { - if (!IsValid(Node.Value)) - { - NodesToRemoveGUID.Emplace(Node.Key); - } - } - - for (const FGuid& Guid : NodesToRemoveGUID) - { - UnregisterNode(Guid); - } - - ReconcileBaseAssetParams(FFlowAssetParamsUtils::GetLastSavedTimestampForObject(this)); - } -} - -void UFlowAsset::PreSaveRoot(FObjectPreSaveRootContext ObjectSaveContext) -{ - ReconcileBaseAssetParams(FDateTime::Now()); -} - -void UFlowAsset::ReconcileBaseAssetParams(const FDateTime& AssetLastSavedTimestamp) -{ - if (BaseAssetParams.AssetPtr.IsNull()) - { - return; - } - - UFlowAssetParams* BaseAssetParamsPtr = BaseAssetParams.AssetPtr.LoadSynchronous(); - if (!IsValid(BaseAssetParamsPtr)) - { - UE_LOG(LogFlow, Error, TEXT("Failed to load BaseAssetParams: %s"), *BaseAssetParams.AssetPtr.ToString()); - return; - } - - IFlowNamedPropertiesSupplierInterface* NamedPropertiesSupplier = Cast(GetDefaultEntryNode()); - if (!NamedPropertiesSupplier) - { - UE_LOG(LogFlow, Error, TEXT("No NamedPropertiesSupplier (e.g., Start node) found in FlowAsset: %s"), *GetPathName()); - return; - } - - TArray& MutableStartNodeProperties = NamedPropertiesSupplier->GetMutableNamedProperties(); - const EFlowReconcilePropertiesResult ReconcileResult = - BaseAssetParamsPtr->ReconcilePropertiesWithStartNode(AssetLastSavedTimestamp, this, MutableStartNodeProperties); - - if (EFlowReconcilePropertiesResult_Classifiers::IsErrorResult(ReconcileResult)) - { - UE_LOG(LogFlow, Error, TEXT("Failed to reconcile BaseAssetParams for %s: %s"), - *BaseAssetParamsPtr->GetPathName(), *UEnum::GetDisplayValueAsText(ReconcileResult).ToString()); - } -} - -UFlowAssetParams* UFlowAsset::GenerateParamsFromStartNode() -{ - if (BaseAssetParams.AssetPtr.IsValid()) - { - UE_LOG(LogFlow, Warning, TEXT("BaseAssetParams already exists for %s: %s"), *GetPathName(), *BaseAssetParams.AssetPtr.ToString()); - return BaseAssetParams.AssetPtr.LoadSynchronous(); - } - - // Get the Start node - IFlowNamedPropertiesSupplierInterface* NamedPropertiesSupplier = Cast(GetDefaultEntryNode()); - if (!NamedPropertiesSupplier) - { - UE_LOG(LogFlow, Error, TEXT("No valid Start node found for generating params in %s"), *GetPathName()); - return nullptr; - } - - // Determine the params asset name - const FString ParamsAssetName = GenerateParamsAssetName(); - if (ParamsAssetName.IsEmpty()) - { - UE_LOG(LogFlow, Error, TEXT("Generated empty params asset name for %s"), *GetPathName()); - return nullptr; - } - - // Create the params asset - FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); - const FString PackagePath = FPackageName::GetLongPackagePath(GetPackage()->GetPathName()); - FString UniquePackageName, UniqueAssetName; - AssetToolsModule.Get().CreateUniqueAssetName(PackagePath + TEXT("/") + ParamsAssetName, TEXT(""), UniquePackageName, UniqueAssetName); - - UFlowAssetParams* NewParams = Cast( - AssetToolsModule.Get().CreateAsset(UniqueAssetName, PackagePath, UFlowAssetParams::StaticClass(), nullptr)); - if (!IsValid(NewParams)) - { - UE_LOG(LogFlow, Error, TEXT("Failed to create Flow Asset Params: %s"), *UniqueAssetName); - return nullptr; - } - - // Reconfigure with the new properties - NewParams->ConfigureFlowAssetParams(this, nullptr, NamedPropertiesSupplier->GetMutableNamedProperties()); - - // Source control integration - 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()); - } - } - - // Assign to BaseAssetParams and sync Content Browser - BaseAssetParams.AssetPtr = NewParams; - - FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); - AssetRegistryModule.Get().AssetCreated(NewParams); - - FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked("ContentBrowser"); - TArray AssetsToSync = {NewParams}; - ContentBrowserModule.Get().SyncBrowserToAssets(AssetsToSync, true); - - return NewParams; -} - -FString UFlowAsset::GenerateParamsAssetName() const -{ - const FString FlowAssetName = GetName(); - - const int32 UnderscoreIndex = FlowAssetName.Find(TEXT("_"), ESearchCase::CaseSensitive); - - if (UnderscoreIndex != INDEX_NONE) - { - const FString Prefix = FlowAssetName.Left(UnderscoreIndex); - const FString Suffix = FlowAssetName.Mid(UnderscoreIndex + 1); - return FString::Printf(TEXT("%sParams_%s"), *Prefix, *Suffix); - } - else - { - return FlowAssetName + TEXT("Params"); - } -} - -EDataValidationResult UFlowAsset::ValidateAsset(FFlowMessageLog& MessageLog) -{ - // validate nodes - for (const TPair& Node : ObjectPtrDecay(Nodes)) - { - if (IsValid(Node.Value)) - { - FText FailureReason; - if (!IsNodeOrAddOnClassAllowed(Node.Value->GetClass(), &FailureReason)) - { - const FString ErrorMsg = - FailureReason.IsEmpty() - ? FString::Format(*ValidationError_NodeClassNotAllowed, {*Node.Value->GetClass()->GetName()}) - : FailureReason.ToString(); - - MessageLog.Error(*ErrorMsg, Node.Value); - } - - Node.Value->ValidationLog.Messages.Empty(); - Node.Value->ValidateNode(); - MessageLog.Messages.Append(Node.Value->ValidationLog.Messages); - - // Validate AddOns - for (UFlowNodeAddOn* AddOn : Node.Value->GetFlowNodeAddOnChildren()) - { - if (IsValid(AddOn)) - { - ValidateAddOnTree(*AddOn, MessageLog); - } - else - { - const FString ErrorMsg = FString::Format(*ValidationError_NullAddOnNodeInstance, {*Node.Key.ToString()}); - MessageLog.Error(*ErrorMsg, this); - } - } - } - else - { - const FString ErrorMsg = FString::Format(*ValidationError_NullNodeInstance, {*Node.Key.ToString()}); - MessageLog.Error(*ErrorMsg, this); - } - } - - // if at least one error has been has been logged : mark the asset as invalid - for (const TSharedRef& Msg : MessageLog.Messages) - { - if (Msg->GetSeverity() == EMessageSeverity::Error) - { - return EDataValidationResult::Invalid; - } - } - - // otherwise, the asset is considered valid (even with warnings or notes) - return EDataValidationResult::Valid; -} - -bool UFlowAsset::IsNodeOrAddOnClassAllowed(const UClass* FlowNodeOrAddOnClass, FText* OutOptionalFailureReason) const -{ - if (!IsValid(FlowNodeOrAddOnClass)) - { - return false; - } - - if (!CanFlowNodeClassBeUsedByFlowAsset(*FlowNodeOrAddOnClass)) - { - return false; - } - - if (!CanFlowAssetUseFlowNodeClass(*FlowNodeOrAddOnClass)) - { - return false; - } - - // Confirm plugin reference restrictions are being respected - if (!CanFlowAssetReferenceFlowNode(*FlowNodeOrAddOnClass, OutOptionalFailureReason)) - { - return false; - } - - return true; -} - -bool UFlowAsset::CanFlowNodeClassBeUsedByFlowAsset(const UClass& FlowNodeClass) const -{ - UFlowNode* NodeDefaults = Cast(FlowNodeClass.GetDefaultObject()); - if (!NodeDefaults) - { - check(FlowNodeClass.IsChildOf()); - - // AddOns don't have the AllowedAssetClasses/DeniedAssetClasses - // (yet? maybe we move it up to the base?) - return true; - } - - // UFlowNode class limits which UFlowAsset class can use it - const TArray>& DeniedAssetClasses = NodeDefaults->DeniedAssetClasses; - for (const UClass* DeniedAssetClass : DeniedAssetClasses) - { - if (DeniedAssetClass && GetClass()->IsChildOf(DeniedAssetClass)) - { - return false; - } - } - - const TArray>& AllowedAssetClasses = NodeDefaults->AllowedAssetClasses; - if (AllowedAssetClasses.Num() > 0) - { - bool bAllowedInAsset = false; - for (const UClass* AllowedAssetClass : AllowedAssetClasses) - { - if (AllowedAssetClass && GetClass()->IsChildOf(AllowedAssetClass)) - { - bAllowedInAsset = true; - break; - } - } - if (!bAllowedInAsset) - { - return false; - } - } - - return true; -} - -bool UFlowAsset::CanFlowAssetUseFlowNodeClass(const UClass& FlowNodeClass) const -{ - // UFlowAsset class can limit which UFlowNodeBase classes can be used - if (IsFlowNodeClassInDeniedClasses(FlowNodeClass)) - { - return false; - } - - if (!IsFlowNodeClassInAllowedClasses(FlowNodeClass)) - { - return false; - } - - return true; -} - -bool UFlowAsset::IsFlowNodeClassInDeniedClasses(const UClass& FlowNodeClass) const -{ - for (const TSubclassOf& DeniedNodeClass : DeniedNodeClasses) - { - if (DeniedNodeClass && FlowNodeClass.IsChildOf(DeniedNodeClass)) - { - // Subclasses of a DeniedNodeClass can opt back in to being allowed - if (!IsFlowNodeClassInAllowedClasses(FlowNodeClass, DeniedNodeClass)) - { - return true; - } - } - } - - return false; -} - -void UFlowAsset::ValidateAddOnTree(UFlowNodeAddOn& AddOn, FFlowMessageLog& MessageLog) -{ - // Filter unauthorized addon nodes - FText FailureReason; - if (!IsNodeOrAddOnClassAllowed(AddOn.GetClass(), &FailureReason)) - { - const FString ErrorMsg = - FailureReason.IsEmpty() - ? FString::Format(*ValidationError_AddOnNodeClassNotAllowed, {*AddOn.GetClass()->GetName()}) - : FailureReason.ToString(); - - MessageLog.Error(*ErrorMsg, AddOn.GetFlowNodeSelfOrOwner()); - } - - // Validate AddOn - AddOn.ValidationLog.Messages.Empty(); - AddOn.ValidateNode(); - MessageLog.Messages.Append(AddOn.ValidationLog.Messages); - - // Validate Children - for (UFlowNodeAddOn* Child : AddOn.GetFlowNodeAddOnChildren()) - { - if (IsValid(Child)) - { - ValidateAddOnTree(*Child, MessageLog); - } - } -} - -bool UFlowAsset::IsFlowNodeClassInAllowedClasses(const UClass& FlowNodeClass, - const TSubclassOf& RequiredAncestor) const -{ - if (AllowedNodeClasses.Num() > 0) - { - bool bAllowedInAsset = false; - for (const TSubclassOf& AllowedNodeClass : AllowedNodeClasses) - { - // If a RequiredAncestor is provided, the AllowedNodeClass must be a subclass of the RequiredAncestor - if (AllowedNodeClass && FlowNodeClass.IsChildOf(AllowedNodeClass) && (!RequiredAncestor || AllowedNodeClass->IsChildOf(RequiredAncestor))) - { - bAllowedInAsset = true; - - break; - } - } - - if (!bAllowedInAsset) - { - return false; - } - } - - return true; -} - -bool UFlowAsset::CanFlowAssetReferenceFlowNode(const UClass& FlowNodeClass, FText* OutOptionalFailureReason) const -{ - if (!GEditor || !IsValid(&FlowNodeClass)) - { - return false; - } - - // Confirm plugin reference restrictions are being respected - FAssetReferenceFilterContext AssetReferenceFilterContext; - AssetReferenceFilterContext.AddReferencingAsset(FAssetData(this)); - const TSharedPtr FlowAssetReferenceFilter = GEditor->MakeAssetReferenceFilter(AssetReferenceFilterContext); - if (FlowAssetReferenceFilter.IsValid()) - { - const FAssetData FlowNodeAssetData(&FlowNodeClass); - if (!FlowAssetReferenceFilter->PassesFilter(FlowNodeAssetData, OutOptionalFailureReason)) - { - return false; - } - } - - return true; -} - -UFlowNode* UFlowAsset::CreateNode(const UClass* NodeClass, UEdGraphNode* GraphNode) -{ - UFlowNode* NewNode = NewObject(this, NodeClass, NAME_None, RF_Transactional); - NewNode->SetGraphNode(GraphNode); - - RegisterNode(GraphNode->NodeGuid, NewNode); - return NewNode; -} - -void UFlowAsset::RegisterNode(const FGuid& NewGuid, UFlowNode* NewNode) -{ - NewNode->SetGuid(NewGuid); - Nodes.Emplace(NewGuid, NewNode); - - HarvestNodeConnections(); - - if (NewNode->TryUpdateAutoDataPins()) - { - (void)NewNode->OnReconstructionRequested.ExecuteIfBound(); - } -} - -void UFlowAsset::UnregisterNode(const FGuid& NodeGuid) -{ - Nodes.Remove(NodeGuid); - Nodes.Compact(); - - HarvestNodeConnections(); - - MarkPackageDirty(); -} - -void UFlowAsset::HarvestNodeConnections(UFlowNode* TargetNode) -{ - TArray TargetNodes; - - if (IsValid(TargetNode)) - { - TargetNodes.Reserve(1); - TargetNodes.Add(TargetNode); - } - else - { - TargetNodes.Reserve(Nodes.Num()); - for (const TPair& Pair : ObjectPtrDecay(Nodes)) - { - TargetNodes.Add(Pair.Value); - } - } - - // Remove any invalid nodes - for (auto NodeIt = TargetNodes.CreateIterator(); NodeIt; ++NodeIt) - { - if (*NodeIt == nullptr) - { - NodeIt.RemoveCurrent(); - Modify(); - } - } - - for (UFlowNode* FlowNode : TargetNodes) - { - bool bNodeDirty = false; - - TMap FoundConnections; - const TArray& GraphNodePins = FlowNode->GetGraphNode()->Pins; - - for (const UEdGraphPin* ThisPin : GraphNodePins) - { - const bool bIsExecPin = FFlowPin::IsExecPinCategory(ThisPin->PinType.PinCategory); - const bool bIsDataPin = !bIsExecPin; - const bool bIsOutputPin = (ThisPin->Direction == EGPD_Output); - const bool bIsInputPin = (ThisPin->Direction == EGPD_Input); - const bool bHasAtLeastOneConnection = ThisPin->LinkedTo.Num() > 0; - - if (bIsExecPin && bIsOutputPin && bHasAtLeastOneConnection) - { - // For Exec Pins, harvest the 0th connection (we should have only 1 connection, because of schema rules) - if (const UEdGraphPin* LinkedPin = ThisPin->LinkedTo[0]) - { - const UEdGraphNode* LinkedNode = LinkedPin->GetOwningNode(); - FoundConnections.Add(ThisPin->PinName, FConnectedPin(LinkedNode->NodeGuid, LinkedPin->PinName)); - } - } - else if (bIsDataPin && bIsInputPin && bHasAtLeastOneConnection) - { - // For Data Pins, harvest the 0th connection (we should have only 1 connection, because of schema rules) - if (const UEdGraphPin* LinkedPin = ThisPin->LinkedTo[0]) - { - const UEdGraphNode* LinkedNode = LinkedPin->GetOwningNode(); - FoundConnections.Add(ThisPin->PinName, FConnectedPin(LinkedNode->NodeGuid, LinkedPin->PinName)); - } - } - } - - // This check exists to ensure that we don't mark graph dirty, if none of connections changed - { - const TMap& OldConnections = FlowNode->Connections; - if (FoundConnections.Num() != OldConnections.Num()) - { - bNodeDirty = true; - } - else - { - for (const TPair& FoundConnection : FoundConnections) - { - if (const FConnectedPin* OldConnection = OldConnections.Find(FoundConnection.Key)) - { - if (FoundConnection.Value != *OldConnection) - { - bNodeDirty = true; - break; - } - } - else - { - bNodeDirty = true; - break; - } - } - } - } - - if (bNodeDirty) - { - FlowNode->SetFlags(RF_Transactional); - FlowNode->Modify(); - - FlowNode->SetConnections(FoundConnections); - FlowNode->PostEditChange(); - } - } -} - -bool UFlowAsset::TryGetDefaultForInputPinName(const FStructProperty& StructProperty, const void* Container, FString& OutString) -{ - // We also look in the USTRUCT for DefaultForInputFlowPin - const FString* DefaultForInputFlowPinName = StructProperty.Struct->FindMetaData(FFlowPin::MetadataKey_DefaultForInputFlowPin); - - if (DefaultForInputFlowPinName) - { - OutString = *DefaultForInputFlowPinName; - - return true; - } - - // For blueprint use, we allow the Value structs to set input pins via editor-only data - - const FFlowDataPinValue* DataPinValue = FlowStructUtils::CastStructValue(StructProperty, Container); - if (DataPinValue && DataPinValue->IsInputPin()) - { - OutString.Empty(); - - return true; - } - - return false; -} - -#endif - -UFlowNode* UFlowAsset::GetDefaultEntryNode() const -{ - UFlowNode* FirstStartNode = nullptr; - - for (const TPair& Node : ObjectPtrDecay(Nodes)) - { - if (UFlowNode_Start* StartNode = Cast(Node.Value)) - { - if (StartNode->GatherConnectedNodes().Num() > 0) - { - return StartNode; - } - else if (FirstStartNode == nullptr) - { - FirstStartNode = StartNode; - } - } - } - - // If none of the found start nodes have connections, fallback to the first start node we found - return FirstStartNode; -} - -#if WITH_EDITOR -void UFlowAsset::AddCustomInput(const FName& EventName) -{ - if (!CustomInputs.Contains(EventName)) - { - CustomInputs.Add(EventName); - } -} - -void UFlowAsset::RemoveCustomInput(const FName& EventName) -{ - if (CustomInputs.Contains(EventName)) - { - CustomInputs.Remove(EventName); - } -} - -void UFlowAsset::AddCustomOutput(const FName& EventName) -{ - if (!CustomOutputs.Contains(EventName)) - { - CustomOutputs.Add(EventName); - } -} - -void UFlowAsset::RemoveCustomOutput(const FName& EventName) -{ - if (CustomOutputs.Contains(EventName)) - { - CustomOutputs.Remove(EventName); - } -} -#endif // WITH_EDITOR - -UFlowNode_CustomInput* UFlowAsset::TryFindCustomInputNodeByEventName(const FName& EventName) const -{ - for (const TPair& Node : ObjectPtrDecay(Nodes)) - { - if (UFlowNode_CustomInput* CustomInput = Cast(Node.Value)) - { - if (CustomInput->GetEventName() == EventName) - { - return CustomInput; - } - } - } - - return nullptr; -} - -UFlowNode_CustomOutput* UFlowAsset::TryFindCustomOutputNodeByEventName(const FName& EventName) const -{ - for (const TPair& Node : ObjectPtrDecay(Nodes)) - { - if (UFlowNode_CustomOutput* CustomOutput = Cast(Node.Value)) - { - if (CustomOutput->GetEventName() == EventName) - { - return CustomOutput; - } - } - } - - return nullptr; -} - -TArray UFlowAsset::GatherCustomInputNodeEventNames() const -{ - // Runtime-safe gathering of the CustomInputs (which is editor-only data) - // from the actual flow nodes - TArray Results; - - for (const TPair& Node : ObjectPtrDecay(Nodes)) - { - if (UFlowNode_CustomInput* CustomInput = Cast(Node.Value)) - { - Results.Add(CustomInput->GetEventName()); - } - } - - return Results; -} - -TArray UFlowAsset::GatherCustomOutputNodeEventNames() const -{ - // Runtime-safe gathering of the CustomOutputs (which is editor-only data) - // from the actual flow nodes - TArray Results; - - for (const TPair& Node : ObjectPtrDecay(Nodes)) - { - if (UFlowNode_CustomOutput* CustomOutput = Cast(Node.Value)) - { - Results.Add(CustomOutput->GetEventName()); - } - } - - return Results; -} - -TArray UFlowAsset::GetNodesInExecutionOrder(UFlowNode* FirstIteratedNode, const TSubclassOf FlowNodeClass) const -{ - TArray FoundNodes; - GetNodesInExecutionOrder(FirstIteratedNode, FoundNodes); - - // filter out nodes by class - for (int32 i = FoundNodes.Num() - 1; i >= 0; i--) - { - if (!FoundNodes[i]->GetClass()->IsChildOf(FlowNodeClass)) - { - FoundNodes.RemoveAt(i); - } - } - FoundNodes.Shrink(); - - return FoundNodes; -} - -TArray UFlowAsset::GatherNodesConnectedToAllInputs() const -{ - TSet> IteratedNodes; - TArray ConnectedNodes; - - // Nodes connected to the Start node - UFlowNode* DefaultEntryNode = GetDefaultEntryNode(); - GetNodesInExecutionOrder_Recursive(DefaultEntryNode, IteratedNodes, ConnectedNodes); - - // Nodes connected to Custom Input node(s) - for (const TPair& Node : ObjectPtrDecay(Nodes)) - { - if (UFlowNode_CustomInput* CustomInput = Cast(Node.Value)) - { - GetNodesInExecutionOrder_Recursive(CustomInput, IteratedNodes, ConnectedNodes); - } - } - - return ConnectedNodes; -} - -TArray UFlowAsset::GatherPinsConnectedToPin(const FConnectedPin& Pin) const -{ - TArray ConnectedPins; - - // Connections are only stored on one of the Nodes they connect depending on pin type. - // As such, we need to iterate all Nodes to find all possible Connections for the Pin. - for (const auto& GuidNodePair : Nodes) - { - if (IsValid(GuidNodePair.Value)) - { - ConnectedPins.Append(GuidNodePair.Value->GetKnownConnectionsToPin(Pin)); - } - } - - return ConnectedPins; -} - -TArray UFlowAsset::GetAllNodes() const -{ - TArray> AllNodes; - AllNodes.Reserve(Nodes.Num()); - Nodes.GenerateValueArray(AllNodes); - - return ObjectPtrDecay(AllNodes); -} - -void UFlowAsset::AddInstance(UFlowAsset* Instance) -{ - ActiveInstances.Add(Instance); -} - -int32 UFlowAsset::RemoveInstance(UFlowAsset* Instance) -{ -#if WITH_EDITOR - if (InspectedInstance.IsValid() && InspectedInstance.Get() == Instance) - { - SetInspectedInstance(nullptr); - } -#endif - - ActiveInstances.Remove(Instance); - return ActiveInstances.Num(); -} - -void UFlowAsset::ClearInstances() -{ -#if WITH_EDITOR - if (InspectedInstance.IsValid()) - { - SetInspectedInstance(nullptr); - } -#endif - - for (int32 i = ActiveInstances.Num() - 1; i >= 0; i--) - { - if (ActiveInstances.IsValidIndex(i) && ActiveInstances[i]) - { - ActiveInstances[i]->FinishFlowAndDeinitializeInstance(EFlowFinishPolicy::Keep); - } - } - - ActiveInstances.Empty(); -} - -#if WITH_EDITOR -void UFlowAsset::SetInspectedInstance(TWeakObjectPtr NewInspectedInstance) -{ - if (NewInspectedInstance.IsValid()) - { - if (InspectedInstance == NewInspectedInstance) - { - // Nothing changed - return; - } - - bool bIsNewInstancePresent = Algo::AnyOf(ActiveInstances, [NewInspectedInstance](const UFlowAsset* ActiveInstance) - { - return ActiveInstance && ActiveInstance == NewInspectedInstance; - }); - - if (!ensureMsgf(bIsNewInstancePresent, TEXT("Trying to set %s as InspectedInstance, but it is not one of the ActiveInstances"), *NewInspectedInstance->GetName())) - { - NewInspectedInstance = nullptr; - } - } - - InspectedInstance = NewInspectedInstance; - BroadcastDebuggerRefresh(); -} - -void UFlowAsset::BroadcastDebuggerRefresh() const -{ - RefreshDebuggerEvent.Broadcast(); -} - -void UFlowAsset::BroadcastRuntimeMessageAdded(const TSharedRef& Message) const -{ - RuntimeMessageEvent.Broadcast(this, Message); -} - -void UFlowAsset::SetupForEditing() -{ - InitializePinConnectionPolicy(); - - // Initialize any customizable Policies before we instantiate nodes - InitializePreloadPolicy(); -} -#endif // WITH_EDITOR - -void UFlowAsset::InitializeInstance(const TWeakObjectPtr InOwner, UFlowAsset& InTemplateAsset) -{ - check(!IsInstanceInitialized()); - - Owner = InOwner; - TemplateAsset = &InTemplateAsset; - - // Initialize any customizable Policies before we instantiate nodes - InitializePreloadPolicy(); - - for (TPair>& Node : Nodes) - { - UFlowNode* NewNodeInstance = NewObject(this, Node.Value->GetClass(), NAME_None, RF_Transient, Node.Value, false, nullptr); - Node.Value = NewNodeInstance; - - if (UFlowNode_CustomInput* CustomInput = Cast(NewNodeInstance)) - { - if (!CustomInput->EventName.IsNone()) - { - CustomInputNodes.Emplace(CustomInput); - } - } - - NewNodeInstance->InitializeInstance(); - } -} - -void UFlowAsset::DeinitializeInstance() -{ - // These should have been flushed in FinishFlow() - check(DeferredTransitionScopes.IsEmpty()); - - if (IsInstanceInitialized()) - { - for (const TPair& Node : ObjectPtrDecay(Nodes)) - { - if (IsValid(Node.Value)) - { - Node.Value->DeinitializeInstance(); - } - } - - const int32 ActiveInstancesLeft = TemplateAsset->RemoveInstance(this); - if (ActiveInstancesLeft == 0 && GetFlowSubsystem()) - { - GetFlowSubsystem()->RemoveInstancedTemplate(TemplateAsset); - } - - TemplateAsset = nullptr; - } -} - -void UFlowAsset::FinishFlowAndDeinitializeInstance(const EFlowFinishPolicy InFinishPolicy) -{ - FinishFlow(InFinishPolicy); - DeinitializeInstance(); -} - -void UFlowAsset::PreStartFlow() -{ - ResetNodes(); - -#if WITH_EDITOR - check(IsInstanceInitialized()); - - if (TemplateAsset->ActiveInstances.Num() == 1) - { - // this instance is the only active one, set it directly as Inspected Instance - TemplateAsset->SetInspectedInstance(this); - } - else - { - // request to refresh list to show newly created instance - TemplateAsset->BroadcastDebuggerRefresh(); - } -#endif -} - -void UFlowAsset::StartFlow(IFlowDataPinValueSupplierInterface* DataPinValueSupplier, IFlowGraphOutputDataReceiverInterface* InOutputDataReceiver) -{ - InitializeOutputDataReceiverAndValues(InOutputDataReceiver); - - PreStartFlow(); - - if (UFlowNode* ConnectedEntryNode = GetDefaultEntryNode()) - { - RecordedNodes.Add(ConnectedEntryNode); - - if (IFlowNodeWithExternalDataPinSupplierInterface* ExternalPinSuppliedNode = Cast(ConnectedEntryNode)) - { - ExternalPinSuppliedNode->SetDataPinValueSupplier(DataPinValueSupplier); - } - - ConnectedEntryNode->TriggerFirstOutput(true); - } -} - -void UFlowAsset::InitializeOutputDataReceiverAndValues(IFlowGraphOutputDataReceiverInterface* InOutputDataReceiver) -{ - OutputDataReceiver = Cast(InOutputDataReceiver); - - // Initialize the live output store from the template asset's declarations - OutputDataPinValues.Values.Reset(); - - if (const UFlowAsset* Template = TemplateAsset.Get()) - { - for (const FFlowNamedDataPinProperty& Declaration : Template->OutputDataPinDeclarations) - { - if (Declaration.IsValid()) - { - OutputDataPinValues.Values.Add(Declaration.Name, Declaration.DataPinValue); - } - else - { - UE_LOG(LogFlow, Warning, TEXT("Invalid OutputDataPin %s"), *Declaration.Name.ToString()); - } - } - } -} - -void UFlowAsset::WriteOutputDataPinValue(const FName& PinName, const TInstancedStruct& Value) -{ - if (OutputDataPinValues.Values.Contains(PinName)) - { - OutputDataPinValues.Values[PinName] = Value; - } - else - { - UE_LOG(LogFlow, Warning, TEXT("Could not find pin named %s in WriteOutputDataPinValue"), *PinName.ToString()); - } -} - -void UFlowAsset::FlushOutputDataPinValuesToReceiver() -{ - if (IFlowGraphOutputDataReceiverInterface* Receiver = Cast(OutputDataReceiver.Get())) - { - // Do an immediate push to the receiver - Receiver->ReceiveOutputDataSnapshot(OutputDataPinValues); - } -} - -void UFlowAsset::FinishFlow(const EFlowFinishPolicy InFinishPolicy) -{ - FinishPolicy = InFinishPolicy; - - CancelAndWarnForUnflushedDeferredTriggers(); - - // end execution of this asset and all of its nodes - for (UFlowNode* Node : ActiveNodes) - { - Node->Deactivate(); - } - ActiveNodes.Empty(); -} - -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; - - const FString ToNodeName = ToNode ? ToNode->GetName() : TEXT(""); - const FString FromNodeName = FromNode ? FromNode->GetName() : TEXT(""); - - 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"), - *ToNodeName, - *Trigger.NodeGuid.ToString(), - *Trigger.PinName.ToString(), - *FromNodeName, - *Trigger.FromPin.NodeGuid.ToString(), - *Trigger.FromPin.PinName.ToString() - ); - } - } - - ClearAllDeferredTriggerScopes(); - } -} - -bool UFlowAsset::HasStartedFlow() const -{ - return RecordedNodes.Num() > 0; -} - -AActor* UFlowAsset::TryFindActorOwner() const -{ - UObject* OwnerObject = GetOwner(); - if (!IsValid(OwnerObject)) - { - return nullptr; - } - - // If the owner is already an Actor, return it directly - if (AActor* OwnerAsActor = Cast(OwnerObject)) - { - return OwnerAsActor; - } - - // If the owner is a Component, return its owning Actor - if (const UActorComponent* OwnerAsComponent = Cast(OwnerObject)) - { - return OwnerAsComponent->GetOwner(); - } - - return nullptr; -} - -TWeakObjectPtr UFlowAsset::GetFlowInstance(UFlowNode_SubGraph* SubGraphNode) const -{ - return ActiveSubGraphs.FindRef(SubGraphNode); -} - -void UFlowAsset::TriggerCustomInput_FromSubGraph(UFlowNode_SubGraph* SubGraphNode, const FName& EventName) const -{ - // 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). - - const TWeakObjectPtr FlowInstance = ActiveSubGraphs.FindRef(SubGraphNode); - if (FlowInstance.IsValid()) - { - FlowInstance->TriggerCustomInput(EventName, SubGraphNode); - } -} - -void UFlowAsset::TriggerCustomInput(const FName& EventName, IFlowDataPinValueSupplierInterface* DataPinValueSupplier) -{ - for (UFlowNode_CustomInput* CustomInputNode : CustomInputNodes) - { - if (CustomInputNode->EventName == EventName) - { - RecordedNodes.Add(CustomInputNode); - - // 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). - - if (IFlowNodeWithExternalDataPinSupplierInterface* ExternalPinSuppliedNode = Cast(CustomInputNode)) - { - ExternalPinSuppliedNode->SetDataPinValueSupplier(DataPinValueSupplier); - } - - CustomInputNode->ExecuteInput(EventName); - } - } -} - -void UFlowAsset::TriggerCustomOutput(const FName& EventName) -{ - if (NodeOwningThisAssetInstance.IsValid()) - { - // it's a SubGraph - NodeOwningThisAssetInstance->TriggerOutput(EventName); - } - else - { - // it's a Root Flow, so the intention here might be to call event on the Flow Component - if (UFlowComponent* FlowComponent = Cast(GetOwner())) - { - FlowComponent->DispatchRootFlowCustomEvent(this, EventName); - } - } -} - -void UFlowAsset::TriggerInput(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin) -{ - if (FFlowExecutionGate::IsHalted()) - { - // Halt always takes precedence for debugger correctness - EnqueueDeferredTrigger(NodeGuid, PinName, FromPin); - } - else if (ShouldDeferTriggers()) - { - // Defer only if we have an open the top scope - if (!DeferredTransitionScopes.IsEmpty() && DeferredTransitionScopes.Top()->IsOpen()) - { - EnqueueDeferredTrigger(NodeGuid, PinName, FromPin); - } - else - { - const TSharedPtr CurrentScope = PushDeferredTransitionScope(); - TriggerInputDirect(NodeGuid, PinName, FromPin); - PopDeferredTransitionScope(CurrentScope); - } - } - 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)) - { - ActiveNodes.Add(Node); - RecordedNodes.Add(Node); - } - - Node->TriggerInput(PinName); - } -} - -bool UFlowAsset::ShouldDeferTriggers() 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)) - { - ActiveNodes.Remove(Node); - - // if graph reached Finish and this asset instance was created by SubGraph node - if (Node->CanFinishGraph()) - { - if (IFlowGraphOutputDataReceiverInterface* Receiver = Cast(OutputDataReceiver.Get())) - { - Receiver->ReceiveOutputDataSnapshot(OutputDataPinValues); - } - - if (NodeOwningThisAssetInstance.IsValid()) - { - NodeOwningThisAssetInstance.Get()->TriggerFirstOutput(true); - - return; - } - - // if this instance is a Root Flow, we need to deregister it from the subsystem first. This will - // finalize and deinitialize the root flow. - if (Owner.IsValid()) - { - const TSet& RootFlowInstances = GetFlowSubsystem()->GetRootInstancesByOwner(Owner.Get()); - if (RootFlowInstances.Contains(this)) - { - GetFlowSubsystem()->FinishAndDeinitializeRootFlow(Owner.Get(), TemplateAsset, EFlowFinishPolicy::Keep); - - return; - } - } - - FinishFlow(EFlowFinishPolicy::Keep); - } - } -} - -void UFlowAsset::ResetNodes() -{ - for (UFlowNode* Node : RecordedNodes) - { - Node->ResetRecords(); - } - - RecordedNodes.Empty(); -} - -UFlowSubsystem* UFlowAsset::GetFlowSubsystem() const -{ - return Cast(GetOuter()); -} - -FName UFlowAsset::GetDisplayName() const -{ - return GetFName(); -} - -UFlowNode_SubGraph* UFlowAsset::GetNodeOwningThisAssetInstance() const -{ - return NodeOwningThisAssetInstance.Get(); -} - -UFlowAsset* UFlowAsset::GetParentInstance() const -{ - return NodeOwningThisAssetInstance.IsValid() ? NodeOwningThisAssetInstance.Get()->GetFlowAsset() : nullptr; -} - -FFlowAssetSaveData UFlowAsset::SaveInstance(TArray& SavedFlowInstances) -{ - FFlowAssetSaveData AssetRecord; - AssetRecord.WorldName = IsBoundToWorld() ? GetWorld()->GetName() : FString(); - AssetRecord.InstanceName = GetName(); - - // opportunity to collect data before serializing asset - OnSave(); - - // iterate nodes - TArray NodesInExecutionOrder; - GetNodesInExecutionOrder(GetDefaultEntryNode(), NodesInExecutionOrder); - for (UFlowNode* Node : NodesInExecutionOrder) - { - if (Node && Node->ShouldSave()) - { - // iterate SubGraphs - if (UFlowNode_SubGraph* SubGraphNode = Cast(Node)) - { - const TWeakObjectPtr SubFlowInstance = GetFlowInstance(SubGraphNode); - if (SubFlowInstance.IsValid()) - { - const FFlowAssetSaveData SubAssetRecord = SubFlowInstance->SaveInstance(SavedFlowInstances); - SubGraphNode->SavedAssetInstanceName = SubAssetRecord.InstanceName; - } - } - - FFlowNodeSaveData NodeRecord; - Node->SaveInstance(NodeRecord); - - AssetRecord.NodeRecords.Emplace(NodeRecord); - } - } - - // serialize asset - FMemoryWriter MemoryWriter(AssetRecord.AssetData, true); - FFlowArchive Ar(MemoryWriter); - Serialize(Ar); - - // write archive to SaveGame - SavedFlowInstances.Emplace(AssetRecord); - - return AssetRecord; -} - -void UFlowAsset::LoadInstance(const FFlowAssetSaveData& AssetRecord) -{ - FMemoryReader MemoryReader(AssetRecord.AssetData, true); - FFlowArchive Ar(MemoryReader); - Serialize(Ar); - - PreStartFlow(); - - // iterate graph "from the end", backward to execution order - // prevents issue when the preceding node would instantly fire output to a not-yet-loaded node - for (int32 i = AssetRecord.NodeRecords.Num() - 1; i >= 0; i--) - { - if (UFlowNode* Node = Nodes.FindRef(AssetRecord.NodeRecords[i].NodeGuid)) - { - Node->LoadInstance(AssetRecord.NodeRecords[i]); - } - } - - OnLoad(); -} - -void UFlowAsset::OnActivationStateLoaded(UFlowNode* Node) -{ - if (Node->ActivationState != EFlowNodeState::NeverActivated) - { - RecordedNodes.Emplace(Node); - } - - if (Node->ActivationState == EFlowNodeState::Active) - { - ActiveNodes.Emplace(Node); - } -} - -void UFlowAsset::OnSave_Implementation() -{ -} - -void UFlowAsset::OnLoad_Implementation() -{ -} - -bool UFlowAsset::IsBoundToWorld_Implementation() const -{ - return bWorldBound; -} - -const FFlowPinConnectionPolicy& UFlowAsset::GetPinConnectionPolicy() const -{ - // Runtime instances delegate to their template, which holds the serialized policy - if (!PinConnectionPolicy.IsValid() && IsValid(TemplateAsset)) - { - return TemplateAsset->GetPinConnectionPolicy(); - } - - // Graceful fallback: if PinConnectionPolicy was never initialized (asset predates this feature, - // or was never opened in editor), read directly from project settings at runtime. - if (!PinConnectionPolicy.IsValid()) - { - const FFlowPinConnectionPolicy* SettingsPolicy = GetDefault()->GetPinConnectionPolicy(); - ensureAlways(SettingsPolicy); - if (SettingsPolicy) - { - return *SettingsPolicy; - } - } - - check(PinConnectionPolicy.IsValid()); - return PinConnectionPolicy.Get(); -} - -const FFlowPreloadPolicy& UFlowAsset::GetPreloadPolicy() const -{ - checkf(PreloadPolicy.IsValid(), TEXT("PreloadPolicy must be initialized prior to calling GetPreloadPolicy()")); - return PreloadPolicy.Get(); -} - -#if WITH_EDITOR - -void UFlowAsset::InitializePinConnectionPolicy() -{ - const FInstancedStruct& SourceStruct = GetDefault()->PinConnectionPolicy; - if (ensure(SourceStruct.IsValid())) - { - PinConnectionPolicy.InitializeAsScriptStruct(SourceStruct.GetScriptStruct(), SourceStruct.GetMemory()); - } -} - -#endif - -void UFlowAsset::InitializePreloadPolicy() -{ - if (PreloadPolicy.IsValid()) - { - // use per-class policy - PreloadPolicy.InitializeAsScriptStruct(PreloadPolicy.GetScriptStruct(), PreloadPolicy.GetMemory()); - } - else - { - // fallback to project's default policy - const FInstancedStruct& DefaultPolicy = GetDefault()->PreloadPolicy; - if (ensure(DefaultPolicy.IsValid())) - { - PreloadPolicy.InitializeAsScriptStruct(DefaultPolicy.GetScriptStruct(), DefaultPolicy.GetMemory()); - } - } - - ensureAlwaysMsgf(PreloadPolicy.IsValid(), TEXT("There's no valid Preload Policy set in the project!")); -} - -#if WITH_EDITOR - -void UFlowAsset::LogError(const FString& MessageToLog, const UFlowNodeBase* Node) const -{ - LogRuntimeMessage(EMessageSeverity::Error, MessageToLog, Node); -} - -void UFlowAsset::LogWarning(const FString& MessageToLog, const UFlowNodeBase* Node) const -{ - LogRuntimeMessage(EMessageSeverity::Warning, MessageToLog, Node); -} - -void UFlowAsset::LogNote(const FString& MessageToLog, const UFlowNodeBase* Node) const -{ - LogRuntimeMessage(EMessageSeverity::Info, MessageToLog, Node); -} - -void UFlowAsset::LogRuntimeMessage(EMessageSeverity::Type Severity, const FString& MessageToLog, const UFlowNodeBase* Node) const -{ - // this is runtime log which should only be called on runtime instances of asset - if (TemplateAsset) - { - UE_LOG(LogFlow, Log, TEXT("Attempted to use Runtime Log on asset instance %s"), *MessageToLog); - } - - if (RuntimeLog.Get()) - { - TSharedPtr TokenizedMessage = nullptr; - switch (Severity) - { - case EMessageSeverity::Error: - TokenizedMessage = RuntimeLog.Get()->Error(*MessageToLog, Node); - break; - - case EMessageSeverity::Warning: - TokenizedMessage = RuntimeLog.Get()->Warning(*MessageToLog, Node); - break; - - default: - TokenizedMessage = RuntimeLog.Get()->Note(*MessageToLog, Node); - break; - } - - BroadcastRuntimeMessageAdded(TokenizedMessage.ToSharedRef()); - } -} -#endif \ No newline at end of file diff --git a/Source/FlowAsset.h.ourLatest b/Source/FlowAsset.h.ourLatest deleted file mode 100644 index e1f0984b..00000000 --- a/Source/FlowAsset.h.ourLatest +++ /dev/null @@ -1,567 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors -#pragma once - -#include "FlowSave.h" -#include "FlowTypes.h" -#include "Asset/FlowAssetParamsTypes.h" -#include "Asset/FlowDeferredTransitionScope.h" -#include "Interfaces/FlowGraphOutputDataReceiverInterface.h" -#include "Nodes/FlowNode.h" -#include "Types/FlowDataPinValue.h" -#include "Types/FlowNamedDataPinProperty.h" -#include "Types/FlowOutputDataPinValues.h" - -#if WITH_EDITOR -#include "FlowMessageLog.h" -#endif - -#include "StructUtils/InstancedStruct.h" -#include "Templates/SharedPointer.h" -#include "UObject/ObjectKey.h" - -#include "FlowAsset.generated.h" - -class UFlowNode_CustomOutput; -class UFlowNode_CustomInput; -class UFlowNode_SubGraph; -class UFlowSubsystem; -struct FFlowPreloadPolicy; -struct FFlowPinConnectionPolicy; - -class UEdGraph; -class UEdGraphNode; -class UFlowAsset; -class UFlowAssetParams; - -#if !UE_BUILD_SHIPPING -DECLARE_DELEGATE(FFlowGraphEvent); -DECLARE_DELEGATE_TwoParams(FFlowSignalEvent, UFlowNode* /*FlowNode*/, const FName& /*PinName*/); -#endif - -/** - * Asset containing Flow nodes organized as non-linear graph. - */ -UCLASS(BlueprintType, hideCategories = Object) -class FLOW_API UFlowAsset : public UObject -{ - GENERATED_UCLASS_BODY() - -public: - friend class UFlowNode; - friend class UFlowNode_CustomOutput; - friend class UFlowNode_SubGraph; - friend class UFlowSubsystem; - - 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. */ - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Flow Asset") - bool bWorldBound; - -////////////////////////////////////////////////////////////////////////// -// Graph (editor-only) - -public: -#if WITH_EDITOR -public: - friend class UFlowGraph; - - // UObject - static void AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector); - virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; - virtual void PostDuplicate(bool bDuplicateForPIE) override; - virtual void PostLoad() override; - virtual void PreSaveRoot(FObjectPreSaveRootContext ObjectSaveContext) override; - // -- -#endif - -#if WITH_EDITORONLY_DATA -public: - FSimpleDelegate OnDetailsRefreshRequested; - - static FString ValidationError_NodeClassNotAllowed; - static FString ValidationError_AddOnNodeClassNotAllowed; - static FString ValidationError_NullNodeInstance; - static FString ValidationError_NullAddOnNodeInstance; - -private: - UPROPERTY() - TObjectPtr FlowGraph; -#endif - -#if WITH_EDITOR -public: - void SetupForEditing(); - - UEdGraph* GetGraph() const { return FlowGraph; } - - virtual EDataValidationResult ValidateAsset(FFlowMessageLog& MessageLog); - - /* 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(); } - -protected: - bool CanFlowNodeClassBeUsedByFlowAsset(const UClass& FlowNodeClass) const; - bool CanFlowAssetUseFlowNodeClass(const UClass& FlowNodeClass) const; - bool CanFlowAssetReferenceFlowNode(const UClass& FlowNodeClass, FText* OutOptionalFailureReason = nullptr) const; - - bool IsFlowNodeClassInAllowedClasses(const UClass& FlowNodeClass, const TSubclassOf& RequiredAncestor = nullptr) const; - bool IsFlowNodeClassInDeniedClasses(const UClass& FlowNodeClass) const; - -private: - /* Recursively validates the given addon and its children. */ - void ValidateAddOnTree(UFlowNodeAddOn& AddOn, FFlowMessageLog& MessageLog); -#endif - -////////////////////////////////////////////////////////////////////////// -// Nodes - -protected: - TArray> AllowedNodeClasses; - TArray> DeniedNodeClasses; - - TArray> AllowedInSubgraphNodeClasses; - TArray> DeniedInSubgraphNodeClasses; - - bool bStartNodePlacedAsGhostNode; - -private: - UPROPERTY() - TMap> Nodes; - -public: - const TArray& GetOutputDataPinDeclarations() const { return OutputDataPinDeclarations; } - -protected: - /* Output Data Pins define typed data values that this graph produces when it finishes. - * Sub Graph node using this Flow Asset will generate a context Output Data Pin for every entry on this list. */ - UPROPERTY(EditAnywhere, Category = "Sub Graph") - TArray OutputDataPinDeclarations; - -public: -#if WITH_EDITOR - FFlowGraphEvent OnSubGraphReconstructionRequested; - - UFlowNode* CreateNode(const UClass* NodeClass, UEdGraphNode* GraphNode); - - 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). */ - void HarvestNodeConnections(UFlowNode* TargetNode = nullptr); - - static bool TryGetDefaultForInputPinName(const FStructProperty& StructProperty, const void* Container, FString& OutString); -#endif - -public: - const TMap& GetNodes() const { return ObjectPtrDecay(Nodes); } - TArray GetAllNodes() const; - - UFlowNode* GetNode(const FGuid& Guid) const { return Nodes.FindRef(Guid); } - - template - T* GetNode(const FGuid& Guid) const - { - static_assert(TPointerIsConvertibleFromTo::Value, "'T' template parameter to GetNode must be derived from UFlowNode"); - - if (UFlowNode* Node = Nodes.FindRef(Guid)) - { - return Cast(Node); - } - - return nullptr; - } - - UFUNCTION(BlueprintPure, Category = "FlowAsset", meta = (DeterminesOutputType = "FlowNodeClass")) - TArray GetNodesInExecutionOrder(UFlowNode* FirstIteratedNode, const TSubclassOf FlowNodeClass) const; - - template - void GetNodesInExecutionOrder(UFlowNode* FirstIteratedNode, TArray& OutNodes) const - { - static_assert(TPointerIsConvertibleFromTo::Value, "'T' template parameter to GetNodesInExecutionOrder must be derived from UFlowNode"); - - if (FirstIteratedNode) - { - TSet> IteratedNodes; - GetNodesInExecutionOrder_Recursive(FirstIteratedNode, IteratedNodes, OutNodes); - } - } - -protected: - template - void GetNodesInExecutionOrder_Recursive(UFlowNode* Node, TSet>& IteratedNodes, TArray& OutNodes) const - { - IteratedNodes.Add(Node); - - if (T* NodeOfRequiredType = Cast(Node)) - { - OutNodes.Emplace(NodeOfRequiredType); - } - - for (UFlowNode* ConnectedNode : Node->GatherConnectedNodes()) - { - if (ConnectedNode && !IteratedNodes.Contains(ConnectedNode)) - { - GetNodesInExecutionOrder_Recursive(ConnectedNode, IteratedNodes, OutNodes); - } - } - } - -public: - UFUNCTION(BlueprintPure, Category = "FlowAsset") - virtual UFlowNode* GetDefaultEntryNode() const; - -////////////////////////////////////////////////////////////////////////// -// Custom Inputs/Outputs - -#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. */ - 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. */ - UPROPERTY(EditAnywhere, Category = "Sub Graph") - TArray CustomOutputs; -#endif - -public: - /* Gathers all the nodes that are connected to the Start & Custom Inputs of the flow graph. */ - TArray GatherNodesConnectedToAllInputs() const; - - UFlowNode_CustomInput* TryFindCustomInputNodeByEventName(const FName& EventName) const; - UFlowNode_CustomOutput* TryFindCustomOutputNodeByEventName(const FName& EventName) const; - - TArray GatherCustomInputNodeEventNames() const; - TArray GatherCustomOutputNodeEventNames() const; - -#if WITH_EDITOR - const TArray& GetCustomInputs() const { return CustomInputs; } - const TArray& GetCustomOutputs() const { return CustomOutputs; } - -protected: - void AddCustomInput(const FName& EventName); - void RemoveCustomInput(const FName& EventName); - - void AddCustomOutput(const FName& EventName); - void RemoveCustomOutput(const FName& EventName); -#endif - -////////////////////////////////////////////////////////////////////////// -// Pin connections - -protected: - /* Policy for UFlowGraphSchema (and others) to use to enforce pin connectivity. - * Also used at runtime by predicates (e.g., CompareValues) for type classification queries. */ - UPROPERTY(VisibleAnywhere, AdvancedDisplay, Category = PinConnection) - TInstancedStruct PinConnectionPolicy; - -public: -#if WITH_EDITOR - /* Override these functions to set up unique policy(ies) for a UFlowAsset subclass */ - virtual void InitializePinConnectionPolicy(); -#endif - - const FFlowPinConnectionPolicy& GetPinConnectionPolicy() const; - - /* Return all other Pins connected to the passed Pin. */ - TArray GatherPinsConnectedToPin(const FConnectedPin& Pin) const; - -////////////////////////////////////////////////////////////////////////// -// FlowAssetParams support (Start node params for a Flow graph) - - /* Default parameters asset for this Flow Asset (optional). */ - UPROPERTY(EditAnywhere, Category = FlowAssetParams, meta = (ShowCreateNew, HideChildParams)) - FFlowAssetParamsPtr BaseAssetParams; - -#if WITH_EDITOR - /* 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. */ - virtual FString GenerateParamsAssetName() const; - -protected: - - void ReconcileBaseAssetParams(const FDateTime& AssetLastSavedTimestamp); -#endif - -////////////////////////////////////////////////////////////////////////// -// Instances of the template asset - -private: - /* 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. */ - TSharedPtr RuntimeLog; -#endif - -public: - void AddInstance(UFlowAsset* Instance); - int32 RemoveInstance(UFlowAsset* Instance); - TConstArrayView> GetActiveInstances() const { return ActiveInstances; } - - void ClearInstances(); - int32 GetInstancesNum() const { return ActiveInstances.Num(); } - -#if WITH_EDITOR - void SetInspectedInstance(TWeakObjectPtr NewInspectedInstance); - const UFlowAsset* GetInspectedInstance() const { return InspectedInstance.IsValid() ? InspectedInstance.Get() : nullptr; } - - DECLARE_EVENT(UFlowAsset, FRefreshDebuggerEvent); - - FRefreshDebuggerEvent& OnDebuggerRefresh() { return RefreshDebuggerEvent; } - FRefreshDebuggerEvent RefreshDebuggerEvent; - - DECLARE_EVENT_TwoParams(UFlowAsset, FRuntimeMessageEvent, const UFlowAsset*, const TSharedRef&); - - FRuntimeMessageEvent& OnRuntimeMessageAdded() { return RuntimeMessageEvent; } - FRuntimeMessageEvent RuntimeMessageEvent; - -private: - void BroadcastDebuggerRefresh() const; - void BroadcastRuntimeMessageAdded(const TSharedRef& Message) const; -#endif - -////////////////////////////////////////////////////////////////////////// -// Executing asset instance - -protected: - 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. */ - TWeakObjectPtr Owner; - - /* SubGraph node that created this Flow Asset instance. */ - TWeakObjectPtr NodeOwningThisAssetInstance; - - /* 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. */ - UPROPERTY() - TSet> CustomInputNodes; - - /* Nodes that have any work left, not marked as Finished yet. */ - UPROPERTY() - TArray> ActiveNodes; - - /* All nodes active in the past, done their work. */ - UPROPERTY() - TArray> RecordedNodes; - - UPROPERTY(Transient) - EFlowFinishPolicy FinishPolicy; - - /* Receiver that will be given a snapshot of OutputDataPinValues when this graph finishes. - * Typically the SubGraph node that created this instance. */ - UPROPERTY(Transient) - TWeakObjectPtr OutputDataReceiver; - - /* Live output data pin values for this running instance. - * Initialized from OutputDataPinDeclarations defaults at StartFlow; updated by SetGraphOutput/Finish nodes. */ - UPROPERTY(Transient) - FFlowOutputDataPinValues OutputDataPinValues; - -public: - virtual void InitializeInstance(const TWeakObjectPtr InOwner, UFlowAsset& InTemplateAsset); - virtual void DeinitializeInstance(); - bool IsInstanceInitialized() const { return IsValid(TemplateAsset); } - - void FinishFlowAndDeinitializeInstance(const EFlowFinishPolicy InFinishPolicy); - - 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. */ - UFUNCTION(BlueprintPure, Category = "Flow") - UObject* GetOwner() const { return Owner.Get(); } - - template - TWeakObjectPtr GetOwner() const - { - return Owner.IsValid() ? Cast(Owner) : nullptr; - } - - /* 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; - - virtual void PreStartFlow(); - virtual void StartFlow(IFlowDataPinValueSupplierInterface* DataPinValueSupplier = nullptr, IFlowGraphOutputDataReceiverInterface* InOutputDataReceiver = nullptr); - - /* Write a single output data pin value into the live store for this running instance. - * Called by SetGraphOutput and Finish nodes for each connected output pin. */ - void WriteOutputDataPinValue(const FName& PinName, const TInstancedStruct& Value); - - /* Flush all of the OutputDataPinValues to the receiver (if set) */ - void FlushOutputDataPinValuesToReceiver(); - - virtual void FinishFlow(const EFlowFinishPolicy InFinishPolicy); - - bool HasStartedFlow() const; - -protected: - virtual void FinishNode(UFlowNode* Node); - void ResetNodes(); - - void InitializeOutputDataReceiverAndValues(IFlowGraphOutputDataReceiverInterface* InOutputDataReceiver); - -public: - UFlowSubsystem* GetFlowSubsystem() const; - FName GetDisplayName() const; - - UFlowNode_SubGraph* GetNodeOwningThisAssetInstance() const; - UFlowAsset* GetParentInstance() const; - - /* Get Flow Asset instance created by the given SubGraph node. */ - TWeakObjectPtr GetFlowInstance(UFlowNode_SubGraph* SubGraphNode) const; - - /* 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. */ - UFUNCTION(BlueprintPure, Category = "Flow") - const TArray& GetActiveNodes() const { return ActiveNodes; } - - /* Returns nodes active in the past, done their work. */ - UFUNCTION(BlueprintPure, Category = "Flow") - const TArray& GetRecordedNodes() const { return RecordedNodes; } - -////////////////////////////////////////////////////////////////////////// -// Preload policy - -protected: - /* Policy controlling when nodes implementing IFlowPreloadableInterface preload and flush their content. - * Initialized from UFlowSettings defaults. Override InitializePreloadPolicy() in a subclass to set a unique policy. */ - UPROPERTY(VisibleAnywhere, AdvancedDisplay, Category = Preload) - TInstancedStruct PreloadPolicy; - - /* Override these functions to set up unique policy(ies) for a UFlowAsset subclass. */ - virtual void InitializePreloadPolicy(); - -public: - const FFlowPreloadPolicy& GetPreloadPolicy() const; - -////////////////////////////////////////////////////////////////////////// -// Trigger Input - -#if !UE_BUILD_SHIPPING -public: - FFlowSignalEvent OnPinTriggered; -#endif - -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; - -public: - void TriggerCustomInput(const FName& EventName, IFlowDataPinValueSupplierInterface* DataPinValueSupplier = nullptr); - - void TriggerCustomInput_FromSubGraph(UFlowNode_SubGraph* Node, const FName& EventName) const; - void TriggerCustomOutput(const FName& EventName); - - /* todo: Extend FromPin through to Node level Trigger functions. */ - virtual void TriggerInput(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin); - -protected: - /* Trigger the node directly (no deferral, no new scope). */ - void TriggerInputDirect(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin); - - /* Allow subclasses to disable the standard defer trigger mechanism */ - virtual bool ShouldDeferTriggers() const; - -protected: - void EnqueueDeferredTrigger(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin); - TSharedPtr PushDeferredTransitionScope(); - void PopDeferredTransitionScope(const TSharedPtr& Scope) { TryFlushAndRemoveDeferredTransitionScope(Scope); } - - bool TryFlushAndRemoveDeferredTransitionScope(const TSharedPtr& Scope); - -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: - 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; - -////////////////////////////////////////////////////////////////////////// -// Expected Owner Class support - -protected: - /* 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; - -public: - UClass* GetExpectedOwnerClass() const { return ExpectedOwnerClass; } - -////////////////////////////////////////////////////////////////////////// -// SaveGame support - -public: - UFUNCTION(BlueprintCallable, Category = "SaveGame") - FFlowAssetSaveData SaveInstance(TArray& SavedFlowInstances); - - UFUNCTION(BlueprintCallable, Category = "SaveGame") - void LoadInstance(const FFlowAssetSaveData& AssetRecord); - -protected: - virtual void OnActivationStateLoaded(UFlowNode* Node); - - UFUNCTION(BlueprintNativeEvent, Category = "SaveGame") - void OnSave(); - - UFUNCTION(BlueprintNativeEvent, Category = "SaveGame") - void OnLoad(); - -public: - UFUNCTION(BlueprintNativeEvent, Category = "SaveGame") - bool IsBoundToWorld() const; - -////////////////////////////////////////////////////////////////////////// -// Utils - -#if WITH_EDITOR -public: - void LogError(const FString& MessageToLog, const UFlowNodeBase* Node) const; - void LogWarning(const FString& MessageToLog, const UFlowNodeBase* Node) const; - void LogNote(const FString& MessageToLog, const UFlowNodeBase* Node) const; - -private: - /* Shared implementation for LogError/LogWarning/LogNote to avoid code duplication. */ - void LogRuntimeMessage(EMessageSeverity::Type Severity, const FString& MessageToLog, const UFlowNodeBase* Node) const; -#endif -}; \ No newline at end of file diff --git a/docs/.gitattributes b/docs/.gitattributes deleted file mode 100644 index d18bf85f..00000000 --- a/docs/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Enforce LF line endings for all text files -* text=auto eol=lf diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index 0d010b15..00000000 --- a/docs/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -_site/ -.jekyll-cache/ -.jekyll-metadata -Gemfile.lock