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/FlowAsset.cpp b/Source/Flow/Private/FlowAsset.cpp index caae5b4a..7225988b 100644 --- a/Source/Flow/Private/FlowAsset.cpp +++ b/Source/Flow/Private/FlowAsset.cpp @@ -15,6 +15,7 @@ #include "Nodes/Graph/FlowNode_Start.h" #include "Nodes/Graph/FlowNode_SubGraph.h" #include "Policies/FlowPinConnectionPolicy.h" +#include "Policies/FlowPreloadPolicy.h" #include "Types/FlowDataPinValue.h" #include "Types/FlowStructUtils.h" @@ -55,6 +56,7 @@ UFlowAsset::UFlowAsset(const FObjectInitializer& ObjectInitializer) , TemplateAsset(nullptr) , FinishPolicy(EFlowFinishPolicy::Keep) , PinConnectionPolicy() + , PreloadPolicy() { if (!AssetGuid.IsValid()) { @@ -70,6 +72,7 @@ void UFlowAsset::PostInitProperties() #if WITH_EDITOR InitializePinConnectionPolicy(); + InitializePreloadPolicy(); #endif } @@ -1012,13 +1015,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) { @@ -1104,6 +1100,19 @@ AActor* UFlowAsset::TryFindActorOwner() const return OwnerAsActor; } + // If the owner is a Component, return its owning Actor + if (const UActorComponent* OwnerAsComponent = Cast(OwnerObject)) + { + { + return nullptr; + } + + // If the owner is already an Actor, return it directly + if (AActor* OwnerAsActor = Cast(OwnerObject)) + { + return OwnerAsActor; + } + // If the owner is a Component, return its owning Actor if (const UActorComponent* OwnerAsComponent = Cast(OwnerObject)) { @@ -1449,6 +1458,7 @@ const FFlowPinConnectionPolicy& UFlowAsset::GetPinConnectionPolicy() const } // Graceful fallback: if PinConnectionPolicy was never initialized (asset predates this feature, + // or was never opened in editor), read directly from project settings at runtime. // or was never opened in editor), read directly from Project Settings at runtime. if (!PinConnectionPolicy.IsValid()) { @@ -1464,6 +1474,31 @@ const FFlowPinConnectionPolicy& UFlowAsset::GetPinConnectionPolicy() const return PinConnectionPolicy.Get(); } +const FFlowPreloadPolicy& UFlowAsset::GetPreloadPolicy() const +{ + // Runtime instances delegate to their template, which holds the serialized policy. + if (!PreloadPolicy.IsValid() && IsValid(TemplateAsset)) + { + return TemplateAsset->GetPreloadPolicy(); + } +} + + // Graceful fallback: if PreloadPolicy was never initialized (asset predates this feature, + // or was never opened in editor), read directly from project settings at runtime. + if (!PreloadPolicy.IsValid()) + { + const FFlowPreloadPolicy* SettingsPolicy = GetDefault()->GetPreloadPolicy(); + ensureAlways(SettingsPolicy); + if (SettingsPolicy) + { + return *SettingsPolicy; + } + } + + check(PreloadPolicy.IsValid()); + return PreloadPolicy.Get(); +} + #if WITH_EDITOR void UFlowAsset::InitializePinConnectionPolicy() @@ -1475,6 +1510,15 @@ void UFlowAsset::InitializePinConnectionPolicy() } } +void UFlowAsset::InitializePreloadPolicy() +{ + const FInstancedStruct& SourceStruct = GetDefault()->PreloadPolicy; + if (ensure(SourceStruct.IsValid())) + { + PreloadPolicy.InitializeAsScriptStruct(SourceStruct.GetScriptStruct(), SourceStruct.GetMemory()); + } +} + void UFlowAsset::LogError(const FString& MessageToLog, const UFlowNodeBase* Node) const { LogRuntimeMessage(EMessageSeverity::Error, MessageToLog, Node); diff --git a/Source/Flow/Private/FlowSettings.cpp b/Source/Flow/Private/FlowSettings.cpp index 4a7ab3d2..b000394a 100644 --- a/Source/Flow/Private/FlowSettings.cpp +++ b/Source/Flow/Private/FlowSettings.cpp @@ -2,6 +2,9 @@ #include "FlowSettings.h" #include "FlowComponent.h" +#include "Policies/FlowPreloadPolicy.h" +#include "Policies/FlowStandardPinConnectionPolicies.h" +#include "Policies/FlowStandardPreloadPolicies.h" #include "Policies/FlowStandardPinConnectionPolicies.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(FlowSettings) @@ -9,6 +12,7 @@ UFlowSettings::UFlowSettings(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) , PinConnectionPolicy(FFlowPinConnectionPolicy_VeryRelaxed::StaticStruct()) + , PreloadPolicy(FFlowPreloadPolicy_Standard::StaticStruct()) , bDeferTriggeredOutputsWhileTriggering(true) , bLogOnSignalDisabled(true) , bLogOnSignalPassthrough(true) @@ -24,7 +28,13 @@ 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); @@ -34,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/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/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/FlowAsset.h b/Source/Flow/Public/FlowAsset.h index 5570f78e..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,7 @@ class UFlowNode_CustomOutput; class UFlowNode_CustomInput; class UFlowNode_SubGraph; class UFlowSubsystem; +struct FFlowPreloadPolicy; struct FFlowPinConnectionPolicy; class UEdGraph; @@ -305,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; @@ -341,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); @@ -398,14 +394,21 @@ class FLOW_API UFlowAsset : public UObject 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 diff --git a/Source/Flow/Public/FlowSettings.h b/Source/Flow/Public/FlowSettings.h index 879ff3c7..c86d3787 100644 --- a/Source/Flow/Public/FlowSettings.h +++ b/Source/Flow/Public/FlowSettings.h @@ -8,6 +8,7 @@ #include "FlowSettings.generated.h" struct FFlowPinConnectionPolicy; +struct FFlowPreloadPolicy; /** * Mostly runtime settings of the Flow Graph. @@ -19,7 +20,10 @@ class FLOW_API UFlowSettings : public UDeveloperSettings // 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 @@ -28,6 +32,9 @@ class FLOW_API UFlowSettings : public UDeveloperSettings 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/FlowPinTypeMatchPolicy.h b/Source/Flow/Public/Policies/FlowPinTypeMatchPolicy.h index fc675e54..ce1e9177 100644 --- a/Source/Flow/Public/Policies/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() @@ -37,6 +39,6 @@ struct FFlowPinTypeMatchPolicy EFlowPinTypeMatchRules PinTypeMatchRules = EFlowPinTypeMatchRules::StandardPinTypeMatchRulesMask; /* Pin categories to allow beyond an exact match. */ - UPROPERTY(EditAnywhere, Category = PinConnection) + UPROPERTY(EditAnywhere, Category = PinConnection, DisplayName = "Allow Conversion From PinTypes") TSet PinCategories; }; 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/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/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(); } }