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] 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;