diff --git a/Config/BaseFlow.ini b/Config/BaseFlow.ini deleted file mode 100644 index f42430df5..000000000 --- a/Config/BaseFlow.ini +++ /dev/null @@ -1,3 +0,0 @@ -[CoreRedirects] -+ClassRedirects=(OldName="/Script/Flow.FlowNode_CustomEvent",NewName="/Script/Flow.FlowNode_CustomInput") -+PropertyRedirects=(OldName="FlowAsset.CustomEvents",NewName="CustomInputs") diff --git a/Config/DefaultFlow.ini b/Config/DefaultFlow.ini index f42430df5..fe5504715 100644 --- a/Config/DefaultFlow.ini +++ b/Config/DefaultFlow.ini @@ -1,3 +1,6 @@ [CoreRedirects] +ClassRedirects=(OldName="/Script/Flow.FlowNode_CustomEvent",NewName="/Script/Flow.FlowNode_CustomInput") -+PropertyRedirects=(OldName="FlowAsset.CustomEvents",NewName="CustomInputs") ++PropertyRedirects=(OldName="FlowAsset.CustomEvents",NewName="FlowAsset.CustomInputs") ++PropertyRedirects=(OldName="FlowGraphNode.FlowNode",NewName="FlowGraphNode.NodeInstance") ++StructRedirects=(OldName="/Script/Flow.FlowNamedDataPinOutputProperty",NewName="/Script/Flow.FlowNamedDataPinProperty") ++PropertyRedirects=(OldName="FlowNode_DefineProperties.OutputProperties",NewName="NamedProperties") diff --git a/Flow.uplugin b/Flow.uplugin index fdba8368f..056891469 100644 --- a/Flow.uplugin +++ b/Flow.uplugin @@ -1,15 +1,14 @@ { "FileVersion" : 3, - "Version" : 1.3, + "Version" : 2.3, "FriendlyName" : "Flow", "Description" : "Design-agnostic node editor for scripting game’s flow.", "Category" : "Gameplay", "CreatedByURL" : "https://github.com/MothCocoon/FlowGraph/graphs/contributors", - "DocsURL" : "https://github.com/MothCocoon/FlowGraph/wiki", + "DocsURL" : "https://mothcocoon.github.io/FlowGraph/", "MarketplaceURL" : "", - "SupportURL": "https://discord.gg/zMtMQ2vUUa", - "EngineAssociation": "5.0", - "EnabledByDefault" : true, + "SupportURL": "https://discord.gg/Xmtr6GhbmW", + "EnabledByDefault" : false, "CanContainContent" : false, "IsBetaVersion" : false, "Installed" : false, @@ -20,16 +19,29 @@ "Type" : "Runtime", "LoadingPhase" : "PreDefault" }, + { + "Name" : "FlowDebugger", + "Type" : "DeveloperTool", + "LoadingPhase" : "PreDefault" + }, { "Name" : "FlowEditor", "Type" : "Editor", - "LoadingPhase" : "Default" + "LoadingPhase" : "PreDefault" } ], "Plugins": [ { "Name": "AssetSearch", "Enabled": true + }, + { + "Name": "EditorScriptingUtilities", + "Enabled": true + }, + { + "Name": "EngineAssetDefinitions", + "Enabled": true } ] } \ No newline at end of file diff --git a/LICENSE b/LICENSE index 60c62699d..2ab42e296 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) Krzysztof Justyński +Copyright (c) https://github.com/MothCocoon/FlowGraph/graphs/contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index d6f18b4d0..6516d42ab 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,31 @@ ## Concept -Flow plug-in for Unreal Engine is a design-agnostic event node editor. It provides a graph editor tailored for scripting flow of events in virtual worlds. It's based on a decade of experience with designing and implementing narrative in video games. All we need here is simplicity. +Flow plug-in for Unreal Engine is a design-agnostic event node editor. It provides a graph editor tailored for scripting flow of events in virtual worlds. It's based on a decade of experience with designing and implementing a narrative layer in video games. All we need here is simplicity. -The aim of publishing it as open-source project is to let people tell great stories and construct immersive worlds easier. That allows us to enrich video game storytelling so we can inspire people and make our world a better place. +The aim of publishing it as an open-source project is to let people tell great stories and construct immersive worlds more easily. That allows us to enrich video game storytelling so we can inspire people and make our world a better place. ![Flow101](https://user-images.githubusercontent.com/5065057/103543817-6d924080-4e9f-11eb-87d9-15ab092c3875.png) -* A single node in this graph is a simple UObject, not a function like in blueprints. This allows you to encapsulate the entire gameplay element (logic with its data) within a single Flow Node. The idea is that your write a repeatable "event script" only once for the entire game! -* Unlike blueprints, Flow Node is async/latent by design. Active node usually subscribe to delegates, so it can react to event by triggering output pin (or whatever you choose to). +* A single node in this graph is a simple UObject, not a function like in blueprints. This allows you to encapsulate the entire gameplay element (logic with its data) within a single Flow Node. The idea is that you write a repeatable "event script" only once for the entire game! +* Unlike blueprints, Flow Node is async/latent by design. Active nodes usually subscribe to delegates, so they can react to events by triggering output pins (or whatever you choose). * Every node defines its own set of input/output pins. It's dead simple to design the flow of the game - just connect nodes representing features. -* Developers creating a Flow Node can call the execution of pins any way they need. API is extremely simple. -* Editor supports convenient displaying debug information on nodes and wires while playing a game. You simply provide what kind of message would be displayed over active Flow Nodes - you can't have that with blueprint functions. -* It's up to you to add game-specific functionalities by writing your nodes and editor customizations. It's not like a marketplace providing the very specific implementation of systems. It's a convenient base for building systems tailored to fit your needs. -* Read documentation on the project wiki. I'd recommend starting from reading about [design philosophy](https://github.com/MothCocoon/FlowGraph/wiki). -* It's easy to include plugin in your own project, follow this short [Getting Started](https://github.com/MothCocoon/FlowGraph/wiki/Getting-Started) guide. +* Developers creating a Flow Node can call the execution of pins in any way they need. API is extremely simple. +* Editor supports conveniently displaying debug information on nodes and wires while playing a game. You simply provide what kind of message would be displayed over active Flow Nodes - you can't have that with blueprint functions. +* It's up to you to add game-specific functionalities by writing your nodes and editor customizations. It's not like a marketplace providing a very specific implementation of systems. It's a convenient base for building systems tailored to fit your needs. +* Follow this short [Getting Started](https://mothcocoon.github.io/FlowGraph/Overview/GettingStarted) guide to start working with this plugin. ## In-depth video presentation -This 24-minute presentation breaks down the concept of the Flow Graph. It goes through everything written in this ReadMe but in greater detail. +This 24-minute presentation breaks down the concept of the Flow Graph. Trust me, you want to understand the concept properly before diving into implementation. -[![Introducing Flow Graph for Unreal Engine](https://img.youtube.com/vi/Rj76JP1f-I4/0.jpg)](https://www.youtube.com/watch?v=BAqhccgKx_k) +[![Introducing Flow Graph for Unreal Engine](https://img.youtube.com/vi/BAqhccgKx_k/0.jpg)](https://www.youtube.com/watch?v=BAqhccgKx_k) + +## Documentation +[Plugin documentation on GitHub Pages.](https://mothcocoon.github.io/FlowGraph/) ## Acknowledgements I got an opportunity to work on something like the Flow Graph at Reikon Games. They shared my enthusiasm for providing the plugin as open source and as such allowed me to publish this work and keep expanding it as a personal project. Kudos, guys! Reikon badly wanted to build a better tool for implementing game flow rather than level blueprints or existing Marketplace plug-ins. I was very much interested in this since the studio was just starting with the production of a new title. And we did exactly that, created a node editor dedicated to scripting game flow. Kudos to Dariusz Murawski - a programmer who spent a few months with me to establish the working system and editor. And who had to endure my never-ending feedback and requests. -I feel it's important to mention that I didn't invent anything new here, with the Flow Graph. It's an old and proven concept. I'm just one of many developers who decided it would be crazy useful to adopt it for Unreal Engine. And this time, also to make it publicly available as an open-source project. -* Such simple graph-based tools for scripting game screenplay are utilized for a long time. Traditionally, RPG games needed such tools as there a lot of stories, quests, dialogues. -* The best narrative toolset I had the opportunity to work with is what CD Projekt RED built for The Witcher series. Sadly, you can't download the modding toolkit for The Witcher 2 - yeah, it was publically available for some time. Still... you can watch the GDC talk by Piotr Tomsiński on [Cinematic Dialogues in The Witcher 3: Wild Hunt](https://www.youtube.com/watch?v=chf3REzAjgI) - it includes a brief presentation how Quest and Dialogue editors look like. It wouldn't be possible to create such an amazing narrative game without this kind of toolset. I did miss that so much when I moved to the Unreal Engine... -* At some point I felt comfortable enough with programming editor tools so I decided to build my own version of such toolset, meant to be published as an open-source project. I am thankful to Reikon bosses they see no issues with me releasing Flow Graph, which is "obviously" similar to our internal tool in many ways. I mean, it's so simple concept of "single node representing a single game feature"... and it's based on the same UE4 node graph API. Some corporations might have an issue with that. +I feel it's important to mention that I didn't invent anything new here, with the Flow Graph. It's an old and proven concept. I'm just one of many developers who decided it would be crazy useful to adopt it for Unreal Engine. This time, also be made publicly available as an open-source project. +* Such simple graph-based tools for scripting game screenplay have been utilized for a long time. Traditionally, RPG games needed such tools as there are a lot of stories, quests, and dialogues. +* The best narrative toolset I had the opportunity to work with is what CD Projekt RED built for The Witcher series. Sadly, you can't download the modding toolkit for The Witcher 2 - yeah, it was publicly available for some time. Still... you can watch the GDC talk by Piotr Tomsiński on [Cinematic Dialogues in The Witcher 3: Wild Hunt](https://www.youtube.com/watch?v=chf3REzAjgI) - it includes a brief presentation on how Quest and Dialogue editors look like. It wouldn't be possible to create such an amazing narrative game without this kind of toolset. I did miss that so much when I moved to the Unreal Engine... +* At some point, I felt comfortable enough with programming editor tools, so I decided to build my own version of such a toolset, meant to be published as an open-source project. I am thankful to Reikon leaders, they see no issues with me releasing Flow Graph, which is "obviously" similar to our internal tool in many ways. I mean, it's so simple concept of "single node representing a single game feature"... and it's based on the same UE4 node graph API. Some corporations might have an issue with that. diff --git a/Source/Flow/Flow.Build.cs b/Source/Flow/Flow.Build.cs index b69475b87..c6847ce45 100644 --- a/Source/Flow/Flow.Build.cs +++ b/Source/Flow/Flow.Build.cs @@ -1,34 +1,41 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - using UnrealBuildTool; public class Flow : ModuleRules { - public Flow(ReadOnlyTargetRules Target) : base(Target) + public Flow(ReadOnlyTargetRules target) : base(target) { PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; - PublicDependencyModuleNames.AddRange(new[] - { + PublicDependencyModuleNames.AddRange( + [ "LevelSequence" - }); - - PrivateDependencyModuleNames.AddRange(new[] - { - "Core", + ]); + + PrivateDependencyModuleNames.AddRange( + [ + "Core", "CoreUObject", - "DeveloperSettings", + "DeveloperSettings", "Engine", - "GameplayTags", + "GameplayTags", "MovieScene", "MovieSceneTracks", - "Slate", - "SlateCore" - }); + "NetCore", + "Slate", + "SlateCore" + ]); - if (Target.Type == TargetType.Editor) - { - PublicDependencyModuleNames.Add("UnrealEd"); - } - } -} + if (target.Type == TargetType.Editor) + { + PublicDependencyModuleNames.AddRange( + [ + "GraphEditor", + "MessageLog", + "PropertyEditor", + "SourceControl", + "UnrealEd" + ]); + } + } +} \ No newline at end of file diff --git a/Source/Flow/Private/AddOns/FlowNodeAddOn.cpp b/Source/Flow/Private/AddOns/FlowNodeAddOn.cpp new file mode 100644 index 000000000..b79ee7ad1 --- /dev/null +++ b/Source/Flow/Private/AddOns/FlowNodeAddOn.cpp @@ -0,0 +1,172 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "AddOns/FlowNodeAddOn.h" + +#include "FlowLogChannels.h" + +#include "Nodes/FlowNode.h" + +#include "Misc/RuntimeErrors.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNodeAddOn) + +UFlowNodeAddOn::UFlowNodeAddOn() +{ +#if WITH_EDITOR + NodeDisplayStyle = FlowNodeStyle::AddOn; +#endif +} + +void UFlowNodeAddOn::InitializeInstance() +{ + CacheFlowNode(); + + Super::InitializeInstance(); +} + +void UFlowNodeAddOn::DeinitializeInstance() +{ + Super::DeinitializeInstance(); + + FlowNode = nullptr; +} + +void UFlowNodeAddOn::TriggerFirstOutput(const bool bFinish) +{ + if (ensure(FlowNode)) + { + FlowNode->TriggerFirstOutput(bFinish); + } +} + +void UFlowNodeAddOn::TriggerOutput(const FName PinName, const bool bFinish, const EFlowPinActivationType ActivationType) +{ + if (ensure(FlowNode)) + { + FlowNode->TriggerOutput(PinName, bFinish, ActivationType); + } +} + +void UFlowNodeAddOn::Finish() +{ + if (ensure(FlowNode)) + { + FlowNode->Finish(); + } +} + +EFlowAddOnAcceptResult UFlowNodeAddOn::AcceptFlowNodeAddOnParent_Implementation( + const UFlowNodeBase* ParentTemplate, + const TArray& AdditionalAddOnsToAssumeAreChildren) const +{ + // Subclasses may override this function to opt in to parent classes + 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 + // and that we are not calling this method before the addon has been initialized. + ensure(FlowNode); + + return FlowNode; +} + +UFlowNode* UFlowNodeAddOn::FindOwningFlowNode() const +{ + UObject* OuterObject = GetOuter(); + UFlowNode* ParentFlowNode = nullptr; + + while (IsValid(OuterObject)) + { + ParentFlowNode = Cast(OuterObject); + if (ParentFlowNode) + { + break; + } + + OuterObject = OuterObject->GetOuter(); + } + + return ParentFlowNode; +} + +int32 UFlowNodeAddOn::GetRandomSeed() const +{ + if (ensure(FlowNode)) + { + return FlowNode->GetRandomSeed(); + } + + return 0; +} + +bool UFlowNodeAddOn::IsSupportedInputPinName(const FName& PinName) const +{ + if (InputPins.IsEmpty()) + { + return true; + } + + if (const FFlowPin* FoundFlowPin = FindFlowPinByName(PinName, InputPins)) + { + return true; + } + else + { + return false; + } +} + +void UFlowNodeAddOn::CacheFlowNode() +{ + FlowNode = FindOwningFlowNode(); + + ensureAsRuntimeWarning(FlowNode); +} + +#if WITH_EDITOR +TArray UFlowNodeAddOn::GetPinsForContext(const TArray& Context) const +{ + TArray ContextPins = Super::GetContextInputs(); + + ContextPins.Reserve(ContextPins.Num() + Context.Num()); + + for (const FFlowPin& InputPin : Context) + { + if (InputPin.IsValid()) + { + ContextPins.Add(InputPin); + } + else + { + UE_LOG(LogFlow, Warning, TEXT("Addon %s has invalid pins (name: None), you should clean these up."), *GetName()); + } + } + + return ContextPins; +} + +TArray UFlowNodeAddOn::GetContextInputs() const +{ + return GetPinsForContext(InputPins); +} + +TArray UFlowNodeAddOn::GetContextOutputs() const +{ + return GetPinsForContext(OutputPins); +} + +void UFlowNodeAddOn::RequestReconstructionOnOwningFlowNode() const +{ + (void)OnAddOnRequestedParentReconstruction.ExecuteIfBound(); +} +#endif // WITH_EDITOR diff --git a/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateAND.cpp b/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateAND.cpp new file mode 100644 index 000000000..5a0f3ec8f --- /dev/null +++ b/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateAND.cpp @@ -0,0 +1,55 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "AddOns/FlowNodeAddOn_PredicateAND.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNodeAddOn_PredicateAND) + +UFlowNodeAddOn_PredicateAND::UFlowNodeAddOn_PredicateAND() + : Super() +{ +#if WITH_EDITOR + NodeDisplayStyle = FlowNodeStyle::AddOn_Predicate_Composite; + Category = TEXT("Composite"); +#endif +} + +EFlowAddOnAcceptResult UFlowNodeAddOn_PredicateAND::AcceptFlowNodeAddOnChild_Implementation( + const UFlowNodeAddOn* AddOnTemplate, + const TArray& AdditionalAddOnsToAssumeAreChildren) const +{ + if (IFlowPredicateInterface::ImplementsInterfaceSafe(AddOnTemplate)) + { + return EFlowAddOnAcceptResult::TentativeAccept; + } + else + { + // All AddOn children MUST implement IFlowPredicateInterface + // (so do not return Super's implementation which will return Undetermined) + return EFlowAddOnAcceptResult::Reject; + } +} + +bool UFlowNodeAddOn_PredicateAND::EvaluatePredicate_Implementation() const +{ + return EvaluatePredicateAND(AddOns); +} + +bool UFlowNodeAddOn_PredicateAND::EvaluatePredicateAND(const TArray& AddOns) +{ + for (int Index = 0; Index < AddOns.Num(); ++Index) + { + const UFlowNodeAddOn* AddOn = AddOns[Index]; + + if (IFlowPredicateInterface::ImplementsInterfaceSafe(AddOn)) + { + const bool bResult = IFlowPredicateInterface::Execute_EvaluatePredicate(AddOn); + + if (!bResult) + { + return false; + } + } + } + + return true; +} diff --git a/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateCompareValues.cpp b/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateCompareValues.cpp new file mode 100644 index 000000000..7e3d6cfa0 --- /dev/null +++ b/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateCompareValues.cpp @@ -0,0 +1,721 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "AddOns/FlowNodeAddOn_PredicateCompareValues.h" + +#include "FlowAsset.h" +#include "FlowSettings.h" +#include "Policies/FlowPinConnectionPolicy.h" +#include "Types/FlowPinTypeNamesStandard.h" +#include "Types/FlowPinTypesStandard.h" + +#define LOCTEXT_NAMESPACE "FlowNodeAddOn_PredicateCompareValues" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNodeAddOn_PredicateCompareValues) + +namespace +{ +#if WITH_EDITOR + static void ForceNamedPropertyPinDirection(FFlowNamedDataPinProperty& NamedProperty, const bool bIsInput) + { + const UScriptStruct* ScriptStruct = NamedProperty.DataPinValue.GetScriptStruct(); + if (IsValid(ScriptStruct) && ScriptStruct->IsChildOf()) + { + FFlowDataPinValue& WrapperValue = NamedProperty.DataPinValue.GetMutable(); + WrapperValue.bIsInputPin = bIsInput; + } + } +#endif // WITH_EDITOR +} + +UFlowNodeAddOn_PredicateCompareValues::UFlowNodeAddOn_PredicateCompareValues() + : Super() +{ +#if WITH_EDITOR + NodeDisplayStyle = FlowNodeStyle::AddOn_Predicate; + Category = TEXT("DataPins"); +#endif + + if (GetAuthoredValueName(LeftValue).IsNone()) + { + LeftValue.Name = TEXT("Compare Left Value"); + } + if (GetAuthoredValueName(RightValue).IsNone()) + { + RightValue.Name = TEXT("Compare Right Value"); + } + +#if WITH_EDITORONLY_DATA + // Encourage input pins by default; PostEditChangeChainProperty enforces it. + if (FFlowDataPinValue* LeftWrapper = LeftValue.DataPinValue.GetMutablePtr()) + { + LeftWrapper->bIsInputPin = true; + } + if (FFlowDataPinValue* RightWrapper = RightValue.DataPinValue.GetMutablePtr()) + { + RightWrapper->bIsInputPin = true; + } +#endif +} + +bool UFlowNodeAddOn_PredicateCompareValues::TryFindPropertyByPinName(const FName& PinName, const FProperty*& OutFoundProperty, TInstancedStruct& OutFoundInstancedStruct) const +{ + // TODO (gtaylor) It would be nicer if the base IFlowDataPinValueOwnerInterface::TryFindPropertyByPinName implementation + // could find member FFlowNamedDataPinProperty's by their Name field, but that would require a property search, + // so we don't need to special-case these. Maybe we can think of a more clever version at some point. + if (GetAuthoredValueName(LeftValue) == PinName) + { + OutFoundInstancedStruct = LeftValue.DataPinValue; + + return true; + } + + if (GetAuthoredValueName(RightValue) == PinName) + { + OutFoundInstancedStruct = RightValue.DataPinValue; + + return true; + } + + if (Super::TryFindPropertyByPinName(PinName, OutFoundProperty, OutFoundInstancedStruct)) + { + return true; + } + + return false; +} + +bool UFlowNodeAddOn_PredicateCompareValues::IsEqualityOp() const +{ + return EFlowPredicateCompareOperatorType_Classifiers::IsEqualityOperation(OperatorType); +} + +bool UFlowNodeAddOn_PredicateCompareValues::IsArithmeticOp() const +{ + return EFlowPredicateCompareOperatorType_Classifiers::IsArithmeticOperation(OperatorType); +} + +bool UFlowNodeAddOn_PredicateCompareValues::IsNumericTypeName( + const FFlowPinConnectionPolicy& PinConnectionPolicy, + const FName& TypeName) +{ + return + PinConnectionPolicy.GetAllSupportedIntegerTypes().Contains(TypeName) || + PinConnectionPolicy.GetAllSupportedFloatTypes().Contains(TypeName); +} + +bool UFlowNodeAddOn_PredicateCompareValues::IsFloatingPointType( + const FFlowPinConnectionPolicy& PinConnectionPolicy, + const FName& TypeName) +{ + return PinConnectionPolicy.GetAllSupportedFloatTypes().Contains(TypeName); +} + +bool UFlowNodeAddOn_PredicateCompareValues::IsIntegerType( + const FFlowPinConnectionPolicy& PinConnectionPolicy, + const FName& TypeName) +{ + return PinConnectionPolicy.GetAllSupportedIntegerTypes().Contains(TypeName); +} + +bool UFlowNodeAddOn_PredicateCompareValues::IsTextType(const FName& TypeName) +{ + return TypeName == FFlowPinTypeNamesStandard::PinTypeNameText; +} + +bool UFlowNodeAddOn_PredicateCompareValues::IsStringType(const FName& TypeName) +{ + return TypeName == FFlowPinTypeNamesStandard::PinTypeNameString; +} + +bool UFlowNodeAddOn_PredicateCompareValues::IsNameLikeType(const FName& TypeName) +{ + // Treat Enum as "Name-like" for comparisons (case-insensitive) + return + TypeName == FFlowPinTypeNamesStandard::PinTypeNameName || + TypeName == FFlowPinTypeNamesStandard::PinTypeNameEnum; +} + +bool UFlowNodeAddOn_PredicateCompareValues::IsAnyStringLikeTypeName( + const FFlowPinConnectionPolicy& PinConnectionPolicy, + const FName& TypeName) +{ + // Special-casing NameLike, since the CompareValues predicate counts Enums as Names + return + IsNameLikeType(TypeName) || + PinConnectionPolicy.GetAllSupportedStringLikeTypes().Contains(TypeName); +} + +bool UFlowNodeAddOn_PredicateCompareValues::IsGameplayTagLikeTypeName( + const FFlowPinConnectionPolicy& PinConnectionPolicy, + const FName& TypeName) +{ + return PinConnectionPolicy.GetAllSupportedGameplayTagTypes().Contains(TypeName); +} + +bool UFlowNodeAddOn_PredicateCompareValues::IsBoolTypeName(const FName& TypeName) +{ + return TypeName == FFlowPinTypeNamesStandard::PinTypeNameBool; +} + +bool UFlowNodeAddOn_PredicateCompareValues::IsVectorTypeName(const FName& TypeName) +{ + return TypeName == FFlowPinTypeNamesStandard::PinTypeNameVector; +} + +bool UFlowNodeAddOn_PredicateCompareValues::IsRotatorTypeName(const FName& TypeName) +{ + return TypeName == FFlowPinTypeNamesStandard::PinTypeNameRotator; +} + +bool UFlowNodeAddOn_PredicateCompareValues::IsTransformTypeName(const FName& TypeName) +{ + return TypeName == FFlowPinTypeNamesStandard::PinTypeNameTransform; +} + +bool UFlowNodeAddOn_PredicateCompareValues::IsObjectTypeName(const FName& TypeName) +{ + return TypeName == FFlowPinTypeNamesStandard::PinTypeNameObject; +} + +bool UFlowNodeAddOn_PredicateCompareValues::IsClassTypeName(const FName& TypeName) +{ + return TypeName == FFlowPinTypeNamesStandard::PinTypeNameClass; +} + +bool UFlowNodeAddOn_PredicateCompareValues::IsInstancedStructTypeName(const FName& TypeName) +{ + return TypeName == FFlowPinTypeNamesStandard::PinTypeNameInstancedStruct; +} + +#if WITH_EDITOR + +void UFlowNodeAddOn_PredicateCompareValues::PostEditChangeChainProperty(FPropertyChangedChainEvent& PropertyChangedEvent) +{ + // Force both FFlowNamedDataPinProperty values to always be input pins. + const auto& ChangedProperty = PropertyChangedEvent.PropertyChain.GetActiveMemberNode()->GetValue(); + constexpr bool bIsInput = true; + OnPostEditEnsureAllNamedPropertiesPinDirection(*ChangedProperty, bIsInput); + + Super::PostEditChangeChainProperty(PropertyChangedEvent); + OnReconstructionRequested.ExecuteIfBound(); +} + +void UFlowNodeAddOn_PredicateCompareValues::OnPostEditEnsureAllNamedPropertiesPinDirection(const FProperty& Property, bool bIsInput) +{ + if (Property.GetFName() == GetLeftValuePropertyName()) + { + ForceNamedPropertyPinDirection(LeftValue, bIsInput); + } + else if (Property.GetFName() == GetRightValuePropertyName()) + { + ForceNamedPropertyPinDirection(RightValue, bIsInput); + } +} + +EDataValidationResult UFlowNodeAddOn_PredicateCompareValues::ValidateNode() +{ + EDataValidationResult Result = Super::ValidateNode(); + + // Validate that both values are configured + if (!LeftValue.IsValid()) + { + LogValidationError(TEXT("LeftValue is not configured (missing name or pin type).")); + Result = EDataValidationResult::Invalid; + } + + if (!RightValue.IsValid()) + { + LogValidationError(TEXT("RightValue is not configured (missing name or pin type).")); + Result = EDataValidationResult::Invalid; + } + + // Remaining checks require both values to be valid + if (!LeftValue.IsValid() || !RightValue.IsValid()) + { + return Result; + } + + const FFlowPinTypeName LeftPinTypeName = LeftValue.DataPinValue.Get().GetPinTypeName(); + const FFlowPinTypeName RightPinTypeName = RightValue.DataPinValue.Get().GetPinTypeName(); + + // Validate pin type names are set + if (LeftPinTypeName.IsNone()) + { + LogValidationError(TEXT("LeftValue has an unknown or unset pin type.")); + Result = EDataValidationResult::Invalid; + } + + if (RightPinTypeName.IsNone()) + { + LogValidationError(TEXT("RightValue has an unknown or unset pin type.")); + Result = EDataValidationResult::Invalid; + } + + if (LeftPinTypeName.IsNone() || RightPinTypeName.IsNone()) + { + return Result; + } + + // 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 && !AreComparablePinTypes(PinConnectionPolicy, LeftTypeName, RightTypeName)) + { + LogValidationError(FString::Printf( + TEXT("Pin types are not comparable: '%s' vs '%s'."), + *LeftTypeName.ToString(), + *RightTypeName.ToString())); + Result = EDataValidationResult::Invalid; + } + + // Validate arithmetic operators are only used with numeric types + 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'."), + *EFlowPredicateCompareOperatorType_Classifiers::GetOperatorSymbolString(OperatorType), + *LeftTypeName.ToString(), + *RightTypeName.ToString())); + Result = EDataValidationResult::Invalid; + } + + // Warn if both sides have the same authored name (potential user confusion) + if (GetAuthoredValueName(LeftValue) == GetAuthoredValueName(RightValue)) + { + LogValidationWarning(FString::Printf( + TEXT("LeftValue and RightValue have the same name '%s'. This may cause confusion with pin disambiguation."), + *GetAuthoredValueName(LeftValue).ToString())); + } + + if (Result == EDataValidationResult::NotValidated) + { + Result = EDataValidationResult::Valid; + } + + return Result; +} + +FText UFlowNodeAddOn_PredicateCompareValues::K2_GetNodeTitle_Implementation() const +{ + using namespace EFlowPredicateCompareOperatorType_Classifiers; + + const bool bIsClassDefault = HasAnyFlags(RF_ArchetypeObject | RF_ClassDefaultObject); + + if (!bIsClassDefault && + GetDefault()->bUseAdaptiveNodeTitles) + { + const FText LeftDisplayName = FText::FromName(GetAuthoredValueName(LeftValue)); + const FText RightDisplayName = FText::FromName(GetAuthoredValueName(RightValue)); + + const FString OperatorString = GetOperatorSymbolString(OperatorType); + const FText OperatorText = FText::FromString(OperatorString); + + return FText::Format(LOCTEXT("CompareValuesTitle", "{0} {1} {2}"), { LeftDisplayName, OperatorText, RightDisplayName }); + } + + return Super::K2_GetNodeTitle_Implementation(); +} + +#endif // WITH_EDITOR + +bool UFlowNodeAddOn_PredicateCompareValues::AreComparablePinTypes(const FFlowPinConnectionPolicy& PinConnectionPolicy, const FName& LeftPinTypeName, const FName& RightPinTypeName) +{ + return PinConnectionPolicy.CanConnectPinTypeNames(LeftPinTypeName, RightPinTypeName); +} + +bool UFlowNodeAddOn_PredicateCompareValues::CacheTypeNames(FCachedTypeNames& OutCache) const +{ + OutCache.Reset(); + + if (!LeftValue.IsValid() || !RightValue.IsValid()) + { + LogError(TEXT("Compare Values requires both LeftValue and RightValue to be configured.")); + return false; + } + + OutCache.LeftTypeName = LeftValue.DataPinValue.Get().GetPinTypeName().Name; + OutCache.RightTypeName = RightValue.DataPinValue.Get().GetPinTypeName().Name; + OutCache.bIsValid = true; + + return true; +} + +bool UFlowNodeAddOn_PredicateCompareValues::TryCheckGameplayTagsEqual(bool& bOutIsEqual) const +{ + // Compare both sides as containers; pin type templates should allow tag->container conversion. + FGameplayTagContainer LeftContainer; + { + const EFlowDataPinResolveResult ResolveResult = + TryResolveDataPinValue(GetDisambiguatedValueName(LeftValue), LeftContainer, SingleFromArray); + + if (!FlowPinType::IsSuccess(ResolveResult)) + { + LogError(TEXT("Failed to resolve LeftValue as GameplayTagContainer.")); + return false; + } + } + + FGameplayTagContainer RightContainer; + { + const EFlowDataPinResolveResult ResolveResult = + TryResolveDataPinValue(GetDisambiguatedValueName(RightValue), RightContainer, SingleFromArray); + + if (!FlowPinType::IsSuccess(ResolveResult)) + { + LogError(TEXT("Failed to resolve RightValue as GameplayTagContainer.")); + return false; + } + } + + bOutIsEqual = (LeftContainer == RightContainer); + return true; +} + +bool UFlowNodeAddOn_PredicateCompareValues::TryCheckFallbackStringEqual(bool& bOutIsEqual) const +{ + // Fallback path: try to convert both sides to string via their FFlowDataPinValue::TryConvertValuesToString. + // This enables user-added pin types (from other plugins) to participate in equality comparisons + // as long as they implement TryConvertValuesToString on their FFlowDataPinValue subclass. + + const FFlowDataPinValue* LeftDataPinValue = LeftValue.DataPinValue.GetPtr(); + const FFlowDataPinValue* RightDataPinValue = RightValue.DataPinValue.GetPtr(); + + if (!LeftDataPinValue || !RightDataPinValue) + { + return false; + } + + FString LeftString; + if (!LeftDataPinValue->TryConvertValuesToString(LeftString)) + { + LogError(TEXT("Failed to convert LeftValue to String for fallback comparison.")); + return false; + } + + FString RightString; + if (!RightDataPinValue->TryConvertValuesToString(RightString)) + { + LogError(TEXT("Failed to convert RightValue to String for fallback comparison.")); + return false; + } + + bOutIsEqual = (LeftString == RightString); + return true; +} + +bool UFlowNodeAddOn_PredicateCompareValues::CompareDoubleUsingOperator(double LeftValueAsDouble, double RightValueAsDouble) const +{ + FLOW_ASSERT_ENUM_MAX(EFlowPredicateCompareOperatorType, 6); + switch (OperatorType) + { + case EFlowPredicateCompareOperatorType::Equal: + return FMath::IsNearlyEqual(LeftValueAsDouble, RightValueAsDouble, static_cast(SMALL_NUMBER)); + + case EFlowPredicateCompareOperatorType::NotEqual: + return !FMath::IsNearlyEqual(LeftValueAsDouble, RightValueAsDouble, static_cast(SMALL_NUMBER)); + + case EFlowPredicateCompareOperatorType::Less: + return (LeftValueAsDouble < RightValueAsDouble); + + case EFlowPredicateCompareOperatorType::LessOrEqual: + return (LeftValueAsDouble <= RightValueAsDouble + static_cast(SMALL_NUMBER)); + + case EFlowPredicateCompareOperatorType::Greater: + return (LeftValueAsDouble > RightValueAsDouble); + + case EFlowPredicateCompareOperatorType::GreaterOrEqual: + return (LeftValueAsDouble >= RightValueAsDouble - static_cast(SMALL_NUMBER)); + + default: + break; + } + + return false; +} + +bool UFlowNodeAddOn_PredicateCompareValues::CompareInt64UsingOperator(int64 LeftValueAsInt64, int64 RightValueAsInt64) const +{ + FLOW_ASSERT_ENUM_MAX(EFlowPredicateCompareOperatorType, 6); + switch (OperatorType) + { + case EFlowPredicateCompareOperatorType::Equal: + return (LeftValueAsInt64 == RightValueAsInt64); + + case EFlowPredicateCompareOperatorType::NotEqual: + return (LeftValueAsInt64 != RightValueAsInt64); + + case EFlowPredicateCompareOperatorType::Less: + return (LeftValueAsInt64 < RightValueAsInt64); + + case EFlowPredicateCompareOperatorType::LessOrEqual: + return (LeftValueAsInt64 <= RightValueAsInt64); + + case EFlowPredicateCompareOperatorType::Greater: + return (LeftValueAsInt64 > RightValueAsInt64); + + case EFlowPredicateCompareOperatorType::GreaterOrEqual: + return (LeftValueAsInt64 >= RightValueAsInt64); + + default: + break; + } + + return false; +} + +const FName& UFlowNodeAddOn_PredicateCompareValues::GetLeftValuePropertyName() const +{ + static const FName LeftValueName = GET_MEMBER_NAME_CHECKED(ThisClass, LeftValue); + return LeftValueName; +} + +const FName& UFlowNodeAddOn_PredicateCompareValues::GetRightValuePropertyName() const +{ + static const FName RightValueName = GET_MEMBER_NAME_CHECKED(ThisClass, RightValue); + return RightValueName; +} + +bool UFlowNodeAddOn_PredicateCompareValues::TryCompareAsDouble() const +{ + double LeftDouble = 0.0; + { + const EFlowDataPinResolveResult ResolveResult = + TryResolveDataPinValue(GetDisambiguatedValueName(LeftValue), LeftDouble, SingleFromArray); + + if (!FlowPinType::IsSuccess(ResolveResult)) + { + LogError(TEXT("Failed to resolve LeftValue as Double.")); + return false; + } + } + + double RightDouble = 0.0; + { + const EFlowDataPinResolveResult ResolveResult = + TryResolveDataPinValue(GetDisambiguatedValueName(RightValue), RightDouble, SingleFromArray); + + if (!FlowPinType::IsSuccess(ResolveResult)) + { + LogError(TEXT("Failed to resolve RightValue as Double.")); + return false; + } + } + + return CompareDoubleUsingOperator(LeftDouble, RightDouble); +} + +bool UFlowNodeAddOn_PredicateCompareValues::TryCompareAsInt64() const +{ + int64 LeftInt64 = 0; + { + const EFlowDataPinResolveResult ResolveResult = + TryResolveDataPinValue(GetDisambiguatedValueName(LeftValue), LeftInt64, SingleFromArray); + + if (!FlowPinType::IsSuccess(ResolveResult)) + { + LogError(TEXT("Failed to resolve LeftValue as Int64.")); + return false; + } + } + + int64 RightInt64 = 0; + { + const EFlowDataPinResolveResult ResolveResult = + TryResolveDataPinValue(GetDisambiguatedValueName(RightValue), RightInt64, SingleFromArray); + + if (!FlowPinType::IsSuccess(ResolveResult)) + { + LogError(TEXT("Failed to resolve RightValue as Int64.")); + return false; + } + } + + return CompareInt64UsingOperator(LeftInt64, RightInt64); +} + +bool UFlowNodeAddOn_PredicateCompareValues::EvaluateEqualityBlock(const TCHAR* TypeLabel, const TFunctionRef CompareFunc) const +{ + if (!IsEqualityOp()) + { + LogError(FString::Printf(TEXT("Arithmetic operators are not supported for %s comparisons."), TypeLabel)); + return false; + } + + bool bIsEqual = false; + if (!CompareFunc(bIsEqual)) + { + return false; + } + + return (OperatorType == EFlowPredicateCompareOperatorType::Equal) == bIsEqual; +} + +bool UFlowNodeAddOn_PredicateCompareValues::EvaluatePredicate_Implementation() const +{ + // Cache type names once to avoid repeated TInstancedStruct::Get() virtual dispatch. + FCachedTypeNames Cache; + if (!CacheTypeNames(Cache)) + { + return false; + } + + const UFlowAsset* FlowAsset = GetFlowAsset(); + check(IsValid(FlowAsset)); + const FFlowPinConnectionPolicy& PinConnectionPolicy = FlowAsset->GetPinConnectionPolicy(); + + const FName& LeftTypeName = Cache.LeftTypeName; + const FName& RightTypeName = Cache.RightTypeName; + + const bool bSameType = (LeftTypeName == RightTypeName); + + // Type compatibility gate. + // Same-type unknowns are allowed through for the fallback path at the bottom. + if (!bSameType && !AreComparablePinTypes(PinConnectionPolicy, LeftTypeName, RightTypeName)) + { + LogError(FString::Printf( + TEXT("Compare Values pin types are not comparable: '%s' vs '%s'."), + *LeftTypeName.ToString(), + *RightTypeName.ToString())); + + return false; + } + + // Arithmetic operators: numeric only (fast reject before the cascade) + 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(PinConnectionPolicy, LeftTypeName) && IsNumericTypeName(PinConnectionPolicy, RightTypeName)) + { + if (IsFloatingPointType(PinConnectionPolicy, LeftTypeName) || IsFloatingPointType(PinConnectionPolicy, RightTypeName)) + { + return TryCompareAsDouble(); + } + + return TryCompareAsInt64(); + } + + // Gameplay tags: compare as container (superset). Equality ops only. + 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(PinConnectionPolicy, LeftTypeName) || IsAnyStringLikeTypeName(PinConnectionPolicy, RightTypeName)) + { + // Dispatch order is significant: + // 1) Name-like (Name OR Enum) => case-insensitive compare via FString + // 2) Text => FText::EqualTo (culture-aware) + // 3) String => exact FString equality + if (IsNameLikeType(LeftTypeName) || IsNameLikeType(RightTypeName)) + { + return EvaluateEqualityBlock(TEXT("Name/Enum"), + [this](bool& bIsEqual) + { + return TryCheckResolvedValuesEqual(bIsEqual, TEXT("String (Name-like)"), + [](const FString& L, const FString& R) { return L.Equals(R, ESearchCase::IgnoreCase); }); + }); + } + + if (IsTextType(LeftTypeName) || IsTextType(RightTypeName)) + { + return EvaluateEqualityBlock(TEXT("Text"), + [this](bool& bIsEqual) + { + return TryCheckResolvedValuesEqual(bIsEqual, TEXT("Text"), + [](const FText& L, const FText& R) { return L.EqualTo(R); }); + }); + } + + return EvaluateEqualityBlock(TEXT("String"), + [this](bool& bIsEqual) + { + return TryCheckResolvedValuesEqual(bIsEqual, TEXT("String")); + }); + } + + // Bool. Equality ops only. + if (IsBoolTypeName(LeftTypeName) && IsBoolTypeName(RightTypeName)) + { + return EvaluateEqualityBlock(TEXT("Bool"), + [this](bool& bIsEqual) { return TryCheckResolvedValuesEqual(bIsEqual, TEXT("Bool")); }); + } + + // Vector. Equality ops only, strict comparison (no tolerance). + if (IsVectorTypeName(LeftTypeName) && IsVectorTypeName(RightTypeName)) + { + return EvaluateEqualityBlock(TEXT("Vector"), + [this](bool& bIsEqual) { return TryCheckResolvedValuesEqual(bIsEqual, TEXT("Vector")); }); + } + + // Rotator. Equality ops only, strict comparison (no tolerance). + if (IsRotatorTypeName(LeftTypeName) && IsRotatorTypeName(RightTypeName)) + { + return EvaluateEqualityBlock(TEXT("Rotator"), + [this](bool& bIsEqual) { return TryCheckResolvedValuesEqual(bIsEqual, TEXT("Rotator")); }); + } + + // Transform. Equality ops only, strict comparison (zero tolerance). + if (IsTransformTypeName(LeftTypeName) && IsTransformTypeName(RightTypeName)) + { + return EvaluateEqualityBlock(TEXT("Transform"), + [this](bool& bIsEqual) + { + return TryCheckResolvedValuesEqual(bIsEqual, TEXT("Transform"), + [](const FTransform& L, const FTransform& R) { return L.Equals(R, 0.0); }); + }); + } + + // Object. Equality ops only, pointer identity. + if (IsObjectTypeName(LeftTypeName) && IsObjectTypeName(RightTypeName)) + { + return EvaluateEqualityBlock(TEXT("Object"), + [this](bool& bIsEqual) { return TryCheckResolvedValuesEqual(bIsEqual, TEXT("Object")); }); + } + + // Class. Equality ops only, strict class identity (not "is derived from"). + if (IsClassTypeName(LeftTypeName) && IsClassTypeName(RightTypeName)) + { + return EvaluateEqualityBlock(TEXT("Class"), + [this](bool& bIsEqual) { return TryCheckResolvedValuesEqual(bIsEqual, TEXT("Class")); }); + } + + // InstancedStruct. Equality ops only, struct type + data equality. + if (IsInstancedStructTypeName(LeftTypeName) && IsInstancedStructTypeName(RightTypeName)) + { + return EvaluateEqualityBlock(TEXT("InstancedStruct"), + [this](bool& bIsEqual) { return TryCheckResolvedValuesEqual(bIsEqual, TEXT("InstancedStruct")); }); + } + + // Fallback: same-type comparison via string conversion. + // This supports user-added types from other plugins as long as they + // implement TryConvertValuesToString on their FFlowDataPinValue subclass. + if (bSameType) + { + return EvaluateEqualityBlock(*LeftTypeName.ToString(), + [this](bool& bIsEqual) { return TryCheckFallbackStringEqual(bIsEqual); }); + } + + LogError(FString::Printf( + TEXT("Compare Values does not support comparing pin types '%s' and '%s'."), + *LeftTypeName.ToString(), + *RightTypeName.ToString())); + + return false; +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateNOT.cpp b/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateNOT.cpp new file mode 100644 index 000000000..74952da78 --- /dev/null +++ b/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateNOT.cpp @@ -0,0 +1,65 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "AddOns/FlowNodeAddOn_PredicateNOT.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNodeAddOn_PredicateNOT) + +UFlowNodeAddOn_PredicateNOT::UFlowNodeAddOn_PredicateNOT() + : Super() +{ +#if WITH_EDITOR + NodeDisplayStyle = FlowNodeStyle::AddOn_Predicate_Composite; + Category = TEXT("Composite"); +#endif +} + +EFlowAddOnAcceptResult UFlowNodeAddOn_PredicateNOT::AcceptFlowNodeAddOnChild_Implementation( + const UFlowNodeAddOn* AddOnTemplate, + const TArray& AdditionalAddOnsToAssumeAreChildren) const +{ + if (AddOns.Num() >= 1 || !AdditionalAddOnsToAssumeAreChildren.IsEmpty()) + { + // Must not have more than one child Add-On under any circumstances + return EFlowAddOnAcceptResult::Reject; + } + else if (IFlowPredicateInterface::ImplementsInterfaceSafe(AddOnTemplate)) + { + return EFlowAddOnAcceptResult::TentativeAccept; + } + else + { + return EFlowAddOnAcceptResult::Reject; + } +} + +bool UFlowNodeAddOn_PredicateNOT::EvaluatePredicate_Implementation() const +{ + if (AddOns.IsEmpty()) + { + // For parity with PredicateAND, the "no AddOns (that qualify)" case results in a "true" result + return true; + } + + if (AddOns.Num() > 1) + { + const FString Message = FString::Printf(TEXT("%s may only have a single predicate AddOn child"), *GetName()); + UFlowNodeAddOn_PredicateNOT* MutableThis = const_cast(this); + MutableThis->LogError(Message); + } + + UFlowNodeAddOn* SingleChildAddOn = AddOns[0]; + + if (!IFlowPredicateInterface::ImplementsInterfaceSafe(SingleChildAddOn)) + { + const FString Message = FString::Printf(TEXT("%s requires a child AddOn that implements the IFlowPredicateInterface interface!"), *GetName()); + UFlowNodeAddOn_PredicateNOT* MutableThis = const_cast(this); + MutableThis->LogError(Message); + + // For parity with PredicateAND, the "no AddOns (that qualify)" case results in a "true" result + return true; + } + + const bool bResult = !IFlowPredicateInterface::Execute_EvaluatePredicate(SingleChildAddOn); + + return bResult; +} \ No newline at end of file diff --git a/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateOR.cpp b/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateOR.cpp new file mode 100644 index 000000000..4dfa04223 --- /dev/null +++ b/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateOR.cpp @@ -0,0 +1,68 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "AddOns/FlowNodeAddOn_PredicateOR.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNodeAddOn_PredicateOR) + +UFlowNodeAddOn_PredicateOR::UFlowNodeAddOn_PredicateOR() + : Super() +{ +#if WITH_EDITOR + NodeDisplayStyle = FlowNodeStyle::AddOn_Predicate_Composite; + Category = TEXT("Composite"); +#endif +} + +EFlowAddOnAcceptResult UFlowNodeAddOn_PredicateOR::AcceptFlowNodeAddOnChild_Implementation( + const UFlowNodeAddOn* AddOnTemplate, + const TArray& AdditionalAddOnsToAssumeAreChildren) const +{ + if (IFlowPredicateInterface::ImplementsInterfaceSafe(AddOnTemplate)) + { + return EFlowAddOnAcceptResult::TentativeAccept; + } + else + { + // All AddOn children MUST implement IFlowPredicateInterface + // (so do not return Super's implementation which will return Undetermined) + return EFlowAddOnAcceptResult::Reject; + } +} + +bool UFlowNodeAddOn_PredicateOR::EvaluatePredicate_Implementation() const +{ + return EvaluatePredicateOR(AddOns); +} + +bool UFlowNodeAddOn_PredicateOR::EvaluatePredicateOR(const TArray& AddOns) +{ + int32 FalseCount = 0; + for (int Index = 0; Index < AddOns.Num(); ++Index) + { + const UFlowNodeAddOn* AddOn = AddOns[Index]; + + if (IFlowPredicateInterface::ImplementsInterfaceSafe(AddOn)) + { + const bool bResult = IFlowPredicateInterface::Execute_EvaluatePredicate(AddOn); + + if (bResult) + { + return true; + } + else + { + ++FalseCount; + } + } + } + + if (FalseCount == 0) + { + // For parity with PredicateAND, the "no AddOns (that qualify)" case results in a "true" result + return true; + } + else + { + return false; + } +} diff --git a/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateRequireGameplayTags.cpp b/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateRequireGameplayTags.cpp new file mode 100644 index 000000000..c3ee2d5f3 --- /dev/null +++ b/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateRequireGameplayTags.cpp @@ -0,0 +1,95 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "AddOns/FlowNodeAddOn_PredicateRequireGameplayTags.h" +#include "FlowLogChannels.h" +#include "Nodes/FlowNode.h" +#include "Logging/LogMacros.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNodeAddOn_PredicateRequireGameplayTags) + +UFlowNodeAddOn_PredicateRequireGameplayTags::UFlowNodeAddOn_PredicateRequireGameplayTags() + : Super() +{ +#if WITH_EDITOR + NodeDisplayStyle = FlowNodeStyle::AddOn_Predicate; + Category = TEXT("DataPins"); +#endif +} + +bool UFlowNodeAddOn_PredicateRequireGameplayTags::EvaluatePredicate_Implementation() const +{ + if (Requirements.IsEmpty()) + { + // And Empty Requirements results in a "true" result + return true; + } + + FGameplayTagContainer TagsValue; + + // Sourcing the tags from the data pin + if (!TryGetTagsToCheckFromDataPin(TagsValue)) + { + return false; + } + + // Execute the Tags vs the Requirements + const bool bResult = Requirements.RequirementsMet(TagsValue); + return bResult; +} + +#if WITH_EDITOR +void UFlowNodeAddOn_PredicateRequireGameplayTags::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + UpdateNodeConfigText(); +} + +void UFlowNodeAddOn_PredicateRequireGameplayTags::OnEditorPinConnectionsChanged(const TArray& Changes) +{ + Super::OnEditorPinConnectionsChanged(Changes); + + UpdateNodeConfigText(); +} +#endif + +bool UFlowNodeAddOn_PredicateRequireGameplayTags::TryGetTagsToCheckFromDataPin(FGameplayTagContainer& TagsToCheckValue) const +{ + static const FName TagsName = GET_MEMBER_NAME_CHECKED(UFlowNodeAddOn_PredicateRequireGameplayTags, Tags); + + const EFlowDataPinResolveResult ResultEnum = TryResolveDataPinValue(TagsName, TagsToCheckValue); + + if (FlowPinType::IsSuccess(ResultEnum)) + { + return true; + } + else + { + UE_LOG(LogFlow, Error, TEXT("Cannot EvaluatePredicate on a data pin value we cannot resolve: %s"), *UEnum::GetDisplayValueAsText(ResultEnum).ToString()); + + return false; + } +} + +void UFlowNodeAddOn_PredicateRequireGameplayTags::UpdateNodeConfigText_Implementation() +{ +#if WITH_EDITOR + const FName TagsName = GET_MEMBER_NAME_CHECKED(UFlowNodeAddOn_PredicateRequireGameplayTags, Tags); + FTextBuilder TextBuilder; + if (Requirements.IsEmpty()) + { + const FName RequirementsName = GET_MEMBER_NAME_CHECKED(UFlowNodeAddOn_PredicateRequireGameplayTags, Requirements); + TextBuilder.AppendLine(FString::Printf(TEXT(""), *RequirementsName.ToString())); + } + else if (Tags.IsEmpty() && !GetFlowNode()->IsInputConnected(TagsName)) + { + TextBuilder.AppendLine(FString::Printf(TEXT(""), *TagsName.ToString())); + } + else + { + TextBuilder.AppendLine(Requirements.ToString()); + } + + SetNodeConfigText(TextBuilder.ToText()); +#endif // WITH_EDITOR +} \ No newline at end of file diff --git a/Source/Flow/Private/AddOns/FlowNodeAddOn_SwitchCase.cpp b/Source/Flow/Private/AddOns/FlowNodeAddOn_SwitchCase.cpp new file mode 100644 index 000000000..6a0d9c6cc --- /dev/null +++ b/Source/Flow/Private/AddOns/FlowNodeAddOn_SwitchCase.cpp @@ -0,0 +1,149 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "AddOns/FlowNodeAddOn_SwitchCase.h" +#include "AddOns/FlowNodeAddOn_PredicateAND.h" +#include "AddOns/FlowNodeAddOn_PredicateOR.h" +#include "FlowSettings.h" +#include "Nodes/FlowNode.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNodeAddOn_SwitchCase) + +#define LOCTEXT_NAMESPACE "FlowNodeAddOn_SwitchCase" + +const FName UFlowNodeAddOn_SwitchCase::DefaultCaseName = "Case"; + +UFlowNodeAddOn_SwitchCase::UFlowNodeAddOn_SwitchCase() + : Super() + , CaseName(DefaultCaseName) + , OutputPinName(DefaultCaseName) +{ +#if WITH_EDITOR + NodeDisplayStyle = FlowNodeStyle::AddOn_SwitchCase; + Category = TEXT("Switch"); +#endif +} + +#if WITH_EDITOR + +void UFlowNodeAddOn_SwitchCase::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + if (PropertyChangedEvent.GetMemberPropertyName() == GET_MEMBER_NAME_CHECKED(ThisClass, CaseName)) + { + if (CaseName.IsNone()) + { + CaseName = DefaultCaseName; + } + + RequestReconstructionOnOwningFlowNode(); + } +} + +TArray UFlowNodeAddOn_SwitchCase::GetContextOutputs() const +{ + const UFlowNode* FlowNodeOwner = GetFlowNode(); + check(IsValid(FlowNodeOwner)); + + int32 DuplicateCount = 0; + int32 DuplicateCountForThis = 0; + int32 ThisIndex = INDEX_NONE; + + const TArray& OwnerAddOns = FlowNodeOwner->GetFlowNodeAddOnChildren(); + for (int32 Index = 0; Index < OwnerAddOns.Num(); ++Index) + { + const UFlowNodeAddOn_SwitchCase* SwitchCaseAddOn = Cast(OwnerAddOns[Index]); + if (!IsValid(SwitchCaseAddOn)) + { + continue; + } + + const bool bIsThisAddOn = SwitchCaseAddOn == this; + + if (CaseName == SwitchCaseAddOn->CaseName) + { + ++DuplicateCount; + } + + if (bIsThisAddOn) + { + ThisIndex = Index; + + DuplicateCountForThis = DuplicateCount; + } + } + + check(ThisIndex != INDEX_NONE); + + if (DuplicateCount > 1) + { + OutputPinName = FName(FString::Printf(TEXT("%s (%d)"), *CaseName.ToString(), DuplicateCountForThis)); + } + else + { + OutputPinName = FName(FString::Printf(TEXT("%s"), *CaseName.ToString())); + } + + return { FFlowPin(OutputPinName) }; +} +#endif + +EFlowAddOnAcceptResult UFlowNodeAddOn_SwitchCase::AcceptFlowNodeAddOnChild_Implementation( + const UFlowNodeAddOn* AddOnTemplate, + const TArray& AdditionalAddOnsToAssumeAreChildren) const +{ + if (IFlowPredicateInterface::ImplementsInterfaceSafe(AddOnTemplate)) + { + return EFlowAddOnAcceptResult::TentativeAccept; + } + else + { + // All AddOn children MUST implement IFlowPredicateInterface + // (so do not return Super's implementation which will return Undetermined) + return EFlowAddOnAcceptResult::Reject; + } +} + +bool UFlowNodeAddOn_SwitchCase::TryTriggerForCase_Implementation() const +{ + bool bResult = false; + FLOW_ASSERT_ENUM_MAX(EFlowPredicateCombinationRule, 2); + if (BranchCombinationRule == EFlowPredicateCombinationRule::AND) + { + bResult = UFlowNodeAddOn_PredicateAND::EvaluatePredicateAND(AddOns); + } + else + { + check(BranchCombinationRule == EFlowPredicateCombinationRule::OR); + + bResult = UFlowNodeAddOn_PredicateOR::EvaluatePredicateOR(AddOns); + } + + if (bResult) + { + constexpr bool bFinish = true; + GetFlowNode()->TriggerOutput(OutputPinName, bFinish); + } + + return bResult; +} + +FText UFlowNodeAddOn_SwitchCase::K2_GetNodeTitle_Implementation() const +{ + if (GetDefault()->bUseAdaptiveNodeTitles) + { + FLOW_ASSERT_ENUM_MAX(EFlowPredicateCombinationRule, 2); + if (BranchCombinationRule != EFlowPredicateCombinationRule::AND) + { + return FText::Format(LOCTEXT("SwitchCaseTitle", "{0} ({1})"), { FText::FromName(OutputPinName), UEnum::GetDisplayValueAsText(BranchCombinationRule) }); + } + else + { + return FText::Format(LOCTEXT("SwitchCaseTitle", "{0}"), { FText::FromName(OutputPinName) }); + } + } + + return Super::K2_GetNodeTitle_Implementation(); +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/Flow/Private/Asset/FlowAssetParams.cpp b/Source/Flow/Private/Asset/FlowAssetParams.cpp new file mode 100644 index 000000000..e9983aef8 --- /dev/null +++ b/Source/Flow/Private/Asset/FlowAssetParams.cpp @@ -0,0 +1,386 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/FlowAssetParams.h" +#include "FlowAsset.h" +#include "FlowLogChannels.h" +#include "Asset/FlowAssetParamsUtils.h" +#include "Types/FlowDataPinValuesStandard.h" + +#if WITH_EDITOR +#include "Misc/DataValidation.h" +#include "UObject/ObjectSaveContext.h" +#endif + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowAssetParams) + +#if WITH_EDITOR +void UFlowAssetParams::PostLoad() +{ + Super::PostLoad(); + + if (!HasAnyFlags(RF_ArchetypeObject | RF_ClassDefaultObject)) + { + // Migrate the named properties over to the new structs + + bool bMadeAnyChanges = false; + for (FFlowNamedDataPinProperty& NamedProperty : Properties) + { + bMadeAnyChanges |= NamedProperty.FixupDataPinProperty(); + } + + if (bMadeAnyChanges) + { + ModifyAndRebuildPropertiesMap(); + } + } + + const EFlowReconcilePropertiesResult ReconcileResult = ReconcilePropertiesWithParentParams(); + if (EFlowReconcilePropertiesResult_Classifiers::IsErrorResult(ReconcileResult)) + { + UE_LOG(LogFlow, Error, TEXT("Failed to reconcile ParentParams during PostLoad() for %s: %s"), + *GetPathName(), *UEnum::GetDisplayValueAsText(ReconcileResult).ToString()); + } +} + +void UFlowAssetParams::PreSaveRoot(FObjectPreSaveRootContext ObjectSaveContext) +{ + Super::PreSaveRoot(ObjectSaveContext); + + const EFlowReconcilePropertiesResult ReconcileResult = ReconcilePropertiesWithParentParams(); + if (EFlowReconcilePropertiesResult_Classifiers::IsErrorResult(ReconcileResult)) + { + UE_LOG(LogFlow, Error, TEXT("Failed to reconcile ParentParams during PreSaveRoot() for %s: %s"), + *GetPathName(), *UEnum::GetDisplayValueAsText(ReconcileResult).ToString()); + } +} +#endif + +void UFlowAssetParams::Serialize(FArchive& Ar) +{ +#if WITH_EDITOR + if (Ar.IsCooking()) + { + const EFlowReconcilePropertiesResult ReconcileResult = ReconcilePropertiesWithParentParams(); + if (EFlowReconcilePropertiesResult_Classifiers::IsErrorResult(ReconcileResult)) + { + UE_LOG(LogFlow, Error, TEXT("Failed to reconcile ParentParams during cooking for %s: %s"), + *GetPathName(), *UEnum::GetDisplayValueAsText(ReconcileResult).ToString()); + } + } + +#endif + Super::Serialize(Ar); +} + +UFlowAsset* UFlowAssetParams::ProvideFlowAsset() const +{ +#if WITH_EDITOR + return OwnerFlowAsset.LoadSynchronous(); +#else + // We don't have knowledge of the OwnerFlowAsset in non-editor builds + checkNoEntry(); + return nullptr; +#endif +} + +#if WITH_EDITOR +EDataValidationResult UFlowAssetParams::IsDataValid(FDataValidationContext& Context) const +{ + EDataValidationResult Result = Super::IsDataValid(Context); + + if (OwnerFlowAsset.IsNull()) + { + Context.AddError(FText::FromString(TEXT("OwnerFlowAsset is null"))); + Result = CombineDataValidationResults(Result, EDataValidationResult::Invalid); + } + else if (!OwnerFlowAsset.IsValid() && !OwnerFlowAsset.LoadSynchronous()) + { + Context.AddError(FText::FromString(FString::Printf(TEXT("Failed to load OwnerFlowAsset: %s"), *OwnerFlowAsset.ToString()))); + Result = CombineDataValidationResults(Result, EDataValidationResult::Invalid); + } + + const EFlowReconcilePropertiesResult CycleResult = CheckForParentCycle(); + if (EFlowReconcilePropertiesResult_Classifiers::IsErrorResult(CycleResult)) + { + Context.AddError(FText::FromString(TEXT("Cyclic inheritance detected"))); + Result = CombineDataValidationResults(Result, EDataValidationResult::Invalid); + } + + TSet SeenGuids; + for (int32 Index = 0; Index < Properties.Num(); ++Index) + { + const FFlowNamedDataPinProperty& Property = Properties[Index]; + if (Property.Name == NAME_None) + { + Context.AddError(FText::FromString(FString::Printf(TEXT("Property at index %d has invalid name"), Index))); + Result = CombineDataValidationResults(Result, EDataValidationResult::Invalid); + } + + if (!Property.DataPinValue.IsValid()) + { + Context.AddError(FText::FromString(FString::Printf(TEXT("Property at index %d has invalid DataPinValue"), Index))); + Result = CombineDataValidationResults(Result, EDataValidationResult::Invalid); + } + + if (!Property.Guid.IsValid()) + { + Context.AddError(FText::FromString(FString::Printf(TEXT("Property at index %d has invalid Guid"), Index))); + Result = CombineDataValidationResults(Result, EDataValidationResult::Invalid); + } + else if (SeenGuids.Contains(Property.Guid)) + { + Context.AddError(FText::FromString(FString::Printf(TEXT("Duplicate Guid found for property at index %d"), Index))); + Result = CombineDataValidationResults(Result, EDataValidationResult::Invalid); + } + else + { + SeenGuids.Add(Property.Guid); + } + + if (Property.bMayChangeNameAndType) + { + Context.AddError(FText::FromString(FString::Printf(TEXT("Property at index %d has bMayChangeNameAndType = true in UFlowAssetParams"), Index))); + Result = CombineDataValidationResults(Result, EDataValidationResult::Invalid); + } + } + + return Result; +} + +EFlowReconcilePropertiesResult UFlowAssetParams::ReconcilePropertiesWithStartNode( + const FDateTime& FlowAssetLastSaveTimeStamp, + const TSoftObjectPtr& InOwnerFlowAsset, + TArray& MutablePropertiesFromStartNode) +{ + OwnerFlowAsset = InOwnerFlowAsset; + + if (OwnerFlowAsset.IsNull()) + { + return EFlowReconcilePropertiesResult::Error_InvalidAsset; + } + + const EFlowReconcilePropertiesResult PropertiesMatchResult = FFlowAssetParamsUtils::CheckPropertiesMatch(Properties, MutablePropertiesFromStartNode); + const FDateTime ParamsTimestamp = FFlowAssetParamsUtils::GetLastSavedTimestampForObject(this); + + if (FlowAssetLastSaveTimeStamp >= ParamsTimestamp || + EFlowReconcilePropertiesResult_Classifiers::IsErrorResult(PropertiesMatchResult)) + { + ConfigureFlowAssetParams(InOwnerFlowAsset, nullptr, MutablePropertiesFromStartNode); + + return EFlowReconcilePropertiesResult::ParamsPropertiesUpdated; + } + + MutablePropertiesFromStartNode = Properties; + + FFlowNamedDataPinProperty::ConfigurePropertiesForFlowAssetParams(MutablePropertiesFromStartNode); + + return EFlowReconcilePropertiesResult::AssetPropertyValuesUpdated; +} + +EFlowReconcilePropertiesResult UFlowAssetParams::ReconcilePropertiesWithParentParams() +{ + const EFlowReconcilePropertiesResult CycleResult = CheckForParentCycle(); + if (EFlowReconcilePropertiesResult_Classifiers::IsErrorResult(CycleResult)) + { + return CycleResult; + } + + if (ParentParams.AssetPtr.IsNull()) + { + return EFlowReconcilePropertiesResult::NoChanges; + } + + UFlowAssetParams* Parent = ParentParams.AssetPtr.LoadSynchronous(); + if (!Parent) + { + UE_LOG(LogFlow, Warning, TEXT("Failed to load ParentParams: %s"), *ParentParams.AssetPtr.ToString()); + + return EFlowReconcilePropertiesResult::Error_UnloadableParent; + } + + const EFlowReconcilePropertiesResult ParentResult = Parent->ReconcilePropertiesWithParentParams(); + if (EFlowReconcilePropertiesResult_Classifiers::IsErrorResult(ParentResult)) + { + return ParentResult; + } + + const TArray& ParentProps = Parent->Properties; + TArray NewProperties; + + for (const FFlowNamedDataPinProperty& ParentProp : ParentProps) + { + FFlowNamedDataPinProperty* LocalProp = FFlowAssetParamsUtils::FindPropertyByGuid(Properties, ParentProp.Guid); + if (LocalProp != nullptr) + { + // We have a version of ParentProp locally. + // Determine if our local property has been modified since our last reconcile. + // A local property is considered modified if we've never added it to PropertyMap or is different from what currently exists in PropertyMap. + bool bLocalPropHasChanged = true; + if (PropertyMap.Contains(LocalProp->Name)) + { + FFlowNamedDataPinProperty PreviousLocalProp = *LocalProp; + PreviousLocalProp.DataPinValue = PropertyMap[LocalProp->Name]; + bLocalPropHasChanged = !FFlowAssetParamsUtils::ArePropertiesEqual(*LocalProp, PreviousLocalProp); + } + + if (bLocalPropHasChanged) + { + // If the local property has been changed then compare it to the parent value to determine if it is an override or not. + if (FFlowAssetParamsUtils::ArePropertiesEqual(*LocalProp, ParentProp)) + { + FFlowNamedDataPinProperty& NewProp = NewProperties.Add_GetRef(ParentProp); + NewProp.bIsOverride = false; + } + else + { + FFlowNamedDataPinProperty& NewProp = NewProperties.Add_GetRef(*LocalProp); + NewProp.Name = ParentProp.Name; + NewProp.bIsOverride = true; + } + } + else + { + // If the local property has not been changed then check whether it is an override. + // Overrides will get copied over while non-overrides will be updated to match the parent. + if (LocalProp->bIsOverride) + { + FFlowNamedDataPinProperty& NewProp = NewProperties.Add_GetRef(*LocalProp); + NewProp.Name = ParentProp.Name; + NewProp.bIsOverride = true; + } + else + { + FFlowNamedDataPinProperty& NewProp = NewProperties.Add_GetRef(ParentProp); + NewProp.bIsOverride = false; + } + } + } + else + { + // We do not have a version of ParentProp. Just make a non-override copy. + FFlowNamedDataPinProperty& NewProp = NewProperties.Add_GetRef(ParentProp); + NewProp.bIsOverride = false; + } + } + + for (FFlowNamedDataPinProperty& LocalProp : Properties) + { + if (!FFlowAssetParamsUtils::FindPropertyByGuid(ParentProps, LocalProp.Guid)) + { + LocalProp.bIsOverride = true; + + NewProperties.Add(LocalProp); + } + } + + Properties = NewProperties; + + ModifyAndRebuildPropertiesMap(); + + return EFlowReconcilePropertiesResult::ParamsPropertiesUpdated; +} + +void UFlowAssetParams::ConfigureFlowAssetParams(TSoftObjectPtr OwnerAsset, TSoftObjectPtr InParentParams, const TArray& InProperties) +{ + ParentParams.AssetPtr = InParentParams; + OwnerFlowAsset = OwnerAsset; + Properties = InProperties; + FFlowNamedDataPinProperty::ConfigurePropertiesForFlowAssetParams(Properties); + + ModifyAndRebuildPropertiesMap(); +} + +bool UFlowAssetParams::CanModifyFlowDataPinType() const +{ + // These are set by the Flow asset, which is authoritative + return false; +} + +bool UFlowAssetParams::ShowFlowDataPinValueInputPinCheckbox() const +{ + // These are set by the Flow asset, which is authoritative + return false; +} + +bool UFlowAssetParams::ShowFlowDataPinValueClassFilter(const FFlowDataPinValue* Value) const +{ + return true; +} + +bool UFlowAssetParams::CanEditFlowDataPinValueClassFilter(const FFlowDataPinValue* Value) const +{ + // These are set by the Flow asset, which is authoritative + return false; +} + +EFlowReconcilePropertiesResult UFlowAssetParams::CheckForParentCycle() const +{ + TSet> Visited; + TSoftObjectPtr Current = ParentParams.AssetPtr; + + while (!Current.IsNull()) + { + if (Visited.Contains(Current)) + { + UE_LOG(LogFlow, Error, TEXT("Cyclic inheritance detected at: %s"), *Current.ToString()); + return EFlowReconcilePropertiesResult::Error_CyclicInheritance; + } + + Visited.Add(Current); + const UFlowAssetParams* CurrentParams = Current.LoadSynchronous(); + if (!CurrentParams) + { + UE_LOG(LogFlow, Warning, TEXT("Failed to load ParentParams: %s"), *Current.ToString()); + return EFlowReconcilePropertiesResult::Error_UnloadableParent; + } + + Current = CurrentParams->ParentParams.AssetPtr; + } + + return EFlowReconcilePropertiesResult::NoChanges; +} + +void UFlowAssetParams::ModifyAndRebuildPropertiesMap() +{ + Modify(); + + RebuildPropertiesMap(); + + MarkPackageDirty(); +} + +void UFlowAssetParams::RebuildPropertiesMap() +{ + PropertyMap.Reset(); + + for (const FFlowNamedDataPinProperty& Prop : Properties) + { + if (Prop.IsValid()) + { + PropertyMap.Add(Prop.Name, Prop.DataPinValue); + } + else + { + UE_LOG(LogFlow, Warning, TEXT("Skipping invalid property %s during rebuild for %s"), *Prop.Name.ToString(), *GetPathName()); + } + } +} +#endif + +bool UFlowAssetParams::CanSupplyDataPinValues() const +{ + return !PropertyMap.IsEmpty(); +} + +FFlowDataPinResult UFlowAssetParams::TrySupplyDataPin(FName PinName) const +{ + if (const TInstancedStruct* Found = PropertyMap.Find(PinName)) + { + FFlowDataPinResult DataPinResult(EFlowDataPinResolveResult::Success); + DataPinResult.ResultValue = (*Found); + + return DataPinResult; + } + + return FFlowDataPinResult(EFlowDataPinResolveResult::FailedUnknownPin); +} diff --git a/Source/Flow/Private/Asset/FlowAssetParamsTypes.cpp b/Source/Flow/Private/Asset/FlowAssetParamsTypes.cpp new file mode 100644 index 000000000..48043832b --- /dev/null +++ b/Source/Flow/Private/Asset/FlowAssetParamsTypes.cpp @@ -0,0 +1,11 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/FlowAssetParamsTypes.h" +#include "Asset/FlowAssetParams.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowAssetParamsTypes) + +UFlowAssetParams* FFlowAssetParamsPtr::ResolveFlowAssetParams() const +{ + return AssetPtr.LoadSynchronous(); +} diff --git a/Source/Flow/Private/Asset/FlowAssetParamsUtils.cpp b/Source/Flow/Private/Asset/FlowAssetParamsUtils.cpp new file mode 100644 index 000000000..f7688920a --- /dev/null +++ b/Source/Flow/Private/Asset/FlowAssetParamsUtils.cpp @@ -0,0 +1,259 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/FlowAssetParamsUtils.h" +#include "Types/FlowNamedDataPinProperty.h" + +#include "Misc/DateTime.h" +#include "HAL/FileManager.h" + +#if WITH_EDITOR +#include "Asset/FlowAssetParams.h" +#include "FlowLogChannels.h" + +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetToolsModule.h" +#include "ContentBrowserModule.h" +#include "FileHelpers.h" +#include "IContentBrowserSingleton.h" +#include "Misc/MessageDialog.h" +#include "Modules/ModuleManager.h" +#include "SourceControlHelpers.h" +#endif + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowAssetParamsUtils) + +#if WITH_EDITOR +FDateTime FFlowAssetParamsUtils::GetLastSavedTimestampForObject(const UObject* Object) +{ + if (!Object) + { + return FDateTime::MinValue(); + } + + const FString PackagePath = Object->GetPathName(); + return IFileManager::Get().GetTimeStamp(*PackagePath); +} + +EFlowReconcilePropertiesResult FFlowAssetParamsUtils::CheckPropertiesMatch( + const TArray& PropertiesA, + const TArray& PropertiesB) +{ + if (PropertiesA.Num() != PropertiesB.Num()) + { + return EFlowReconcilePropertiesResult::Error_PropertyCountMismatch; + } + + for (int32 Index = 0; Index < PropertiesA.Num(); ++Index) + { + const FFlowNamedDataPinProperty& PropA = PropertiesA[Index]; + const FFlowNamedDataPinProperty& PropB = PropertiesB[Index]; + const UScriptStruct* ScriptStructA = PropA.DataPinValue.GetScriptStruct(); + const UScriptStruct* ScriptStructB = PropB.DataPinValue.GetScriptStruct(); + + if (PropA.Name != PropB.Name || + ScriptStructA != ScriptStructB || + !IsValid(ScriptStructA)) + { + return EFlowReconcilePropertiesResult::Error_PropertyTypeMismatch; + } + } + + return EFlowReconcilePropertiesResult::NoChanges; +} + +const FFlowNamedDataPinProperty* FFlowAssetParamsUtils::FindPropertyByGuid( + const TArray& Props, + const FGuid& Guid) +{ + for (const FFlowNamedDataPinProperty& Prop : Props) + { + if (Prop.Guid == Guid) + { + return &Prop; + } + } + + return nullptr; +} + +FFlowNamedDataPinProperty* FFlowAssetParamsUtils::FindPropertyByGuid( + TArray& Props, + const FGuid& Guid) +{ + for (FFlowNamedDataPinProperty& Prop : Props) + { + if (Prop.Guid == Guid) + { + return &Prop; + } + } + + return nullptr; +} + +bool FFlowAssetParamsUtils::ArePropertyArraysEqual( + const TArray& A, + const TArray& B) +{ + if (A.Num() != B.Num()) + { + return false; + } + + for (int32 Index = 0; Index < A.Num(); ++Index) + { + if (!ArePropertiesEqual(A[Index], B[Index])) + { + return false; + } + } + + return true; +} + +bool FFlowAssetParamsUtils::ArePropertiesEqual( + const FFlowNamedDataPinProperty& A, + const FFlowNamedDataPinProperty& B) +{ + if (A.Name != B.Name || A.Guid != B.Guid) + { + return false; + } + + const UScriptStruct* ScriptStructA = A.DataPinValue.GetScriptStruct(); + const UScriptStruct* ScriptStructB = B.DataPinValue.GetScriptStruct(); + if (ScriptStructA != ScriptStructB) + { + return false; + } + + return A.DataPinValue == B.DataPinValue; +} + +UFlowAssetParams* FFlowAssetParamsUtils::CreateChildParamsAsset(UFlowAssetParams& ParentParams, const bool bShowDialogs, FText* OutOptionalFailureReason) +{ + if (!IsValid(&ParentParams)) + { + FailCreateChild( + NSLOCTEXT("FlowAssetParamsUtils", "InvalidParent", "Invalid Parent Flow Asset Params."), + bShowDialogs, + OutOptionalFailureReason); + + return nullptr; + } + + const FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); + + const FString PackagePath = FPackageName::GetLongPackagePath(ParentParams.GetPackage()->GetPathName()); + const FString BaseAssetName = ParentParams.GetName(); + + // Generate a unique name for the new asset params + FString UniquePackageName; + FString UniqueAssetName; + AssetToolsModule.Get().CreateUniqueAssetName(PackagePath + TEXT("/") + BaseAssetName, TEXT(""), UniquePackageName, UniqueAssetName); + + if (UniqueAssetName.IsEmpty()) + { + FailCreateChild( + FText::Format( + NSLOCTEXT("FlowAssetParamsUtils", "UniqueNameFail", "Failed to generate unique asset name for child params of {0}."), + FText::FromString(BaseAssetName)), + bShowDialogs, + OutOptionalFailureReason); + + return nullptr; + } + + // Create the new asset params + UFlowAssetParams* NewParams = Cast( + AssetToolsModule.Get().CreateAsset(UniqueAssetName, PackagePath, ParentParams.GetClass(), nullptr)); + + if (!IsValid(NewParams)) + { + FailCreateChild( + FText::Format( + NSLOCTEXT("FlowAssetParamsUtils", "CreateAssetFail", "Failed to create child Flow Asset Params: {0}."), + FText::FromString(UniqueAssetName)), + bShowDialogs, + OutOptionalFailureReason); + + return nullptr; + } + + // Best-effort source control integration (before save) + if (USourceControlHelpers::IsAvailable()) + { + const FString FileName = USourceControlHelpers::PackageFilename(NewParams->GetPathName()); + if (!USourceControlHelpers::CheckOutOrAddFile(FileName)) + { + UE_LOG(LogFlow, Warning, TEXT("Failed to check out/add %s; saved in-memory only"), *NewParams->GetPathName()); + } + } + + // Configure from parent (copies OwnerFlowAsset + Properties, sets ParentParams, rebuilds PropertyMap, marks dirty) + NewParams->ConfigureFlowAssetParams(ParentParams.OwnerFlowAsset, &ParentParams, ParentParams.Properties); + + // Reconcile (cycle detection, flattened inheritance, etc.) + const EFlowReconcilePropertiesResult ReconcileResult = NewParams->ReconcilePropertiesWithParentParams(); + if (EFlowReconcilePropertiesResult_Classifiers::IsErrorResult(ReconcileResult)) + { + FailCreateChild( + FText::Format( + NSLOCTEXT("FlowAssetParamsUtils", "ReconcileFail", + "Created asset but reconciliation failed.\n\nAsset: {0}\nError: {1}\n\nThe asset may be invalid and should be reviewed."), + FText::FromString(NewParams->GetPathName()), + UEnum::GetDisplayValueAsText(ReconcileResult)), + bShowDialogs, + OutOptionalFailureReason); + + // Keep going: asset exists and may still be useful for debugging/fixing + } + + // Save the package (force save even if not prompted) + { + UPackage* Package = NewParams->GetPackage(); + const TArray PackagesToSave = { Package }; + + constexpr bool bForceSave = true; + if (!UEditorLoadingAndSavingUtils::SavePackages(PackagesToSave, bForceSave)) + { + FailCreateChild( + FText::Format( + NSLOCTEXT("FlowAssetParamsUtils", "SaveFail", "Failed to save child Flow Asset Params: {0}."), + FText::FromString(NewParams->GetPathName())), + bShowDialogs, + OutOptionalFailureReason); + + // Still return the in-memory asset + } + } + + // Register + sync to Content Browser + { + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + AssetRegistryModule.Get().AssetCreated(NewParams); + + const FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked("ContentBrowser"); + const TArray AssetsToSync = { NewParams }; + ContentBrowserModule.Get().SyncBrowserToAssets(AssetsToSync, true); + } + + return NewParams; +} + +void FFlowAssetParamsUtils::FailCreateChild(const FText& Reason, const bool bShowDialogs, FText* OutOptionalFailureReason) +{ + if (OutOptionalFailureReason) + { + *OutOptionalFailureReason = Reason; + } + + UE_LOG(LogFlow, Error, TEXT("%s"), *Reason.ToString()); + + if (bShowDialogs) + { + FMessageDialog::Open(EAppMsgType::Ok, Reason); + } +} + +#endif \ No newline at end of file diff --git a/Source/Flow/Private/Asset/FlowDeferredTransitionScope.cpp b/Source/Flow/Private/Asset/FlowDeferredTransitionScope.cpp new file mode 100644 index 000000000..c5ac2b4db --- /dev/null +++ b/Source/Flow/Private/Asset/FlowDeferredTransitionScope.cpp @@ -0,0 +1,32 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/FlowDeferredTransitionScope.h" +#include "FlowAsset.h" +#include "Interfaces/FlowExecutionGate.h" + +void FFlowDeferredTransitionScope::EnqueueDeferredTrigger(const FFlowDeferredTriggerInput& Entry) +{ + check(bIsOpen); + + DeferredTriggers.Add(Entry); +} + +bool FFlowDeferredTransitionScope::TryFlushDeferredTriggers(UFlowAsset& OwningFlowAsset) +{ + // Ensure the scope is closed before beginning flushing + CloseScope(); + + // Remove and trigger each deferred trigger input + while (!DeferredTriggers.IsEmpty() && !FFlowExecutionGate::IsHalted()) + { + const FFlowDeferredTriggerInput Entry = DeferredTriggers[0]; + DeferredTriggers.RemoveAt(0, 1, EAllowShrinking::No); + + OwningFlowAsset.TriggerInput(Entry.NodeGuid, Entry.PinName, Entry.FromPin); + } + + check(DeferredTriggers.IsEmpty() || FFlowExecutionGate::IsHalted()); + + // Return true if everything flushed without being interrupted by an ExecutionGate + return DeferredTriggers.IsEmpty(); +} \ No newline at end of file diff --git a/Source/Flow/Private/FlowAsset.cpp b/Source/Flow/Private/FlowAsset.cpp index 7744705f8..aba469583 100644 --- a/Source/Flow/Private/FlowAsset.cpp +++ b/Source/Flow/Private/FlowAsset.cpp @@ -1,35 +1,67 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #include "FlowAsset.h" + +#include "FlowLogChannels.h" #include "FlowSettings.h" #include "FlowSubsystem.h" - -#include "Nodes/FlowNode.h" -#include "Nodes/Route/FlowNode_CustomInput.h" -#include "Nodes/Route/FlowNode_Start.h" -#include "Nodes/Route/FlowNode_Finish.h" -#include "Nodes/Route/FlowNode_SubGraph.h" +#include "AddOns/FlowNodeAddOn.h" +#include "Asset/FlowAssetParams.h" +#include "Asset/FlowAssetParamsUtils.h" +#include "Interfaces/FlowExecutionGate.h" +#include "Nodes/FlowNodeBase.h" +#include "Nodes/Graph/FlowNode_CustomInput.h" +#include "Nodes/Graph/FlowNode_CustomOutput.h" +#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" #include "Engine/World.h" #include "Serialization/MemoryReader.h" #include "Serialization/MemoryWriter.h" +#include "Algo/AnyOf.h" + +#if WITH_EDITOR +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetToolsModule.h" +#include "ContentBrowserModule.h" +#include "IContentBrowserSingleton.h" +#include "Editor.h" +#include "Editor/EditorEngine.h" +#include "Modules/ModuleManager.h" +#include "SourceControlHelpers.h" +#include "UObject/ObjectSaveContext.h" +#include "UObject/Package.h" + +FString UFlowAsset::ValidationError_NodeClassNotAllowed = TEXT("Node class {0} is not allowed in this asset."); +FString UFlowAsset::ValidationError_AddOnNodeClassNotAllowed = TEXT("AddOn Node class {0} is not allowed in this asset."); +FString UFlowAsset::ValidationError_NullNodeInstance = TEXT("Node with GUID {0} is NULL"); +FString UFlowAsset::ValidationError_NullAddOnNodeInstance = TEXT("Node with GUID {0} has NULL AddOn(s)"); +#endif + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowAsset) UFlowAsset::UFlowAsset(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) , bWorldBound(true) -#if WITH_EDITOR +#if WITH_EDITORONLY_DATA , FlowGraph(nullptr) #endif - , AllowedNodeClasses({UFlowNode::StaticClass()}) + , AllowedNodeClasses({UFlowNodeBase::StaticClass()}) + , AllowedInSubgraphNodeClasses({UFlowNode_SubGraph::StaticClass()}) , bStartNodePlacedAsGhostNode(false) , TemplateAsset(nullptr) - , StartNode(nullptr) , FinishPolicy(EFlowFinishPolicy::Keep) { if (!AssetGuid.IsValid()) { AssetGuid = FGuid::NewGuid(); } + + ExpectedOwnerClass = GetDefault()->GetDefaultExpectedOwnerClass(); } #if WITH_EDITOR @@ -63,31 +95,274 @@ void UFlowAsset::PostDuplicate(bool bDuplicateForPIE) } } -EDataValidationResult UFlowAsset::IsDataValid(TArray& ValidationErrors) +void UFlowAsset::PostLoad() { - for (const TPair& Node : Nodes) + Super::PostLoad(); + + const UPackage* Package = GetPackage(); + if (IsValid(Package) && !FPackageName::IsTempPackage(Package->GetPathName())) { - if (Node.Value == nullptr || Node.Value->IsDataValid(ValidationErrors) == EDataValidationResult::Invalid) + // If we removed or moved a flow node blueprint (and there is no redirector) we might lose the reference to it resulting + // in null pointers in the Nodes FGUID->UFlowNode* Map. So here we iterate over all the Nodes and remove all pairs that + // are nulled out. + + TSet NodesToRemoveGUID; + + for (const TPair& Node : GetNodes()) { - // refresh data if Node is missing, i.e. its class has been deleted - if (Node.Value == nullptr) + if (!IsValid(Node.Value)) { - HarvestNodeConnections(); + NodesToRemoveGUID.Emplace(Node.Key); } + } + + for (const FGuid& Guid : NodesToRemoveGUID) + { + UnregisterNode(Guid); + } + ReconcileBaseAssetParams(FFlowAssetParamsUtils::GetLastSavedTimestampForObject(this)); + } +} + +void UFlowAsset::PreSaveRoot(FObjectPreSaveRootContext ObjectSaveContext) +{ + ReconcileBaseAssetParams(FDateTime::Now()); +} + +EDataValidationResult UFlowAsset::ValidateAsset(FFlowMessageLog& MessageLog) +{ + // validate nodes + for (const TPair& Node : ObjectPtrDecay(Nodes)) + { + if (IsValid(Node.Value)) + { + FText FailureReason; + if (!IsNodeOrAddOnClassAllowed(Node.Value->GetClass(), &FailureReason)) + { + const FString ErrorMsg = + FailureReason.IsEmpty() + ? FString::Format(*ValidationError_NodeClassNotAllowed, {*Node.Value->GetClass()->GetName()}) + : FailureReason.ToString(); + + MessageLog.Error(*ErrorMsg, Node.Value); + } + + Node.Value->ValidationLog.Messages.Empty(); + Node.Value->ValidateNode(); + MessageLog.Messages.Append(Node.Value->ValidationLog.Messages); + + // Validate AddOns + for (UFlowNodeAddOn* AddOn : Node.Value->GetFlowNodeAddOnChildren()) + { + if (IsValid(AddOn)) + { + ValidateAddOnTree(*AddOn, MessageLog); + } + else + { + const FString ErrorMsg = FString::Format(*ValidationError_NullAddOnNodeInstance, {*Node.Key.ToString()}); + MessageLog.Error(*ErrorMsg, this); + } + } + } + else + { + const FString ErrorMsg = FString::Format(*ValidationError_NullNodeInstance, {*Node.Key.ToString()}); + MessageLog.Error(*ErrorMsg, this); + } + } + + // if at least one error has been logged : mark the asset as invalid + for (const TSharedRef& Msg : MessageLog.Messages) + { + if (Msg->GetSeverity() == EMessageSeverity::Error) + { return EDataValidationResult::Invalid; } } + // otherwise, the asset is considered valid (even with warnings or notes) return EDataValidationResult::Valid; } -TSharedPtr UFlowAsset::FlowGraphInterface = nullptr; +bool UFlowAsset::IsNodeOrAddOnClassAllowed(const UClass* FlowNodeOrAddOnClass, FText* OutOptionalFailureReason) const +{ + if (!IsValid(FlowNodeOrAddOnClass)) + { + return false; + } + + if (!CanFlowNodeClassBeUsedByFlowAsset(*FlowNodeOrAddOnClass)) + { + return false; + } + + if (!CanFlowAssetUseFlowNodeClass(*FlowNodeOrAddOnClass)) + { + return false; + } + + // Confirm plugin reference restrictions are being respected + if (!CanFlowAssetReferenceFlowNode(*FlowNodeOrAddOnClass, OutOptionalFailureReason)) + { + return false; + } + + return true; +} + +bool UFlowAsset::CanFlowNodeClassBeUsedByFlowAsset(const UClass& FlowNodeClass) const +{ + const UFlowNode* NodeDefaults = Cast(FlowNodeClass.GetDefaultObject()); + if (!NodeDefaults) + { + check(FlowNodeClass.IsChildOf()); + + // AddOns don't have the AllowedAssetClasses/DeniedAssetClasses + // (yet? maybe we move it up to the base?) + return true; + } + + // UFlowNode class limits which UFlowAsset class can use it + const TArray>& DeniedAssetClasses = NodeDefaults->DeniedAssetClasses; + for (const UClass* DeniedAssetClass : DeniedAssetClasses) + { + if (DeniedAssetClass && GetClass()->IsChildOf(DeniedAssetClass)) + { + return false; + } + } -void UFlowAsset::SetFlowGraphInterface(TSharedPtr InFlowAssetEditor) + const TArray>& AllowedAssetClasses = NodeDefaults->AllowedAssetClasses; + if (AllowedAssetClasses.Num() > 0) + { + bool bAllowedInAsset = false; + for (const UClass* AllowedAssetClass : AllowedAssetClasses) + { + if (AllowedAssetClass && GetClass()->IsChildOf(AllowedAssetClass)) + { + bAllowedInAsset = true; + break; + } + } + if (!bAllowedInAsset) + { + return false; + } + } + + return true; +} + +bool UFlowAsset::CanFlowAssetUseFlowNodeClass(const UClass& FlowNodeClass) const { - check(!FlowGraphInterface.IsValid()); - FlowGraphInterface = InFlowAssetEditor; + // UFlowAsset class can limit which UFlowNodeBase classes can be used + if (IsFlowNodeClassInDeniedClasses(FlowNodeClass)) + { + return false; + } + + if (!IsFlowNodeClassInAllowedClasses(FlowNodeClass)) + { + return false; + } + + return true; +} + +bool UFlowAsset::CanFlowAssetReferenceFlowNode(const UClass& FlowNodeClass, FText* OutOptionalFailureReason) const +{ + if (!GEditor || !IsValid(&FlowNodeClass)) + { + return false; + } + + // Confirm plugin reference restrictions are being respected + FAssetReferenceFilterContext AssetReferenceFilterContext; + AssetReferenceFilterContext.AddReferencingAsset(FAssetData(this)); + const TSharedPtr FlowAssetReferenceFilter = GEditor->MakeAssetReferenceFilter(AssetReferenceFilterContext); + if (FlowAssetReferenceFilter.IsValid()) + { + const FAssetData FlowNodeAssetData(&FlowNodeClass); + if (!FlowAssetReferenceFilter->PassesFilter(FlowNodeAssetData, OutOptionalFailureReason)) + { + return false; + } + } + + return true; +} + +bool UFlowAsset::IsFlowNodeClassInAllowedClasses(const UClass& FlowNodeClass, const TSubclassOf& RequiredAncestor) const +{ + if (AllowedNodeClasses.Num() > 0) + { + bool bAllowedInAsset = false; + for (const TSubclassOf& AllowedNodeClass : AllowedNodeClasses) + { + // If a RequiredAncestor is provided, the AllowedNodeClass must be a subclass of the RequiredAncestor + if (AllowedNodeClass && FlowNodeClass.IsChildOf(AllowedNodeClass) && (!RequiredAncestor || AllowedNodeClass->IsChildOf(RequiredAncestor))) + { + bAllowedInAsset = true; + + break; + } + } + + if (!bAllowedInAsset) + { + return false; + } + } + + return true; +} + +bool UFlowAsset::IsFlowNodeClassInDeniedClasses(const UClass& FlowNodeClass) const +{ + for (const TSubclassOf& DeniedNodeClass : DeniedNodeClasses) + { + if (DeniedNodeClass && FlowNodeClass.IsChildOf(DeniedNodeClass)) + { + // Subclasses of a DeniedNodeClass can opt back in to being allowed + if (!IsFlowNodeClassInAllowedClasses(FlowNodeClass, DeniedNodeClass)) + { + return true; + } + } + } + + return false; +} + +void UFlowAsset::ValidateAddOnTree(UFlowNodeAddOn& AddOn, FFlowMessageLog& MessageLog) +{ + // Filter unauthorized addon nodes + FText FailureReason; + if (!IsNodeOrAddOnClassAllowed(AddOn.GetClass(), &FailureReason)) + { + const FString ErrorMsg = + FailureReason.IsEmpty() + ? FString::Format(*ValidationError_AddOnNodeClassNotAllowed, {*AddOn.GetClass()->GetName()}) + : FailureReason.ToString(); + + MessageLog.Error(*ErrorMsg, AddOn.GetFlowNodeSelfOrOwner()); + } + + // Validate AddOn + AddOn.ValidationLog.Messages.Empty(); + AddOn.ValidateNode(); + MessageLog.Messages.Append(AddOn.ValidationLog.Messages); + + // Validate Children + for (UFlowNodeAddOn* Child : AddOn.GetFlowNodeAddOnChildren()) + { + if (IsValid(Child)) + { + ValidateAddOnTree(*Child, MessageLog); + } + } } UFlowNode* UFlowAsset::CreateNode(const UClass* NodeClass, UEdGraphNode* GraphNode) @@ -105,6 +380,11 @@ void UFlowAsset::RegisterNode(const FGuid& NewGuid, UFlowNode* NewNode) Nodes.Emplace(NewGuid, NewNode); HarvestNodeConnections(); + + if (NewNode->TryUpdateAutoDataPins()) + { + (void)NewNode->OnReconstructionRequested.ExecuteIfBound(); + } } void UFlowAsset::UnregisterNode(const FGuid& NodeGuid) @@ -113,34 +393,65 @@ void UFlowAsset::UnregisterNode(const FGuid& NodeGuid) Nodes.Compact(); HarvestNodeConnections(); - MarkPackageDirty(); + + (void)MarkPackageDirty(); } -void UFlowAsset::HarvestNodeConnections() +void UFlowAsset::HarvestNodeConnections(UFlowNode* TargetNode) { - TMap Connections; - bool bGraphDirty = false; + TArray TargetNodes; - // last moment to remove invalid nodes - for (auto NodeIt = Nodes.CreateIterator(); NodeIt; ++NodeIt) + if (IsValid(TargetNode)) { - const TPair& Pair = *NodeIt; - if (Pair.Value == nullptr) + TargetNodes.Reserve(1); + TargetNodes.Add(TargetNode); + } + else + { + TargetNodes.Reserve(Nodes.Num()); + for (const TPair& Pair : ObjectPtrDecay(Nodes)) + { + TargetNodes.Add(Pair.Value); + } + } + + // Remove any invalid nodes + for (auto NodeIt = TargetNodes.CreateIterator(); NodeIt; ++NodeIt) + { + if (*NodeIt == nullptr) { NodeIt.RemoveCurrent(); - bGraphDirty = true; + Modify(); } } - for (const TPair& Pair : Nodes) + for (UFlowNode* FlowNode : TargetNodes) { - UFlowNode* Node = Pair.Value; + bool bNodeDirty = false; + TMap FoundConnections; + const TArray& GraphNodePins = FlowNode->GetGraphNode()->Pins; - for (const UEdGraphPin* ThisPin : Node->GetGraphNode()->Pins) + for (const UEdGraphPin* ThisPin : GraphNodePins) { - if (ThisPin->Direction == EGPD_Output && ThisPin->LinkedTo.Num() > 0) + const bool bIsExecPin = FFlowPin::IsExecPinCategory(ThisPin->PinType.PinCategory); + const bool bIsDataPin = !bIsExecPin; + const bool bIsOutputPin = (ThisPin->Direction == EGPD_Output); + const bool bIsInputPin = (ThisPin->Direction == EGPD_Input); + const bool bHasAtLeastOneConnection = ThisPin->LinkedTo.Num() > 0; + + if (bIsExecPin && bIsOutputPin && bHasAtLeastOneConnection) + { + // For Exec Pins, harvest the 0th connection (we should have only 1 connection, because of schema rules) + if (const UEdGraphPin* LinkedPin = ThisPin->LinkedTo[0]) + { + const UEdGraphNode* LinkedNode = LinkedPin->GetOwningNode(); + FoundConnections.Add(ThisPin->PinName, FConnectedPin(LinkedNode->NodeGuid, LinkedPin->PinName)); + } + } + else if (bIsDataPin && bIsInputPin && bHasAtLeastOneConnection) { + // For Data Pins, harvest the 0th connection (we should have only 1 connection, because of schema rules) if (const UEdGraphPin* LinkedPin = ThisPin->LinkedTo[0]) { const UEdGraphNode* LinkedNode = LinkedPin->GetOwningNode(); @@ -150,51 +461,410 @@ void UFlowAsset::HarvestNodeConnections() } // This check exists to ensure that we don't mark graph dirty, if none of connections changed - // Optimization: we need check it only until the first node would be marked dirty, as this already marks Flow Asset package dirty - if (bGraphDirty == false) { - if (FoundConnections.Num() != Node->Connections.Num()) + const TMap& OldConnections = FlowNode->Connections; + if (FoundConnections.Num() != OldConnections.Num()) { - bGraphDirty = true; + bNodeDirty = true; } else { for (const TPair& FoundConnection : FoundConnections) { - if (const FConnectedPin* OldConnection = Node->Connections.Find(FoundConnection.Key)) + if (const FConnectedPin* OldConnection = OldConnections.Find(FoundConnection.Key)) { if (FoundConnection.Value != *OldConnection) { - bGraphDirty = true; + bNodeDirty = true; break; } } else { - bGraphDirty = true; + bNodeDirty = true; break; } } } } - if (bGraphDirty) - { - Node->SetFlags(RF_Transactional); - Node->Modify(); + if (bNodeDirty) + { + FlowNode->SetFlags(RF_Transactional); + FlowNode->Modify(); + + FlowNode->SetConnections(FoundConnections); + FlowNode->PostEditChange(); + } + } +} + +bool UFlowAsset::TryGetDefaultForInputPinName(const FStructProperty& StructProperty, const void* Container, FString& OutString) +{ + // We also look in the USTRUCT for DefaultForInputFlowPin + const FString* DefaultForInputFlowPinName = StructProperty.Struct->FindMetaData(FFlowPin::MetadataKey_DefaultForInputFlowPin); + + if (DefaultForInputFlowPinName) + { + OutString = *DefaultForInputFlowPinName; + + return true; + } + + // For blueprint use, we allow the Value structs to set input pins via editor-only data + + const FFlowDataPinValue* DataPinValue = FlowStructUtils::CastStructValue(StructProperty, Container); + if (DataPinValue && DataPinValue->IsInputPin()) + { + OutString.Empty(); + + return true; + } + + return false; +} + +#endif + +TArray UFlowAsset::GetAllNodes() const +{ + TArray> AllNodes; + AllNodes.Reserve(Nodes.Num()); + Nodes.GenerateValueArray(AllNodes); + + return ObjectPtrDecay(AllNodes); +} + +TArray UFlowAsset::GetNodesInExecutionOrder(UFlowNode* FirstIteratedNode, const TSubclassOf FlowNodeClass) const +{ + TArray FoundNodes; + GetNodesInExecutionOrder(FirstIteratedNode, FoundNodes); + + // filter out nodes by class + for (int32 i = FoundNodes.Num() - 1; i >= 0; i--) + { + if (!FoundNodes[i]->GetClass()->IsChildOf(FlowNodeClass)) + { + FoundNodes.RemoveAt(i); + } + } + FoundNodes.Shrink(); + + return FoundNodes; +} + +UFlowNode* UFlowAsset::GetDefaultEntryNode() const +{ + UFlowNode* FirstStartNode = nullptr; + + for (const TPair& Node : ObjectPtrDecay(Nodes)) + { + if (UFlowNode_Start* StartNode = Cast(Node.Value)) + { + if (StartNode->GatherConnectedNodes().Num() > 0) + { + return StartNode; + } + else if (FirstStartNode == nullptr) + { + FirstStartNode = StartNode; + } + } + } + + // If none of the found start nodes have connections, fallback to the first start node we found + return FirstStartNode; +} + +TArray UFlowAsset::GatherNodesConnectedToAllInputs() const +{ + TSet> IteratedNodes; + TArray ConnectedNodes; + + // Nodes connected to the Start node + UFlowNode* DefaultEntryNode = GetDefaultEntryNode(); + GetNodesInExecutionOrder_Recursive(DefaultEntryNode, IteratedNodes, ConnectedNodes); + + // Nodes connected to Custom Input node(s) + for (const TPair& Node : ObjectPtrDecay(Nodes)) + { + if (UFlowNode_CustomInput* CustomInput = Cast(Node.Value)) + { + GetNodesInExecutionOrder_Recursive(CustomInput, IteratedNodes, ConnectedNodes); + } + } + + return ConnectedNodes; +} + +UFlowNode_CustomInput* UFlowAsset::TryFindCustomInputNodeByEventName(const FName& EventName) const +{ + for (const TPair& Node : ObjectPtrDecay(Nodes)) + { + if (UFlowNode_CustomInput* CustomInput = Cast(Node.Value)) + { + if (CustomInput->GetEventName() == EventName) + { + return CustomInput; + } + } + } + + return nullptr; +} + +UFlowNode_CustomOutput* UFlowAsset::TryFindCustomOutputNodeByEventName(const FName& EventName) const +{ + for (const TPair& Node : ObjectPtrDecay(Nodes)) + { + if (UFlowNode_CustomOutput* CustomOutput = Cast(Node.Value)) + { + if (CustomOutput->GetEventName() == EventName) + { + return CustomOutput; + } + } + } + + return nullptr; +} + +TArray UFlowAsset::GatherCustomInputNodeEventNames() const +{ + // Runtime-safe gathering of the CustomInputs (which is editor-only data) + // from the actual flow nodes + TArray Results; + + for (const TPair& Node : ObjectPtrDecay(Nodes)) + { + if (UFlowNode_CustomInput* CustomInput = Cast(Node.Value)) + { + Results.Add(CustomInput->GetEventName()); + } + } + + return Results; +} + +TArray UFlowAsset::GatherCustomOutputNodeEventNames() const +{ + // Runtime-safe gathering of the CustomOutputs (which is editor-only data) + // from the actual flow nodes + TArray Results; + + for (const TPair& Node : ObjectPtrDecay(Nodes)) + { + if (UFlowNode_CustomOutput* CustomOutput = Cast(Node.Value)) + { + Results.Add(CustomOutput->GetEventName()); + } + } + + return Results; +} + +#if WITH_EDITOR +void UFlowAsset::AddCustomInput(const FName& EventName) +{ + if (!CustomInputs.Contains(EventName)) + { + CustomInputs.Add(EventName); + } +} + +void UFlowAsset::RemoveCustomInput(const FName& EventName) +{ + if (CustomInputs.Contains(EventName)) + { + CustomInputs.Remove(EventName); + } +} + +void UFlowAsset::AddCustomOutput(const FName& EventName) +{ + if (!CustomOutputs.Contains(EventName)) + { + CustomOutputs.Add(EventName); + } +} + +void UFlowAsset::RemoveCustomOutput(const FName& EventName) +{ + if (CustomOutputs.Contains(EventName)) + { + CustomOutputs.Remove(EventName); + } +} +#endif // WITH_EDITOR + +#if WITH_EDITOR +void UFlowAsset::InitializePinConnectionPolicy() +{ + const FInstancedStruct& SourceStruct = GetDefault()->PinConnectionPolicy; + if (ensure(SourceStruct.IsValid())) + { + PinConnectionPolicy.InitializeAsScriptStruct(SourceStruct.GetScriptStruct(), SourceStruct.GetMemory()); + } +} +#endif + +const FFlowPinConnectionPolicy& UFlowAsset::GetPinConnectionPolicy() const +{ + // Runtime instances delegate to their template, which holds the serialized policy + if (!PinConnectionPolicy.IsValid() && IsValid(TemplateAsset)) + { + return TemplateAsset->GetPinConnectionPolicy(); + } + + // Graceful fallback: if PinConnectionPolicy was never initialized (asset predates this feature, + // or was never opened in editor), read directly from Project Settings at runtime. + if (!PinConnectionPolicy.IsValid()) + { + const FFlowPinConnectionPolicy* SettingsPolicy = GetDefault()->GetPinConnectionPolicy(); + ensureAlways(SettingsPolicy); + if (SettingsPolicy) + { + return *SettingsPolicy; + } + } + + check(PinConnectionPolicy.IsValid()); + return PinConnectionPolicy.Get(); +} + +TArray UFlowAsset::GatherPinsConnectedToPin(const FConnectedPin& Pin) const +{ + TArray ConnectedPins; + + // Connections are only stored on one of the Nodes they connect depending on pin type. + // As such, we need to iterate all Nodes to find all possible Connections for the Pin. + for (const auto& GuidNodePair : Nodes) + { + if (IsValid(GuidNodePair.Value)) + { + ConnectedPins.Append(GuidNodePair.Value->GetKnownConnectionsToPin(Pin)); + } + } + + return ConnectedPins; +} + +#if WITH_EDITOR +UFlowAssetParams* UFlowAsset::GenerateParamsFromStartNode() +{ + if (BaseAssetParams.AssetPtr.IsValid()) + { + UE_LOG(LogFlow, Warning, TEXT("BaseAssetParams already exists for %s: %s"), *GetPathName(), *BaseAssetParams.AssetPtr.ToString()); + return BaseAssetParams.AssetPtr.LoadSynchronous(); + } + + // Get the Start node + IFlowNamedPropertiesSupplierInterface* NamedPropertiesSupplier = Cast(GetDefaultEntryNode()); + if (!NamedPropertiesSupplier) + { + UE_LOG(LogFlow, Error, TEXT("No valid Start node found for generating params in %s"), *GetPathName()); + return nullptr; + } + + // Determine the params asset name + const FString ParamsAssetName = GenerateParamsAssetName(); + if (ParamsAssetName.IsEmpty()) + { + UE_LOG(LogFlow, Error, TEXT("Generated empty params asset name for %s"), *GetPathName()); + return nullptr; + } + + // Create the params asset + const FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); + const FString PackagePath = FPackageName::GetLongPackagePath(GetPackage()->GetPathName()); + FString UniquePackageName, UniqueAssetName; + AssetToolsModule.Get().CreateUniqueAssetName(PackagePath + TEXT("/") + ParamsAssetName, TEXT(""), UniquePackageName, UniqueAssetName); + + UFlowAssetParams* NewParams = Cast( + AssetToolsModule.Get().CreateAsset(UniqueAssetName, PackagePath, UFlowAssetParams::StaticClass(), nullptr)); + if (!IsValid(NewParams)) + { + UE_LOG(LogFlow, Error, TEXT("Failed to create Flow Asset Params: %s"), *UniqueAssetName); + return nullptr; + } + + // Reconfigure with the new properties + NewParams->ConfigureFlowAssetParams(this, nullptr, NamedPropertiesSupplier->GetMutableNamedProperties()); + + // Source control integration + if (USourceControlHelpers::IsAvailable()) + { + const FString FileName = USourceControlHelpers::PackageFilename(NewParams->GetPathName()); + if (!USourceControlHelpers::CheckOutOrAddFile(FileName)) + { + UE_LOG(LogFlow, Warning, TEXT("Failed to check out/add %s; saved in-memory only"), *NewParams->GetPathName()); + } + } + + // Assign to BaseAssetParams and sync Content Browser + BaseAssetParams.AssetPtr = NewParams; + + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + AssetRegistryModule.Get().AssetCreated(NewParams); + + const FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked("ContentBrowser"); + const TArray AssetsToSync = {NewParams}; + ContentBrowserModule.Get().SyncBrowserToAssets(AssetsToSync, true); + + return NewParams; +} + +FString UFlowAsset::GenerateParamsAssetName() const +{ + const FString FlowAssetName = GetName(); + + const int32 UnderscoreIndex = FlowAssetName.Find(TEXT("_"), ESearchCase::CaseSensitive); + + if (UnderscoreIndex != INDEX_NONE) + { + const FString Prefix = FlowAssetName.Left(UnderscoreIndex); + const FString Suffix = FlowAssetName.Mid(UnderscoreIndex + 1); + return FString::Printf(TEXT("%sParams_%s"), *Prefix, *Suffix); + } + else + { + return FlowAssetName + TEXT("Params"); + } +} + +void UFlowAsset::ReconcileBaseAssetParams(const FDateTime& AssetLastSavedTimestamp) +{ + if (BaseAssetParams.AssetPtr.IsNull()) + { + return; + } + + UFlowAssetParams* BaseAssetParamsPtr = BaseAssetParams.AssetPtr.LoadSynchronous(); + if (!IsValid(BaseAssetParamsPtr)) + { + UE_LOG(LogFlow, Error, TEXT("Failed to load BaseAssetParams: %s"), *BaseAssetParams.AssetPtr.ToString()); + return; + } + + IFlowNamedPropertiesSupplierInterface* NamedPropertiesSupplier = Cast(GetDefaultEntryNode()); + if (!NamedPropertiesSupplier) + { + UE_LOG(LogFlow, Error, TEXT("No NamedPropertiesSupplier (e.g., Start node) found in FlowAsset: %s"), *GetPathName()); + return; + } - Node->SetConnections(FoundConnections); - Node->PostEditChange(); - } + TArray& MutableStartNodeProperties = NamedPropertiesSupplier->GetMutableNamedProperties(); + const EFlowReconcilePropertiesResult ReconcileResult = + BaseAssetParamsPtr->ReconcilePropertiesWithStartNode(AssetLastSavedTimestamp, this, MutableStartNodeProperties); + + if (EFlowReconcilePropertiesResult_Classifiers::IsErrorResult(ReconcileResult)) + { + UE_LOG(LogFlow, Error, TEXT("Failed to reconcile BaseAssetParams for %s: %s"), + *BaseAssetParamsPtr->GetPathName(), *UEnum::GetDisplayValueAsText(ReconcileResult).ToString()); } } #endif -UFlowNode* UFlowAsset::GetNode(const FGuid& Guid) const -{ - return Nodes.FindRef(Guid); -} - void UFlowAsset::AddInstance(UFlowAsset* Instance) { ActiveInstances.Add(Instance); @@ -205,7 +875,7 @@ int32 UFlowAsset::RemoveInstance(UFlowAsset* Instance) #if WITH_EDITOR if (InspectedInstance.IsValid() && InspectedInstance.Get() == Instance) { - SetInspectedInstance(NAME_None); + SetInspectedInstance(nullptr); } #endif @@ -218,7 +888,7 @@ void UFlowAsset::ClearInstances() #if WITH_EDITOR if (InspectedInstance.IsValid()) { - SetInspectedInstance(NAME_None); + SetInspectedInstance(nullptr); } #endif @@ -234,55 +904,65 @@ void UFlowAsset::ClearInstances() } #if WITH_EDITOR -void UFlowAsset::GetInstanceDisplayNames(TArray>& OutDisplayNames) const +void UFlowAsset::SetInspectedInstance(TWeakObjectPtr NewInspectedInstance) { - for (const UFlowAsset* Instance : ActiveInstances) + if (NewInspectedInstance.IsValid()) { - OutDisplayNames.Emplace(MakeShareable(new FName(Instance->GetDisplayName()))); - } -} + if (InspectedInstance == NewInspectedInstance) + { + // Nothing changed + return; + } -void UFlowAsset::SetInspectedInstance(const FName& NewInspectedInstanceName) -{ - if (NewInspectedInstanceName.IsNone()) - { - InspectedInstance = nullptr; - } - else - { - for (UFlowAsset* ActiveInstance : ActiveInstances) + bool bIsNewInstancePresent = Algo::AnyOf(ActiveInstances, [NewInspectedInstance](const UFlowAsset* ActiveInstance) { - if (ActiveInstance && ActiveInstance->GetDisplayName() == NewInspectedInstanceName) - { - if (!InspectedInstance.IsValid() || InspectedInstance != ActiveInstance) - { - InspectedInstance = ActiveInstance; - } - break; - } + return ActiveInstance && ActiveInstance == NewInspectedInstance; + }); + + if (!ensureMsgf(bIsNewInstancePresent, TEXT("Trying to set %s as InspectedInstance, but it is not one of the ActiveInstances"), *NewInspectedInstance->GetName())) + { + NewInspectedInstance = nullptr; } } + InspectedInstance = NewInspectedInstance; BroadcastDebuggerRefresh(); } -#endif -void UFlowAsset::InitializeInstance(const TWeakObjectPtr InOwner, UFlowAsset* InTemplateAsset) +void UFlowAsset::BroadcastDebuggerRefresh() const +{ + RefreshDebuggerEvent.Broadcast(); +} + +void UFlowAsset::BroadcastRuntimeMessageAdded(const TSharedRef& Message) const +{ + RuntimeMessageEvent.Broadcast(this, Message); +} + +void UFlowAsset::SetupForEditing() +{ + InitializePinConnectionPolicy(); + + // Initialize any customizable Policies before we instantiate nodes + InitializePreloadPolicy(); +} +#endif // WITH_EDITOR + +void UFlowAsset::InitializeInstance(const TWeakObjectPtr InOwner, UFlowAsset& InTemplateAsset) { + check(!IsInstanceInitialized()); + Owner = InOwner; - TemplateAsset = InTemplateAsset; + TemplateAsset = &InTemplateAsset; - for (TPair& Node : Nodes) + // Initialize any customizable Policies before we instantiate nodes + InitializePreloadPolicy(); + + for (TPair>& Node : Nodes) { UFlowNode* NewNodeInstance = NewObject(this, Node.Value->GetClass(), NAME_None, RF_Transient, Node.Value, false, nullptr); Node.Value = NewNodeInstance; - // there can be only one, automatically added while creating graph - if (UFlowNode_Start* InNode = Cast(NewNodeInstance)) - { - StartNode = InNode; - } - if (UFlowNode_CustomInput* CustomInput = Cast(NewNodeInstance)) { if (!CustomInput->EventName.IsNone()) @@ -295,35 +975,52 @@ void UFlowAsset::InitializeInstance(const TWeakObjectPtr InOwner, UFlow } } -void UFlowAsset::PreloadNodes() +void UFlowAsset::DeinitializeInstance() { - TArray GraphEntryNodes = {StartNode}; - for (UFlowNode_CustomInput* CustomInput : CustomInputNodes) - { - GraphEntryNodes.Emplace(CustomInput); - } + // These should have been flushed in FinishFlow() + check(DeferredTransitionScopes.IsEmpty()); - // NOTE: this is just the example algorithm of gathering nodes for pre-load - for (UFlowNode* EntryNode : GraphEntryNodes) + if (IsInstanceInitialized()) { - for (const TPair, int32>& Node : UFlowSettings::Get()->DefaultPreloadDepth) + for (const TPair& Node : ObjectPtrDecay(Nodes)) { - if (Node.Value > 0) + if (IsValid(Node.Value)) { - TArray FoundNodes; - UFlowNode::RecursiveFindNodesByClass(EntryNode, Node.Key, Node.Value, FoundNodes); - - for (UFlowNode* FoundNode : FoundNodes) - { - if (!PreloadedNodes.Contains(FoundNode)) - { - FoundNode->TriggerPreload(); - PreloadedNodes.Emplace(FoundNode); - } - } + Node.Value->DeinitializeInstance(); } } + + const int32 ActiveInstancesLeft = TemplateAsset->RemoveInstance(this); + if (ActiveInstancesLeft == 0 && GetFlowSubsystem()) + { + GetFlowSubsystem()->RemoveInstancedTemplate(TemplateAsset); + } + + TemplateAsset = nullptr; + } +} + +AActor* UFlowAsset::TryFindActorOwner() const +{ + UObject* OwnerObject = GetOwner(); + if (!IsValid(OwnerObject)) + { + return nullptr; + } + + // If the owner is already an Actor, return it directly + if (AActor* OwnerAsActor = Cast(OwnerObject)) + { + return OwnerAsActor; + } + + // If the owner is a Component, return its owning Actor + if (const UActorComponent* OwnerAsComponent = Cast(OwnerObject)) + { + return OwnerAsComponent->GetOwner(); } + + return nullptr; } void UFlowAsset::PreStartFlow() @@ -331,10 +1028,12 @@ void UFlowAsset::PreStartFlow() ResetNodes(); #if WITH_EDITOR + check(IsInstanceInitialized()); + if (TemplateAsset->ActiveInstances.Num() == 1) { // this instance is the only active one, set it directly as Inspected Instance - TemplateAsset->SetInspectedInstance(GetDisplayName()); + TemplateAsset->SetInspectedInstance(this); } else { @@ -344,19 +1043,77 @@ void UFlowAsset::PreStartFlow() #endif } -void UFlowAsset::StartFlow() +void UFlowAsset::StartFlow(IFlowDataPinValueSupplierInterface* DataPinValueSupplier) { PreStartFlow(); - ensureAlways(StartNode); - RecordedNodes.Add(StartNode); - StartNode->TriggerFirstOutput(true); + if (UFlowNode* ConnectedEntryNode = GetDefaultEntryNode()) + { + RecordedNodes.Add(ConnectedEntryNode); + + if (IFlowNodeWithExternalDataPinSupplierInterface* ExternalPinSuppliedNode = Cast(ConnectedEntryNode)) + { + ExternalPinSuppliedNode->SetDataPinValueSupplier(DataPinValueSupplier); + } + + ConnectedEntryNode->TriggerFirstOutput(true); + } +} + +bool UFlowAsset::HasStartedFlow() const +{ + return RecordedNodes.Num() > 0; +} + +void UFlowAsset::FinishNode(UFlowNode* Node) +{ + if (ActiveNodes.Contains(Node)) + { + ActiveNodes.Remove(Node); + + // if graph reached Finish and this asset instance was created by SubGraph node + if (Node->CanFinishGraph()) + { + if (NodeOwningThisAssetInstance.IsValid()) + { + NodeOwningThisAssetInstance.Get()->TriggerFirstOutput(true); + + return; + } + + // if this instance is a Root Flow, we need to deregister it from the subsystem first + if (Owner.IsValid()) + { + const TSet& RootFlowInstances = GetFlowSubsystem()->GetRootInstancesByOwner(Owner.Get()); + if (RootFlowInstances.Contains(this)) + { + GetFlowSubsystem()->FinishRootFlow(Owner.Get(), TemplateAsset, EFlowFinishPolicy::Keep); + + return; + } + } + + FinishFlow(EFlowFinishPolicy::Keep); + } + } +} + +void UFlowAsset::ResetNodes() +{ + for (UFlowNode* Node : RecordedNodes) + { + Node->ResetRecords(); + } + + RecordedNodes.Empty(); } -void UFlowAsset::FinishFlow(const EFlowFinishPolicy InFinishPolicy) +void UFlowAsset::FinishFlow(const EFlowFinishPolicy InFinishPolicy, const bool bRemoveInstance /*= true*/) { FinishPolicy = InFinishPolicy; + CancelAndWarnForUnflushedDeferredTriggers(); + // end execution of this asset and all of its nodes for (UFlowNode* Node : ActiveNodes) { @@ -364,19 +1121,26 @@ void UFlowAsset::FinishFlow(const EFlowFinishPolicy InFinishPolicy) } ActiveNodes.Empty(); - // flush preloaded content - for (UFlowNode* PreloadedNode : PreloadedNodes) + // provides option to finish game-specific logic prior to removing asset instance + if (bRemoveInstance) { - PreloadedNode->TriggerFlush(); + DeinitializeInstance(); } - PreloadedNodes.Empty(); +} - // clear instance entries - const int32 ActiveInstancesLeft = TemplateAsset->RemoveInstance(this); - if (ActiveInstancesLeft == 0 && GetFlowSubsystem()) - { - GetFlowSubsystem()->RemoveInstancedTemplate(TemplateAsset); - } +UFlowSubsystem* UFlowAsset::GetFlowSubsystem() const +{ + return Cast(GetOuter()); +} + +UFlowNode_SubGraph* UFlowAsset::GetNodeOwningThisAssetInstance() const +{ + return NodeOwningThisAssetInstance.Get(); +} + +UFlowAsset* UFlowAsset::GetParentInstance() const +{ + return NodeOwningThisAssetInstance.IsValid() ? NodeOwningThisAssetInstance.Get()->GetFlowAsset() : nullptr; } TWeakObjectPtr UFlowAsset::GetFlowInstance(UFlowNode_SubGraph* SubGraphNode) const @@ -384,28 +1148,112 @@ TWeakObjectPtr UFlowAsset::GetFlowInstance(UFlowNode_SubGraph* SubGr return ActiveSubGraphs.FindRef(SubGraphNode); } -void UFlowAsset::TriggerCustomEvent(UFlowNode_SubGraph* Node, const FName& EventName) const +void UFlowAsset::InitializePreloadPolicy() { - const TWeakObjectPtr FlowInstance = ActiveSubGraphs.FindRef(Node); - if (FlowInstance.IsValid()) + if (PreloadPolicy.IsValid()) + { + // use per-class policy + PreloadPolicy.InitializeAsScriptStruct(PreloadPolicy.GetScriptStruct(), PreloadPolicy.GetMemory()); + } + else + { + // fallback to project's default policy + const FInstancedStruct& DefaultPolicy = GetDefault()->PreloadPolicy; + if (ensure(DefaultPolicy.IsValid())) + { + PreloadPolicy.InitializeAsScriptStruct(DefaultPolicy.GetScriptStruct(), DefaultPolicy.GetMemory()); + } + } + + ensureAlwaysMsgf(PreloadPolicy.IsValid(), TEXT("There's no valid Preload Policy set in the project!")); +} + +const FFlowPreloadPolicy& UFlowAsset::GetPreloadPolicy() const +{ + checkf(PreloadPolicy.IsValid(), TEXT("PreloadPolicy must be initialized prior to calling GetPreloadPolicy()")); + return PreloadPolicy.Get(); +} + +void UFlowAsset::TriggerCustomInput(const FName& EventName, IFlowDataPinValueSupplierInterface* DataPinValueSupplier) +{ + for (UFlowNode_CustomInput* CustomInputNode : CustomInputNodes) { - for (UFlowNode_CustomInput* CustomInput : FlowInstance->CustomInputNodes) + if (CustomInputNode->EventName == EventName) { - if (CustomInput->EventName == EventName) + RecordedNodes.Add(CustomInputNode); + + // NOTE (gtaylor) Custom Input nodes cannot currently add data pins (like Start or DefineProperties nodes can) + // but we may want to allow them to source parameters, so I am providing the subgraph node as the + // IFlowDataPinValueSupplierInterface when triggering the node (even though it's not used at this time). + + if (IFlowNodeWithExternalDataPinSupplierInterface* ExternalPinSuppliedNode = Cast(CustomInputNode)) { - FlowInstance->RecordedNodes.Add(CustomInput); - CustomInput->TriggerFirstOutput(true); + ExternalPinSuppliedNode->SetDataPinValueSupplier(DataPinValueSupplier); } + + CustomInputNode->ExecuteInput(EventName); + } + } +} + +void UFlowAsset::TriggerCustomInput_FromSubGraph(UFlowNode_SubGraph* SubGraphNode, const FName& EventName) const +{ + // NOTE (gtaylor) Custom Input nodes cannot currently add data pins (like Start or DefineProperties nodes can) + // but we may want to allow them to source parameters, so I am providing the subgraph node as the + // IFlowDataPinValueSupplierInterface when triggering the node (even though it's not used at this time). + + const TWeakObjectPtr FlowInstance = ActiveSubGraphs.FindRef(SubGraphNode); + if (FlowInstance.IsValid()) + { + FlowInstance->TriggerCustomInput(EventName, SubGraphNode); + } +} + +void UFlowAsset::TriggerCustomOutput(const FName& EventName) +{ + if (NodeOwningThisAssetInstance.IsValid()) + { + // it's a SubGraph + NodeOwningThisAssetInstance->TriggerOutput(EventName); + } + else + { + // it's a Root Flow, so the intention here might be to call event on the Flow Component + if (UFlowComponent* FlowComponent = Cast(GetOwner())) + { + FlowComponent->DispatchRootFlowCustomEvent(this, EventName); } } } -void UFlowAsset::TriggerCustomOutput(const FName& EventName) const +void UFlowAsset::TriggerInput(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin) { - NodeOwningThisAssetInstance->TriggerOutput(EventName); + if (FFlowExecutionGate::IsHalted()) + { + // Halt always takes precedence for debugger correctness + EnqueueDeferredTrigger(NodeGuid, PinName, FromPin); + } + else if (ShouldDeferTriggers()) + { + // Defer only if we have an open the top scope + if (!DeferredTransitionScopes.IsEmpty() && DeferredTransitionScopes.Top()->IsOpen()) + { + EnqueueDeferredTrigger(NodeGuid, PinName, FromPin); + } + else + { + const TSharedPtr CurrentScope = PushDeferredTransitionScope(); + TriggerInputDirect(NodeGuid, PinName, FromPin); + PopDeferredTransitionScope(CurrentScope); + } + } + else + { + TriggerInputDirect(NodeGuid, PinName, FromPin); + } } -void UFlowAsset::TriggerInput(const FGuid& NodeGuid, const FName& PinName) +void UFlowAsset::TriggerInputDirect(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin) { if (UFlowNode* Node = Nodes.FindRef(NodeGuid)) { @@ -419,55 +1267,142 @@ void UFlowAsset::TriggerInput(const FGuid& NodeGuid, const FName& PinName) } } -void UFlowAsset::FinishNode(UFlowNode* Node) +bool UFlowAsset::ShouldDeferTriggers() const { - if (ActiveNodes.Contains(Node)) - { - ActiveNodes.Remove(Node); + return GetDefault()->bDeferTriggeredOutputsWhileTriggering; +} - // if graph reached Finish and this asset instance was created by SubGraph node - if (Node->GetClass()->IsChildOf(UFlowNode_Finish::StaticClass())) - { - if (NodeOwningThisAssetInstance.IsValid()) - { - NodeOwningThisAssetInstance.Get()->TriggerFirstOutput(true); - } - else - { - FinishFlow(EFlowFinishPolicy::Keep); - } - } +void UFlowAsset::EnqueueDeferredTrigger(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin) +{ + if (DeferredTransitionScopes.IsEmpty() || !DeferredTransitionScopes.Top()->IsOpen()) + { + // This should only occur when halted at an execution gate + check(FFlowExecutionGate::IsHalted()); + PushDeferredTransitionScope(); } + + // Always enqueue to the current innermost (top) scope + DeferredTransitionScopes.Top()->EnqueueDeferredTrigger(FFlowDeferredTriggerInput{NodeGuid, PinName, FromPin}); } -void UFlowAsset::ResetNodes() +TSharedPtr UFlowAsset::PushDeferredTransitionScope() { - for (UFlowNode* Node : RecordedNodes) + // Close the former top scope (if any) + if (!DeferredTransitionScopes.IsEmpty()) { - Node->ResetRecords(); + const TSharedPtr& FormerTop = DeferredTransitionScopes.Top(); + FormerTop->CloseScope(); } - RecordedNodes.Empty(); + // Push a fresh open scope + return DeferredTransitionScopes.Add_GetRef(MakeShared()); } -UFlowSubsystem* UFlowAsset::GetFlowSubsystem() const +void UFlowAsset::PopDeferredTransitionScope(const TSharedPtr& Scope) { - return Cast(GetOuter()); + TryFlushAndRemoveDeferredTransitionScope(Scope); } -FName UFlowAsset::GetDisplayName() const +bool UFlowAsset::TryFlushAndRemoveDeferredTransitionScope(const TSharedPtr& ScopeToFlush) { - return GetFName(); + if (ScopeToFlush->TryFlushDeferredTriggers(*this)) + { + // Remove the exact instance we were holding (handles nested push/pop cases) + DeferredTransitionScopes.RemoveSingle(ScopeToFlush); + return true; + } + else + { + // Flush was interrupted — should only happen due to execution gate halt + check(FFlowExecutionGate::IsHalted()); + return false; + } } -UFlowNode_SubGraph* UFlowAsset::GetNodeOwningThisAssetInstance() const +bool UFlowAsset::TryFlushAllDeferredTriggerScopes() { - return NodeOwningThisAssetInstance.Get(); + while (const TSharedPtr TopScope = GetTopDeferredTransitionScope()) + { + if (!TryFlushAndRemoveDeferredTransitionScope(TopScope)) + { + break; + } + + // Keep flushing until stack is empty, or we hit an ExecutionGate halt + } + + check(DeferredTransitionScopes.IsEmpty() || FFlowExecutionGate::IsHalted()); + + return DeferredTransitionScopes.IsEmpty(); } -UFlowAsset* UFlowAsset::GetMasterInstance() const +void UFlowAsset::ClearAllDeferredTriggerScopes() { - return NodeOwningThisAssetInstance.IsValid() ? NodeOwningThisAssetInstance.Get()->GetFlowAsset() : nullptr; + DeferredTransitionScopes.Reset(); +} + +void UFlowAsset::CancelAndWarnForUnflushedDeferredTriggers() +{ + // Aggressively drop any pending deferred triggers — graph is done + // In normal execution these should have been flushed via PopDeferredTransitionScope() in TriggerInputDirect + // In the debugger they should have been flushed by ResumePIE + // Remaining scopes here usually mean: + // - early/abnormal termination (e.g. FinishFlow called from unexpected place) + // - exception/early return before Pop + // - forced deinitialization during active execution (e.g. PIE stop, subsystem cleanup) + if (!DeferredTransitionScopes.IsEmpty()) + { + int32 TotalDroppedTriggers = 0; + + for (const TSharedPtr& ScopePtr : DeferredTransitionScopes) + { + if (!ScopePtr.IsValid()) + { + continue; + } + + const TArray& Triggers = ScopePtr->GetDeferredTriggers(); + + if (TotalDroppedTriggers == 0 && !Triggers.IsEmpty()) + { + UE_LOG(LogFlow, Warning, TEXT("FlowAsset '%s' is finishing with %d lingering deferred transition scope(s) — dropping them. " + "This is usually unexpected and may indicate a bug or abnormal termination."), + *GetName(), DeferredTransitionScopes.Num()); + } + + TotalDroppedTriggers += Triggers.Num(); + + for (const FFlowDeferredTriggerInput& Trigger : Triggers) + { + const UFlowNode* ToNode = GetNode(Trigger.NodeGuid); + const UFlowNode* FromNode = Trigger.FromPin.NodeGuid.IsValid() ? GetNode(Trigger.FromPin.NodeGuid) : nullptr; + + const FString ToNodeName = ToNode ? ToNode->GetName() : TEXT(""); + const FString FromNodeName = FromNode ? FromNode->GetName() : TEXT(""); + + UE_LOG(LogFlow, Error, + TEXT(" → Dropped deferred trigger:\n") + TEXT(" To Node: %s (%s)\n") + TEXT(" To Pin: %s\n") + TEXT(" From Node: %s (%s)\n") + TEXT(" From Pin: %s"), + *ToNodeName, + *Trigger.NodeGuid.ToString(), + *Trigger.PinName.ToString(), + *FromNodeName, + *Trigger.FromPin.NodeGuid.ToString(), + *Trigger.FromPin.PinName.ToString() + ); + } + } + + ClearAllDeferredTriggerScopes(); + } +} + +TSharedPtr UFlowAsset::GetTopDeferredTransitionScope() const +{ + return !DeferredTransitionScopes.IsEmpty() ? DeferredTransitionScopes.Top() : nullptr; } FFlowAssetSaveData UFlowAsset::SaveInstance(TArray& SavedFlowInstances) @@ -479,12 +1414,15 @@ FFlowAssetSaveData UFlowAsset::SaveInstance(TArray& SavedFlo // opportunity to collect data before serializing asset OnSave(); - // iterate SubGraphs - for (const TPair& Node : Nodes) + // iterate nodes + TArray NodesInExecutionOrder; + GetNodesInExecutionOrder(GetDefaultEntryNode(), NodesInExecutionOrder); + for (UFlowNode* Node : NodesInExecutionOrder) { - if (Node.Value && Node.Value->ActivationState == EFlowNodeState::Active) + if (Node && Node->ShouldSave()) { - if (UFlowNode_SubGraph* SubGraphNode = Cast(Node.Value)) + // iterate SubGraphs + if (UFlowNode_SubGraph* SubGraphNode = Cast(Node)) { const TWeakObjectPtr SubFlowInstance = GetFlowInstance(SubGraphNode); if (SubFlowInstance.IsValid()) @@ -495,7 +1433,7 @@ FFlowAssetSaveData UFlowAsset::SaveInstance(TArray& SavedFlo } FFlowNodeSaveData NodeRecord; - Node.Value->SaveInstance(NodeRecord); + Node->SaveInstance(NodeRecord); AssetRecord.NodeRecords.Emplace(NodeRecord); } @@ -520,11 +1458,13 @@ void UFlowAsset::LoadInstance(const FFlowAssetSaveData& AssetRecord) PreStartFlow(); - for (const FFlowNodeSaveData& NodeRecord : AssetRecord.NodeRecords) + // iterate graph "from the end", backward to execution order + // prevents issue when the preceding node would instantly fire output to a not-yet-loaded node + for (int32 i = AssetRecord.NodeRecords.Num() - 1; i >= 0; i--) { - if (UFlowNode* Node = Nodes.FindRef(NodeRecord.NodeGuid)) + if (UFlowNode* Node = Nodes.FindRef(AssetRecord.NodeRecords[i].NodeGuid)) { - Node->LoadInstance(NodeRecord); + Node->LoadInstance(AssetRecord.NodeRecords[i]); } } @@ -552,7 +1492,55 @@ void UFlowAsset::OnLoad_Implementation() { } -bool UFlowAsset::IsBoundToWorld_Implementation() +bool UFlowAsset::IsBoundToWorld_Implementation() const { return bWorldBound; } + +#if WITH_EDITOR + +void UFlowAsset::LogError(const FString& MessageToLog, const UFlowNodeBase* Node) const +{ + LogRuntimeMessage(EMessageSeverity::Error, MessageToLog, Node); +} + +void UFlowAsset::LogWarning(const FString& MessageToLog, const UFlowNodeBase* Node) const +{ + LogRuntimeMessage(EMessageSeverity::Warning, MessageToLog, Node); +} + +void UFlowAsset::LogNote(const FString& MessageToLog, const UFlowNodeBase* Node) const +{ + LogRuntimeMessage(EMessageSeverity::Info, MessageToLog, Node); +} + +void UFlowAsset::LogRuntimeMessage(EMessageSeverity::Type Severity, const FString& MessageToLog, const UFlowNodeBase* Node) const +{ + // this is runtime log which should only be called on runtime instances of asset + if (TemplateAsset) + { + UE_LOG(LogFlow, Log, TEXT("Attempted to use Runtime Log on asset instance %s"), *MessageToLog); + } + + if (RuntimeLog.Get()) + { + TSharedPtr TokenizedMessage = nullptr; + switch (Severity) + { + case EMessageSeverity::Error: + TokenizedMessage = RuntimeLog.Get()->Error(*MessageToLog, Node); + break; + + case EMessageSeverity::Warning: + TokenizedMessage = RuntimeLog.Get()->Warning(*MessageToLog, Node); + break; + + default: + TokenizedMessage = RuntimeLog.Get()->Note(*MessageToLog, Node); + break; + } + + BroadcastRuntimeMessageAdded(TokenizedMessage.ToSharedRef()); + } +} +#endif \ No newline at end of file diff --git a/Source/Flow/Private/FlowComponent.cpp b/Source/Flow/Private/FlowComponent.cpp index 8635991ab..da1dbe1f6 100644 --- a/Source/Flow/Private/FlowComponent.cpp +++ b/Source/Flow/Private/FlowComponent.cpp @@ -2,8 +2,9 @@ #include "FlowComponent.h" +#include "Asset/FlowAssetParams.h" #include "FlowAsset.h" -#include "FlowModule.h" +#include "FlowLogChannels.h" #include "FlowSettings.h" #include "FlowSubsystem.h" @@ -12,9 +13,12 @@ #include "Engine/ViewportStatsSubsystem.h" #include "Engine/World.h" #include "Net/UnrealNetwork.h" +#include "Net/Core/PushModel/PushModel.h" #include "Serialization/MemoryReader.h" #include "Serialization/MemoryWriter.h" +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowComponent) + UFlowComponent::UFlowComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) , RootFlow(nullptr) @@ -32,51 +36,72 @@ void UFlowComponent::GetLifetimeReplicatedProps(TArray& OutLi { Super::GetLifetimeReplicatedProps(OutLifetimeProps); - DOREPLIFETIME(UFlowComponent, AddedIdentityTags); - DOREPLIFETIME(UFlowComponent, RemovedIdentityTags); +#if WITH_PUSH_MODEL + FDoRepLifetimeParams Params; + Params.bIsPushBased = true; + + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, IdentityTags, Params); - DOREPLIFETIME(UFlowComponent, RecentlySentNotifyTags); - DOREPLIFETIME(UFlowComponent, NotifyTagsFromGraph); - DOREPLIFETIME(UFlowComponent, NotifyTagsFromAnotherComponent); + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, RecentlySentNotifyTags, Params); + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, NotifyTagsFromGraph, Params); + DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, NotifyTagsFromAnotherComponent, Params); +#else + DOREPLIFETIME(ThisClass, IdentityTags); + + DOREPLIFETIME(ThisClass, RecentlySentNotifyTags); + DOREPLIFETIME(ThisClass, NotifyTagsFromGraph); + DOREPLIFETIME(ThisClass, NotifyTagsFromAnotherComponent); +#endif } void UFlowComponent::BeginPlay() { Super::BeginPlay(); + RegisterWithFlowSubsystem(); +} + +void UFlowComponent::RegisterWithFlowSubsystem() +{ if (UFlowSubsystem* FlowSubsystem = GetFlowSubsystem()) { - bool bComponentLoadedFromSaveGame = false; - if (GetFlowSubsystem()->GetLoadedSaveGame()) - { - bComponentLoadedFromSaveGame = LoadInstance(); - } + const bool bComponentLoadedFromSaveGame = LoadInstance(FlowSubsystem); FlowSubsystem->RegisterComponent(this); - if (RootFlow) + BeginRootFlow(bComponentLoadedFromSaveGame); + } +} + +void UFlowComponent::BeginRootFlow(bool bComponentLoadedFromSaveGame) +{ + if (RootFlow) + { + if (bComponentLoadedFromSaveGame) { - if (bComponentLoadedFromSaveGame) - { - LoadRootFlow(); - } - else if (bAutoStartRootFlow) - { - StartRootFlow(); - } + LoadRootFlow(); + } + else if (bAutoStartRootFlow) + { + StartRootFlow(); } } } void UFlowComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + UnregisterWithFlowSubsystem(); + + Super::EndPlay(EndPlayReason); +} + +void UFlowComponent::UnregisterWithFlowSubsystem() { if (UFlowSubsystem* FlowSubsystem = GetFlowSubsystem()) { FlowSubsystem->FinishAllRootFlows(this, EFlowFinishPolicy::Keep); FlowSubsystem->UnregisterComponent(this); } - - Super::EndPlay(EndPlayReason); } void UFlowComponent::AddIdentityTag(const FGameplayTag Tag, const EFlowNetMode NetMode /* = EFlowNetMode::Authority*/) @@ -84,7 +109,12 @@ void UFlowComponent::AddIdentityTag(const FGameplayTag Tag, const EFlowNetMode N if (IsFlowNetMode(NetMode) && Tag.IsValid() && !IdentityTags.HasTagExact(Tag)) { IdentityTags.AddTag(Tag); - +#if WITH_PUSH_MODEL + if (GetNetMode() < NM_Client) + { + MARK_PROPERTY_DIRTY_FROM_NAME(UFlowComponent, IdentityTags, this); + } +#endif if (HasBegunPlay()) { OnIdentityTagsAdded.Broadcast(this, FGameplayTagContainer(Tag)); @@ -93,11 +123,6 @@ void UFlowComponent::AddIdentityTag(const FGameplayTag Tag, const EFlowNetMode N { FlowSubsystem->OnIdentityTagAdded(this, Tag); } - - if (IsNetMode(NM_DedicatedServer) || IsNetMode(NM_ListenServer)) - { - AddedIdentityTags = FGameplayTagContainer(Tag); - } } } } @@ -117,18 +142,22 @@ void UFlowComponent::AddIdentityTags(FGameplayTagContainer Tags, const EFlowNetM } } - if (ValidatedTags.Num() > 0 && HasBegunPlay()) + if (ValidatedTags.Num() > 0) { - OnIdentityTagsAdded.Broadcast(this, ValidatedTags); - - if (UFlowSubsystem* FlowSubsystem = GetFlowSubsystem()) +#if WITH_PUSH_MODEL + if (GetNetMode() < NM_Client) { - FlowSubsystem->OnIdentityTagsAdded(this, ValidatedTags); + MARK_PROPERTY_DIRTY_FROM_NAME(UFlowComponent, IdentityTags, this); } - - if (IsNetMode(NM_DedicatedServer) || IsNetMode(NM_ListenServer)) +#endif + if (HasBegunPlay()) { - AddedIdentityTags = ValidatedTags; + OnIdentityTagsAdded.Broadcast(this, ValidatedTags); + + if (UFlowSubsystem* FlowSubsystem = GetFlowSubsystem()) + { + FlowSubsystem->OnIdentityTagsAdded(this, ValidatedTags); + } } } } @@ -139,7 +168,12 @@ void UFlowComponent::RemoveIdentityTag(const FGameplayTag Tag, const EFlowNetMod if (IsFlowNetMode(NetMode) && Tag.IsValid() && IdentityTags.HasTagExact(Tag)) { IdentityTags.RemoveTag(Tag); - +#if WITH_PUSH_MODEL + if (GetNetMode() < NM_Client) + { + MARK_PROPERTY_DIRTY_FROM_NAME(UFlowComponent, IdentityTags, this); + } +#endif if (HasBegunPlay()) { OnIdentityTagsRemoved.Broadcast(this, FGameplayTagContainer(Tag)); @@ -148,11 +182,6 @@ void UFlowComponent::RemoveIdentityTag(const FGameplayTag Tag, const EFlowNetMod { FlowSubsystem->OnIdentityTagRemoved(this, Tag); } - - if (IsNetMode(NM_DedicatedServer) || IsNetMode(NM_ListenServer)) - { - RemovedIdentityTags = FGameplayTagContainer(Tag); - } } } } @@ -172,89 +201,129 @@ void UFlowComponent::RemoveIdentityTags(FGameplayTagContainer Tags, const EFlowN } } - if (ValidatedTags.Num() > 0 && HasBegunPlay()) + if (ValidatedTags.Num() > 0) { - OnIdentityTagsRemoved.Broadcast(this, ValidatedTags); - - if (UFlowSubsystem* FlowSubsystem = GetWorld()->GetGameInstance()->GetSubsystem()) +#if WITH_PUSH_MODEL + if (GetNetMode() < NM_Client) { - FlowSubsystem->OnIdentityTagsRemoved(this, ValidatedTags); + MARK_PROPERTY_DIRTY_FROM_NAME(UFlowComponent, IdentityTags, this); } - - if (IsNetMode(NM_DedicatedServer) || IsNetMode(NM_ListenServer)) +#endif + if (HasBegunPlay()) { - RemovedIdentityTags = ValidatedTags; + OnIdentityTagsRemoved.Broadcast(this, ValidatedTags); + + if (UFlowSubsystem* FlowSubsystem = GetWorld()->GetGameInstance()->GetSubsystem()) + { + FlowSubsystem->OnIdentityTagsRemoved(this, ValidatedTags); + } } } } } -void UFlowComponent::OnRep_AddedIdentityTags() +void UFlowComponent::OnRep_IdentityTags(const FGameplayTagContainer& PreviousTags) { - IdentityTags.AppendTags(AddedIdentityTags); - OnIdentityTagsAdded.Broadcast(this, AddedIdentityTags); - - if (UFlowSubsystem* FlowSubsystem = GetFlowSubsystem()) + // Any tags that are now in the IdentityTags container but haven't been previously must have been added. + FGameplayTagContainer AddedTags; + for (const FGameplayTag& Tag : IdentityTags) { - FlowSubsystem->OnIdentityTagsAdded(this, AddedIdentityTags); + if (!PreviousTags.HasTagExact(Tag)) + { + AddedTags.AddTag(Tag); + } } -} -void UFlowComponent::OnRep_RemovedIdentityTags() -{ - IdentityTags.RemoveTags(RemovedIdentityTags); - OnIdentityTagsRemoved.Broadcast(this, RemovedIdentityTags); + if (AddedTags.Num() > 0) + { + OnIdentityTagsAdded.Broadcast(this, AddedTags); - if (UFlowSubsystem* FlowSubsystem = GetWorld()->GetGameInstance()->GetSubsystem()) + if (UFlowSubsystem* FlowSubsystem = GetFlowSubsystem()) + { + FlowSubsystem->OnIdentityTagsAdded(this, AddedTags); + } + } + + // Any tags that have been in the IdentityTags container previously but aren't in it anymore after the replication update must have been removed. + FGameplayTagContainer RemovedTags; + for (const FGameplayTag& Tag : PreviousTags) + { + if (!IdentityTags.HasTagExact(Tag)) + { + RemovedTags.AddTag(Tag); + } + } + if (RemovedTags.Num() > 0) { - FlowSubsystem->OnIdentityTagsRemoved(this, RemovedIdentityTags); + OnIdentityTagsRemoved.Broadcast(this, RemovedTags); + + if (UFlowSubsystem* FlowSubsystem = GetFlowSubsystem()) + { + FlowSubsystem->OnIdentityTagsRemoved(this, RemovedTags); + } } } void UFlowComponent::VerifyIdentityTags() const { - if (IdentityTags.IsEmpty() && UFlowSettings::Get()->bWarnAboutMissingIdentityTags) +#if !NO_LOGGING || UE_ENABLE_DEBUG_DRAWING + if (IdentityTags.IsEmpty() && GetDefault()->bWarnAboutMissingIdentityTags) { FString Message = TEXT("Missing Identity Tags on the Flow Component creating Flow Asset instance! This gonna break loading SaveGame for this component!"); Message.Append(LINE_TERMINATOR).Append(TEXT("If you're not using SaveSystem, you can silence this warning by unchecking bWarnAboutMissingIdentityTags flag in Flow Settings.")); LogError(Message); } +#endif } void UFlowComponent::LogError(FString Message, const EFlowOnScreenMessageType OnScreenMessageType) const { +#if !NO_LOGGING || UE_ENABLE_DEBUG_DRAWING Message += TEXT(" --- Flow Component in actor ") + GetOwner()->GetName(); - + UE_LOG(LogFlow, Error, TEXT("%s"), *Message); +#endif + +#if UE_ENABLE_DEBUG_DRAWING if (OnScreenMessageType == EFlowOnScreenMessageType::Permanent) { - if (GetWorld()) + if (UWorld* World = GetWorld()) { - if (UViewportStatsSubsystem* StatsSubsystem = GetWorld()->GetSubsystem()) + if (UViewportStatsSubsystem* StatsSubsystem = World->GetSubsystem()) { - StatsSubsystem->AddDisplayDelegate([this, Message](FText& OutText, FLinearColor& OutColor) + StatsSubsystem->AddDisplayDelegate([WeakThis = TWeakObjectPtr(this), Message](FText& OutText, FLinearColor& OutColor) { - OutText = FText::FromString(Message); - OutColor = FLinearColor::Red; - return IsValid(this); + if (WeakThis.Get()) + { + OutText = FText::FromString(Message); + OutColor = FLinearColor::Red; + return true; + } + + return false; }); } } } - else + else if (OnScreenMessageType == EFlowOnScreenMessageType::Temporary) { GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, Message); } - - UE_LOG(LogFlow, Error, TEXT("%s"), *Message); +#endif } void UFlowComponent::NotifyGraph(const FGameplayTag NotifyTag, const EFlowNetMode NetMode /* = EFlowNetMode::Authority*/) { if (IsFlowNetMode(NetMode) && NotifyTag.IsValid() && HasBegunPlay()) { - // save recently notify, this allow for the retroactive check in nodes + // save recently notify, this allows for the retroactive check in nodes // if retroactive check wouldn't be performed, this is only used by the network replication RecentlySentNotifyTags = FGameplayTagContainer(NotifyTag); +#if WITH_PUSH_MODEL + if (IsNetMode(NM_DedicatedServer) || IsNetMode(NM_ListenServer)) + { + MARK_PROPERTY_DIRTY_FROM_NAME(UFlowComponent, RecentlySentNotifyTags, this); + } +#endif OnRep_SentNotifyTags(); } @@ -275,9 +344,15 @@ void UFlowComponent::BulkNotifyGraph(const FGameplayTagContainer NotifyTags, con if (ValidatedTags.Num() > 0) { - // save recently notify, this allow for the retroactive check in nodes + // save recently notify, this allows for the retroactive check in nodes // if retroactive check wouldn't be performed, this is only used by the network replication RecentlySentNotifyTags = ValidatedTags; +#if WITH_PUSH_MODEL + if (IsNetMode(NM_DedicatedServer) || IsNetMode(NM_ListenServer)) + { + MARK_PROPERTY_DIRTY_FROM_NAME(UFlowComponent, RecentlySentNotifyTags, this); + } +#endif OnRep_SentNotifyTags(); } @@ -315,6 +390,9 @@ void UFlowComponent::NotifyFromGraph(const FGameplayTagContainer& NotifyTags, co if (IsNetMode(NM_DedicatedServer) || IsNetMode(NM_ListenServer)) { NotifyTagsFromGraph = ValidatedTags; +#if WITH_PUSH_MODEL + MARK_PROPERTY_DIRTY_FROM_NAME(UFlowComponent, NotifyTagsFromGraph, this); +#endif } } } @@ -344,6 +422,9 @@ void UFlowComponent::NotifyActor(const FGameplayTag ActorTag, const FGameplayTag { NotifyTagsFromAnotherComponent.Empty(); NotifyTagsFromAnotherComponent.Add(FNotifyTagReplication(ActorTag, NotifyTag)); +#if WITH_PUSH_MODEL + MARK_PROPERTY_DIRTY_FROM_NAME(UFlowComponent, NotifyTagsFromAnotherComponent, this); +#endif } } } @@ -370,7 +451,8 @@ void UFlowComponent::StartRootFlow() { VerifyIdentityTags(); - FlowSubsystem->StartRootFlow(this, RootFlow, bAllowMultipleInstances); + const TScriptInterface RootFlowParamsAsInterface = RootFlowParams.ResolveFlowAssetParams(); + FlowSubsystem->StartRootFlow(this, RootFlow, RootFlowParamsAsInterface, bAllowMultipleInstances); } } } @@ -385,9 +467,11 @@ void UFlowComponent::FinishRootFlow(UFlowAsset* TemplateAsset, const EFlowFinish TSet UFlowComponent::GetRootInstances(const UObject* Owner) const { + const UObject* OwnerToCheck = IsValid(Owner) ? Owner : this; + if (const UFlowSubsystem* FlowSubsystem = GetFlowSubsystem()) { - return FlowSubsystem->GetRootInstancesByOwner(this); + return FlowSubsystem->GetRootInstancesByOwner(OwnerToCheck); } return TSet(); @@ -407,6 +491,27 @@ UFlowAsset* UFlowComponent::GetRootFlowInstance() const return nullptr; } +void UFlowComponent::TriggerRootFlowCustomInput(const FName& EventName) const +{ + if (RootFlow && IsFlowNetMode(RootFlowMode)) + { + if (const UFlowSubsystem* FlowSubsystem = GetFlowSubsystem()) + { + UFlowAsset* RootFlowInstance = FlowSubsystem->GetRootFlow(this); + if (IsValid(RootFlowInstance)) + { + RootFlowInstance->TriggerCustomInput(EventName); + } + } + } +} + +void UFlowComponent::DispatchRootFlowCustomEvent(UFlowAsset* RootFlowInstance, const FName& EventName) +{ + BP_OnRootFlowCustomEvent(RootFlowInstance, EventName); + OnRootFlowCustomEvent(RootFlowInstance, EventName); +} + void UFlowComponent::SaveRootFlow(TArray& SavedFlowInstances) { if (UFlowAsset* FlowAssetInstance = GetRootFlowInstance()) @@ -425,7 +530,7 @@ void UFlowComponent::LoadRootFlow() { VerifyIdentityTags(); - GetFlowSubsystem()->LoadRootFlow(this, RootFlow, SavedAssetInstanceName); + GetFlowSubsystem()->LoadRootFlow(this, RootFlow, SavedAssetInstanceName, bAllowMultipleInstances); SavedAssetInstanceName = FString(); } } @@ -447,22 +552,18 @@ FFlowComponentSaveData UFlowComponent::SaveInstance() return ComponentRecord; } -bool UFlowComponent::LoadInstance() +bool UFlowComponent::LoadInstance(const UFlowSubsystem* FlowSubsystem) { - const UFlowSaveGame* SaveGame = GetFlowSubsystem()->GetLoadedSaveGame(); - if (SaveGame->FlowComponents.Num() > 0) + if (FlowSubsystem && CanSave()) { - for (const FFlowComponentSaveData& ComponentRecord : SaveGame->FlowComponents) + if (const FFlowComponentSaveData* Record = FlowSubsystem->GetLoadedComponentRecord(this)) { - if (ComponentRecord.WorldName == GetWorld()->GetName() && ComponentRecord.ActorInstanceName == GetOwner()->GetName()) - { - FMemoryReader MemoryReader(ComponentRecord.ComponentData, true); - FFlowArchive Ar(MemoryReader); - Serialize(Ar); + FMemoryReader MemoryReader(Record->ComponentData, true); + FFlowArchive Ar(MemoryReader); + Serialize(Ar); - OnLoad(); - return true; - } + OnLoad(); + return true; } } @@ -496,7 +597,7 @@ bool UFlowComponent::IsFlowNetMode(const EFlowNetMode NetMode) const case EFlowNetMode::Authority: return GetOwner()->HasAuthority(); case EFlowNetMode::ClientOnly: - return IsNetMode(NM_Client) && UFlowSettings::Get()->bCreateFlowSubsystemOnClients; + return IsNetMode(NM_Client) && GetDefault()->bCreateFlowSubsystemOnClients; case EFlowNetMode::ServerOnly: return IsNetMode(NM_DedicatedServer) || IsNetMode(NM_ListenServer); case EFlowNetMode::SinglePlayerOnly: diff --git a/Source/Flow/Private/FlowExecutableActorComponent.cpp b/Source/Flow/Private/FlowExecutableActorComponent.cpp new file mode 100644 index 000000000..e46967677 --- /dev/null +++ b/Source/Flow/Private/FlowExecutableActorComponent.cpp @@ -0,0 +1,48 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "FlowExecutableActorComponent.h" +#include "Nodes/FlowNode.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowExecutableActorComponent) + +#if WITH_EDITOR +bool UFlowExecutableActorComponent::CanModifyFlowDataPinType() const +{ + return IsDefaultObject(); +} + +bool UFlowExecutableActorComponent::ShowFlowDataPinValueInputPinCheckbox() const +{ + return IsDefaultObject(); +} + +bool UFlowExecutableActorComponent::ShowFlowDataPinValueClassFilter(const FFlowDataPinValue* Value) const +{ + return IsDefaultObject(); +} + +bool UFlowExecutableActorComponent::CanEditFlowDataPinValueClassFilter(const FFlowDataPinValue* Value) const +{ + return IsDefaultObject(); +} + +void UFlowExecutableActorComponent::SetFlowDataPinValuesRebuildDelegate(FSimpleDelegate InDelegate) +{ + FlowDataPinValuesRebuildDelegate = InDelegate; +} + +void UFlowExecutableActorComponent::RequestFlowDataPinValuesDetailsRebuild() +{ + if (FlowDataPinValuesRebuildDelegate.IsBound()) + { + FlowDataPinValuesRebuildDelegate.Execute(); + } +} +#endif + +void UFlowExecutableActorComponent::PreActivateExternalFlowExecutable(UFlowNodeBase& FlowNodeBase) +{ + IFlowExternalExecutableInterface::PreActivateExternalFlowExecutable(FlowNodeBase); + + FlowNodeProxy = &FlowNodeBase; +} diff --git a/Source/Flow/Private/FlowLogChannels.cpp b/Source/Flow/Private/FlowLogChannels.cpp new file mode 100644 index 000000000..fa8abf671 --- /dev/null +++ b/Source/Flow/Private/FlowLogChannels.cpp @@ -0,0 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "FlowLogChannels.h" + +DEFINE_LOG_CATEGORY(LogFlow); diff --git a/Source/Flow/Private/FlowMessageLog.cpp b/Source/Flow/Private/FlowMessageLog.cpp new file mode 100644 index 000000000..fd3feb5d8 --- /dev/null +++ b/Source/Flow/Private/FlowMessageLog.cpp @@ -0,0 +1,88 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "FlowMessageLog.h" + +#if WITH_EDITOR +#include "Nodes/FlowNode.h" +#include "FlowAsset.h" + +#define LOCTEXT_NAMESPACE "FlowMessageLog" + +const FName FFlowMessageLog::LogName(TEXT("FlowGraph")); + +FFlowGraphToken::FFlowGraphToken(const UFlowAsset* InFlowAsset) +{ + CachedText = FText::FromString(InFlowAsset->GetClass()->GetPathName()); +} + +FFlowGraphToken::FFlowGraphToken(const UFlowNodeBase* InFlowNodeBase) + : GraphNode(InFlowNodeBase->GetGraphNode()) +{ + CachedText = InFlowNodeBase->GetNodeTitle(); +} + +FFlowGraphToken::FFlowGraphToken(const UEdGraphNode* InGraphNode, const UEdGraphPin* InPin) + : GraphNode(InGraphNode) + , GraphPin(InPin) +{ + if (InPin) + { + CachedText = InPin->GetDisplayName(); + if (CachedText.IsEmpty()) + { + CachedText = LOCTEXT("UnnamedPin", ""); + } + } + else + { + CachedText = GraphNode->GetNodeTitle(ENodeTitleType::ListView); + } +} + +TSharedPtr FFlowGraphToken::Create(const UFlowAsset* InFlowAsset, FTokenizedMessage& Message) +{ + if (InFlowAsset) + { + Message.AddToken(MakeShareable(new FFlowGraphToken(InFlowAsset))); + return Message.GetMessageTokens().Last(); + } + + return nullptr; +} + +TSharedPtr FFlowGraphToken::Create(const UFlowNodeBase* InFlowNodeBase, FTokenizedMessage& Message) +{ + if (InFlowNodeBase) + { + Message.AddToken(MakeShareable(new FFlowGraphToken(InFlowNodeBase))); + return Message.GetMessageTokens().Last(); + } + + return nullptr; +} + +TSharedPtr FFlowGraphToken::Create(const UEdGraphNode* InGraphNode, FTokenizedMessage& Message) +{ + if (InGraphNode) + { + Message.AddToken(MakeShareable(new FFlowGraphToken(InGraphNode, nullptr))); + return Message.GetMessageTokens().Last(); + } + + return nullptr; +} + +TSharedPtr FFlowGraphToken::Create(const UEdGraphPin* InPin, FTokenizedMessage& Message) +{ + if (InPin && InPin->GetOwningNode()) + { + Message.AddToken(MakeShareable(new FFlowGraphToken(InPin->GetOwningNode(), InPin))); + return Message.GetMessageTokens().Last(); + } + + return nullptr; +} + +#undef LOCTEXT_NAMESPACE + +#endif // WITH_EDITOR diff --git a/Source/Flow/Private/FlowModule.cpp b/Source/Flow/Private/FlowModule.cpp index 4c202ca89..dd7f12f14 100644 --- a/Source/Flow/Private/FlowModule.cpp +++ b/Source/Flow/Private/FlowModule.cpp @@ -4,8 +4,6 @@ #include "Modules/ModuleManager.h" -#define LOCTEXT_NAMESPACE "Flow" - void FFlowModule::StartupModule() { } @@ -14,7 +12,4 @@ void FFlowModule::ShutdownModule() { } -#undef LOCTEXT_NAMESPACE - IMPLEMENT_MODULE(FFlowModule, Flow) -DEFINE_LOG_CATEGORY(LogFlow); diff --git a/Source/Flow/Private/FlowPinSubsystem.cpp b/Source/Flow/Private/FlowPinSubsystem.cpp new file mode 100644 index 000000000..3fc28d9ae --- /dev/null +++ b/Source/Flow/Private/FlowPinSubsystem.cpp @@ -0,0 +1,83 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "FlowPinSubsystem.h" +#include "Types/FlowPinTypesStandard.h" + +#include "Engine/Engine.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowPinSubsystem) + +UFlowPinSubsystem* UFlowPinSubsystem::Get() +{ + return GEngine->GetEngineSubsystem(); +} + +bool UFlowPinSubsystem::ShouldCreateSubsystem(UObject* Outer) const +{ + // Only create an instance if there is no override implementation defined elsewhere + TArray ChildClasses; + GetDerivedClasses(GetClass(), ChildClasses, false); + return (ChildClasses.Num() == 0); +} + +void UFlowPinSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + + check(PinTypes.IsEmpty()); + + // Register standard types + RegisterPinType(); + RegisterPinType(); + RegisterPinType(); + RegisterPinType(); + RegisterPinType(); + RegisterPinType(); + RegisterPinType(); + RegisterPinType(); + RegisterPinType(); + RegisterPinType(); + RegisterPinType(); + RegisterPinType(); + RegisterPinType(); + RegisterPinType(); + RegisterPinType(); + RegisterPinType(); + RegisterPinType(); +} + +void UFlowPinSubsystem::Deinitialize() +{ + UnregisterAllPinTypes(); + + Super::Deinitialize(); +} + +void UFlowPinSubsystem::UnregisterAllPinTypes() +{ + const TArray PinTypeNames = GetPinTypeNames(); + for (const FFlowPinTypeName& PinTypeName : PinTypeNames) + { + UnregisterPinType(PinTypeName); + } + + check(PinTypes.IsEmpty()); +} + +void UFlowPinSubsystem::RegisterPinType(const FFlowPinTypeName& TypeName, const TInstancedStruct& PinType) +{ + PinTypes.Add(TypeName, PinType); +} + +void UFlowPinSubsystem::UnregisterPinType(const FFlowPinTypeName& TypeName) +{ + PinTypes.Remove(TypeName); +} + +TArray UFlowPinSubsystem::GetPinTypeNames() const +{ + TArray TypeNames; + PinTypes.GetKeys(TypeNames); + + return TypeNames; +} diff --git a/Source/Flow/Private/FlowSettings.cpp b/Source/Flow/Private/FlowSettings.cpp index c287a3aef..3a450e3bf 100644 --- a/Source/Flow/Private/FlowSettings.cpp +++ b/Source/Flow/Private/FlowSettings.cpp @@ -1,10 +1,59 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #include "FlowSettings.h" +#include "FlowComponent.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) , bCreateFlowSubsystemOnClients(true) + , bUseAdaptiveNodeTitles(false) + , DefaultExpectedOwnerClass(UFlowComponent::StaticClass()) , bWarnAboutMissingIdentityTags(true) { } + +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); + + if (PropertyChangedEvent.GetMemberPropertyName() == GET_MEMBER_NAME_CHECKED(UFlowSettings, bUseAdaptiveNodeTitles)) + { + (void)OnAdaptiveNodeTitlesChanged.ExecuteIfBound(); + } +} + +#endif + +UClass* UFlowSettings::GetDefaultExpectedOwnerClass() const +{ + UClass* Result = DefaultExpectedOwnerClass.ResolveClass(); + + if (Result == nullptr) + { + Result = DefaultExpectedOwnerClass.TryLoadClass(); + } + + return CastChecked(Result, ECastCheckedType::NullAllowed); +} diff --git a/Source/Flow/Private/FlowSubsystem.cpp b/Source/Flow/Private/FlowSubsystem.cpp index bcbdde0e9..c2996aa97 100644 --- a/Source/Flow/Private/FlowSubsystem.cpp +++ b/Source/Flow/Private/FlowSubsystem.cpp @@ -4,19 +4,29 @@ #include "FlowAsset.h" #include "FlowComponent.h" -#include "FlowModule.h" +#include "FlowLogChannels.h" #include "FlowSave.h" #include "FlowSettings.h" -#include "Nodes/Route/FlowNode_SubGraph.h" +#include "Interfaces/FlowExecutionGate.h" +#include "Nodes/Graph/FlowNode_SubGraph.h" #include "Engine/GameInstance.h" #include "Engine/World.h" -#include "Misc/FileHelper.h" +#include "Logging/MessageLog.h" #include "Misc/Paths.h" #include "UObject/UObjectHash.h" +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowSubsystem) + +#if !UE_BUILD_SHIPPING +FNativeFlowAssetEvent UFlowSubsystem::OnInstancedTemplateAdded; +FNativeFlowAssetEvent UFlowSubsystem::OnInstancedTemplateRemoved; +#endif + +#define LOCTEXT_NAMESPACE "FlowSubsystem" + UFlowSubsystem::UFlowSubsystem() - : UGameInstanceSubsystem() + : LoadedSaveGame(nullptr) { } @@ -31,7 +41,7 @@ bool UFlowSubsystem::ShouldCreateSubsystem(UObject* Outer) const } // in this case, we simply create subsystem for every instance of the game - if (UFlowSettings::Get()->bCreateFlowSubsystemOnClients) + if (GetDefault()->bCreateFlowSubsystemOnClients) { return true; } @@ -39,13 +49,15 @@ bool UFlowSubsystem::ShouldCreateSubsystem(UObject* Outer) const return Outer->GetWorld()->GetNetMode() < NM_Client; } -void UFlowSubsystem::Initialize(FSubsystemCollectionBase& Collection) +UWorld* UFlowSubsystem::GetWorld() const { + return GetGameInstance()->GetWorld(); } void UFlowSubsystem::Deinitialize() { AbortActiveFlows(); + ClearLoadedSaveGame(); } void UFlowSubsystem::AbortActiveFlows() @@ -67,18 +79,27 @@ void UFlowSubsystem::AbortActiveFlows() RootInstances.Empty(); } -void UFlowSubsystem::StartRootFlow(UObject* Owner, UFlowAsset* FlowAsset, const bool bAllowMultipleInstances /* = true */) +void UFlowSubsystem::StartRootFlow(UObject* Owner, UFlowAsset* FlowAsset, const TScriptInterface DataPinValueSupplier, const bool bAllowMultipleInstances) { - UFlowAsset* NewFlow = CreateRootFlow(Owner, FlowAsset, bAllowMultipleInstances); - if (NewFlow) + if (FlowAsset) + { + if (UFlowAsset* NewFlow = CreateRootFlow(Owner, FlowAsset, bAllowMultipleInstances)) + { + NewFlow->StartFlow(DataPinValueSupplier.GetInterface()); + } + } +#if WITH_EDITOR + else { - NewFlow->StartFlow(); + FMessageLog("PIE").Error(LOCTEXT("StartRootFlowNullAsset", "Attempted to start Root Flow with a null asset.")) + ->AddToken(FUObjectToken::Create(Owner)); } +#endif } -UFlowAsset* UFlowSubsystem::CreateRootFlow(UObject* Owner, UFlowAsset* FlowAsset, const bool bAllowMultipleInstances) +UFlowAsset* UFlowSubsystem::CreateRootFlow(UObject* Owner, UFlowAsset* FlowAsset, const bool bAllowMultipleInstances, const FString& NewInstanceName) { - for (const TPair>& RootInstance : RootInstances) + for (const TPair>& RootInstance : ObjectPtrDecay(RootInstances)) { if (Owner == RootInstance.Value.Get() && FlowAsset == RootInstance.Key->GetTemplateAsset()) { @@ -93,8 +114,11 @@ UFlowAsset* UFlowSubsystem::CreateRootFlow(UObject* Owner, UFlowAsset* FlowAsset return nullptr; } - UFlowAsset* NewFlow = CreateFlowInstance(Owner, FlowAsset); - RootInstances.Add(NewFlow, Owner); + UFlowAsset* NewFlow = CreateFlowInstance(Owner, FlowAsset, NewInstanceName); + if (NewFlow) + { + RootInstances.Add(NewFlow, Owner); + } return NewFlow; } @@ -102,8 +126,8 @@ UFlowAsset* UFlowSubsystem::CreateRootFlow(UObject* Owner, UFlowAsset* FlowAsset void UFlowSubsystem::FinishRootFlow(UObject* Owner, UFlowAsset* TemplateAsset, const EFlowFinishPolicy FinishPolicy) { UFlowAsset* InstanceToFinish = nullptr; - - for (TPair>& RootInstance : RootInstances) + + for (TPair, TWeakObjectPtr>& RootInstance : RootInstances) { if (Owner && Owner == RootInstance.Value.Get() && RootInstance.Key && RootInstance.Key->GetTemplateAsset() == TemplateAsset) { @@ -122,8 +146,8 @@ void UFlowSubsystem::FinishRootFlow(UObject* Owner, UFlowAsset* TemplateAsset, c void UFlowSubsystem::FinishAllRootFlows(UObject* Owner, const EFlowFinishPolicy FinishPolicy) { TArray InstancesToFinish; - - for (TPair>& RootInstance : RootInstances) + + for (TPair, TWeakObjectPtr>& RootInstance : RootInstances) { if (Owner && Owner == RootInstance.Value.Get() && RootInstance.Key) { @@ -138,26 +162,25 @@ void UFlowSubsystem::FinishAllRootFlows(UObject* Owner, const EFlowFinishPolicy } } -UFlowAsset* UFlowSubsystem::CreateSubFlow(UFlowNode_SubGraph* SubGraphNode, const FString SavedInstanceName, const bool bPreloading /* = false */) +UFlowAsset* UFlowSubsystem::CreateSubFlow(UFlowNode_SubGraph* SubGraphNode, const FString& SavedInstanceName, const bool bPreloading /* = false */) { - UFlowAsset* NewInstance = nullptr; + UFlowAsset* AssetInstance = nullptr; if (!InstancedSubFlows.Contains(SubGraphNode)) { const TWeakObjectPtr Owner = SubGraphNode->GetFlowAsset() ? SubGraphNode->GetFlowAsset()->GetOwner() : nullptr; - NewInstance = CreateFlowInstance(Owner, SubGraphNode->Asset, SavedInstanceName); - InstancedSubFlows.Add(SubGraphNode, NewInstance); + AssetInstance = CreateFlowInstance(Owner, SubGraphNode->Asset.LoadSynchronous(), SavedInstanceName); - if (bPreloading) + if (AssetInstance) { - NewInstance->PreloadNodes(); + InstancedSubFlows.Add(SubGraphNode, AssetInstance); } } - if (!bPreloading) + if (InstancedSubFlows.Contains(SubGraphNode) && !bPreloading) { // get instanced asset from map - in case it was already instanced by calling CreateSubFlow() with bPreloading == true - UFlowAsset* AssetInstance = InstancedSubFlows[SubGraphNode]; + AssetInstance = InstancedSubFlows[SubGraphNode]; AssetInstance->NodeOwningThisAssetInstance = SubGraphNode; SubGraphNode->GetFlowAsset()->ActiveSubGraphs.Add(SubGraphNode, AssetInstance); @@ -165,11 +188,11 @@ UFlowAsset* UFlowSubsystem::CreateSubFlow(UFlowNode_SubGraph* SubGraphNode, cons // don't activate Start Node if we're loading Sub Graph from SaveGame if (SavedInstanceName.IsEmpty()) { - AssetInstance->StartFlow(); + AssetInstance->StartFlow(SubGraphNode); } } - return NewInstance; + return AssetInstance; } void UFlowSubsystem::RemoveSubFlow(UFlowNode_SubGraph* SubGraphNode, const EFlowFinishPolicy FinishPolicy) @@ -177,57 +200,132 @@ void UFlowSubsystem::RemoveSubFlow(UFlowNode_SubGraph* SubGraphNode, const EFlow if (InstancedSubFlows.Contains(SubGraphNode)) { UFlowAsset* AssetInstance = InstancedSubFlows[SubGraphNode]; - AssetInstance->NodeOwningThisAssetInstance = nullptr; SubGraphNode->GetFlowAsset()->ActiveSubGraphs.Remove(SubGraphNode); InstancedSubFlows.Remove(SubGraphNode); AssetInstance->FinishFlow(FinishPolicy); + + // Make sure to set the NodeOwningThisAssetInstance after the FinishFlow call, as it may be needed in the FinishFlow method + AssetInstance->NodeOwningThisAssetInstance = nullptr; } } -UFlowAsset* UFlowSubsystem::CreateFlowInstance(const TWeakObjectPtr Owner, TSoftObjectPtr FlowAsset, FString NewInstanceName) +UFlowAsset* UFlowSubsystem::CreateFlowInstance(const TWeakObjectPtr Owner, UFlowAsset* LoadedFlowAsset, FString NewInstanceName) { - check(!FlowAsset.IsNull()); - - if (FlowAsset.IsPending() || !FlowAsset.IsValid()) + if (LoadedFlowAsset == nullptr) { - FlowAsset = Cast(Streamable.LoadSynchronous(FlowAsset.ToSoftObjectPath(), false)); + return nullptr; } - InstancedTemplates.Add(FlowAsset.Get()); + AddInstancedTemplate(LoadedFlowAsset); #if WITH_EDITOR if (GetWorld()->WorldType != EWorldType::Game) { // Fix connections - even in packaged game if assets haven't been re-saved in the editor after changing node's definition - FlowAsset.Get()->HarvestNodeConnections(); + LoadedFlowAsset->HarvestNodeConnections(); } #endif // it won't be empty, if we're restoring Flow Asset instance from the SaveGame if (NewInstanceName.IsEmpty()) { - NewInstanceName = FPaths::GetBaseFilename(FlowAsset.Get()->GetPathName()) + TEXT("_") + FString::FromInt(FlowAsset.Get()->GetInstancesNum()); + NewInstanceName = MakeUniqueObjectName(this, UFlowAsset::StaticClass(), *FPaths::GetBaseFilename(LoadedFlowAsset->GetPathName())).ToString(); } - UFlowAsset* NewInstance = NewObject(this, FlowAsset->GetClass(), *NewInstanceName, RF_Transient, FlowAsset.Get(), false, nullptr); - NewInstance->InitializeInstance(Owner, FlowAsset.Get()); + UFlowAsset* NewInstance = NewObject(this, LoadedFlowAsset->GetClass(), *NewInstanceName, RF_Transient, LoadedFlowAsset, false, nullptr); + NewInstance->InitializeInstance(Owner, *LoadedFlowAsset); - FlowAsset.Get()->AddInstance(NewInstance); + LoadedFlowAsset->AddInstance(NewInstance); return NewInstance; } +void UFlowSubsystem::AddInstancedTemplate(UFlowAsset* Template) +{ + if (!InstancedTemplates.Contains(Template)) + { + InstancedTemplates.Add(Template); + +#if WITH_EDITOR + Template->RuntimeLog = MakeShareable(new FFlowMessageLog()); + OnInstancedTemplateAdded.ExecuteIfBound(Template); +#endif + } +} + void UFlowSubsystem::RemoveInstancedTemplate(UFlowAsset* Template) { +#if WITH_EDITOR + OnInstancedTemplateRemoved.ExecuteIfBound(Template); + Template->RuntimeLog.Reset(); +#endif + InstancedTemplates.Remove(Template); } +bool UFlowSubsystem::TryFlushAllDeferredTriggerScopes() const +{ + // Flush deferred triggers on all active runtime instances. + // Flush order follows InstancedTemplates iteration + per-template ActiveInstances. + // This provides reasonable per-asset FIFO but is not a strict global FIFO across assets. + // A more precise global queue could be implemented later if cross-asset ordering becomes critical. + const TArray CapturedInstancedTemplates = InstancedTemplates; + for (const UFlowAsset* Template : CapturedInstancedTemplates) + { + if (!IsValid(Template)) + { + continue; + } + + for (UFlowAsset* Instance : Template->GetActiveInstances()) + { + if (FFlowExecutionGate::IsHalted()) + { + break; + } + + if (IsValid(Instance)) + { + const bool bFlushed = Instance->TryFlushAllDeferredTriggerScopes(); + + // The only case where we allow a flush to stop before completing + // is if we hit an execution gate halt + check(bFlushed || FFlowExecutionGate::IsHalted()); + } + } + } + + // The only case where we allow a flush to stop before completing + // is if we hit an execution gate halt + const bool bCompletedFlushAll = !FFlowExecutionGate::IsHalted(); + return bCompletedFlushAll; +} + +void UFlowSubsystem::ClearAllDeferredTriggerScopes() +{ + for (const UFlowAsset* Template : InstancedTemplates) + { + if (!IsValid(Template)) + { + continue; + } + + for (UFlowAsset* Instance : Template->GetActiveInstances()) + { + if (IsValid(Instance)) + { + Instance->ClearAllDeferredTriggerScopes(); + } + } + } +} + TMap UFlowSubsystem::GetRootInstances() const { TMap Result; - for (const TPair>& RootInstance : RootInstances) + for (const TPair>& RootInstance : ObjectPtrDecay(RootInstances)) { Result.Emplace(RootInstance.Value.Get(), RootInstance.Key); } @@ -237,7 +335,7 @@ TMap UFlowSubsystem::GetRootInstances() const TSet UFlowSubsystem::GetRootInstancesByOwner(const UObject* Owner) const { TSet Result; - for (const TPair>& RootInstance : RootInstances) + for (const TPair>& RootInstance : ObjectPtrDecay(RootInstances)) { if (Owner && RootInstance.Value == Owner) { @@ -258,123 +356,172 @@ UFlowAsset* UFlowSubsystem::GetRootFlow(const UObject* Owner) const return nullptr; } -UWorld* UFlowSubsystem::GetWorld() const +void UFlowSubsystem::OnGameSaved(UFlowSaveGame* SaveGame) { - return GetGameInstance()->GetWorld(); + if (SaveGame) + { + OnGameSaved(SaveGame->FlowComponents, SaveGame->FlowInstances); + } } -void UFlowSubsystem::OnGameSaved(UFlowSaveGame* SaveGame) +void UFlowSubsystem::OnGameSaved(TArray& FlowComponents, TArray& FlowInstances) { - // clear existing data, in case we received reused SaveGame instance - // we only remove data for the current world + global Flow Graph instances (i.e. not bound to any world if created by UGameInstanceSubsystem) - // we keep data bound to other worlds + // Clear existing data, in case we received data from a reused Save container. + // We only remove data for the current world, and Flow Graph instances are not bound to any world. + // We keep data bound to other worlds. if (GetWorld()) { const FString& WorldName = GetWorld()->GetName(); - for (int32 i = SaveGame->FlowInstances.Num() - 1; i >= 0; i--) + for (int32 i = FlowInstances.Num() - 1; i >= 0; i--) { - if (SaveGame->FlowInstances[i].WorldName.IsEmpty() || SaveGame->FlowInstances[i].WorldName == WorldName) + if (FlowInstances[i].WorldName.IsEmpty() || FlowInstances[i].WorldName == WorldName) { - SaveGame->FlowInstances.RemoveAt(i); + FlowInstances.RemoveAt(i); } } - for (int32 i = SaveGame->FlowComponents.Num() - 1; i >= 0; i--) + for (int32 i = FlowComponents.Num() - 1; i >= 0; i--) { - if (SaveGame->FlowComponents[i].WorldName.IsEmpty() || SaveGame->FlowComponents[i].WorldName == WorldName) + if (FlowComponents[i].WorldName.IsEmpty() || FlowComponents[i].WorldName == WorldName) { - SaveGame->FlowComponents.RemoveAt(i); + FlowComponents.RemoveAt(i); } } } - // save Flow Graphs - for (const TPair>& RootInstance : RootInstances) + // Save Flow Graphs. + for (const TPair>& RootInstance : ObjectPtrDecay(RootInstances)) { if (RootInstance.Key && RootInstance.Value.IsValid()) { if (UFlowComponent* FlowComponent = Cast(RootInstance.Value)) { - FlowComponent->SaveRootFlow(SaveGame->FlowInstances); + if (FlowComponent->CanSave()) + { + FlowComponent->SaveRootFlow(FlowInstances); + } } else { - RootInstance.Key->SaveInstance(SaveGame->FlowInstances); + RootInstance.Key->SaveInstance(FlowInstances); } } } - // save Flow Components + // Save Flow Components. { - // retrieve all registered components + // Retrieve all registered components. TArray> ComponentsArray; FlowComponentRegistry.GenerateValueArray(ComponentsArray); - // ensure uniqueness of entries + // Ensure uniqueness of entries. const TSet> RegisteredComponents = TSet>(ComponentsArray); - // write archives to SaveGame for (const TWeakObjectPtr RegisteredComponent : RegisteredComponents) { - SaveGame->FlowComponents.Emplace(RegisteredComponent->SaveInstance()); + if (RegisteredComponent->CanSave()) + { + FlowComponents.Emplace(RegisteredComponent->SaveInstance()); + } } } } void UFlowSubsystem::OnGameLoaded(UFlowSaveGame* SaveGame) { + // Receive a standard Flow Save data container. LoadedSaveGame = SaveGame; + + // Here's an opportunity to apply loaded data to custom systems. + // Do this by overriding this method in the subclass. +} + +void UFlowSubsystem::OnGameLoaded(TArray& FlowComponents, TArray& FlowInstances) +{ + // Create an object to store Flow Save data loaded from the custom data container. + LoadedSaveGame = NewObject(GetTransientPackage(), UFlowSaveGame::StaticClass()); + LoadedSaveGame->FlowComponents = FlowComponents; + LoadedSaveGame->FlowInstances = FlowInstances; } -void UFlowSubsystem::LoadRootFlow(UObject* Owner, UFlowAsset* FlowAsset, const FString& SavedAssetInstanceName) +void UFlowSubsystem::LoadRootFlow(UObject* Owner, UFlowAsset* FlowAsset, const FString& SavedAssetInstanceName, const bool bAllowMultipleInstances) { if (FlowAsset == nullptr || SavedAssetInstanceName.IsEmpty()) { return; } - for (const FFlowAssetSaveData& AssetRecord : LoadedSaveGame->FlowInstances) + if (const FFlowAssetSaveData* AssetRecord = GetLoadedAssetRecord(Owner, FlowAsset, SavedAssetInstanceName)) { - if (AssetRecord.InstanceName == SavedAssetInstanceName - && (FlowAsset->IsBoundToWorld() == false || AssetRecord.WorldName == GetWorld()->GetName())) + if (UFlowAsset* LoadedInstance = CreateRootFlow(Owner, FlowAsset, bAllowMultipleInstances)) { - UFlowAsset* LoadedInstance = CreateRootFlow(Owner, FlowAsset, false); - if (LoadedInstance) - { - LoadedInstance->LoadInstance(AssetRecord); - } - return; + LoadedInstance->LoadInstance(*AssetRecord); } } } void UFlowSubsystem::LoadSubFlow(UFlowNode_SubGraph* SubGraphNode, const FString& SavedAssetInstanceName) { - if (SubGraphNode->Asset.IsNull()) + ensureAlways(SubGraphNode); + + const UFlowAsset* SubGraphAsset = SubGraphNode->Asset.LoadSynchronous(); + if (SubGraphAsset == nullptr || SavedAssetInstanceName.IsEmpty()) { return; } - if (SubGraphNode->Asset.IsPending()) + if (const FFlowAssetSaveData* AssetRecord = GetLoadedAssetRecord(SubGraphNode, SubGraphAsset, SavedAssetInstanceName)) { - const FSoftObjectPath& AssetRef = SubGraphNode->Asset.ToSoftObjectPath(); - Streamable.LoadSynchronous(AssetRef, false); + UFlowAsset* LoadedInstance = CreateSubFlow(SubGraphNode, SavedAssetInstanceName); + if (LoadedInstance) + { + LoadedInstance->LoadInstance(*AssetRecord); + } } +} - for (const FFlowAssetSaveData& AssetRecord : LoadedSaveGame->FlowInstances) +const FFlowComponentSaveData* UFlowSubsystem::GetLoadedComponentRecord(const UFlowComponent* Component) const +{ + if (LoadedSaveGame) { - if (AssetRecord.InstanceName == SavedAssetInstanceName - && ((SubGraphNode->Asset && SubGraphNode->Asset->IsBoundToWorld() == false) || AssetRecord.WorldName == GetWorld()->GetName())) + const FString WorldName = Component->GetWorld()->GetName(); + const FString ActorName = Component->GetOwner()->GetName(); + + for (const FFlowComponentSaveData& ComponentRecord : LoadedSaveGame->FlowComponents) { - UFlowAsset* LoadedInstance = CreateSubFlow(SubGraphNode, SavedAssetInstanceName); - if (LoadedInstance) + if (ComponentRecord.WorldName == WorldName && ComponentRecord.ActorInstanceName == ActorName) { - LoadedInstance->LoadInstance(AssetRecord); + return &ComponentRecord; } - return; } } + + return nullptr; +} + +const FFlowAssetSaveData* UFlowSubsystem::GetLoadedAssetRecord(const UObject* Owner, const UFlowAsset* Asset, const FString& SavedAssetInstanceName) const +{ + if (LoadedSaveGame) + { + const FName& WorldName = GetWorld()->GetFName(); + const bool bAssetBoundToWorld = Asset->IsBoundToWorld(); + + for (const FFlowAssetSaveData& AssetRecord : LoadedSaveGame->FlowInstances) + { + if (AssetRecord.InstanceName == SavedAssetInstanceName && (!bAssetBoundToWorld || AssetRecord.WorldName == WorldName)) + { + return &AssetRecord; + } + } + } + + return nullptr; +} + +void UFlowSubsystem::ClearLoadedSaveGame() +{ + LoadedSaveGame = nullptr; } void UFlowSubsystem::RegisterComponent(UFlowComponent* Component) @@ -608,14 +755,18 @@ void UFlowSubsystem::FindComponents(const FGameplayTagContainer& Tags, const EGa TArray> ComponentsPerTag; FindComponents(Tag, bExactMatch, ComponentsPerTag); ComponentsWithAnyTag.Append(ComponentsPerTag); + break; } for (const TWeakObjectPtr& Component : ComponentsWithAnyTag) { - if (Component.IsValid() && Component->IdentityTags.HasAllExact(Tags)) + if (Component.IsValid() && + (bExactMatch ? Component->IdentityTags.HasAllExact(Tags) : Component->IdentityTags.HasAll(Tags))) { OutComponents.Emplace(Component); } } } } + +#undef LOCTEXT_NAMESPACE diff --git a/Source/Flow/Private/FlowTags.cpp b/Source/Flow/Private/FlowTags.cpp new file mode 100644 index 000000000..349d2ed36 --- /dev/null +++ b/Source/Flow/Private/FlowTags.cpp @@ -0,0 +1,23 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "FlowTags.h" + +UE_DEFINE_GAMEPLAY_TAG(FlowNodeStyle::CategoryName, "Flow.NodeStyle"); +UE_DEFINE_GAMEPLAY_TAG(FlowNodeStyle::Custom, "Flow.NodeStyle.Custom"); + +UE_DEFINE_GAMEPLAY_TAG(FlowNodeStyle::Node, "Flow.NodeStyle.Node"); +UE_DEFINE_GAMEPLAY_TAG(FlowNodeStyle::Default, "Flow.NodeStyle.Node.Default"); +UE_DEFINE_GAMEPLAY_TAG(FlowNodeStyle::Condition, "Flow.NodeStyle.Node.Condition"); +UE_DEFINE_GAMEPLAY_TAG(FlowNodeStyle::Deprecated, "Flow.NodeStyle.Node.Deprecated"); +UE_DEFINE_GAMEPLAY_TAG(FlowNodeStyle::Developer, "Flow.NodeStyle.Node.Developer"); +UE_DEFINE_GAMEPLAY_TAG(FlowNodeStyle::InOut, "Flow.NodeStyle.Node.InOut"); +UE_DEFINE_GAMEPLAY_TAG(FlowNodeStyle::Latent, "Flow.NodeStyle.Node.Latent"); +UE_DEFINE_GAMEPLAY_TAG(FlowNodeStyle::Logic, "Flow.NodeStyle.Node.Logic"); +UE_DEFINE_GAMEPLAY_TAG(FlowNodeStyle::SubGraph, "Flow.NodeStyle.Node.SubGraph"); +UE_DEFINE_GAMEPLAY_TAG(FlowNodeStyle::Terminal, "Flow.NodeStyle.Node.Terminal"); + +UE_DEFINE_GAMEPLAY_TAG(FlowNodeStyle::AddOn, "Flow.NodeStyle.AddOn"); +UE_DEFINE_GAMEPLAY_TAG(FlowNodeStyle::AddOn_PerSpawnedActor, "Flow.NodeStyle.AddOn.PerSpawnedActor"); +UE_DEFINE_GAMEPLAY_TAG(FlowNodeStyle::AddOn_Predicate, "Flow.NodeStyle.AddOn.Predicate"); +UE_DEFINE_GAMEPLAY_TAG(FlowNodeStyle::AddOn_Predicate_Composite, "Flow.NodeStyle.AddOn.Predicate.Composite"); +UE_DEFINE_GAMEPLAY_TAG(FlowNodeStyle::AddOn_SwitchCase, "Flow.NodeStyle.AddOn.SwitchCase"); diff --git a/Source/Flow/Private/FlowWorldSettings.cpp b/Source/Flow/Private/FlowWorldSettings.cpp index 82526f4b7..91d805584 100644 --- a/Source/Flow/Private/FlowWorldSettings.cpp +++ b/Source/Flow/Private/FlowWorldSettings.cpp @@ -2,7 +2,8 @@ #include "FlowWorldSettings.h" #include "FlowComponent.h" -#include "FlowSubsystem.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowWorldSettings) AFlowWorldSettings::AFlowWorldSettings(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) @@ -13,38 +14,3 @@ AFlowWorldSettings::AFlowWorldSettings(const FObjectInitializer& ObjectInitializ // In this case engine would call BeginPlay multiple times... for AFlowWorldSettings and every inherited AWorldSettings class... FlowComponent->bAllowMultipleInstances = false; } - -void AFlowWorldSettings::PostLoad() -{ - Super::PostLoad(); - - if (FlowAsset_DEPRECATED) - { - FlowComponent->RootFlow = FlowAsset_DEPRECATED; - } -} - -void AFlowWorldSettings::PostInitializeComponents() -{ - Super::PostInitializeComponents(); - - if (!IsValidInstance()) - { - GetFlowComponent()->bAutoStartRootFlow = false; - } -} - -bool AFlowWorldSettings::IsValidInstance() const -{ - if (const UWorld* World = GetWorld()) - { - // workaround to prevent starting Flow from stray AWorldSettings actor that still exists in the world - // cause of this issue fixed in UE 5.0: https://github.com/EpicGames/UnrealEngine/commit/001f50b8b55507940f9c2cb1349592c692aae2c1?diff=unified - if (World->GetWorldSettings() == this) - { - return true; - } - } - - return false; -} diff --git a/Source/Flow/Private/Interfaces/FlowAssetProviderInterface.cpp b/Source/Flow/Private/Interfaces/FlowAssetProviderInterface.cpp new file mode 100644 index 000000000..bbf8e4583 --- /dev/null +++ b/Source/Flow/Private/Interfaces/FlowAssetProviderInterface.cpp @@ -0,0 +1,9 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Interfaces/FlowAssetProviderInterface.h" +#include "FlowAsset.h" + +UFlowAsset* IFlowAssetProviderInterface::ProvideFlowAsset() const +{ + return Execute_K2_ProvideFlowAsset(Cast(this)); +} diff --git a/Source/Flow/Private/Interfaces/FlowContextPinSupplierInterface.cpp b/Source/Flow/Private/Interfaces/FlowContextPinSupplierInterface.cpp new file mode 100644 index 000000000..61bd406b4 --- /dev/null +++ b/Source/Flow/Private/Interfaces/FlowContextPinSupplierInterface.cpp @@ -0,0 +1,18 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Interfaces/FlowContextPinSupplierInterface.h" + +bool IFlowContextPinSupplierInterface::K2_SupportsContextPins_Implementation() const +{ + return false; +} + +TArray IFlowContextPinSupplierInterface::K2_GetContextInputs_Implementation() const +{ + return TArray(); +} + +TArray IFlowContextPinSupplierInterface::K2_GetContextOutputs_Implementation() const +{ + return TArray(); +} diff --git a/Source/Flow/Private/Interfaces/FlowDataPinValueOwnerInterface.cpp b/Source/Flow/Private/Interfaces/FlowDataPinValueOwnerInterface.cpp new file mode 100644 index 000000000..918050430 --- /dev/null +++ b/Source/Flow/Private/Interfaces/FlowDataPinValueOwnerInterface.cpp @@ -0,0 +1,74 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Interfaces/FlowDataPinValueOwnerInterface.h" +#include "Interfaces/FlowNamedPropertiesSupplierInterface.h" +#include "Types/FlowAutoDataPinsWorkingData.h" +#include "Types/FlowNamedDataPinProperty.h" + +#if WITH_EDITOR +void IFlowDataPinValueOwnerInterface::AutoGenerateDataPins(FFlowDataPinValueOwner& ValueOwner, FFlowAutoDataPinsWorkingData& InOutWorkingData) +{ + // Generate the auto-pins for class properties (ie, FFlowDataPinValue subclasses) + InOutWorkingData.AddFlowDataPinsForClassProperties(ValueOwner); + + // Generate the auto-pins for named property suppliers' pins (ie, TArray) + if (IFlowNamedPropertiesSupplierInterface* NamedPropertySupplier = Cast(ValueOwner.GetValueOwnerAsObject())) + { + TArray& NamedProperties = NamedPropertySupplier->GetMutableNamedProperties(); + for (FFlowNamedDataPinProperty& DataPinProperty : NamedProperties) + { + DataPinProperty.AutoGenerateDataPinForProperty(ValueOwner, InOutWorkingData); + } + } + + // Subclasses may want to also include additional properties (eg, FFlowNamedDataPinProperty) +} +#endif + +bool IFlowDataPinValueOwnerInterface::TryFindPropertyByPinName( + const FName& PinName, + const FProperty*& OutFoundProperty, + TInstancedStruct& OutFoundInstancedStruct) const +{ + const UObject* ThisAsObject = Cast(this); + return TryFindPropertyByPinName_Static(*ThisAsObject, PinName, OutFoundProperty, OutFoundInstancedStruct); +} + +bool IFlowDataPinValueOwnerInterface::TryFindPropertyByPinName_Static(const UObject& PropertyOwnerObject, const FName& PinName, const FProperty*& OutFoundProperty, TInstancedStruct& OutFoundInstancedStruct) +{ + // First check if the PinName matches a NamedProperties array name + if (const IFlowNamedPropertiesSupplierInterface* NamedPropertySupplier = Cast(&PropertyOwnerObject)) + { + const TArray& NamedProperties = NamedPropertySupplier->GetNamedProperties(); + for (const FFlowNamedDataPinProperty& NamedProperty : NamedProperties) + { + if (NamedProperty.Name == PinName && NamedProperty.IsValid()) + { + OutFoundInstancedStruct = NamedProperty.DataPinValue; + + return true; + } + } + } + + // Try direct property match + OutFoundProperty = PropertyOwnerObject.GetClass()->FindPropertyByName(PinName); + if (OutFoundProperty) + { + const FStructProperty* StructProperty = CastField(OutFoundProperty); + if (StructProperty && StructProperty->Struct->IsChildOf(FFlowDataPinValue::StaticStruct())) + { + // Initialize to match property's struct + OutFoundInstancedStruct.InitializeAsScriptStruct(StructProperty->Struct); + + StructProperty->GetValue_InContainer(&PropertyOwnerObject, OutFoundInstancedStruct.GetMutableMemory()); + return true; + } + + // Raw property (e.g., bool, TArray) is valid + check(OutFoundProperty != nullptr); + return true; + } + + return false; +} \ No newline at end of file diff --git a/Source/Flow/Private/Interfaces/FlowExecutionGate.cpp b/Source/Flow/Private/Interfaces/FlowExecutionGate.cpp new file mode 100644 index 000000000..e09b7d02a --- /dev/null +++ b/Source/Flow/Private/Interfaces/FlowExecutionGate.cpp @@ -0,0 +1,23 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Interfaces/FlowExecutionGate.h" + +#include "FlowAsset.h" +#include "Nodes/FlowPin.h" + +IFlowExecutionGate* FFlowExecutionGate::Gate = nullptr; + +void FFlowExecutionGate::SetGate(IFlowExecutionGate* InGate) +{ + Gate = InGate; +} + +IFlowExecutionGate* FFlowExecutionGate::GetGate() +{ + return Gate; +} + +bool FFlowExecutionGate::IsHalted() +{ + return (Gate != nullptr) && Gate->IsFlowExecutionHalted(); +} \ No newline at end of file diff --git a/Source/Flow/Private/Interfaces/FlowExternalExecutableInterface.cpp b/Source/Flow/Private/Interfaces/FlowExternalExecutableInterface.cpp new file mode 100644 index 000000000..6697d5405 --- /dev/null +++ b/Source/Flow/Private/Interfaces/FlowExternalExecutableInterface.cpp @@ -0,0 +1,8 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Interfaces/FlowExternalExecutableInterface.h" + +void IFlowExternalExecutableInterface::PreActivateExternalFlowExecutable(UFlowNodeBase& FlowNodeBase) +{ + Execute_K2_PreActivateExternalFlowExecutable(Cast(this), &FlowNodeBase); +} diff --git a/Source/Flow/Private/Interfaces/FlowPredicateInterface.cpp b/Source/Flow/Private/Interfaces/FlowPredicateInterface.cpp new file mode 100644 index 000000000..45754cb7e --- /dev/null +++ b/Source/Flow/Private/Interfaces/FlowPredicateInterface.cpp @@ -0,0 +1,22 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Interfaces/FlowPredicateInterface.h" +#include "AddOns/FlowNodeAddOn.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowPredicateInterface) + +bool IFlowPredicateInterface::ImplementsInterfaceSafe(const UFlowNodeAddOn* AddOnTemplate) +{ + if (!IsValid(AddOnTemplate)) + { + return false; + } + + const UClass* AddOnClass = AddOnTemplate->GetClass(); + if (AddOnClass->ImplementsInterface(UFlowPredicateInterface::StaticClass())) + { + return true; + } + + return false; +} diff --git a/Source/Flow/Private/Interfaces/FlowPreloadableInterface.cpp b/Source/Flow/Private/Interfaces/FlowPreloadableInterface.cpp new file mode 100644 index 000000000..d3f91eda7 --- /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/Interfaces/FlowSwitchCaseInterface.cpp b/Source/Flow/Private/Interfaces/FlowSwitchCaseInterface.cpp new file mode 100644 index 000000000..0973034db --- /dev/null +++ b/Source/Flow/Private/Interfaces/FlowSwitchCaseInterface.cpp @@ -0,0 +1,20 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Interfaces/FlowSwitchCaseInterface.h" +#include "AddOns/FlowNodeAddOn.h" + +bool IFlowSwitchCaseInterface::ImplementsInterfaceSafe(const UFlowNodeAddOn* AddOnTemplate) +{ + if (!IsValid(AddOnTemplate)) + { + return false; + } + + UClass* AddOnClass = AddOnTemplate->GetClass(); + if (AddOnClass->ImplementsInterface(UFlowSwitchCaseInterface::StaticClass())) + { + return true; + } + + return false; +} diff --git a/Source/Flow/Private/LevelSequence/FlowLevelSequenceActor.cpp b/Source/Flow/Private/LevelSequence/FlowLevelSequenceActor.cpp index bcb0cdedb..88b0f35cc 100644 --- a/Source/Flow/Private/LevelSequence/FlowLevelSequenceActor.cpp +++ b/Source/Flow/Private/LevelSequence/FlowLevelSequenceActor.cpp @@ -3,10 +3,15 @@ #include "LevelSequence/FlowLevelSequenceActor.h" #include "LevelSequence/FlowLevelSequencePlayer.h" #include "Net/UnrealNetwork.h" +#include "Runtime/Launch/Resources/Version.h" + +#include "DefaultLevelSequenceInstanceData.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowLevelSequenceActor) AFlowLevelSequenceActor::AFlowLevelSequenceActor(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer - .SetDefaultSubobjectClass("AnimationPlayer")) + : Super(ObjectInitializer.SetDefaultSubobjectClass("AnimationPlayer")) + , ReplicatedLevelSequenceAsset(nullptr) { } @@ -17,6 +22,12 @@ void AFlowLevelSequenceActor::GetLifetimeReplicatedProps(TArraySetPlaybackSettings(PlaybackSettings); +} + void AFlowLevelSequenceActor::SetReplicatedLevelSequenceAsset(ULevelSequence* Asset) { if (HasAuthority()) @@ -29,9 +40,15 @@ void AFlowLevelSequenceActor::SetReplicatedLevelSequenceAsset(ULevelSequence* As void AFlowLevelSequenceActor::OnRep_ReplicatedLevelSequenceAsset() { LevelSequenceAsset = ReplicatedLevelSequenceAsset; -} + ReplicatedLevelSequenceAsset = nullptr; + + // InstanceData is not replicated to the client. + // However, it can be assumed that the spawn transform of the level sequence actor is the transform origin for the sequence. + if (UDefaultLevelSequenceInstanceData* InstanceData = Cast(DefaultInstanceData)) + { + bOverrideInstanceData = true; + InstanceData->TransformOriginActor = this; + } -void AFlowLevelSequenceActor::RPC_InitializePlayer_Implementation() -{ InitializePlayer(); -}; +} diff --git a/Source/Flow/Private/LevelSequence/FlowLevelSequencePlayer.cpp b/Source/Flow/Private/LevelSequence/FlowLevelSequencePlayer.cpp index 011760d90..acd9e98d7 100644 --- a/Source/Flow/Private/LevelSequence/FlowLevelSequencePlayer.cpp +++ b/Source/Flow/Private/LevelSequence/FlowLevelSequencePlayer.cpp @@ -5,6 +5,9 @@ #include "Nodes/FlowNode.h" #include "DefaultLevelSequenceInstanceData.h" +#include "Runtime/Launch/Resources/Version.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowLevelSequencePlayer) UFlowLevelSequencePlayer::UFlowLevelSequencePlayer(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) @@ -12,7 +15,16 @@ UFlowLevelSequencePlayer::UFlowLevelSequencePlayer(const FObjectInitializer& Obj { } -UFlowLevelSequencePlayer* UFlowLevelSequencePlayer::CreateFlowLevelSequencePlayer(UObject* WorldContextObject, ULevelSequence* LevelSequence, FMovieSceneSequencePlaybackSettings Settings, FLevelSequenceCameraSettings CameraSettings, AActor* TransformOriginActor, bool bReplicates, ALevelSequenceActor*& OutActor) +UFlowLevelSequencePlayer* UFlowLevelSequencePlayer::CreateFlowLevelSequencePlayer( + const UObject* WorldContextObject, + ULevelSequence* LevelSequence, + FMovieSceneSequencePlaybackSettings Settings, + FLevelSequenceCameraSettings CameraSettings, + AActor* TransformOriginActor, + const bool bReplicates, + const bool bAlwaysRelevant, + ALevelSequenceActor*& OutActor +) { if (LevelSequence == nullptr) { @@ -25,54 +37,54 @@ UFlowLevelSequencePlayer* UFlowLevelSequencePlayer::CreateFlowLevelSequencePlaye return nullptr; } - FActorSpawnParameters SpawnParams; - SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; - SpawnParams.ObjectFlags |= RF_Transient; - SpawnParams.bAllowDuringConstructionScript = true; + // Sequence Actor might be spawned exactly where playback happens + FTransform SpawnTransform = FTransform::Identity; + { + // apply Transform Origin + // https://dev.epicgames.com/documentation/en-us/unreal-engine/creating-level-sequences-with-dynamic-transforms-in-unreal-engine + if (TransformOriginActor) + { + // moving Level Sequence Actor might allow proper distance-based actor replication in networked games + SpawnTransform = TransformOriginActor->GetTransform(); + SpawnTransform = FTransform(SpawnTransform.GetRotation(), SpawnTransform.GetLocation(), FVector::OneVector); + } + } - // Defer construction for autoplay so that BeginPlay() is called - SpawnParams.bDeferConstruction = true; + // Create Sequence Actor + // We use deferred spawn, so we can set all actor properties prior to its initialization. + // This also helpful in case of multiplayer, since all actor settings are replicated with the spawned actor. No need to call replication just after spawn. + AFlowLevelSequenceActor* Actor = World->SpawnActorDeferred(AFlowLevelSequenceActor::StaticClass(), SpawnTransform, nullptr, nullptr, ESpawnActorCollisionHandlingMethod::AlwaysSpawn); + Actor->SetPlaybackSettings(Settings); + Actor->CameraSettings = CameraSettings; - AFlowLevelSequenceActor* Actor = World->SpawnActor(SpawnParams); + // InstanceData is not set to replicate to the clients, + // so the clients will still play the level sequence using the world origin as the sequence origin. + // Instead of replicating the data, just assume the spawn transform of the level sequence actor is the sequence origin. + // The level sequence actor was either spawned at the world origin or at the transform of TransformOriginActor. + if (UDefaultLevelSequenceInstanceData* InstanceData = Cast(Actor->DefaultInstanceData)) + { + Actor->bOverrideInstanceData = true; + InstanceData->TransformOriginActor = Actor; + } - Actor->PlaybackSettings = Settings; - Actor->CameraSettings = CameraSettings; + // support networking if (bReplicates) { + Actor->bReplicatePlayback = true; + Actor->bAlwaysRelevant = bAlwaysRelevant; Actor->SetReplicatedLevelSequenceAsset(LevelSequence); - Actor->SetReplicatePlayback(true); - Actor->bAlwaysRelevant = true; - Actor->RPC_InitializePlayer(); } else { Actor->LevelSequenceAsset = LevelSequence; - Actor->InitializePlayer(); } - OutActor = Actor; - { - FTransform DefaultTransform; - - // apply Transform Origin - // https://docs.unrealengine.com/5.0/en-US/creating-level-sequences-with-dynamic-transforms-in-unreal-engine/ - if (IsValid(TransformOriginActor)) - { - if (UDefaultLevelSequenceInstanceData* InstanceData = Cast(Actor->DefaultInstanceData)) - { - Actor->bOverrideInstanceData = true; - InstanceData->TransformOriginActor = TransformOriginActor; - - // moving Level Sequence Actor might allow proper distance-based actor replication in networked games - const FTransform OriginTransform = TransformOriginActor->GetTransform(); - DefaultTransform = FTransform(OriginTransform.GetRotation(), OriginTransform.GetLocation(), FVector::OneVector); - } - } + // finish deferred spawn + Actor->FinishSpawning(SpawnTransform); + OutActor = Actor; - Actor->FinishSpawning(DefaultTransform); - } - - return Cast(Actor->SequencePlayer); + // Sequence Player is created by Level Sequence Actor + return Cast(Actor->GetSequencePlayer()); } TArray UFlowLevelSequencePlayer::GetEventContexts() const diff --git a/Source/Flow/Private/MovieScene/MovieSceneFlowRepeaterSection.cpp b/Source/Flow/Private/MovieScene/MovieSceneFlowRepeaterSection.cpp index 6aaaa8afc..5abdb2b83 100644 --- a/Source/Flow/Private/MovieScene/MovieSceneFlowRepeaterSection.cpp +++ b/Source/Flow/Private/MovieScene/MovieSceneFlowRepeaterSection.cpp @@ -1,3 +1,5 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #include "MovieScene/MovieSceneFlowRepeaterSection.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(MovieSceneFlowRepeaterSection) diff --git a/Source/Flow/Private/MovieScene/MovieSceneFlowSectionBase.cpp b/Source/Flow/Private/MovieScene/MovieSceneFlowSectionBase.cpp index 0016b8255..377a20a0f 100644 --- a/Source/Flow/Private/MovieScene/MovieSceneFlowSectionBase.cpp +++ b/Source/Flow/Private/MovieScene/MovieSceneFlowSectionBase.cpp @@ -1,3 +1,5 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #include "MovieScene/MovieSceneFlowSectionBase.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(MovieSceneFlowSectionBase) diff --git a/Source/Flow/Private/MovieScene/MovieSceneFlowTemplate.cpp b/Source/Flow/Private/MovieScene/MovieSceneFlowTemplate.cpp index 6e953a52c..ad6e29590 100644 --- a/Source/Flow/Private/MovieScene/MovieSceneFlowTemplate.cpp +++ b/Source/Flow/Private/MovieScene/MovieSceneFlowTemplate.cpp @@ -2,11 +2,13 @@ #include "MovieScene/MovieSceneFlowTemplate.h" #include "MovieScene/MovieSceneFlowTrack.h" -#include "Nodes/World/FlowNode_PlayLevelSequence.h" +#include "Nodes/Actor/FlowNode_PlayLevelSequence.h" #include "Evaluation/MovieSceneEvaluation.h" #include "IMovieScenePlayer.h" +#include UE_INLINE_GENERATED_CPP_BY_NAME(MovieSceneFlowTemplate) + #define LOCTEXT_NAMESPACE "MovieSceneFlowTemplate" DECLARE_CYCLE_STAT(TEXT("Flow Track Token Execute"), MovieSceneEval_FlowTrack_TokenExecute, STATGROUP_MovieSceneEval); diff --git a/Source/Flow/Private/MovieScene/MovieSceneFlowTrack.cpp b/Source/Flow/Private/MovieScene/MovieSceneFlowTrack.cpp index cb47fe513..24c4d6c76 100644 --- a/Source/Flow/Private/MovieScene/MovieSceneFlowTrack.cpp +++ b/Source/Flow/Private/MovieScene/MovieSceneFlowTrack.cpp @@ -8,6 +8,8 @@ #include "Evaluation/MovieSceneEvaluationTrack.h" #include "IMovieSceneTracksModule.h" +#include UE_INLINE_GENERATED_CPP_BY_NAME(MovieSceneFlowTrack) + #define LOCTEXT_NAMESPACE "MovieSceneFlowTrack" void UMovieSceneFlowTrack::AddSection(UMovieSceneSection& Section) diff --git a/Source/Flow/Private/MovieScene/MovieSceneFlowTriggerSection.cpp b/Source/Flow/Private/MovieScene/MovieSceneFlowTriggerSection.cpp index d1cc40a7e..ea43065e8 100644 --- a/Source/Flow/Private/MovieScene/MovieSceneFlowTriggerSection.cpp +++ b/Source/Flow/Private/MovieScene/MovieSceneFlowTriggerSection.cpp @@ -4,11 +4,13 @@ #include "Channels/MovieSceneChannelProxy.h" +#include UE_INLINE_GENERATED_CPP_BY_NAME(MovieSceneFlowTriggerSection) + UMovieSceneFlowTriggerSection::UMovieSceneFlowTriggerSection(const FObjectInitializer& ObjInit) : Super(ObjInit) { bSupportsInfiniteRange = true; - SetRange(TRange::All()); + UMovieSceneSection::SetRange(TRange::All()); #if WITH_EDITOR ChannelProxy = MakeShared(StringChannel, FMovieSceneChannelMetaData(), TMovieSceneExternalValue::Make()); diff --git a/Source/Flow/Private/Nodes/World/FlowNode_ComponentObserver.cpp b/Source/Flow/Private/Nodes/Actor/FlowNode_ComponentObserver.cpp similarity index 75% rename from Source/Flow/Private/Nodes/World/FlowNode_ComponentObserver.cpp rename to Source/Flow/Private/Nodes/Actor/FlowNode_ComponentObserver.cpp index 0ebde212d..770c0d26e 100644 --- a/Source/Flow/Private/Nodes/World/FlowNode_ComponentObserver.cpp +++ b/Source/Flow/Private/Nodes/Actor/FlowNode_ComponentObserver.cpp @@ -1,33 +1,24 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors -#include "Nodes/World/FlowNode_ComponentObserver.h" +#include "Nodes/Actor/FlowNode_ComponentObserver.h" #include "FlowSubsystem.h" -UFlowNode_ComponentObserver::UFlowNode_ComponentObserver(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) - , IdentityMatchType(EFlowTagContainerMatchType::HasAnyExact) +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_ComponentObserver) + +UFlowNode_ComponentObserver::UFlowNode_ComponentObserver() + : IdentityMatchType(EFlowTagContainerMatchType::HasAnyExact) , SuccessLimit(1) , SuccessCount(0) { #if WITH_EDITOR - NodeStyle = EFlowNodeStyle::Condition; - Category = TEXT("World"); + NodeDisplayStyle = FlowNodeStyle::Condition; + Category = TEXT("Actor"); #endif InputPins = {FFlowPin(TEXT("Start")), FFlowPin(TEXT("Stop"))}; OutputPins = {FFlowPin(TEXT("Success")), FFlowPin(TEXT("Completed")), FFlowPin(TEXT("Stopped"))}; } -void UFlowNode_ComponentObserver::PostLoad() -{ - Super::PostLoad(); - - if (IdentityTag_DEPRECATED.IsValid()) - { - IdentityTags = FGameplayTagContainer(IdentityTag_DEPRECATED); - } -} - void UFlowNode_ComponentObserver::ExecuteInput(const FName& PinName) { if (IdentityTags.IsValid()) @@ -66,25 +57,20 @@ void UFlowNode_ComponentObserver::StartObserving() // collect already registered components for (const TWeakObjectPtr& FoundComponent : FlowSubsystem->GetComponents(IdentityTags, ContainerMatchType, bExactMatch)) { - if (GetActivationState() == EFlowNodeState::Active) - { - ObserveActor(FoundComponent->GetOwner(), FoundComponent); - } - else + ObserveActor(FoundComponent->GetOwner(), FoundComponent); + + // node might finish work immediately as the effect of ObserveActor() + // we should terminate iteration in this case + if (GetActivationState() != EFlowNodeState::Active) { - // node might finish work as the effect of triggering event on the found actor - // we should terminate iteration in this case return; } } - - // clear old bindings before binding again, which might happen while loading a SaveGame - StopObserving(); - - FlowSubsystem->OnComponentRegistered.AddDynamic(this, &UFlowNode_ComponentObserver::OnComponentRegistered); - FlowSubsystem->OnComponentTagAdded.AddDynamic(this, &UFlowNode_ComponentObserver::OnComponentTagAdded); - FlowSubsystem->OnComponentTagRemoved.AddDynamic(this, &UFlowNode_ComponentObserver::OnComponentTagRemoved); - FlowSubsystem->OnComponentUnregistered.AddDynamic(this, &UFlowNode_ComponentObserver::OnComponentUnregistered); + + FlowSubsystem->OnComponentRegistered.AddUniqueDynamic(this, &UFlowNode_ComponentObserver::OnComponentRegistered); + FlowSubsystem->OnComponentTagAdded.AddUniqueDynamic(this, &UFlowNode_ComponentObserver::OnComponentTagAdded); + FlowSubsystem->OnComponentTagRemoved.AddUniqueDynamic(this, &UFlowNode_ComponentObserver::OnComponentTagRemoved); + FlowSubsystem->OnComponentUnregistered.AddUniqueDynamic(this, &UFlowNode_ComponentObserver::OnComponentUnregistered); } } @@ -158,6 +144,8 @@ void UFlowNode_ComponentObserver::Cleanup() RegisteredActors.Empty(); SuccessCount = 0; + + Super::Cleanup(); } #if WITH_EDITOR @@ -166,6 +154,17 @@ FString UFlowNode_ComponentObserver::GetNodeDescription() const return GetIdentityTagsDescription(IdentityTags); } +EDataValidationResult UFlowNode_ComponentObserver::ValidateNode() +{ + if (IdentityTags.IsEmpty()) + { + ValidationLog.Error(*UFlowNode::MissingIdentityTag, this); + return EDataValidationResult::Invalid; + } + + return EDataValidationResult::Valid; +} + FString UFlowNode_ComponentObserver::GetStatusString() const { if (ActivationState == EFlowNodeState::Active && RegisteredActors.Num() == 0) diff --git a/Source/Flow/Private/Nodes/Actor/FlowNode_ExecuteComponent.cpp b/Source/Flow/Private/Nodes/Actor/FlowNode_ExecuteComponent.cpp new file mode 100644 index 000000000..b2faec55c --- /dev/null +++ b/Source/Flow/Private/Nodes/Actor/FlowNode_ExecuteComponent.cpp @@ -0,0 +1,721 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#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" +#include "FlowLogChannels.h" +#include "FlowSettings.h" +#include "Types/FlowAutoDataPinsWorkingData.h" +#include "Types/FlowInjectComponentsHelper.h" +#include "Types/FlowInjectComponentsManager.h" +#include "GameFramework/Actor.h" +#include "Components/ActorComponent.h" + +#define LOCTEXT_NAMESPACE "FlowNode" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_ExecuteComponent) + +UFlowNode_ExecuteComponent::UFlowNode_ExecuteComponent() + : Super() +{ +#if WITH_EDITOR + Category = TEXT("Actor"); +#endif + + InputPins.Reset(); + OutputPins.Reset(); +} + +void UFlowNode_ExecuteComponent::InitializeInstance() +{ + Super::InitializeInstance(); + + (void) TryInjectComponent(); + + if (UActorComponent* ResolvedComp = TryResolveComponent()) + { + if (IFlowCoreExecutableInterface* ComponentAsCoreExecutable = Cast(ResolvedComp)) + { + ComponentAsCoreExecutable->InitializeInstance(); + } + else if (ResolvedComp->Implements()) + { + IFlowCoreExecutableInterface::Execute_K2_InitializeInstance(ResolvedComp); + } + } +} + +void UFlowNode_ExecuteComponent::DeinitializeInstance() +{ + if (UActorComponent* ResolvedComp = TryResolveComponent()) + { + if (IFlowCoreExecutableInterface* ComponentAsCoreExecutable = Cast(ResolvedComp)) + { + ComponentAsCoreExecutable->DeinitializeInstance(); + } + else if (ResolvedComp->Implements()) + { + IFlowCoreExecutableInterface::Execute_K2_DeinitializeInstance(ResolvedComp); + } + } + + if (EExecuteComponentSource_Classifiers::DoesComponentSourceUseInjectManager(ComponentSource)) + { + if (IsValid(InjectComponentsManager)) + { + InjectComponentsManager->ShutdownRuntime(); + } + } + + InjectComponentsManager = nullptr; + + Super::DeinitializeInstance(); +} + +EFlowPreloadResult UFlowNode_ExecuteComponent::PreloadContent() +{ + if (UActorComponent* ResolvedComp = TryResolveComponent()) + { + if (IFlowPreloadableInterface* PreloadableComponent = Cast(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 (IFlowPreloadableInterface* Preloadable = Cast(ResolvedComp)) + { + Preloadable->FlushContent(); + } + } +} + +void UFlowNode_ExecuteComponent::OnActivate() +{ + Super::OnActivate(); + + if (UActorComponent* ResolvedComp = TryResolveComponent()) + { + if (IFlowExternalExecutableInterface* ComponentAsExternalExecutable = Cast(ResolvedComp)) + { + // By convention, we must call the PreActivateExternalFlowExecutable() before OnActivate + // when we (this node) are acting as the proxy for an IFlowExternalExecutableInterface object + ComponentAsExternalExecutable->PreActivateExternalFlowExecutable(*this); + } + else if (ResolvedComp->Implements()) + { + IFlowExternalExecutableInterface::Execute_K2_PreActivateExternalFlowExecutable(ResolvedComp, this); + } + else + { + UE_LOG(LogFlow, Error, TEXT("Expected a valid UActorComponent that implemented the IFlowExternalExecutableInterface (%s)"), *ResolvedComp->GetClass()->GetName()); + } + + if (IFlowCoreExecutableInterface* ComponentAsCoreExecutable = Cast(ResolvedComp)) + { + ComponentAsCoreExecutable->OnActivate(); + } + else if (ResolvedComp->Implements()) + { + IFlowCoreExecutableInterface::Execute_K2_OnActivate(ResolvedComp); + } + else + { + UE_LOG(LogFlow, Error, TEXT("Expected a valid UActorComponent that implemented the IFlowCoreExecutableInterface (%s)"), *ResolvedComp->GetClass()->GetName()); + } + } +} + +void UFlowNode_ExecuteComponent::Cleanup() +{ + if (UActorComponent* ResolvedComp = TryResolveComponent()) + { + if (IFlowCoreExecutableInterface* ComponentAsCoreExecutable = Cast(ResolvedComp)) + { + ComponentAsCoreExecutable->Cleanup(); + } + else if (ResolvedComp->Implements()) + { + IFlowCoreExecutableInterface::Execute_K2_Cleanup(ResolvedComp); + } + } + + Super::Cleanup(); +} + +void UFlowNode_ExecuteComponent::ForceFinishNode() +{ + if (UActorComponent* ResolvedComp = TryResolveComponent()) + { + if (IFlowCoreExecutableInterface* ComponentAsCoreExecutable = Cast(ResolvedComp)) + { + ComponentAsCoreExecutable->ForceFinishNode(); + } + else if (ResolvedComp->Implements()) + { + IFlowCoreExecutableInterface::Execute_K2_ForceFinishNode(ResolvedComp); + } + } + + Super::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()) + { + if (IFlowCoreExecutableInterface* ComponentAsCoreExecutable = Cast(ResolvedComp)) + { + ComponentAsCoreExecutable->ExecuteInput(PinName); + } + else if (ResolvedComp->Implements()) + { + IFlowCoreExecutableInterface::Execute_K2_ExecuteInput(ResolvedComp, PinName); + } + } + else + { + LogError(FString::Printf(TEXT("Could not ExecuteInput %s, because the component was missing or could not be resolved."), *PinName.ToString())); + } +} + +#if WITH_EDITOR +TArray UFlowNode_ExecuteComponent::GetContextInputs() const +{ + TArray ContextInputs; + const UActorComponent* ResolvedComp = GetResolvedComponent(); + + if (!IsValid(ResolvedComp)) + { + // If we don't have a Resolved Component object yet, try to find the expected component object. For + // injected components this will return the CDO + ResolvedComp = TryGetExpectedComponent(); + } + + // NOTE: we have to call GetClass on the Resolved Component; not StaticClass(). This makes it so we can handle classes that only implement the + // interface in Blueprints. + if (ResolvedComp && ResolvedComp->GetClass()->ImplementsInterface(UFlowContextPinSupplierInterface::StaticClass())) + { + if (const IFlowContextPinSupplierInterface* CompAsStaticInterface = Cast(ResolvedComp)) + { + // The native (static) class implements the interface, so we call it directly (default implementation provided by the interface will call the K2 BP version of it). + // Ee assume that the implementor of the interface is responsible for invoking Execute_K2_GetContextInputs in their overrides of GetContextInputs() + ContextInputs = CompAsStaticInterface->GetContextInputs(); + } + else + { + // Only the BP class implements the interface, so we call it here. + ContextInputs = IFlowContextPinSupplierInterface::Execute_K2_GetContextInputs(ResolvedComp); + } + } + + if (ContextInputs.IsEmpty()) + { + // Add the default input if none are desired by the component + ContextInputs.Add(UFlowNode::DefaultInputPin); + } + + ContextInputs.Append(Super::GetContextInputs()); + + return ContextInputs; +} + +TArray UFlowNode_ExecuteComponent::GetContextOutputs() const +{ + TArray ContextOutputs; + const UActorComponent* ResolvedComp = GetResolvedComponent(); + + if (!IsValid(ResolvedComp)) + { + // If we don't have a Resolved Component object yet, try to find the expected component object. For + // injected components this will return the CDO + ResolvedComp = TryGetExpectedComponent(); + } + + // NOTE: we have to call GetClass on the Resolved Component; not StaticClass(). This makes it so we can handle classes that only implement the + // interface in Blueprints. + if (ResolvedComp && ResolvedComp->GetClass()->ImplementsInterface(UFlowContextPinSupplierInterface::StaticClass())) + { + if (const IFlowContextPinSupplierInterface* CompAsStaticInterface = Cast(ResolvedComp)) + { + // The native (static) class implements the interface, so we call it directly (default implementation provided by the interface will call the K2 BP version of it. + // we assume that the implementor of the interface is responsible for invoking K2_GetContextOutputs in their overrides of GetContextOutputs() + ContextOutputs = CompAsStaticInterface->GetContextOutputs(); + } + else + { + // Only the BP class implements the interface, so we call it here. + ContextOutputs = IFlowContextPinSupplierInterface::Execute_K2_GetContextOutputs(ResolvedComp); + } + } + + if (ContextOutputs.IsEmpty()) + { + // Add the default output if none are desired by the component + ContextOutputs.Add(UFlowNode::DefaultOutputPin); + } + + ContextOutputs.Append(Super::GetContextOutputs()); + + return ContextOutputs; +} + +#endif // WITH_EDITOR + +void UFlowNode_ExecuteComponent::GatherDataPinValueOwnerCollection(FFlowDataPinValueOwnerCollection& ValueOwnerCollection) const +{ + Super::GatherDataPinValueOwnerCollection(ValueOwnerCollection); + + // Can also source properties from the resolved component (runtime) or expected component (in-editor) + + // TODO (gtaylor) Eliminate this const_cast (ie, need rework GetResolvedOrExpectedComponent to have a mutable version) + UActorComponent* ResolvedComp = const_cast(GetResolvedOrExpectedComponent()); + IFlowDataPinValueOwnerInterface* ValueOwnerInterface = Cast(ResolvedComp); + if (IsValid(ResolvedComp) && ValueOwnerInterface) + { + ValueOwnerCollection.AddValueOwner(*ValueOwnerInterface); + } +} + +bool UFlowNode_ExecuteComponent::TryInjectComponent() +{ + if (!EExecuteComponentSource_Classifiers::DoesComponentSourceUseInjectManager(ComponentSource)) + { + return false; + } + + AActor* ActorOwner = TryGetRootFlowActorOwner(); + if (!IsValid(ActorOwner)) + { + return false; + } + + // Create the component instance + TArray ComponentInstances; + + FLOW_ASSERT_ENUM_MAX(EExecuteComponentSource, 4); + + switch (ComponentSource) + { + case EExecuteComponentSource::InjectFromTemplate: + { + if (IsValid(ComponentTemplate)) + { + if (UActorComponent* ComponentInstance = FFlowInjectComponentsHelper::TryCreateComponentInstanceForActorFromTemplate(*ActorOwner, *ComponentTemplate)) + { + ComponentInstances.Add(ComponentInstance); + } + } + } + break; + + case EExecuteComponentSource::InjectFromClass: + { + if (IsValid(ComponentClass)) + { + if (bReuseExistingComponent) + { + // Look for the component class existing already on the actor, for potential re-use + + UActorComponent* ExistingComponent = ActorOwner->FindComponentByClass(ComponentClass); + if (IsValid(ExistingComponent)) + { + // Set the ComponentRef directly (for later lookup via TryResolveComponent) + ComponentRef.SetResolvedComponentDirect(*ExistingComponent); + + return true; + } + + if (!bAllowInjectComponent) + { + return false; + } + } + + const FName InstanceBaseName = ComponentClass->GetFName(); + if (UActorComponent* ComponentInstance = FFlowInjectComponentsHelper::TryCreateComponentInstanceForActorFromClass(*ActorOwner, *ComponentClass, InstanceBaseName)) + { + ComponentInstances.Add(ComponentInstance); + } + } + } + break; + + default: + checkNoEntry(); + return false; + } + + // Create the manager object if we're injecting a component + InjectComponentsManager = NewObject(this); + InjectComponentsManager->InitializeRuntime(); + + // Inject the desired component + if (!ComponentInstances.IsEmpty()) + { + check(ComponentInstances.Num() == 1); + + InjectComponentsManager->InjectComponentsOnActor(*ActorOwner, ComponentInstances); + + // Set the ComponentRef directly (for later lookup via TryResolveComponent) + ComponentRef.SetResolvedComponentDirect(*ComponentInstances[0]); + } + + return true; +} + +const UActorComponent* UFlowNode_ExecuteComponent::GetResolvedOrExpectedComponent() const +{ + const UActorComponent* ResolvedComp = ComponentRef.GetResolvedComponent(); + if (IsValid(ResolvedComp)) + { + return ResolvedComp; + } + +#if WITH_EDITOR + const UActorComponent* ExpectedComp = TryGetExpectedComponent(); + if (IsValid(ExpectedComp)) + { + return ExpectedComp; + } +#endif + + return nullptr; +} + +UActorComponent* UFlowNode_ExecuteComponent::TryResolveComponent() +{ + UActorComponent* ResolvedComp = ComponentRef.GetResolvedComponent(); + if (IsValid(ResolvedComp)) + { + return ResolvedComp; + } + + AActor* ActorOwner = TryGetRootFlowActorOwner(); + + if (!IsValid(ActorOwner)) + { + UE_LOG(LogFlow, Error, TEXT("Expected a valid Actor owner to resolve component reference %s"), *ComponentRef.ComponentName.ToString()); + + return nullptr; + } + + // Injected components are totally optional, if they are not there, we have to assume that's intentional. + constexpr bool bAllowWarnIfFailed = true; + ResolvedComp = ComponentRef.TryResolveComponent(*ActorOwner, bAllowWarnIfFailed); + + return ResolvedComp; +} + +UActorComponent* UFlowNode_ExecuteComponent::GetResolvedComponent() const +{ + // This version of the function assumes the component has already been resolved previously. + // (using TryResolveComponent) + UActorComponent* ResolvedComp = ComponentRef.GetResolvedComponent(); + if (IsValid(ResolvedComp)) + { + return ResolvedComp; + } + + return nullptr; +} + +#if WITH_EDITOR +const UActorComponent* UFlowNode_ExecuteComponent::TryGetExpectedComponent() const +{ + const TSubclassOf ExpectedOwnerClass = TryGetExpectedActorOwnerClass(); + + FLOW_ASSERT_ENUM_MAX(EExecuteComponentSource, 4); + + switch (ComponentSource) + { + case EExecuteComponentSource::Undetermined: + { + return nullptr; + } + case EExecuteComponentSource::BindToExisting: + { + return AActor::GetActorClassDefaultComponentByName(ExpectedOwnerClass, ComponentRef.ComponentName); + } + case EExecuteComponentSource::InjectFromTemplate: + { + return ComponentTemplate; + } + case EExecuteComponentSource::InjectFromClass: + { + return IsValid(ComponentClass) ? ComponentClass->GetDefaultObject() : nullptr; + } + + default: + return nullptr; + } +} + +void UFlowNode_ExecuteComponent::PostLoad() +{ + Super::PostLoad(); + + RefreshComponentSource(); +} + +void UFlowNode_ExecuteComponent::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + const FName PropertyName = PropertyChangedEvent.Property->GetFName(); + if (PropertyName == GET_MEMBER_NAME_CHECKED(FFlowActorOwnerComponentRef, ComponentName) || + PropertyName == GET_MEMBER_NAME_CHECKED(UFlowNode_ExecuteComponent, ComponentTemplate) || + PropertyName == GET_MEMBER_NAME_CHECKED(UFlowNode_ExecuteComponent, ComponentClass)) + { + RefreshComponentSource(); + + RefreshPins(); + } +} + +void UFlowNode_ExecuteComponent::RefreshComponentSource() +{ + if (ComponentRef.IsConfigured()) + { + ComponentSource = EExecuteComponentSource::BindToExisting; + } + else if (ComponentTemplate != nullptr) + { + ComponentSource = EExecuteComponentSource::InjectFromTemplate; + } + else if (ComponentClass != nullptr) + { + ComponentSource = EExecuteComponentSource::InjectFromClass; + } + else + { + ComponentSource = EExecuteComponentSource::Undetermined; + } +} + +void UFlowNode_ExecuteComponent::RefreshPins() +{ + OnReconstructionRequested.ExecuteIfBound(); +} + +EDataValidationResult UFlowNode_ExecuteComponent::ValidateNode() +{ + const EDataValidationResult SuperResult = Super::ValidateNode(); + + EDataValidationResult FinalResult = CombineDataValidationResults(SuperResult, EDataValidationResult::Valid); + + if (IsValid(ComponentTemplate) || IsValid(ComponentClass)) + { + return FinalResult; + } + + const bool bHasComponent = ComponentRef.IsConfigured(); + if (!bHasComponent) + { + ValidationLog.Error(TEXT("ExecuteComponent requires a valid Compoennt reference"), this); + + return EDataValidationResult::Invalid; + } + + const TSubclassOf ExpectedActorOwnerClass = TryGetExpectedActorOwnerClass(); + if (!IsValid(ExpectedActorOwnerClass)) + { + ValidationLog.Error(TEXT("Invalid or null Expected Actor Owner Class for this Flow Asset"), this); + + return EDataValidationResult::Invalid; + } + + { + // Check if the component can be found on the expected owner + const UActorComponent* ExpectedComponent = TryGetExpectedComponent(); + if (!IsValid(ExpectedComponent)) + { + ValidationLog.Error(TEXT("Could not resolve component for flow actor owner"), this); + + return EDataValidationResult::Invalid; + } + + // Check that the component implements the expected interfaces + if (!Cast(ExpectedComponent)) + { + ValidationLog.Error(TEXT("Expected component to implement IFlowExternalExecutableInterface"), this); + + return EDataValidationResult::Invalid; + } + + if (!Cast(ExpectedComponent)) + { + ValidationLog.Error(TEXT("Expected component to implement IFlowCoreExecutableInterface"), this); + + return EDataValidationResult::Invalid; + } + } + + return FinalResult; +} + +FString UFlowNode_ExecuteComponent::GetStatusString() const +{ + if (ActivationState != EFlowNodeState::NeverActivated) + { + return UEnum::GetDisplayValueAsText(ActivationState).ToString(); + } + + return Super::GetStatusString(); +} + +TSubclassOf UFlowNode_ExecuteComponent::TryGetExpectedActorOwnerClass() const +{ + const UFlowAsset* FlowAsset = GetFlowAsset(); + if (IsValid(FlowAsset)) + { + return FlowAsset->GetExpectedOwnerClass(); + } + + return nullptr; +} + +FText UFlowNode_ExecuteComponent::K2_GetNodeTitle_Implementation() const +{ + if (GetDefault()->bUseAdaptiveNodeTitles) + { + FLOW_ASSERT_ENUM_MAX(EExecuteComponentSource, 4); + + switch (ComponentSource) + { + case EExecuteComponentSource::Undetermined: + break; + + case EExecuteComponentSource::BindToExisting: + { + if (!ComponentRef.ComponentName.IsNone()) + { + const FText ComponentNameText = FText::FromName(ComponentRef.ComponentName); + + return FText::Format(LOCTEXT("ExecuteComponent", "Execute {0}"), {ComponentNameText}); + } + } + break; + + case EExecuteComponentSource::InjectFromTemplate: + { + if (IsValid(ComponentTemplate)) + { + FString ComponentNameString = ComponentTemplate->GetName(); + ComponentNameString.RemoveFromEnd(TEXT("_C")); + const FText ComponentNameText = FText::FromString(ComponentNameString); + + return FText::Format(LOCTEXT("ExecuteComponent", "Execute {0}"), {ComponentNameText}); + } + } + break; + + case EExecuteComponentSource::InjectFromClass: + { + if (IsValid(ComponentClass)) + { + FString ComponentClassString = ComponentClass->GetName(); + ComponentClassString.RemoveFromEnd(TEXT("_C")); + const FText ComponentNameText = FText::FromString(ComponentClassString); + + return FText::Format(LOCTEXT("ExecuteComponent", "Execute {0}"), {ComponentNameText}); + } + } + break; + + default: break; + } + } + + return Super::K2_GetNodeTitle_Implementation(); +} + +#endif // WITH_EDITOR + +void UFlowNode_ExecuteComponent::UpdateNodeConfigText_Implementation() +{ +#if WITH_EDITOR + FText ComponentNameText; + + const bool bUseAdaptiveNodeTitles = GetDefault()->bUseAdaptiveNodeTitles; + if (!bUseAdaptiveNodeTitles) + { + FLOW_ASSERT_ENUM_MAX(EExecuteComponentSource, 4); + + switch (ComponentSource) + { + case EExecuteComponentSource::Undetermined: + break; + + case EExecuteComponentSource::BindToExisting: + { + if (!ComponentRef.ComponentName.IsNone()) + { + ComponentNameText = FText::FromName(ComponentRef.ComponentName); + } + } + break; + + case EExecuteComponentSource::InjectFromTemplate: + { + if (IsValid(ComponentTemplate)) + { + FString ComponentNameString = ComponentTemplate->GetName(); + ComponentNameString.RemoveFromEnd(TEXT("_C")); + + ComponentNameText = FText::FromString(ComponentNameString); + } + } + break; + + case EExecuteComponentSource::InjectFromClass: + { + if (IsValid(ComponentClass)) + { + FString ComponentClassString = ComponentClass->GetName(); + ComponentClassString.RemoveFromEnd(TEXT("_C")); + + ComponentNameText = FText::FromString(ComponentClassString); + } + } + break; + + default: break; + } + } + + SetNodeConfigText(ComponentNameText); +#endif // WITH_EDITOR +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/Flow/Private/Nodes/World/FlowNode_NotifyActor.cpp b/Source/Flow/Private/Nodes/Actor/FlowNode_NotifyActor.cpp similarity index 55% rename from Source/Flow/Private/Nodes/World/FlowNode_NotifyActor.cpp rename to Source/Flow/Private/Nodes/Actor/FlowNode_NotifyActor.cpp index f9f9c4819..6178b4f6e 100644 --- a/Source/Flow/Private/Nodes/World/FlowNode_NotifyActor.cpp +++ b/Source/Flow/Private/Nodes/Actor/FlowNode_NotifyActor.cpp @@ -1,41 +1,29 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors -#include "Nodes/World/FlowNode_NotifyActor.h" +#include "Nodes/Actor/FlowNode_NotifyActor.h" #include "FlowComponent.h" #include "FlowSubsystem.h" +#include "Engine/GameInstance.h" #include "Engine/World.h" -UFlowNode_NotifyActor::UFlowNode_NotifyActor(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_NotifyActor) + +UFlowNode_NotifyActor::UFlowNode_NotifyActor() + : MatchType(EGameplayContainerMatchType::All) + , bExactMatch(true) , NetMode(EFlowNetMode::Authority) { #if WITH_EDITOR - Category = TEXT("Notifies"); + Category = TEXT("Actor"); #endif } -void UFlowNode_NotifyActor::PostLoad() -{ - Super::PostLoad(); - - if (IdentityTag_DEPRECATED.IsValid()) - { - IdentityTags = FGameplayTagContainer(IdentityTag_DEPRECATED); - } - - if (NotifyTag_DEPRECATED.IsValid()) - { - NotifyTags = FGameplayTagContainer(NotifyTag_DEPRECATED); - NotifyTag_DEPRECATED = FGameplayTag(); - } -} - void UFlowNode_NotifyActor::ExecuteInput(const FName& PinName) { if (const UFlowSubsystem* FlowSubsystem = GetWorld()->GetGameInstance()->GetSubsystem()) { - for (const TWeakObjectPtr& Component : FlowSubsystem->GetComponents(IdentityTags, EGameplayContainerMatchType::Any)) + for (const TWeakObjectPtr& Component : FlowSubsystem->GetComponents(IdentityTags, MatchType, bExactMatch)) { Component->NotifyFromGraph(NotifyTags, NetMode); } @@ -49,4 +37,15 @@ FString UFlowNode_NotifyActor::GetNodeDescription() const { return GetIdentityTagsDescription(IdentityTags) + LINE_TERMINATOR + GetNotifyTagsDescription(NotifyTags); } + +EDataValidationResult UFlowNode_NotifyActor::ValidateNode() +{ + if (IdentityTags.IsEmpty()) + { + ValidationLog.Error(*UFlowNode::MissingIdentityTag, this); + return EDataValidationResult::Invalid; + } + + return EDataValidationResult::Valid; +} #endif diff --git a/Source/Flow/Private/Nodes/World/FlowNode_OnActorRegistered.cpp b/Source/Flow/Private/Nodes/Actor/FlowNode_OnActorRegistered.cpp similarity index 52% rename from Source/Flow/Private/Nodes/World/FlowNode_OnActorRegistered.cpp rename to Source/Flow/Private/Nodes/Actor/FlowNode_OnActorRegistered.cpp index 6ad8ca537..12c1b4931 100644 --- a/Source/Flow/Private/Nodes/World/FlowNode_OnActorRegistered.cpp +++ b/Source/Flow/Private/Nodes/Actor/FlowNode_OnActorRegistered.cpp @@ -1,15 +1,11 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors -#include "Nodes/World/FlowNode_OnActorRegistered.h" +#include "Nodes/Actor/FlowNode_OnActorRegistered.h" -UFlowNode_OnActorRegistered::UFlowNode_OnActorRegistered(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) -{ -} +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_OnActorRegistered) -void UFlowNode_OnActorRegistered::ExecuteInput(const FName& PinName) +UFlowNode_OnActorRegistered::UFlowNode_OnActorRegistered() { - Super::ExecuteInput(PinName); } void UFlowNode_OnActorRegistered::ObserveActor(TWeakObjectPtr Actor, TWeakObjectPtr Component) diff --git a/Source/Flow/Private/Nodes/World/FlowNode_OnActorUnregistered.cpp b/Source/Flow/Private/Nodes/Actor/FlowNode_OnActorUnregistered.cpp similarity index 62% rename from Source/Flow/Private/Nodes/World/FlowNode_OnActorUnregistered.cpp rename to Source/Flow/Private/Nodes/Actor/FlowNode_OnActorUnregistered.cpp index a802ff41e..10d387659 100644 --- a/Source/Flow/Private/Nodes/World/FlowNode_OnActorUnregistered.cpp +++ b/Source/Flow/Private/Nodes/Actor/FlowNode_OnActorUnregistered.cpp @@ -1,15 +1,11 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors -#include "Nodes/World/FlowNode_OnActorUnregistered.h" +#include "Nodes/Actor/FlowNode_OnActorUnregistered.h" -UFlowNode_OnActorUnregistered::UFlowNode_OnActorUnregistered(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) -{ -} +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_OnActorUnregistered) -void UFlowNode_OnActorUnregistered::ExecuteInput(const FName& PinName) +UFlowNode_OnActorUnregistered::UFlowNode_OnActorUnregistered() { - Super::ExecuteInput(PinName); } void UFlowNode_OnActorUnregistered::ObserveActor(TWeakObjectPtr Actor, TWeakObjectPtr Component) diff --git a/Source/Flow/Private/Nodes/World/FlowNode_OnNotifyFromActor.cpp b/Source/Flow/Private/Nodes/Actor/FlowNode_OnNotifyFromActor.cpp similarity index 55% rename from Source/Flow/Private/Nodes/World/FlowNode_OnNotifyFromActor.cpp rename to Source/Flow/Private/Nodes/Actor/FlowNode_OnNotifyFromActor.cpp index 4e64bbec1..575b73260 100644 --- a/Source/Flow/Private/Nodes/World/FlowNode_OnNotifyFromActor.cpp +++ b/Source/Flow/Private/Nodes/Actor/FlowNode_OnNotifyFromActor.cpp @@ -1,33 +1,18 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors -#include "Nodes/World/FlowNode_OnNotifyFromActor.h" +#include "Nodes/Actor/FlowNode_OnNotifyFromActor.h" #include "FlowComponent.h" -UFlowNode_OnNotifyFromActor::UFlowNode_OnNotifyFromActor(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) - , bRetroactive(false) +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_OnNotifyFromActor) + +UFlowNode_OnNotifyFromActor::UFlowNode_OnNotifyFromActor() + : bRetroactive(false) { #if WITH_EDITOR - Category = TEXT("Notifies"); - NodeStyle = EFlowNodeStyle::Condition; + NodeDisplayStyle = FlowNodeStyle::Condition; #endif } -void UFlowNode_OnNotifyFromActor::PostLoad() -{ - Super::PostLoad(); - - if (NotifyTag_DEPRECATED.IsValid()) - { - NotifyTags = FGameplayTagContainer(NotifyTag_DEPRECATED); - } -} - -void UFlowNode_OnNotifyFromActor::ExecuteInput(const FName& PinName) -{ - Super::ExecuteInput(PinName); -} - void UFlowNode_OnNotifyFromActor::ObserveActor(TWeakObjectPtr Actor, TWeakObjectPtr Component) { if (!RegisteredActors.Contains(Actor)) @@ -49,7 +34,25 @@ void UFlowNode_OnNotifyFromActor::ForgetActor(TWeakObjectPtr Actor, TWea void UFlowNode_OnNotifyFromActor::OnNotifyFromComponent(UFlowComponent* Component, const FGameplayTag& Tag) { - if (Component->IdentityTags.HasAnyExact(IdentityTags) && (!NotifyTags.IsValid() || NotifyTags.HasTagExact(Tag))) + bool IdentityMatches = false; + + switch (IdentityMatchType) + { + case EFlowTagContainerMatchType::HasAny: + IdentityMatches = Component->IdentityTags.HasAny(IdentityTags); + break; + case EFlowTagContainerMatchType::HasAnyExact: + IdentityMatches = Component->IdentityTags.HasAnyExact(IdentityTags); + break; + case EFlowTagContainerMatchType::HasAll: + IdentityMatches = Component->IdentityTags.HasAll(IdentityTags); + break; + case EFlowTagContainerMatchType::HasAllExact: + IdentityMatches = Component->IdentityTags.HasAllExact(IdentityTags); + break; + } + + if (IdentityMatches && (!NotifyTags.IsValid() || NotifyTags.HasTagExact(Tag))) { OnEventReceived(); } diff --git a/Source/Flow/Private/Nodes/World/FlowNode_PlayLevelSequence.cpp b/Source/Flow/Private/Nodes/Actor/FlowNode_PlayLevelSequence.cpp similarity index 69% rename from Source/Flow/Private/Nodes/World/FlowNode_PlayLevelSequence.cpp rename to Source/Flow/Private/Nodes/Actor/FlowNode_PlayLevelSequence.cpp index 9a7063abe..3cd94789e 100644 --- a/Source/Flow/Private/Nodes/World/FlowNode_PlayLevelSequence.cpp +++ b/Source/Flow/Private/Nodes/Actor/FlowNode_PlayLevelSequence.cpp @@ -1,26 +1,31 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors -#include "Nodes/World/FlowNode_PlayLevelSequence.h" +#include "Nodes/Actor/FlowNode_PlayLevelSequence.h" #include "FlowAsset.h" -#include "FlowModule.h" +#include "FlowLogChannels.h" #include "FlowSubsystem.h" #include "LevelSequence/FlowLevelSequencePlayer.h" + +#if WITH_EDITOR #include "MovieScene/MovieSceneFlowTrack.h" #include "MovieScene/MovieSceneFlowTriggerSection.h" +#endif #include "LevelSequence.h" #include "LevelSequenceActor.h" #include "VisualLogger/VisualLogger.h" +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_PlayLevelSequence) + FFlowNodeLevelSequenceEvent UFlowNode_PlayLevelSequence::OnPlaybackStarted; FFlowNodeLevelSequenceEvent UFlowNode_PlayLevelSequence::OnPlaybackCompleted; -UFlowNode_PlayLevelSequence::UFlowNode_PlayLevelSequence(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) - , bPlayReverse(false) - , bReplicates(false) +UFlowNode_PlayLevelSequence::UFlowNode_PlayLevelSequence() + : bPlayReverse(false) , bUseGraphOwnerAsTransformOrigin(false) + , bReplicates(false) + , bAlwaysRelevant(false) , bApplyOwnerTimeDilation(true) , LoadedSequence(nullptr) , SequencePlayer(nullptr) @@ -30,12 +35,14 @@ UFlowNode_PlayLevelSequence::UFlowNode_PlayLevelSequence(const FObjectInitialize , TimeDilation(1.0f) { #if WITH_EDITOR - Category = TEXT("World"); - NodeStyle = EFlowNodeStyle::Latent; + Category = TEXT("Actor"); + NodeDisplayStyle = FlowNodeStyle::Latent; #endif InputPins.Empty(); InputPins.Add(FFlowPin(TEXT("Start"))); + InputPins.Add(FFlowPin(TEXT("Pause"))); + InputPins.Add(FFlowPin(TEXT("Resume"))); InputPins.Add(FFlowPin(TEXT("Stop"))); OutputPins.Add(FFlowPin(TEXT("PreStart"))); @@ -45,19 +52,19 @@ UFlowNode_PlayLevelSequence::UFlowNode_PlayLevelSequence(const FObjectInitialize } #if WITH_EDITOR -TArray UFlowNode_PlayLevelSequence::GetContextOutputs() +TArray UFlowNode_PlayLevelSequence::GetContextOutputs() const { + TArray Pins = Super::GetContextOutputs(); + if (Sequence.IsNull()) { - return TArray(); + return Pins; } - TArray PinNames = {}; - - Sequence = Sequence.LoadSynchronous(); + Sequence.LoadSynchronous(); if (Sequence && Sequence->GetMovieScene()) { - for (const UMovieSceneTrack* Track : Sequence->GetMovieScene()->GetMasterTracks()) + for (const UMovieSceneTrack* Track : Sequence->GetMovieScene()->GetTracks()) { if (Track->GetClass() == UMovieSceneFlowTrack::StaticClass()) { @@ -67,9 +74,10 @@ TArray UFlowNode_PlayLevelSequence::GetContextOutputs() { for (const FString& EventName : FlowSection->GetAllEntryPoints()) { - if (!EventName.IsEmpty()) + FFlowPin NewEventPin(EventName); + if (!EventName.IsEmpty() && !Pins.Contains(NewEventPin)) { - PinNames.Emplace(EventName); + Pins.Emplace(NewEventPin); } } } @@ -78,7 +86,7 @@ TArray UFlowNode_PlayLevelSequence::GetContextOutputs() } } - return PinNames; + return Pins; } void UFlowNode_PlayLevelSequence::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) @@ -92,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()); @@ -126,26 +154,12 @@ void UFlowNode_PlayLevelSequence::InitializeInstance() void UFlowNode_PlayLevelSequence::CreatePlayer() { - LoadedSequence = LoadAsset(Sequence); + LoadedSequence = Sequence.LoadSynchronous(); if (LoadedSequence) { ALevelSequenceActor* SequenceActor; - AActor* OwningActor = nullptr; - if (GetFlowAsset()) - { - if (UObject* RootFlowOwner = GetFlowAsset()->GetOwner()) - { - OwningActor = Cast(RootFlowOwner); // in case Root Flow was created directly from some actor - if (OwningActor == nullptr) - { - if (const UActorComponent* OwningComponent = Cast(RootFlowOwner)) - { - OwningActor = OwningComponent->GetOwner(); - } - } - } - } + AActor* OwningActor = TryGetRootFlowActorOwner(); // Apply AActor::CustomTimeDilation from owner of the Root Flow if (IsValid(OwningActor)) @@ -157,7 +171,7 @@ void UFlowNode_PlayLevelSequence::CreatePlayer() AActor* TransformOriginActor = bUseGraphOwnerAsTransformOrigin ? OwningActor : nullptr; // Finally create the player - SequencePlayer = UFlowLevelSequencePlayer::CreateFlowLevelSequencePlayer(this, LoadedSequence, PlaybackSettings, CameraSettings, TransformOriginActor, bReplicates, SequenceActor); + SequencePlayer = UFlowLevelSequencePlayer::CreateFlowLevelSequencePlayer(this, LoadedSequence, PlaybackSettings, CameraSettings, TransformOriginActor, bReplicates, bAlwaysRelevant, SequenceActor); if (SequencePlayer) { @@ -172,9 +186,16 @@ 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 = LoadAsset(Sequence); + LoadedSequence = Sequence.LoadSynchronous(); if (GetFlowSubsystem()->GetWorld() && LoadedSequence) { @@ -205,6 +226,14 @@ void UFlowNode_PlayLevelSequence::ExecuteInput(const FName& PinName) { StopPlayback(); } + else if (PinName == TEXT("Pause")) + { + SequencePlayer->Pause(); + } + else if (PinName == TEXT("Resume") && SequencePlayer->IsPaused()) + { + SequencePlayer->Play(); + } } void UFlowNode_PlayLevelSequence::OnSave_Implementation() @@ -219,8 +248,7 @@ void UFlowNode_PlayLevelSequence::OnLoad_Implementation() { if (ElapsedTime != 0.0f) { - LoadedSequence = LoadAsset(Sequence); - + LoadedSequence = Sequence.LoadSynchronous(); if (GetFlowSubsystem()->GetWorld() && LoadedSequence) { CreatePlayer(); @@ -284,7 +312,10 @@ void UFlowNode_PlayLevelSequence::Cleanup() { SequencePlayer->SetFlowEventReceiver(nullptr); SequencePlayer->OnFinished.RemoveAll(this); - SequencePlayer->Stop(); + if (!PlaybackSettings.bPauseAtEnd) + { + SequencePlayer->Stop(); + } SequencePlayer = nullptr; } @@ -296,13 +327,15 @@ void UFlowNode_PlayLevelSequence::Cleanup() #if ENABLE_VISUAL_LOG UE_VLOG(this, LogFlow, Log, TEXT("Finished playback: %s"), *Sequence.ToString()); #endif + + Super::Cleanup(); } FString UFlowNode_PlayLevelSequence::GetPlaybackProgress() const { if (SequencePlayer && SequencePlayer->IsPlaying()) { - return GetProgressAsString(SequencePlayer->GetCurrentTime().AsSeconds() - StartTime).Append(TEXT(" / ")).Append(GetProgressAsString(SequencePlayer->GetDuration().AsSeconds())); + return FString::Printf(TEXT("%.*f / %.*f"), 2, SequencePlayer->GetCurrentTime().AsSeconds() - StartTime, 2, SequencePlayer->GetDuration().AsSeconds()); } return FString(); @@ -314,6 +347,17 @@ FString UFlowNode_PlayLevelSequence::GetNodeDescription() const return Sequence.IsNull() ? TEXT("[No sequence]") : Sequence.GetAssetName(); } +EDataValidationResult UFlowNode_PlayLevelSequence::ValidateNode() +{ + if (Sequence.IsNull()) + { + ValidationLog.Error(TEXT("Level Sequence asset not assigned or invalid!"), this); + return EDataValidationResult::Invalid; + } + + return EDataValidationResult::Valid; +} + FString UFlowNode_PlayLevelSequence::GetStatusString() const { return GetPlaybackProgress(); @@ -321,7 +365,7 @@ FString UFlowNode_PlayLevelSequence::GetStatusString() const UObject* UFlowNode_PlayLevelSequence::GetAssetToEdit() { - return Sequence.IsNull() ? nullptr : LoadAsset(Sequence); + return Sequence.IsNull() ? nullptr : Sequence.LoadSynchronous(); } #endif diff --git a/Source/Flow/Private/Nodes/Developer/FlowNode_Log.cpp b/Source/Flow/Private/Nodes/Developer/FlowNode_Log.cpp new file mode 100644 index 000000000..54dd736a8 --- /dev/null +++ b/Source/Flow/Private/Nodes/Developer/FlowNode_Log.cpp @@ -0,0 +1,116 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Nodes/Developer/FlowNode_Log.h" +#include "FlowLogChannels.h" + +#include "Engine/Engine.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_Log) + +#define LOCTEXT_NAMESPACE "FlowNode_Log" + +UFlowNode_Log::UFlowNode_Log() + : Message() + , Verbosity(EFlowLogVerbosity::Warning) + , bPrintToScreen(true) + , Duration(5.0f) + , TextColor(FColor::Yellow) +{ +#if WITH_EDITOR + Category = TEXT("Developer"); + NodeDisplayStyle = FlowNodeStyle::Developer; +#endif + + InputPins = { UFlowNode::DefaultInputPin }; + OutputPins = { UFlowNode::DefaultOutputPin }; +} + +void UFlowNode_Log::ExecuteInput(const FName& PinName) +{ + // Get the Message from either the default (Message property) or the data pin (if connected) + FString ResolvedMessage; + const EFlowDataPinResolveResult MessageResult = TryResolveDataPinValue(GET_MEMBER_NAME_CHECKED(ThisClass, Message), ResolvedMessage); + + // #FlowDataPinLegacy - retire this backward compatibility when we remove legacy data pin support? + FLOW_ASSERT_ENUM_MAX(EFlowDataPinResolveResult, 9); + if (MessageResult == EFlowDataPinResolveResult::FailedUnknownPin) + { + // Handle lookup of a FlowNode_Log that predated DataPins + ResolvedMessage = Message; + } + // -- + + // Format Message with named properties + FText FormattedMessage = FText::FromString(ResolvedMessage); + (void) TryFormatTextWithNamedPropertiesAsParameters(FormattedMessage, FormattedMessage); + + // Display the message + + switch (Verbosity) + { + case EFlowLogVerbosity::Error: + UE_LOG(LogFlow, Error, TEXT("%s"), *FormattedMessage.ToString()); + break; + case EFlowLogVerbosity::Warning: + UE_LOG(LogFlow, Warning, TEXT("%s"), *FormattedMessage.ToString()); + break; + case EFlowLogVerbosity::Display: + UE_LOG(LogFlow, Display, TEXT("%s"), *FormattedMessage.ToString()); + break; + case EFlowLogVerbosity::Log: + UE_LOG(LogFlow, Log, TEXT("%s"), *FormattedMessage.ToString()); + break; + case EFlowLogVerbosity::Verbose: + UE_LOG(LogFlow, Verbose, TEXT("%s"), *FormattedMessage.ToString()); + break; + case EFlowLogVerbosity::VeryVerbose: + UE_LOG(LogFlow, VeryVerbose, TEXT("%s"), *FormattedMessage.ToString()); + break; + default: ; + } + + if (bPrintToScreen) + { + GEngine->AddOnScreenDebugMessage(-1, Duration, TextColor, FormattedMessage.ToString()); + } + + TriggerFirstOutput(true); +} + +#if WITH_EDITOR +void UFlowNode_Log::PostEditChangeChainProperty(FPropertyChangedChainEvent& PropertyChainEvent) +{ + const auto& Property = PropertyChainEvent.PropertyChain.GetActiveMemberNode()->GetValue(); + constexpr bool bIsInput = true; + OnPostEditEnsureAllNamedPropertiesPinDirection(*Property, bIsInput); + + Super::PostEditChangeChainProperty(PropertyChainEvent); +} + +void UFlowNode_Log::OnEditorPinConnectionsChanged(const TArray& Changes) +{ + Super::OnEditorPinConnectionsChanged(Changes); + + UpdateNodeConfigText(); +} + +void UFlowNode_Log::UpdateNodeConfigText_Implementation() +{ + constexpr bool bErrorIfInputPinNotFound = true; + + FConnectedPin ConnectedPin; + const bool bIsInputConnected = FindFirstInputPinConnection(GET_MEMBER_NAME_CHECKED(ThisClass, Message), bErrorIfInputPinNotFound, ConnectedPin); + + if (bIsInputConnected) + { + SetNodeConfigText(FText::Format(LOCTEXT("LogFromPin", "Message from: {0}"), { FText::FromString(ConnectedPin.PinName.ToString()) })); + } + else + { + SetNodeConfigText(FText::FromString(Message)); + } +} + +#endif + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/Flow/Private/Nodes/FlowNode.cpp b/Source/Flow/Private/Nodes/FlowNode.cpp index 438b2a9d5..019977027 100644 --- a/Source/Flow/Private/Nodes/FlowNode.cpp +++ b/Source/Flow/Private/Nodes/FlowNode.cpp @@ -1,17 +1,27 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #include "Nodes/FlowNode.h" +#include "AddOns/FlowNodeAddOn.h" #include "FlowAsset.h" -#include "FlowModule.h" -#include "FlowSubsystem.h" -#include "FlowTypes.h" +#include "FlowSettings.h" +#include "Interfaces/FlowPreloadableInterface.h" +#include "Interfaces/FlowNodeWithExternalDataPinSupplierInterface.h" +#include "Policies/FlowPreloadHelper.h" +#include "Policies/FlowPreloadPolicy.h" +#include "Types/FlowAutoDataPinsWorkingData.h" +#include "Types/FlowDataPinValue.h" +#include "Types/FlowPinConnectionChange.h" +#include "Types/FlowPinType.h" + +#include "Components/ActorComponent.h" +#if WITH_EDITOR +#include "Editor.h" +#endif -#include "Engine/Engine.h" -#include "Engine/ViewportStatsSubsystem.h" -#include "Engine/World.h" +#include "Engine/BlueprintGeneratedClass.h" +#include "GameFramework/Actor.h" #include "Misc/App.h" -#include "Misc/Paths.h" #include "Serialization/MemoryReader.h" #include "Serialization/MemoryWriter.h" @@ -23,20 +33,14 @@ FString UFlowNode::MissingNotifyTag = TEXT("Missing Notify Tag"); FString UFlowNode::MissingClass = TEXT("Missing class"); FString UFlowNode::NoActorsFound = TEXT("No actors found"); -UFlowNode::UFlowNode(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) -#if WITH_EDITOR - , GraphNode(nullptr) - , bCanDelete(true) - , bCanDuplicate(true) - , bNodeDeprecated(false) -#endif - , bPreloaded(false) +UFlowNode::UFlowNode() + : AllowedSignalModes({EFlowSignalMode::Enabled, EFlowSignalMode::Disabled, EFlowSignalMode::PassThrough}) + , SignalMode(EFlowSignalMode::Enabled) , ActivationState(EFlowNodeState::NeverActivated) { #if WITH_EDITOR Category = TEXT("Uncategorized"); - NodeStyle = EFlowNodeStyle::Default; + NodeDisplayStyle = FlowNodeStyle::Default; #endif InputPins = {DefaultInputPin}; @@ -48,120 +52,194 @@ void UFlowNode::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEve { Super::PostEditChangeProperty(PropertyChangedEvent); - if (PropertyChangedEvent.Property - && (PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(UFlowNode, InputPins) || PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(UFlowNode, OutputPins))) + if (!PropertyChangedEvent.Property) + { + return; + } + + const FName PropertyName = PropertyChangedEvent.GetPropertyName(); + const FName MemberPropertyName = PropertyChangedEvent.GetMemberPropertyName(); + if (PropertyName == GET_MEMBER_NAME_CHECKED(UFlowNode, InputPins) || PropertyName == GET_MEMBER_NAME_CHECKED(UFlowNode, OutputPins) + || MemberPropertyName == GET_MEMBER_NAME_CHECKED(UFlowNode, InputPins) || MemberPropertyName == GET_MEMBER_NAME_CHECKED(UFlowNode, OutputPins)) { + // Potentially need to rebuild the pins from this node OnReconstructionRequested.ExecuteIfBound(); } } -void UFlowNode::PostLoad() +EDataValidationResult UFlowNode::ValidateNode() { - Super::PostLoad(); + EDataValidationResult ValidationResult = Super::ValidateNode(); - // fix Class Default Object - FixNode(nullptr); + // Validate that output and input pins have unique names + TSet UniquePinNames; + ValidateFlowPinArrayIsUnique(InputPins, UniquePinNames, ValidationResult); + ValidateFlowPinArrayIsUnique(OutputPins, UniquePinNames, ValidationResult); + + return ValidationResult; } -void UFlowNode::FixNode(UEdGraphNode* NewGraph) +void UFlowNode::ValidateFlowPinArrayIsUnique(const TArray& FlowPins, TSet& InOutUniquePinNames, EDataValidationResult& InOutResult) { - // Fix any node pointers that may be out of date - if (NewGraph) - { - GraphNode = NewGraph; - } - - // v1.1 upgraded pins to be defined as structs - if (InputNames_DEPRECATED.Num() > InputPins.Num()) + for (const FFlowPin& FlowPin : FlowPins) { - for (int32 i = InputPins.Num(); i < InputNames_DEPRECATED.Num(); i++) + const FName& ThisPinName = FlowPin.PinName; + if (InOutUniquePinNames.Contains(ThisPinName)) { - InputPins.Emplace(InputNames_DEPRECATED[i]); + ValidationLog.Warning( + *FString::Printf( + TEXT("All pin names on a flow node must be unique, pin name %s is duplicated"), + *ThisPinName.ToString()), + this); + + InOutResult = EDataValidationResult::Invalid; } - } - if (OutputNames_DEPRECATED.Num() > OutputPins.Num()) - { - for (int32 i = OutputPins.Num(); i < OutputNames_DEPRECATED.Num(); i++) + else { - OutputPins.Emplace(OutputNames_DEPRECATED[i]); + InOutUniquePinNames.Add(FlowPin.PinName); } } } -void UFlowNode::SetGraphNode(UEdGraphNode* NewGraph) +void UFlowNode::EnsureAddOnFlowNodePointersForEditor() { - GraphNode = NewGraph; + ForEachAddOn( + [this](UFlowNodeAddOn& AddOn) -> EFlowForEachAddOnFunctionReturnValue + { + AddOn.SetFlowNodeForEditor(this); + return EFlowForEachAddOnFunctionReturnValue::Continue; + }); } -FString UFlowNode::GetNodeCategory() const +#endif + +void UFlowNode::PostLoad() { - if (GetClass()->ClassGeneratedBy) + Super::PostLoad(); + +#if WITH_EDITOR + // fix Class Default Object + FixNode(nullptr); +#endif + + if (!HasAnyFlags(RF_ArchetypeObject | RF_ClassDefaultObject)) { - const FString& BlueprintCategory = Cast(GetClass()->ClassGeneratedBy)->BlueprintCategory; - if (!BlueprintCategory.IsEmpty()) - { - return BlueprintCategory; - } + FixupDataPinTypes(); } - - return Category; } -FText UFlowNode::GetNodeTitle() const +bool UFlowNode::IsSupportedInputPinName(const FName& PinName) const { - if (GetClass()->ClassGeneratedBy) + const FFlowPin* InputPin = FindInputPinByName(PinName); + + if (AddOns.IsEmpty()) { - const FString& BlueprintTitle = Cast(GetClass()->ClassGeneratedBy)->BlueprintDisplayName; - if (!BlueprintTitle.IsEmpty()) - { - return FText::FromString(BlueprintTitle); - } + checkf(InputPin, TEXT("Only AddOns should introduce unknown Pins to a FlowNode, so if we have no AddOns, we should have no unknown pins")); + return true; } - return GetClass()->GetDisplayNameText(); + return (InputPin != nullptr); } -FText UFlowNode::GetNodeToolTip() const +void UFlowNode::AddInputPins(const TArray& Pins) { - if (GetClass()->ClassGeneratedBy) + for (const FFlowPin& Pin : Pins) { - const FString& BlueprintToolTip = Cast(GetClass()->ClassGeneratedBy)->BlueprintDescription; - if (!BlueprintToolTip.IsEmpty()) - { - return FText::FromString(BlueprintToolTip); - } + InputPins.AddUnique(Pin); } - - return GetClass()->GetToolTipText(); } -FString UFlowNode::GetNodeDescription() const +void UFlowNode::AddOutputPins(const TArray& Pins) { - return K2_GetNodeDescription(); + for (const FFlowPin& Pin : Pins) + { + OutputPins.AddUnique(Pin); + } } -#endif -UFlowAsset* UFlowNode::GetFlowAsset() const +#if WITH_EDITOR + +void UFlowNode::SetupForEditing(UEdGraphNode& EdGraphNode) { - return GetOuter() ? Cast(GetOuter()) : nullptr; + Super::SetupForEditing(EdGraphNode); + + // Ensure AddOn editor pointers are correct as soon as we're prepared for editing. + EnsureAddOnFlowNodePointersForEditor(); + + // Initialize the preload helper in editor + TryInitializePreloadHelper(); } -void UFlowNode::AddInputPins(TArray PinNames) +bool UFlowNode::RebuildPinArray(const TArray& NewPinNames, TArray& InOutPins, const FFlowPin& DefaultPin) { - for (const FName& PinName : PinNames) + bool bIsChanged; + + TArray NewPins; + + if (NewPinNames.Num() == 0) + { + bIsChanged = true; + + NewPins.Reserve(1); + + NewPins.Add(DefaultPin); + } + else + { + const bool bIsSameNum = (NewPinNames.Num() == InOutPins.Num()); + + bIsChanged = !bIsSameNum; + + NewPins.Reserve(NewPinNames.Num()); + + for (int32 NewPinIndex = 0; NewPinIndex < NewPinNames.Num(); ++NewPinIndex) + { + const FName& NewPinName = NewPinNames[NewPinIndex]; + NewPins.Add(FFlowPin(NewPinName)); + + if (bIsSameNum) + { + bIsChanged = bIsChanged || (NewPinName != InOutPins[NewPinIndex].PinName); + } + } + } + + if (bIsChanged) { - InputPins.Emplace(PinName); + InOutPins.Reset(); + + check(NewPins.Num() > 0); + + if (&InOutPins == &InputPins) + { + AddInputPins(NewPins); + } + else + { + checkf(&InOutPins == &OutputPins, TEXT("Only expected to be called with one or the other of the pin arrays")); + + AddOutputPins(NewPins); + } } + + return bIsChanged; } -void UFlowNode::AddOutputPins(TArray PinNames) +bool UFlowNode::RebuildPinArray(const TArray& NewPins, TArray& InOutPins, const FFlowPin& DefaultPin) { - for (const FName& PinName : PinNames) + TArray NewPinNames; + NewPinNames.Reserve(NewPins.Num()); + + for (const FFlowPin& NewPin : NewPins) { - OutputPins.Emplace(PinName); + NewPinNames.Add(NewPin.PinName); } + + return RebuildPinArray(NewPinNames, InOutPins, DefaultPin); } +#endif // WITH_EDITOR + void UFlowNode::SetNumberedInputPins(const uint8 FirstNumber, const uint8 LastNumber) { InputPins.Empty(); @@ -182,6 +260,32 @@ void UFlowNode::SetNumberedOutputPins(const uint8 FirstNumber /*= 0*/, const uin } } +uint8 UFlowNode::CountNumberedInputs() const +{ + uint8 Result = 0; + for (const FFlowPin& Pin : InputPins) + { + if (Pin.PinName.ToString().IsNumeric()) + { + Result++; + } + } + return Result; +} + +uint8 UFlowNode::CountNumberedOutputs() const +{ + uint8 Result = 0; + for (const FFlowPin& Pin : OutputPins) + { + if (Pin.PinName.ToString().IsNumeric()) + { + Result++; + } + } + return Result; +} + TArray UFlowNode::GetInputNames() const { TArray Result; @@ -209,6 +313,66 @@ TArray UFlowNode::GetOutputNames() const } #if WITH_EDITOR + +bool UFlowNode::SupportsContextPins() const +{ + if (Super::SupportsContextPins()) + { + return true; + } + + if (!AutoInputDataPins.IsEmpty() || !AutoOutputDataPins.IsEmpty()) + { + return true; + } + + for (const UFlowNodeAddOn* AddOn : AddOns) + { + if (IsValid(AddOn) && AddOn->SupportsContextPins()) + { + return true; + } + } + + return false; +} + +TArray UFlowNode::GetContextInputs() const +{ + TArray ContextInputs = Super::GetContextInputs(); + + if (PreloadHelper.IsValid()) + { + PreloadHelper.Get().GetContextInputs(ContextInputs); + } + + // Add the Auto-Generated DataPins as GetContextInputs + for (const FFlowPin& AutoGeneratedDataPin : AutoInputDataPins) + { + ContextInputs.AddUnique(AutoGeneratedDataPin); + } + + return ContextInputs; +} + +TArray UFlowNode::GetContextOutputs() const +{ + TArray ContextOutputs = Super::GetContextOutputs(); + + // Add the Auto-Generated DataPins as ContextOutputs + for (const FFlowPin& AutoGeneratedDataPin : AutoOutputDataPins) + { + ContextOutputs.AddUnique(AutoGeneratedDataPin); + } + + if (PreloadHelper.IsValid()) + { + PreloadHelper.Get().GetContextOutputs(ContextOutputs); + } + + return ContextOutputs; +} + bool UFlowNode::CanUserAddInput() const { return K2_CanUserAddInput(); @@ -219,54 +383,796 @@ bool UFlowNode::CanUserAddOutput() const return K2_CanUserAddOutput(); } -void UFlowNode::RemoveUserInput() +void UFlowNode::RemoveUserInput(const FName& PinName) { Modify(); - InputPins.RemoveAt(InputPins.Num() - 1); + + int32 RemovedPinIndex = INDEX_NONE; + for (int32 i = 0; i < InputPins.Num(); i++) + { + if (InputPins[i].PinName == PinName) + { + InputPins.RemoveAt(i); + RemovedPinIndex = i; + break; + } + } + + // update remaining pins + if (RemovedPinIndex > INDEX_NONE) + { + for (int32 i = RemovedPinIndex; i < InputPins.Num(); ++i) + { + if (InputPins[i].PinName.ToString().IsNumeric()) + { + InputPins[i].PinName = *FString::FromInt(i); + } + } + } +} + +void UFlowNode::RemoveUserOutput(const FName& PinName) +{ + Modify(); + + int32 RemovedPinIndex = INDEX_NONE; + for (int32 i = 0; i < OutputPins.Num(); i++) + { + if (OutputPins[i].PinName == PinName) + { + OutputPins.RemoveAt(i); + RemovedPinIndex = i; + break; + } + } + + // update remaining pins + if (RemovedPinIndex > INDEX_NONE) + { + for (int32 i = RemovedPinIndex; i < OutputPins.Num(); ++i) + { + if (OutputPins[i].PinName.ToString().IsNumeric()) + { + OutputPins[i].PinName = *FString::FromInt(i); + } + } + } } -void UFlowNode::RemoveUserOutput() +bool UFlowNode::TryUpdateAutoDataPins() { + FFlowAutoDataPinsWorkingData WorkingData(AutoInputDataPins, AutoOutputDataPins); + + FFlowDataPinValueOwnerCollection ValueOwnerCollection; + GatherDataPinValueOwnerCollection(ValueOwnerCollection); + + for (FFlowDataPinValueOwner& ValueOwner : ValueOwnerCollection.GetValueOwners()) + { + check(ValueOwner.IsValid()); + ValueOwner.OwnerInterface->AutoGenerateDataPins(ValueOwner, WorkingData); + } + + FFlowAutoDataPinsWorkingData::FBuildResult BuildResult; + WorkingData.Build(*this, BuildResult); + + const bool bAutoInputDataPinsChanged = !FFlowAutoDataPinsWorkingData::CheckIfProposedPinsMatchPreviousPins(AutoInputDataPins, BuildResult.AutoInputPins); + const bool bAutoOutputDataPinsChanged = !FFlowAutoDataPinsWorkingData::CheckIfProposedPinsMatchPreviousPins(AutoOutputDataPins, BuildResult.AutoOutputPins); + + // Compare runtime map vs proposed editor map + bool bPropertySourceMapChanged = false; + if (MapDataPinNameToPropertySource.Num() != BuildResult.MapDataPinNameToPropertySource.Num()) + { + bPropertySourceMapChanged = true; + } + else + { + for (const TPair& KVP : MapDataPinNameToPropertySource) + { + const FFlowPinPropertySource* Proposed = BuildResult.MapDataPinNameToPropertySource.Find(KVP.Key); + if (!Proposed || Proposed->PropertyName != KVP.Value.PropertyName || Proposed->ValueOwnerIndex != KVP.Value.ValueOwnerIndex) + { + bPropertySourceMapChanged = true; + break; + } + } + } + + bool bAnyWrapperChanged = false; + for (const FFlowAutoDataPinsWorkingData::FDeferredValuePinNamePatch& Patch : BuildResult.DeferredValuePatches) + { + if (Patch.DataPinValue && Patch.DataPinValue->PropertyPinName != Patch.NewPinName) + { + bAnyWrapperChanged = true; + break; + } + } + + const bool bAnyChange = bAutoInputDataPinsChanged || bAutoOutputDataPinsChanged || bPropertySourceMapChanged || bAnyWrapperChanged; + if (!bAnyChange) + { + return false; + } + + // Only Modify() if the regenerated pins are significantly different + SetFlags(RF_Transactional); Modify(); - OutputPins.RemoveAt(OutputPins.Num() - 1); + + if (bAutoInputDataPinsChanged) + { + AutoInputDataPins = MoveTemp(BuildResult.AutoInputPins); + } + + if (bAutoOutputDataPinsChanged) + { + AutoOutputDataPins = MoveTemp(BuildResult.AutoOutputPins); + } + + if (bPropertySourceMapChanged) + { + MapDataPinNameToPropertySource.Reset(); + MapDataPinNameToPropertySource.Reserve(BuildResult.MapDataPinNameToPropertySource.Num()); + + for (const TPair& KVP : BuildResult.MapDataPinNameToPropertySource) + { + MapDataPinNameToPropertySource.Add(KVP.Key, KVP.Value); + } + } + + for (const FFlowAutoDataPinsWorkingData::FDeferredValuePinNamePatch& Patch : BuildResult.DeferredValuePatches) + { + if (Patch.DataPinValue) + { + Patch.DataPinValue->PropertyPinName = Patch.NewPinName; + } + } + + return true; } #endif -TSet UFlowNode::GetConnectedNodes() const +FFlowDataPinResult UFlowNode::TrySupplyDataPin(FName PinName) const +{ + const FFlowPin* FlowPin = FindOutputPinByName(PinName); + if (!FlowPin) + { + // Also look in the Input Pins (for supplying default values for unconnected pins) + FlowPin = FindInputPinByName(PinName); + if (!FlowPin) + { + return FFlowDataPinResult(EFlowDataPinResolveResult::FailedUnknownPin); + } + } + + const FFlowPinType* DataPinType = FlowPin->ResolveFlowPinType(); + if (!DataPinType) + { + return FFlowDataPinResult(EFlowDataPinResolveResult::FailedMismatchedType); + } + + FFlowDataPinResult SuppliedResult; + if (TryGatherPropertyOwnersAndPopulateResult(PinName, *DataPinType, *FlowPin, SuppliedResult)) + { + return SuppliedResult; + } + + return FFlowDataPinResult(EFlowDataPinResolveResult::FailedUnknownPin); +} + +void UFlowNode::GatherDataPinValueOwnerCollection(FFlowDataPinValueOwnerCollection& ValueOwnerCollection) const +{ + // When called in the editor, 'this' may mutate to disambiguate generated data pins + UFlowNode* MutableThis = const_cast(this); + ValueOwnerCollection.AddValueOwner(*MutableThis); + + // Give all the AddOns a chance to supply data pins as well + (void) ForEachAddOn( + [&ValueOwnerCollection](UFlowNodeAddOn& AddOn) + { + ValueOwnerCollection.AddValueOwner(AddOn); + + return EFlowForEachAddOnFunctionReturnValue::Continue; + }); +} + +bool UFlowNode::TryGatherPropertyOwnersAndPopulateResult( + const FName& PinName, + const FFlowPinType& DataPinType, + const FFlowPin& FlowPin, + FFlowDataPinResult& OutSuppliedResult) const +{ + // Gather all potential UObject instances that might own properties + // mapped to data pins on this node (usually the node itself + any referenced objects) + FFlowDataPinValueOwnerCollection ValueOwnerCollection; + GatherDataPinValueOwnerCollection(ValueOwnerCollection); + + // Early out if we have no possible owners at all + if (ValueOwnerCollection.IsEmpty()) + { + LogError(FString::Printf(TEXT("No property owners available for data pin '%s' on node %s"), + *PinName.ToString(), *GetName()), EFlowOnScreenMessageType::Temporary); + + return false; + } + + const FFlowDataPinValueOwner* ValueOwner = nullptr; + FName PropertyNameToLookup; + const TArray& ValueOwners = ValueOwnerCollection.GetValueOwners(); + + // Look up explicit mapping (used for non-default owners or disambiguated pins) + if (const FFlowPinPropertySource* FlowPropertySource = MapDataPinNameToPropertySource.Find(PinName)) + { + const int32 OwnerIndex = FlowPropertySource->ValueOwnerIndex; + + if (ValueOwners.IsValidIndex(OwnerIndex)) + { + ValueOwner = &ValueOwners[OwnerIndex]; + PropertyNameToLookup = FlowPropertySource->PropertyName; + } + else + { + // Critical: mapped index is out of bounds → configuration or generation bug + LogError(FString::Printf(TEXT("Invalid property owner index %d for pin '%s' on node %s (max %d owners)"), + OwnerIndex, *PinName.ToString(), *GetName(), ValueOwners.Num() - 1), + EFlowOnScreenMessageType::Temporary); + + return false; + } + } + else + { + check(!ValueOwners.IsEmpty()); + + // Fallback for unmapped pins → assume default owner (index 0) + pin name == property name + ValueOwner = &ValueOwners[0]; + check(ValueOwner->IsDefaultValueOwner()); + + PropertyNameToLookup = PinName; + } + + if (!ValueOwner) + { + LogError(FString::Printf(TEXT("Failed to resolve property owner for data pin '%s' on node %s"), + *PinName.ToString(), *GetName()), EFlowOnScreenMessageType::Temporary); + + return false; + } + + // Populate the value for the pin on the its owner object + const UObject* ValueOwnerAsObject = Cast(ValueOwner->OwnerInterface); + const UFlowNode& FlowNodeThis = *this; + if (DataPinType.PopulateResult(*ValueOwnerAsObject, FlowNodeThis, PropertyNameToLookup, OutSuppliedResult)) + { + return true; + } + + return false; +} +// -- + +bool UFlowNode::TryGetFlowDataPinSupplierDatasForPinName(const FName& PinName, TFlowPinValueSupplierDataArray& InOutPinValueSupplierDatas) const +{ + const IFlowDataPinValueSupplierInterface* ThisAsPinValueSupplier = Cast(this); + + // This function will build the inverse-priority-ordered array of data suppliers for a given PinName. + // It works in two modes: + // - Standard case - Add a connected node as the priority supplier, and this node as the default value supplier + // - Exception case - for External data supplied nodes, we recurse (below) to crawl further and add the supplier + // for the external supplier's node. In practice, this is a node (A) connected to a Start node, which is + // supplied by its outer SubGraph node, which sources its values from the nodes that are connected to the external inputs + // that the subgraph node added as inputs for its instanced subgraph). The external supplier's value has top priority, + // then it falls to the standard case sources (as above). + + // Potentially add this current node as a default value supplier + // (this will be pushed down the priority queue as higher priority suppliers are found) + FFlowPinValueSupplierData NewPinValueSupplier; + NewPinValueSupplier.PinValueSupplier = ThisAsPinValueSupplier; + NewPinValueSupplier.SupplierPinName = PinName; + TryAddSupplierDataToArray(NewPinValueSupplier, InOutPinValueSupplierDatas); + + // If the pin is connected, try to add the connected node as the priority supplier + FConnectedPin ConnectedPin; + + if (FindConnectedNodeForPinCached(PinName, ConnectedPin)) + { + const FGuid& ConnectedNodeGuid = ConnectedPin.NodeGuid; + + FFlowPinValueSupplierData ConnectedPinValueSupplier; + ConnectedPinValueSupplier.SupplierPinName = ConnectedPin.PinName; + + if (const UFlowAsset* FlowAsset = GetFlowAsset()) + { + const UFlowNode* SupplierFlowNode = FlowAsset->GetNode(ConnectedNodeGuid); + + if (IsValid(SupplierFlowNode)) + { + ConnectedPinValueSupplier.PinValueSupplier = Cast(SupplierFlowNode); + + TryAddSupplierDataToArray(ConnectedPinValueSupplier, InOutPinValueSupplierDatas); + } + } + } + + return !InOutPinValueSupplierDatas.IsEmpty(); +} + +void UFlowNode::TryAddSupplierDataToArray(FFlowPinValueSupplierData& InOutSupplierData, TFlowPinValueSupplierDataArray& InOutPinValueSupplierDatas) const +{ + // If the connected node can supply data pin values, insert it into the top of the priority queue + const UFlowNode* SupplierFlowNode = CastChecked(InOutSupplierData.PinValueSupplier); + if (InOutSupplierData.PinValueSupplier && SupplierFlowNode->CanSupplyDataPinValues()) + { + InOutPinValueSupplierDatas.Add(InOutSupplierData); + } + + // Exception case for nodes with external suppliers, recurse here to crawl further + // to the external supplier's connected pin as our most preferred source (see block comment above). + if (const IFlowNodeWithExternalDataPinSupplierInterface* HasExternalPinSupplierInterface = Cast(SupplierFlowNode)) + { + if (const UFlowNode* ExternalDataPinSupplierFlowNode = Cast(HasExternalPinSupplierInterface->GetExternalDataPinSupplier())) + { + ExternalDataPinSupplierFlowNode->TryGetFlowDataPinSupplierDatasForPinName(InOutSupplierData.SupplierPinName, InOutPinValueSupplierDatas); + } + } +} + +// #FlowDataPinLegacy +void UFlowNode::FixupDataPinTypes() +{ + FixupDataPinTypesForArray(InputPins); + FixupDataPinTypesForArray(OutputPins); +#if WITH_EDITOR + FixupDataPinTypesForArray(AutoInputDataPins); + FixupDataPinTypesForArray(AutoOutputDataPins); +#endif +} + +void UFlowNode::FixupDataPinTypesForArray(TArray& MutableDataPinArray) +{ + for (FFlowPin& MutableFlowPin : MutableDataPinArray) + { + FixupDataPinTypesForPin(MutableFlowPin); + } +} + +void UFlowNode::FixupDataPinTypesForPin(FFlowPin& MutableDataPin) +{ + const FFlowPinTypeName NewPinTypeName = FFlowPin::GetPinTypeNameForLegacyPinType(MutableDataPin.PinType); + + if (!NewPinTypeName.IsNone()) + { + MutableDataPin.SetPinTypeName(NewPinTypeName); + } + + if (MutableDataPin.GetPinTypeName().IsNone()) + { + // Ensure we have a pin type even if the enum was invalid before + MutableDataPin.SetPinTypeName(FFlowPinType_Exec::GetPinTypeNameStatic()); + } + + MutableDataPin.PinType = EFlowPinType::Invalid; +} +// -- + +#if WITH_EDITOR + +void UFlowNode::BuildConnectionChangeList( + const UFlowAsset& FlowAsset, + const TMap& OldConnections, + const TMap& NewConnections, + TArray& OutChanges) +{ + OutChanges.Reset(); + + // Gather union of keys + TSet Keys; + Keys.Reserve(OldConnections.Num() + NewConnections.Num()); + + for (const TPair& KVP : OldConnections) + { + Keys.Add(KVP.Key); + } + + for (const TPair& KVP : NewConnections) + { + Keys.Add(KVP.Key); + } + + for (const FName& PinName : Keys) + { + const FConnectedPin* OldConnectedPin = OldConnections.Find(PinName); + const FConnectedPin* NewConnectedPin = NewConnections.Find(PinName); + + const bool bHadOld = (OldConnectedPin != nullptr); + const bool bHasNew = (NewConnectedPin != nullptr); + + // If present in both and equal => no change + if (bHadOld && bHasNew && (*OldConnectedPin == *NewConnectedPin)) + { + continue; + } + + UFlowNode* OldConnectedNode = nullptr; + FName OldConnectedPinName; + if (bHadOld) + { + OldConnectedNode = FlowAsset.GetNode(OldConnectedPin->NodeGuid); + OldConnectedPinName = OldConnectedPin->PinName; + } + + UFlowNode* NewConnectedNode = nullptr; + FName NewConnectedPinName; + if (bHasNew) + { + NewConnectedNode = FlowAsset.GetNode(NewConnectedPin->NodeGuid); + NewConnectedPinName = NewConnectedPin->PinName; + } + + FFlowPinConnectionChange Change = + FFlowPinConnectionChange( + PinName, + OldConnectedNode, + OldConnectedPinName, + NewConnectedNode, + NewConnectedPinName); + + OutChanges.Add(MoveTemp(Change)); + } +} + +void UFlowNode::BroadcastEditorPinConnectionsChanged(const TArray& Changes) +{ + OnEditorPinConnectionsChanged(Changes); + + ForEachAddOn([&Changes](UFlowNodeAddOn& AddOn) -> EFlowForEachAddOnFunctionReturnValue + { + AddOn.OnEditorPinConnectionsChanged(Changes); + + return EFlowForEachAddOnFunctionReturnValue::Continue; + }); +} + +void UFlowNode::SetConnections(const TMap& InConnections) +{ + const TMap OldConnections = Connections; + + // Early-out if maps are identical (cheap check first, then deep equality). + // Note: TMap equality operator exists for comparable value types; keep explicit check to be safe. + if (OldConnections.Num() == InConnections.Num()) + { + bool bAllEqual = true; + for (const TPair& KVP : OldConnections) + { + const FConnectedPin* Other = InConnections.Find(KVP.Key); + if (!Other || !(*Other == KVP.Value)) + { + bAllEqual = false; + break; + } + } + + if (bAllEqual) + { + return; + } + } + + Connections = InConnections; + + // Compute per-pin deltas and broadcast to self + addons + TArray Changes; + BuildConnectionChangeList(*GetFlowAsset(), OldConnections, Connections, Changes); + + if (!Changes.IsEmpty()) + { + BroadcastEditorPinConnectionsChanged(Changes); + } +} +#endif + +TSet UFlowNode::GatherConnectedNodes() const { TSet Result; for (const TPair& Connection : Connections) { Result.Emplace(GetFlowAsset()->GetNode(Connection.Value.NodeGuid)); } + return Result; } -bool UFlowNode::IsInputConnected(const FName& PinName) const +FName UFlowNode::GetPinConnectedToNode(const FGuid& OtherNodeGuid) { - if (GetFlowAsset()) + for (const TPair& Connection : Connections) { - for (const TPair& Pair : GetFlowAsset()->Nodes) + if (Connection.Value.NodeGuid == OtherNodeGuid) { - if (Pair.Value) + return Connection.Key; + } + } + + return NAME_None; +} + +bool UFlowNode::IsInputConnected(const FName& PinName, bool bErrorIfPinNotFound) const +{ + // TODO (gtaylor) Maybe we make a blueprint accessible version with the FConnectedPin array access + constexpr TArray* ConnectedPins = nullptr; + return FindInputPinConnections(PinName, bErrorIfPinNotFound, ConnectedPins); +} + +bool UFlowNode::IsOutputConnected(const FName& PinName, bool bErrorIfPinNotFound) const +{ + // TODO (gtaylor) Maybe we make a blueprint accessible version with the FConnectedPin array access + constexpr TArray* ConnectedPins = nullptr; + return FindOutputPinConnections(PinName, bErrorIfPinNotFound, ConnectedPins); +} + +bool UFlowNode::FindFirstInputPinConnection(const FName& PinName, bool bErrorIfPinNotFound, FConnectedPin& FirstConnectedPin) const +{ + if (const FFlowPin* FlowPin = FindInputPinByName(PinName)) + { + return FindFirstInputPinConnection(*FlowPin, FirstConnectedPin); + } + + if (bErrorIfPinNotFound) + { + LogError(FString::Printf(TEXT("Unknown input pin %s"), *PinName.ToString()), EFlowOnScreenMessageType::Temporary); + } + + return false; +} + +bool UFlowNode::FindInputPinConnections(const FName& PinName, bool bErrorIfPinNotFound, TArray* ConnectedPins) const +{ + if (const FFlowPin* FlowPin = FindInputPinByName(PinName)) + { + return FindInputPinConnections(*FlowPin, ConnectedPins); + } + + if (bErrorIfPinNotFound) + { + LogError(FString::Printf(TEXT("Unknown input pin %s"), *PinName.ToString()), EFlowOnScreenMessageType::Temporary); + } + + return false; +} + +bool UFlowNode::FindFirstOutputPinConnection(const FName& PinName, bool bErrorIfPinNotFound, FConnectedPin& FirstConnectedPin) const +{ + if (const FFlowPin* FlowPin = FindOutputPinByName(PinName)) + { + return FindFirstOutputPinConnection(*FlowPin, FirstConnectedPin); + } + + if (bErrorIfPinNotFound) + { + LogError(FString::Printf(TEXT("Unknown output pin %s"), *PinName.ToString()), EFlowOnScreenMessageType::Temporary); + } + + return false; +} + +bool UFlowNode::FindOutputPinConnections(const FName& PinName, bool bErrorIfPinNotFound, TArray* ConnectedPins) const +{ + if (const FFlowPin* FlowPin = FindOutputPinByName(PinName)) + { + return FindOutputPinConnections(*FlowPin, ConnectedPins); + } + + if (bErrorIfPinNotFound) + { + LogError(FString::Printf(TEXT("Unknown output pin %s"), *PinName.ToString()), EFlowOnScreenMessageType::Temporary); + } + + return false; +} + +template +bool UFlowNode::FindFirstPinConnection( + const FFlowPin& FlowPin, + const TArray& FlowPinArray, + FConnectedPin& FirstConnectedPin) const +{ + if (!FlowPinArray.Contains(FlowPin.PinName)) + { + return false; + } + + const bool bUseCachedPath = (bExecIsCached == FlowPin.IsExecPin()); + if (bUseCachedPath) + { + // Cached category: fast lookup (0/1 connection) + return FindConnectedNodeForPinCached(FlowPin.PinName, FirstConnectedPin); + } + else + { + // NOTE (gtaylor) For optimal perf, you should use the array signature when asking for uncached path + // (aka optimal use should use this branch) + TArray ConnectedPins; + if (FindConnectedNodeForPinUncached(FlowPin.PinName, &ConnectedPins)) + { + check(ConnectedPins.Num() > 0); + FirstConnectedPin = ConnectedPins[0]; + return true; + } + else + { + return false; + } + } +} + +template +bool UFlowNode::FindPinConnections(const FFlowPin& FlowPin, const TArray& FlowPinArray, TArray* ConnectedPins) const +{ + if (!FlowPinArray.Contains(FlowPin.PinName)) + { + return false; + } + + const bool bUseCachedPath = bExecIsCached == FlowPin.IsExecPin(); + if (bUseCachedPath) + { + // NOTE (gtaylor) For optimal perf, you should use the non-array signature when asking for cached path + // (aka optimal use should use this branch) + FConnectedPin ConnectedPin; + const bool bFoundPin = FindConnectedNodeForPinCached(FlowPin.PinName, ConnectedPin); + if (bFoundPin && ConnectedPins) + { + ConnectedPins->Add(ConnectedPin); + } + + return bFoundPin; + } + else + { + // We don't cache the output data pins for fast lookup in Connections, so use the slow path for them: + + return FindConnectedNodeForPinUncached(FlowPin.PinName, ConnectedPins); + } +} + +bool UFlowNode::FindFirstInputPinConnection(const FFlowPin& FlowPin, FConnectedPin& FirstConnectedPin) const +{ + // Exec Input pins - not cached + // Data Input pins - cached + constexpr bool bIsExecCached = false; + return FindFirstPinConnection(FlowPin, InputPins, FirstConnectedPin); +} + +bool UFlowNode::FindInputPinConnections(const FFlowPin& FlowPin, TArray* ConnectedPins) const +{ + // Exec Input pins - not cached + // Data Input pins - cached + constexpr bool bIsExecCached = false; + return FindPinConnections(FlowPin, InputPins, ConnectedPins); +} + +bool UFlowNode::FindFirstOutputPinConnection(const FFlowPin& FlowPin, FConnectedPin& FirstConnectedPin) const +{ + // Exec Output pins - cached + // Data Output pins - not cached + constexpr bool bIsExecCached = true; + return FindFirstPinConnection(FlowPin, OutputPins, FirstConnectedPin); +} + +bool UFlowNode::FindOutputPinConnections(const FFlowPin& FlowPin, TArray* ConnectedPins) const +{ + // Exec Output pins - cached + // Data Output pins - not cached + constexpr bool bIsExecCached = true; + return FindPinConnections(FlowPin, OutputPins, ConnectedPins); +} + +FFlowPin* UFlowNode::FindInputPinByName(const FName& PinName) +{ + if (FFlowPin* FlowPin = FindFlowPinByName(PinName, InputPins)) + { + return FlowPin; + } + + return nullptr; +} + +FFlowPin* UFlowNode::FindOutputPinByName(const FName& PinName) +{ + if (FFlowPin* FlowPin = FindFlowPinByName(PinName, OutputPins)) + { + return FlowPin; + } + + return nullptr; +} + +bool UFlowNode::FindConnectedNodeForPinCached(const FName& FlowPinName, FConnectedPin& ConnectedPin) const +{ + // NOTE (gtaylor) The Connections array only caches: + // - exec output pins + // - data input pins + // In both cases, there must be only one connection (due to schema rules in Flow). + // For the opposite direction (exec inputs, data outputs, the uncached version must be used. + const FConnectedPin* FoundConnectedPin = Connections.Find(FlowPinName); + if (FoundConnectedPin) + { + ConnectedPin = *FoundConnectedPin; + + return true; + } + + return false; +} + +bool UFlowNode::FindConnectedNodeForPinUncached(const FName& PinName, TArray* ConnectedPins) const +{ + const UFlowAsset* FlowAsset = GetFlowAsset(); + + if (!IsValid(FlowAsset)) + { + return false; + } + + check(!ConnectedPins || ConnectedPins->IsEmpty()); + + for (const TPair& Pair : ObjectPtrDecay(FlowAsset->Nodes)) + { + const UFlowNode* ConnectedFromFlowNode = Pair.Value; + + if (!IsValid(ConnectedFromFlowNode)) + { + continue; + } + + for (const TPair& Connection : ConnectedFromFlowNode->Connections) + { + const FConnectedPin& ConnectedPinStruct = Connection.Value; + + if (ConnectedPinStruct.NodeGuid == NodeGuid && ConnectedPinStruct.PinName == PinName) { - for (const TPair& Connection : Pair.Value->Connections) + if (ConnectedPins) + { + ConnectedPins->Add(ConnectedPinStruct); + } + else { - if (Connection.Value.NodeGuid == NodeGuid && Connection.Value.PinName == PinName) - { - return true; - } + // Early return if not collecting the ConnectedPins, since only connected true/false matters + return true; } } } } + if (ConnectedPins && !ConnectedPins->IsEmpty()) + { + return true; + } + return false; } -bool UFlowNode::IsOutputConnected(const FName& PinName) const +TArray UFlowNode::GetKnownConnectionsToPin(const FConnectedPin& Pin) const { - return OutputPins.Contains(PinName) && Connections.Contains(PinName); + TArray ConnectedPins; + + if (Pin.NodeGuid == NodeGuid) + { + const FConnectedPin& Connection = Connections.FindRef(Pin.PinName); + if (Connection.NodeGuid.IsValid()) + { + ConnectedPins.Add(Connection); + } + } + else + { + for (const TPair& Connection : Connections) + { + if (Connection.Value.NodeGuid == Pin.NodeGuid && Connection.Value.PinName == Pin.PinName) + { + ConnectedPins.Emplace(NodeGuid, Connection.Key); + } + } + } + + return ConnectedPins; } void UFlowNode::RecursiveFindNodesByClass(UFlowNode* Node, const TSubclassOf Class, uint8 Depth, TArray& OutNodes) @@ -285,88 +1191,229 @@ void UFlowNode::RecursiveFindNodesByClass(UFlowNode* Node, const TSubclassOfGetConnectedNodes()) + for (UFlowNode* ConnectedNode : Node->GatherConnectedNodes()) { RecursiveFindNodesByClass(ConnectedNode, Class, Depth, OutNodes); } } } -UFlowSubsystem* UFlowNode::GetFlowSubsystem() const +void UFlowNode::InitializeInstance() +{ + Super::InitializeInstance(); + + TryInitializePreloadHelper(); +} + +void UFlowNode::DeinitializeInstance() { - return GetFlowAsset() ? GetFlowAsset()->GetFlowSubsystem() : nullptr; + DeinitializePreloadHelper(); + + Super::DeinitializeInstance(); } -UWorld* UFlowNode::GetWorld() const +void UFlowNode::OnActivate() { - if (GetFlowAsset() && GetFlowAsset()->GetFlowSubsystem()) + Super::OnActivate(); + + if (FFlowPreloadHelper* Helper = PreloadHelper.GetMutablePtr()) { - return GetFlowAsset()->GetFlowSubsystem()->GetWorld(); + Helper->OnNodeActivate(*this); } +} - return nullptr; +void UFlowNode::Cleanup() +{ + if (FFlowPreloadHelper* Helper = PreloadHelper.GetMutablePtr()) + { + Helper->OnNodeCleanup(*this); + } + + Super::Cleanup(); } -void UFlowNode::InitializeInstance() +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() { - K2_InitializeInstance(); + 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); + } } -void UFlowNode::PreloadContent() +bool UFlowNode::TryInitializePreloadHelper() { - K2_PreloadContent(); + // 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(); + + const 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::FlushContent() +void UFlowNode::DeinitializePreloadHelper() { - K2_FlushContent(); + if (FFlowPreloadHelper* Helper = PreloadHelper.GetMutablePtr()) + { + Helper->OnNodeDeinitializeInstance(*this); + } + + PreloadHelper.Reset(); } -void UFlowNode::TriggerInput(const FName& PinName, const bool bForcedActivation /*= false*/) +void UFlowNode::TriggerInput(const FName& PinName, const EFlowPinActivationType ActivationType /*= Default*/) { + if (SignalMode == EFlowSignalMode::Disabled) + { + // entirely ignore any Input activation + } + if (InputPins.Contains(PinName)) { - ActivationState = EFlowNodeState::Active; + if (SignalMode == EFlowSignalMode::Enabled) + { + const EFlowNodeState PreviousActivationState = ActivationState; + if (PreviousActivationState != EFlowNodeState::Active) + { + OnActivate(); + } + + ActivationState = EFlowNodeState::Active; + } #if !UE_BUILD_SHIPPING // record for debugging TArray& Records = InputRecords.FindOrAdd(PinName); - Records.Add(FPinRecord(FApp::GetCurrentTime(), bForcedActivation)); -#endif // UE_BUILD_SHIPPING + Records.Add(FPinRecord(FApp::GetCurrentTime(), ActivationType)); -#if WITH_EDITOR - if (GetWorld()->WorldType == EWorldType::PIE && UFlowAsset::GetFlowGraphInterface().IsValid()) + if (const UFlowAsset* FlowAssetTemplate = GetFlowAsset()->GetTemplateAsset()) { - UFlowAsset::GetFlowGraphInterface()->OnInputTriggered(GraphNode, InputPins.IndexOfByKey(PinName)); + (void)FlowAssetTemplate->OnPinTriggered.ExecuteIfBound(this, PinName); } -#endif // WITH_EDITOR +#endif } +#if !UE_BUILD_SHIPPING else { -#if !UE_BUILD_SHIPPING LogError(FString::Printf(TEXT("Input Pin name %s invalid"), *PinName.ToString())); -#endif // UE_BUILD_SHIPPING return; } +#endif - ExecuteInput(PinName); -} - -void UFlowNode::ExecuteInput(const FName& PinName) -{ - K2_ExecuteInput(PinName); + switch (SignalMode) + { + case EFlowSignalMode::Enabled: + ExecuteInputForSelfAndAddOns(PinName); + break; + case EFlowSignalMode::Disabled: + if (GetDefault()->bLogOnSignalDisabled) + { + LogNote(FString::Printf(TEXT("Node disabled while triggering input %s"), *PinName.ToString())); + } + break; + case EFlowSignalMode::PassThrough: + if (GetDefault()->bLogOnSignalPassthrough) + { + LogNote(FString::Printf(TEXT("Signal pass-through on triggering input %s"), *PinName.ToString())); + } + OnPassThrough(); + break; + default: ; + } } void UFlowNode::TriggerFirstOutput(const bool bFinish) @@ -377,8 +1424,15 @@ void UFlowNode::TriggerFirstOutput(const bool bFinish) } } -void UFlowNode::TriggerOutput(const FName& PinName, const bool bFinish /*= false*/, const bool bForcedActivation /*= false*/) +void UFlowNode::TriggerOutput(const FName PinName, const bool bFinish /*= false*/, const EFlowPinActivationType ActivationType /*= Default*/) { + if (HasFinished()) + { + // do not trigger output if node is already finished or aborted + LogError(TEXT("Trying to TriggerOutput after finished or aborted"), EFlowOnScreenMessageType::Disabled); + return; + } + // clean up node, if needed if (bFinish) { @@ -390,49 +1444,27 @@ void UFlowNode::TriggerOutput(const FName& PinName, const bool bFinish /*= false { // record for debugging, even if nothing is connected to this pin TArray& Records = OutputRecords.FindOrAdd(PinName); - Records.Add(FPinRecord(FApp::GetCurrentTime(), bForcedActivation)); + Records.Add(FPinRecord(FApp::GetCurrentTime(), ActivationType)); -#if WITH_EDITOR - if (GetWorld()->WorldType == EWorldType::PIE && UFlowAsset::GetFlowGraphInterface().IsValid()) + if (const UFlowAsset* FlowAssetTemplate = GetFlowAsset()->GetTemplateAsset()) { - UFlowAsset::GetFlowGraphInterface()->OnOutputTriggered(GraphNode, OutputPins.IndexOfByKey(PinName)); + FlowAssetTemplate->OnPinTriggered.ExecuteIfBound(this, PinName); } -#endif // WITH_EDITOR } else { LogError(FString::Printf(TEXT("Output Pin name %s invalid"), *PinName.ToString())); } -#endif // UE_BUILD_SHIPPING +#endif // call the next node if (OutputPins.Contains(PinName) && Connections.Contains(PinName)) { const FConnectedPin FlowPin = GetConnection(PinName); - GetFlowAsset()->TriggerInput(FlowPin.NodeGuid, FlowPin.PinName); + GetFlowAsset()->TriggerInput(FlowPin.NodeGuid, FlowPin.PinName, FConnectedPin(GetGuid(), PinName)); } } -void UFlowNode::TriggerOutputPin(const FFlowOutputPinHandle Pin, const bool bFinish, const bool bForcedActivation) -{ - TriggerOutput(Pin.PinName, bFinish, bForcedActivation); -} - -void UFlowNode::TriggerOutput(const FString& PinName, const bool bFinish) -{ - TriggerOutput(*PinName, bFinish); -} - -void UFlowNode::TriggerOutput(const FText& PinName, const bool bFinish) -{ - TriggerOutput(*PinName.ToString(), bFinish); -} - -void UFlowNode::TriggerOutput(const TCHAR* PinName, const bool bFinish) -{ - TriggerOutput(FName(PinName), bFinish); -} - void UFlowNode::Finish() { Deactivate(); @@ -441,6 +1473,12 @@ void UFlowNode::Finish() void UFlowNode::Deactivate() { + if (SignalMode == EFlowSignalMode::PassThrough) + { + // there is nothing to deactivate, node was never active + return; + } + if (GetFlowAsset()->FinishPolicy == EFlowFinishPolicy::Abort) { ActivationState = EFlowNodeState::Aborted; @@ -453,16 +1491,6 @@ void UFlowNode::Deactivate() Cleanup(); } -void UFlowNode::Cleanup() -{ - K2_Cleanup(); -} - -void UFlowNode::ForceFinishNode() -{ - K2_ForceFinishNode(); -} - void UFlowNode::ResetRecords() { ActivationState = EFlowNodeState::NeverActivated; @@ -473,17 +1501,75 @@ void UFlowNode::ResetRecords() #endif } -#if WITH_EDITOR -UFlowNode* UFlowNode::GetInspectedInstance() const +void UFlowNode::SaveInstance(FFlowNodeSaveData& NodeRecord) { - if (const UFlowAsset* FlowInstance = GetFlowAsset()->GetInspectedInstance()) + NodeRecord.NodeGuid = NodeGuid; + OnSave(); + + FMemoryWriter MemoryWriter(NodeRecord.NodeData, true); + FFlowArchive Ar(MemoryWriter); + Serialize(Ar); +} + +void UFlowNode::LoadInstance(const FFlowNodeSaveData& NodeRecord) +{ + FMemoryReader MemoryReader(NodeRecord.NodeData, true); + FFlowArchive Ar(MemoryReader); + Serialize(Ar); + + if (UFlowAsset* FlowAsset = GetFlowAsset()) { - return FlowInstance->GetNode(GetGuid()); + FlowAsset->OnActivationStateLoaded(this); } - return nullptr; + switch (SignalMode) + { + case EFlowSignalMode::Enabled: + OnLoad(); + break; + case EFlowSignalMode::Disabled: + // designer doesn't want to execute this node's logic at all, so we kill it + LogNote(TEXT("Signal disabled while loading Flow Node from SaveGame")); + Finish(); + break; + case EFlowSignalMode::PassThrough: + LogNote(TEXT("Signal pass-through on loading Flow Node from SaveGame")); + OnPassThrough(); + break; + default: ; + } +} + +void UFlowNode::OnSave_Implementation() +{ +} + +void UFlowNode::OnLoad_Implementation() +{ +} + +void UFlowNode::OnPassThrough_Implementation() +{ + // trigger all connected outputs + // pin connections aren't serialized to the SaveGame, so users can safely change connections post game release + for (const FFlowPin& OutputPin : OutputPins) + { + if (Connections.Contains(OutputPin.PinName)) + { + TriggerOutput(OutputPin.PinName, false, EFlowPinActivationType::PassThrough); + } + } + + // deactivate node, so it doesn't get saved to a new SaveGame + Finish(); +} + +bool UFlowNode::ShouldSave_Implementation() +{ + return GetActivationState() == EFlowNodeState::Active; } +#if WITH_EDITOR TMap UFlowNode::GetWireRecords() const { TMap Result; @@ -507,30 +1593,6 @@ TArray UFlowNode::GetPinRecords(const FName& PinName, const EEdGraph } } -FString UFlowNode::GetStatusString() const -{ - return K2_GetStatusString(); -} - -bool UFlowNode::GetStatusBackgroundColor(FLinearColor& OutColor) const -{ - return K2_GetStatusBackgroundColor(OutColor); -} - -FString UFlowNode::GetAssetPath() -{ - return K2_GetAssetPath(); -} - -UObject* UFlowNode::GetAssetToEdit() -{ - return K2_GetAssetToEdit(); -} - -AActor* UFlowNode::GetActorToFocus() -{ - return K2_GetActorToFocus(); -} #endif FString UFlowNode::GetIdentityTagDescription(const FGameplayTag& Tag) @@ -553,98 +1615,65 @@ FString UFlowNode::GetClassDescription(const TSubclassOf Class) return Class ? Class->GetName() : MissingClass; } -FString UFlowNode::GetProgressAsString(float Value) +FString UFlowNode::GetProgressAsString(const float Value) { - // Avoids negative zero - if (Value == 0) - { - Value = 0; - } - - // First create the string - FString TempString = FString::Printf(TEXT("%f"), Value); - if (!TempString.IsNumeric()) - { - // String did not format as a valid decimal number so avoid messing with it - return TempString; - } + return FString::Printf(TEXT("%.*f"), 2, Value); +} - // Get position of the first digit after decimal separator - int32 TrimIndex = INDEX_NONE; - for (int32 CharIndex = 0; CharIndex < TempString.Len(); CharIndex++) +#if WITH_EDITOR +UFlowNode* UFlowNode::GetInspectedInstance() const +{ + if (const UFlowAsset* FlowInstance = GetFlowAsset()->GetInspectedInstance()) { - const TCHAR Char = TempString[CharIndex]; - if (Char == TEXT('.')) - { - TrimIndex = CharIndex + 2; - break; - } - if (TrimIndex == INDEX_NONE && Char != TEXT('0')) - { - TrimIndex = CharIndex + 1; - } + return FlowInstance->GetNode(GetGuid()); } - TempString.RemoveAt(TrimIndex, TempString.Len() - TrimIndex, /*bAllowShrinking*/false); - return TempString; + return nullptr; } -void UFlowNode::LogError(FString Message, const EFlowOnScreenMessageType OnScreenMessageType) const +FString UFlowNode::GetStatusStringForNodeAndAddOns() const { - const FString TemplatePath = GetFlowAsset()->TemplateAsset->GetPathName(); - Message += TEXT(" --- node ") + GetName() + TEXT(", asset ") + FPaths::GetPath(TemplatePath) / FPaths::GetBaseFilename(TemplatePath); + FString CombinedStatusString = GetStatusString(); - if (OnScreenMessageType == EFlowOnScreenMessageType::Permanent) - { - if (GetWorld()) + // Give all of the AddOns a chance to add their status strings as well + (void)ForEachAddOnConst( + [&CombinedStatusString](const UFlowNodeAddOn& AddOn) { - if (UViewportStatsSubsystem* StatsSubsystem = GetWorld()->GetSubsystem()) + const FString AddOnStatusString = AddOn.GetStatusString(); + + if (!AddOnStatusString.IsEmpty()) { - StatsSubsystem->AddDisplayDelegate([this, Message](FText& OutText, FLinearColor& OutColor) + if (!CombinedStatusString.IsEmpty()) { - OutText = FText::FromString(Message); - OutColor = FLinearColor::Red; - return IsValid(this) && ActivationState != EFlowNodeState::NeverActivated; - }); + CombinedStatusString += TEXT("\n"); + } + + CombinedStatusString += AddOnStatusString; } - } - } - else - { - GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, Message); - } - UE_LOG(LogFlow, Error, TEXT("%s"), *Message); + return EFlowForEachAddOnFunctionReturnValue::Continue; + }); + + return CombinedStatusString; } -void UFlowNode::SaveInstance(FFlowNodeSaveData& NodeRecord) +bool UFlowNode::GetStatusBackgroundColor(FLinearColor& OutColor) const { - NodeRecord.NodeGuid = NodeGuid; - OnSave(); - - FMemoryWriter MemoryWriter(NodeRecord.NodeData, true); - FFlowArchive Ar(MemoryWriter); - Serialize(Ar); + return K2_GetStatusBackgroundColor(OutColor); } -void UFlowNode::LoadInstance(const FFlowNodeSaveData& NodeRecord) +FString UFlowNode::GetAssetPath() { - FMemoryReader MemoryReader(NodeRecord.NodeData, true); - FFlowArchive Ar(MemoryReader); - Serialize(Ar); - - if (UFlowAsset* FlowAsset = GetFlowAsset()) - { - FlowAsset->OnActivationStateLoaded(this); - } - - OnLoad(); + return K2_GetAssetPath(); } -void UFlowNode::OnSave_Implementation() +UObject* UFlowNode::GetAssetToEdit() { + return K2_GetAssetToEdit(); } -void UFlowNode::OnLoad_Implementation() +AActor* UFlowNode::GetActorToFocus() { + return K2_GetActorToFocus(); } +#endif diff --git a/Source/Flow/Private/Nodes/FlowNodeAddOnBlueprint.cpp b/Source/Flow/Private/Nodes/FlowNodeAddOnBlueprint.cpp new file mode 100644 index 000000000..5dc91d674 --- /dev/null +++ b/Source/Flow/Private/Nodes/FlowNodeAddOnBlueprint.cpp @@ -0,0 +1,8 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Nodes/FlowNodeAddOnBlueprint.h" + +UFlowNodeAddOnBlueprint::UFlowNodeAddOnBlueprint(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} diff --git a/Source/Flow/Private/Nodes/FlowNodeBase.cpp b/Source/Flow/Private/Nodes/FlowNodeBase.cpp new file mode 100644 index 000000000..04aa5364b --- /dev/null +++ b/Source/Flow/Private/Nodes/FlowNodeBase.cpp @@ -0,0 +1,1163 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Nodes/FlowNodeBase.h" + +#include "FlowAsset.h" +#include "FlowLogChannels.h" +#include "FlowSubsystem.h" +#include "FlowTypes.h" +#include "AddOns/FlowNodeAddOn.h" +#include "Interfaces/FlowDataPinValueSupplierInterface.h" +#include "Interfaces/FlowNamedPropertiesSupplierInterface.h" +#include "Nodes/FlowNode.h" +#include "Types/FlowArray.h" +#include "Types/FlowDataPinResults.h" +#include "Types/FlowPinTypesStandard.h" +#include "Types/FlowNamedDataPinProperty.h" + +#include "Components/ActorComponent.h" +#if WITH_EDITOR +#include "Editor.h" +#endif + +#include "Engine/Blueprint.h" +#include "Engine/Engine.h" +#include "Engine/ViewportStatsSubsystem.h" +#include "Engine/World.h" +#include "GameFramework/Actor.h" +#include "Misc/App.h" +#include "Misc/Paths.h" +#include "Serialization/MemoryReader.h" +#include "Serialization/MemoryWriter.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNodeBase) + +using namespace EFlowForEachAddOnFunctionReturnValue_Classifiers; + +UFlowNodeBase::UFlowNodeBase() +#if WITH_EDITORONLY_DATA + : GraphNode(nullptr) + , bDisplayNodeTitleWithoutPrefix(true) + , bCanDelete(true) + , bCanDuplicate(true) + , bNodeDeprecated(false) + , NodeDisplayStyle(FlowNodeStyle::Node) + , NodeStyle(EFlowNodeStyle::Invalid) + , NodeColor(FLinearColor::Black) +#endif +{ +} + +UWorld* UFlowNodeBase::GetWorld() const +{ + if (const UFlowAsset* FlowAsset = GetFlowAsset()) + { + if (const UObject* FlowAssetOwner = FlowAsset->GetOwner()) + { + return FlowAssetOwner->GetWorld(); + } + } + + if (const UFlowSubsystem* FlowSubsystem = GetFlowSubsystem()) + { + return FlowSubsystem->GetWorld(); + } + + return nullptr; +} + +void UFlowNodeBase::InitializeInstance() +{ + IFlowCoreExecutableInterface::InitializeInstance(); + + if (!AddOns.IsEmpty()) + { + TArray SourceAddOns = AddOns; + AddOns.Reset(); + + for (UFlowNodeAddOn* SourceAddOn : SourceAddOns) + { + // Create a new instance of each AddOn + if (IsValid(SourceAddOn)) + { + UFlowNodeAddOn* NewAddOnInstance = NewObject(this, SourceAddOn->GetClass(), NAME_None, RF_Transient, SourceAddOn, false, nullptr); + AddOns.Add(NewAddOnInstance); + } + else + { + LogError(FString::Printf(TEXT("Null AddOn found in node %s"), *GetName()), EFlowOnScreenMessageType::Permanent); + } + } + + for (UFlowNodeAddOn* AddOn : AddOns) + { + // Initialize all the AddOn instances after they are all allocated + AddOn->InitializeInstance(); + } + } +} + +void UFlowNodeBase::DeinitializeInstance() +{ + for (UFlowNodeAddOn* AddOn : AddOns) + { + AddOn->DeinitializeInstance(); + } + + IFlowCoreExecutableInterface::DeinitializeInstance(); +} + +void UFlowNodeBase::OnActivate() +{ + IFlowCoreExecutableInterface::OnActivate(); + + for (UFlowNodeAddOn* AddOn : AddOns) + { + AddOn->OnActivate(); + } +} + +void UFlowNodeBase::ExecuteInput(const FName& PinName) +{ + IFlowCoreExecutableInterface::ExecuteInput(PinName); +} + +void UFlowNodeBase::ForceFinishNode() +{ + for (UFlowNodeAddOn* AddOn : AddOns) + { + AddOn->ForceFinishNode(); + } + + IFlowCoreExecutableInterface::ForceFinishNode(); +} + +void UFlowNodeBase::Cleanup() +{ + for (UFlowNodeAddOn* AddOn : AddOns) + { + AddOn->Cleanup(); + } + + IFlowCoreExecutableInterface::Cleanup(); +} + +void UFlowNodeBase::ExecuteInputForSelfAndAddOns(const FName& PinName) +{ + // AddOns can introduce input pins to Nodes without the Node being aware of the addition. + // To ensure that Nodes and AddOns only get the input pins signaled that they expect, + // we are filtering the PinName vs. the expected InputPins before carrying on with the ExecuteInput + + if (IsSupportedInputPinName(PinName)) + { + ExecuteInput(PinName); + } + + for (UFlowNodeAddOn* AddOn : AddOns) + { + AddOn->ExecuteInputForSelfAndAddOns(PinName); + } +} + +void UFlowNodeBase::TriggerOutputPin(const FFlowOutputPinHandle Pin, const bool bFinish, const EFlowPinActivationType ActivationType) +{ + TriggerOutput(Pin.PinName, bFinish, ActivationType); +} + +void UFlowNodeBase::TriggerOutput(const FString& PinName, const bool bFinish) +{ + TriggerOutput(FName(PinName), bFinish); +} + +void UFlowNodeBase::TriggerOutput(const FText& PinName, const bool bFinish) +{ + TriggerOutput(FName(PinName.ToString()), bFinish); +} + +void UFlowNodeBase::TriggerOutput(const TCHAR* PinName, const bool bFinish) +{ + TriggerOutput(FName(PinName), bFinish); +} + +const FFlowPin* UFlowNodeBase::FindFlowPinByName(const FName& PinName, const TArray& FlowPins) +{ + return FlowPins.FindByPredicate([&PinName](const FFlowPin& FlowPin) + { + return FlowPin.PinName == PinName; + }); +} + +FFlowPin* UFlowNodeBase::FindFlowPinByName(const FName& PinName, TArray& FlowPins) +{ + return FlowPins.FindByPredicate([&PinName](const FFlowPin& FlowPin) + { + return FlowPin.PinName == PinName; + }); +} + +#if WITH_EDITOR +TArray UFlowNodeBase::GetContextInputs() const +{ + TArray ContextInputs = IFlowContextPinSupplierInterface::GetContextInputs(); + TArray AddOnInputs; + + for (const UFlowNodeAddOn* AddOn : AddOns) + { + if (IsValid(AddOn)) + { + AddOnInputs.Append(AddOn->GetContextInputs()); + } + } + + if (!AddOnInputs.IsEmpty()) + { + for (const FFlowPin& FlowPin : AddOnInputs) + { + ContextInputs.AddUnique(FlowPin); + } + } + + return ContextInputs; +} + +TArray UFlowNodeBase::GetContextOutputs() const +{ + TArray ContextOutputs = IFlowContextPinSupplierInterface::GetContextOutputs(); + TArray AddOnOutputs; + + for (const UFlowNodeAddOn* AddOn : AddOns) + { + if (IsValid(AddOn)) + { + AddOnOutputs.Append(AddOn->GetContextOutputs()); + } + } + + if (!AddOnOutputs.IsEmpty()) + { + for (const FFlowPin& FlowPin : AddOnOutputs) + { + ContextOutputs.AddUnique(FlowPin); + } + } + + return ContextOutputs; +} + +#endif + +void UFlowNodeBase::LogValidationError(const FString& Message) +{ +#if WITH_EDITOR + ValidationLog.Error(*Message, this); +#endif +} + +void UFlowNodeBase::LogValidationWarning(const FString& Message) +{ +#if WITH_EDITOR + ValidationLog.Warning(*Message, this); +#endif +} + +void UFlowNodeBase::LogValidationNote(const FString& Message) +{ +#if WITH_EDITOR + ValidationLog.Note(*Message, this); +#endif +} + +UFlowAsset* UFlowNodeBase::GetFlowAsset() const +{ + // In the case of an AddOn, we want our containing FlowNode's Outer, not our own + const UFlowNode* FlowNode = GetFlowNodeSelfOrOwner(); + return FlowNode && FlowNode->GetOuter() ? Cast(FlowNode->GetOuter()) : Cast(GetOuter()); +} + +const UFlowNode* UFlowNodeBase::GetFlowNodeSelfOrOwner() const +{ + return const_cast(this)->GetFlowNodeSelfOrOwner(); +} + +UFlowSubsystem* UFlowNodeBase::GetFlowSubsystem() const +{ + return GetFlowAsset() ? GetFlowAsset()->GetFlowSubsystem() : nullptr; +} + +AActor* UFlowNodeBase::TryGetRootFlowActorOwner() const +{ + AActor* OwningActor = nullptr; + + UObject* RootFlowOwner = TryGetRootFlowObjectOwner(); + + if (IsValid(RootFlowOwner)) + { + // Check if the immediate parent is an AActor + OwningActor = Cast(RootFlowOwner); + + if (!IsValid(OwningActor)) + { + // Check if the immediate parent is an UActorComponent and return that Component's Owning actor + if (const UActorComponent* OwningComponent = Cast(RootFlowOwner)) + { + OwningActor = OwningComponent->GetOwner(); + } + } + } + + return OwningActor; +} + +UObject* UFlowNodeBase::TryGetRootFlowObjectOwner() const +{ + const UFlowAsset* FlowAsset = GetFlowAsset(); + + if (IsValid(FlowAsset)) + { + return FlowAsset->GetOwner(); + } + + return nullptr; +} + +TArray UFlowNodeBase::BuildFlowNodeBaseAncestorChain(UFlowNodeBase& FromFlowNodeBase, bool bIncludeFromFlowNodeBase) +{ + TArray AncestorChain; + + UFlowNodeBase* CurOuter = Cast(FromFlowNodeBase.GetOuter()); + while (IsValid(CurOuter)) + { + AncestorChain.Add(CurOuter); + + CurOuter = Cast(CurOuter->GetOuter()); + } + + FlowArray::ReverseArray(AncestorChain); + + if (bIncludeFromFlowNodeBase) + { + AncestorChain.Add(&FromFlowNodeBase); + } + + return AncestorChain; +} + +EFlowAddOnAcceptResult UFlowNodeBase::AcceptFlowNodeAddOnChild_Implementation( + const UFlowNodeAddOn* AddOnTemplate, + const TArray& AdditionalAddOnsToAssumeAreChildren) const +{ + // Subclasses may override this function to allow AddOn children classes + return EFlowAddOnAcceptResult::Undetermined; +} + +#if WITH_EDITOR +EFlowAddOnAcceptResult UFlowNodeBase::CheckAcceptFlowNodeAddOnChild( + const UFlowNodeAddOn* AddOnTemplate, + const TArray& AdditionalAddOnsToAssumeAreChildren) const +{ + if (!IsValid(AddOnTemplate)) + { + return EFlowAddOnAcceptResult::Reject; + } + + FLOW_ASSERT_ENUM_MAX(EFlowAddOnAcceptResult, 3); + + EFlowAddOnAcceptResult CombinedResult = EFlowAddOnAcceptResult::Undetermined; + + // Potential parents of AddOns are allowed to decide their eligible AddOn children + const EFlowAddOnAcceptResult AsChildResult = AcceptFlowNodeAddOnChild(AddOnTemplate, AdditionalAddOnsToAssumeAreChildren); + CombinedResult = CombineFlowAddOnAcceptResult(AsChildResult, CombinedResult); + + if (CombinedResult == EFlowAddOnAcceptResult::Reject) + { + return EFlowAddOnAcceptResult::Reject; + } + + // FlowNodeAddOns are allowed to opt in to their parent + const EFlowAddOnAcceptResult AsParentResult = AddOnTemplate->AcceptFlowNodeAddOnParent(this, AdditionalAddOnsToAssumeAreChildren); + + if (AsParentResult != EFlowAddOnAcceptResult::Reject && + AddOnTemplate->IsA()) + { + const FString Message = FString::Printf(TEXT("%s::AcceptFlowNodeAddOnParent must always Reject for UFlowNode subclasses"), *GetClass()->GetName()); + GetFlowAsset()->GetTemplateAsset()->LogError(Message, this); + + return EFlowAddOnAcceptResult::Reject; + } + + CombinedResult = CombineFlowAddOnAcceptResult(AsParentResult, CombinedResult); + + return CombinedResult; +} +#endif + +EFlowForEachAddOnFunctionReturnValue UFlowNodeBase::ForEachAddOnConst( + const FConstFlowNodeAddOnFunction& Function, + EFlowForEachAddOnChildRule AddOnChildRule) const +{ + FLOW_ASSERT_ENUM_MAX(EFlowForEachAddOnFunctionReturnValue, 3); + + EFlowForEachAddOnFunctionReturnValue ReturnValue = EFlowForEachAddOnFunctionReturnValue::Continue; + + for (const UFlowNodeAddOn* AddOn : AddOns) + { + if (!IsValid(AddOn)) + { + continue; + } + + ReturnValue = Function(*AddOn); + + if (!ShouldContinueForEach(ReturnValue)) + { + break; + } + + FLOW_ASSERT_ENUM_MAX(EFlowForEachAddOnChildRule, 2); + if (AddOnChildRule == EFlowForEachAddOnChildRule::AllChildren) + { + ReturnValue = AddOn->ForEachAddOnConst(Function); + + if (!ShouldContinueForEach(ReturnValue)) + { + break; + } + } + } + + return ReturnValue; +} + +EFlowForEachAddOnFunctionReturnValue UFlowNodeBase::ForEachAddOn( + const FFlowNodeAddOnFunction& Function, + EFlowForEachAddOnChildRule AddOnChildRule) const +{ + FLOW_ASSERT_ENUM_MAX(EFlowForEachAddOnFunctionReturnValue, 3); + + EFlowForEachAddOnFunctionReturnValue ReturnValue = EFlowForEachAddOnFunctionReturnValue::Continue; + + for (UFlowNodeAddOn* AddOn : AddOns) + { + if (!IsValid(AddOn)) + { + continue; + } + + ReturnValue = Function(*AddOn); + + if (!ShouldContinueForEach(ReturnValue)) + { + break; + } + + FLOW_ASSERT_ENUM_MAX(EFlowForEachAddOnChildRule, 2); + if (AddOnChildRule == EFlowForEachAddOnChildRule::AllChildren) + { + ReturnValue = AddOn->ForEachAddOn(Function); + + if (!ShouldContinueForEach(ReturnValue)) + { + break; + } + } + } + + return ReturnValue; +} + +EFlowForEachAddOnFunctionReturnValue UFlowNodeBase::ForEachAddOnForClassConst( + const UClass& InterfaceOrClass, + const FConstFlowNodeAddOnFunction& Function, + EFlowForEachAddOnChildRule AddOnChildRule) const +{ + FLOW_ASSERT_ENUM_MAX(EFlowForEachAddOnFunctionReturnValue, 3); + + EFlowForEachAddOnFunctionReturnValue ReturnValue = EFlowForEachAddOnFunctionReturnValue::Continue; + + for (const UFlowNodeAddOn* AddOn : AddOns) + { + if (!IsValid(AddOn)) + { + continue; + } + + if (AddOn->IsClassOrImplementsInterface(InterfaceOrClass)) + { + ReturnValue = Function(*AddOn); + + if (!ShouldContinueForEach(ReturnValue)) + { + break; + } + } + + FLOW_ASSERT_ENUM_MAX(EFlowForEachAddOnChildRule, 2); + if (AddOnChildRule == EFlowForEachAddOnChildRule::AllChildren) + { + ReturnValue = AddOn->ForEachAddOnForClassConst(InterfaceOrClass, Function); + + if (!ShouldContinueForEach(ReturnValue)) + { + break; + } + } + } + + return ReturnValue; +} + +EFlowForEachAddOnFunctionReturnValue UFlowNodeBase::ForEachAddOnForClass( + const UClass& InterfaceOrClass, + const FFlowNodeAddOnFunction& Function, + EFlowForEachAddOnChildRule AddOnChildRule) const +{ + FLOW_ASSERT_ENUM_MAX(EFlowForEachAddOnFunctionReturnValue, 3); + + EFlowForEachAddOnFunctionReturnValue ReturnValue = EFlowForEachAddOnFunctionReturnValue::Continue; + + for (UFlowNodeAddOn* AddOn : AddOns) + { + if (!IsValid(AddOn)) + { + continue; + } + + if (AddOn->IsClassOrImplementsInterface(InterfaceOrClass)) + { + ReturnValue = Function(*AddOn); + + if (!ShouldContinueForEach(ReturnValue)) + { + break; + } + } + + FLOW_ASSERT_ENUM_MAX(EFlowForEachAddOnChildRule, 2); + if (AddOnChildRule == EFlowForEachAddOnChildRule::AllChildren) + { + ReturnValue = AddOn->ForEachAddOnForClass(InterfaceOrClass, Function); + + if (!ShouldContinueForEach(ReturnValue)) + { + break; + } + } + } + + return ReturnValue; +} + +#if WITH_EDITOR +void UFlowNodeBase::PostLoad() +{ + Super::PostLoad(); + + EnsureNodeDisplayStyle(); +} + +void UFlowNodeBase::SetGraphNode(UEdGraphNode* NewGraphNode) +{ + GraphNode = NewGraphNode; + + UpdateNodeConfigText(); +} + +void UFlowNodeBase::SetCanDelete(const bool CanDelete) +{ + bCanDelete = CanDelete; +} + +void UFlowNodeBase::SetupForEditing(UEdGraphNode& EdGraphNode) +{ + SetGraphNode(&EdGraphNode); + + // Refresh the Config text when setting up this FlowNodeBase for editing + UpdateNodeConfigText(); +} + +void UFlowNodeBase::FixNode(UEdGraphNode* NewGraphNode) +{ + // Fix any node pointers that may be out of date + if (NewGraphNode) + { + GraphNode = NewGraphNode; + } +} + +void UFlowNodeBase::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + if (!PropertyChangedEvent.Property) + { + return; + } + + const FName PropertyName = PropertyChangedEvent.GetPropertyName(); + if (PropertyName == GET_MEMBER_NAME_CHECKED(UFlowNode, AddOns)) + { + // Potentially need to rebuild the pins from the AddOns of this node + OnReconstructionRequested.ExecuteIfBound(); + } + + UpdateNodeConfigText(); +} +#endif + +FString UFlowNodeBase::GetStatusString() const +{ + return K2_GetStatusString(); +} + +#if WITH_EDITOR +FString UFlowNodeBase::GetNodeCategory() const +{ + if (GetClass()->ClassGeneratedBy) + { + const FString& BlueprintCategory = Cast(GetClass()->ClassGeneratedBy)->BlueprintCategory; + if (!BlueprintCategory.IsEmpty()) + { + return BlueprintCategory; + } + } + + return Category; +} + +bool UFlowNodeBase::GetDynamicTitleColor(FLinearColor& OutColor) const +{ + // Legacy asset support for NodeStyle == EFlowNodeStyle::Custom + if (NodeDisplayStyle == FlowNodeStyle::Custom || NodeStyle == EFlowNodeStyle::Custom) + { + OutColor = NodeColor; + return true; + } + + return false; +} + +FText UFlowNodeBase::GetGeneratedDisplayName() const +{ + static const FName NAME_GeneratedDisplayName(TEXT("GeneratedDisplayName")); + + if (GetClass()->ClassGeneratedBy) + { + UClass* Class = Cast(GetClass()->ClassGeneratedBy)->GeneratedClass; + return Class->GetMetaDataText(NAME_GeneratedDisplayName); + } + + return GetClass()->GetMetaDataText(NAME_GeneratedDisplayName); +} + +void UFlowNodeBase::EnsureNodeDisplayStyle() +{ + // todo: remove in Flow 2.2 + + // Backward compatibility update to convert NodeStyle to NodeDisplayStyle + FLOW_ASSERT_ENUM_MAX(EFlowNodeStyle, 7); + + const FGameplayTag NodeDisplayStylePrev = NodeDisplayStyle; + + switch (NodeStyle) + { + case EFlowNodeStyle::Condition: + { + NodeDisplayStyle = FlowNodeStyle::Condition; + } + break; + case EFlowNodeStyle::Default: + { + NodeDisplayStyle = FlowNodeStyle::Default; + } + break; + case EFlowNodeStyle::InOut: + { + NodeDisplayStyle = FlowNodeStyle::InOut; + } + break; + case EFlowNodeStyle::Latent: + { + NodeDisplayStyle = FlowNodeStyle::Latent; + } + break; + case EFlowNodeStyle::Logic: + { + NodeDisplayStyle = FlowNodeStyle::Logic; + } + break; + case EFlowNodeStyle::SubGraph: + { + NodeDisplayStyle = FlowNodeStyle::SubGraph; + } + break; + case EFlowNodeStyle::Custom: + { + NodeDisplayStyle = FlowNodeStyle::Custom; + } + break; + default: break; + } + + if (GEditor != nullptr && NodeDisplayStyle != NodeDisplayStylePrev) + { + NodeStyle = EFlowNodeStyle::Invalid; + Modify(); + } +} + +FString UFlowNodeBase::GetNodeDescription() const +{ + return K2_GetNodeDescription(); +} + +FString UFlowNodeBase::GetAddOnDescriptions() const +{ + FString Result; + + for (const UFlowNodeBase* Addon : AddOns) + { + const FString& Description = Addon->GetNodeDescription(); + if (!Description.IsEmpty()) + { + Result.Append(Description).Append(LINE_TERMINATOR); + } + } + + return Result; +} + +bool UFlowNodeBase::CanModifyFlowDataPinType() const +{ + return !IsPlacedInFlowAsset() || IsFlowNamedPropertiesSupplier(); +} + +bool UFlowNodeBase::ShowFlowDataPinValueInputPinCheckbox() const +{ + const bool bIsPlacedInFlowAsset = IsPlacedInFlowAsset(); + return !bIsPlacedInFlowAsset; +} + +bool UFlowNodeBase::ShowFlowDataPinValueClassFilter(const FFlowDataPinValue* Value) const +{ + const bool bIsPlacedInFlowAsset = IsPlacedInFlowAsset(); + const bool bIsFlowNamedPropertiesSupplier = IsFlowNamedPropertiesSupplier(); + return !bIsPlacedInFlowAsset || bIsFlowNamedPropertiesSupplier; +} + +bool UFlowNodeBase::CanEditFlowDataPinValueClassFilter(const FFlowDataPinValue* Value) const +{ + const bool bIsPlacedInFlowAsset = IsPlacedInFlowAsset(); + const bool bIsFlowNamedPropertiesSupplier = IsFlowNamedPropertiesSupplier(); + return !bIsPlacedInFlowAsset || bIsFlowNamedPropertiesSupplier; +} + +bool UFlowNodeBase::IsPlacedInFlowAsset() const +{ + return GetFlowAsset() != nullptr; +} + +bool UFlowNodeBase::IsFlowNamedPropertiesSupplier() const +{ + return Implements(); +} +#endif + +FText UFlowNodeBase::K2_GetNodeTitle_Implementation() const +{ +#if WITH_EDITOR + if (GetClass()->ClassGeneratedBy) + { + const FString& BlueprintTitle = Cast(GetClass()->ClassGeneratedBy)->BlueprintDisplayName; + if (!BlueprintTitle.IsEmpty()) + { + return FText::FromString(BlueprintTitle); + } + } + + static const FName NAME_DisplayName(TEXT("DisplayName")); + if (bDisplayNodeTitleWithoutPrefix && !GetClass()->HasMetaData(NAME_DisplayName)) + { + return GetGeneratedDisplayName(); + } + + return GetClass()->GetDisplayNameText(); +#else + return FText::GetEmpty(); +#endif +} + +FText UFlowNodeBase::K2_GetNodeToolTip_Implementation() const +{ +#if WITH_EDITOR + if (GetClass()->ClassGeneratedBy) + { + const FString& BlueprintToolTip = Cast(GetClass()->ClassGeneratedBy)->BlueprintDescription; + if (!BlueprintToolTip.IsEmpty()) + { + return FText::FromString(BlueprintToolTip); + } + } + + static const FName NAME_Tooltip(TEXT("Tooltip")); + if (bDisplayNodeTitleWithoutPrefix && !GetClass()->HasMetaData(NAME_Tooltip)) + { + return GetGeneratedDisplayName(); + } + + // GetClass()->GetToolTipText() can return meta = (DisplayName = ... ), but ignore BlueprintDisplayName even if it is BP Node + if (GetClass()->ClassGeneratedBy) + { + const FString& BlueprintTitle = Cast(GetClass()->ClassGeneratedBy)->BlueprintDisplayName; + if (!BlueprintTitle.IsEmpty()) + { + return FText::FromString(BlueprintTitle); + } + } + + return GetClass()->GetToolTipText(); +#else + return FText::GetEmpty(); +#endif +} + +FText UFlowNodeBase::GetNodeConfigText() const +{ +#if WITH_EDITORONLY_DATA + return DevNodeConfigText; +#else + return FText::GetEmpty(); +#endif +} + +void UFlowNodeBase::SetNodeConfigText(const FText& NodeConfigText) +{ +#if WITH_EDITOR + if (!NodeConfigText.EqualTo(DevNodeConfigText)) + { + DevNodeConfigText = NodeConfigText; + } +#endif +} + +void UFlowNodeBase::UpdateNodeConfigText_Implementation() +{ +} + +void UFlowNodeBase::LogError(FString Message, const EFlowOnScreenMessageType OnScreenMessageType) const +{ +#if !NO_LOGGING || UE_ENABLE_DEBUG_DRAWING + if (BuildMessage(Message)) + { + // Output Log + UE_LOG(LogFlow, Error, TEXT("%s"), *Message); + +#if WITH_EDITOR + if (GEditor) + { + // Message Log + GetFlowAsset()->GetTemplateAsset()->LogError(Message, this); + } +#endif + +#if UE_ENABLE_DEBUG_DRAWING + // OnScreen Message + if (OnScreenMessageType == EFlowOnScreenMessageType::Permanent) + { + if (UWorld* World = GetWorld()) + { + if (UViewportStatsSubsystem* StatsSubsystem = World->GetSubsystem()) + { + StatsSubsystem->AddDisplayDelegate([WeakThis = TWeakObjectPtr(this), Message](FText& OutText, FLinearColor& OutColor) + { + const UFlowNodeBase* ThisPtr = WeakThis.Get(); + if (ThisPtr && ThisPtr->GetFlowNodeSelfOrOwner() && ThisPtr->GetFlowNodeSelfOrOwner()->GetActivationState() != EFlowNodeState::NeverActivated) + { + OutText = FText::FromString(Message); + OutColor = FLinearColor::Red; + return true; + } + + return false; + }); + } + } + } + else if (OnScreenMessageType == EFlowOnScreenMessageType::Temporary) + { + GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, Message); + } +#endif + } +#endif +} + +void UFlowNodeBase::LogWarning(FString Message) const +{ +#if !NO_LOGGING + if (BuildMessage(Message)) + { + // Output Log + UE_LOG(LogFlow, Warning, TEXT("%s"), *Message); + +#if WITH_EDITOR + if (GEditor) + { + // Message Log + GetFlowAsset()->GetTemplateAsset()->LogWarning(Message, this); + } +#endif + } +#endif +} + +void UFlowNodeBase::LogNote(FString Message) const +{ +#if !NO_LOGGING + if (BuildMessage(Message)) + { + // Output Log + UE_LOG(LogFlow, Log, TEXT("%s"), *Message); + +#if WITH_EDITOR + if (GEditor) + { + // Message Log + GetFlowAsset()->GetTemplateAsset()->LogNote(Message, this); + } +#endif + } +#endif +} + +void UFlowNodeBase::LogVerbose(FString Message) const +{ +#if !NO_LOGGING + if (BuildMessage(Message)) + { + // Output Log + UE_LOG(LogFlow, Verbose, TEXT("%s"), *Message); + } +#endif +} + +#if !NO_LOGGING || UE_ENABLE_DEBUG_DRAWING +bool UFlowNodeBase::BuildMessage(FString& Message) const +{ + const UFlowAsset* FlowAsset = GetFlowAsset(); + if (FlowAsset && FlowAsset->GetTemplateAsset()) // this is runtime log which is should be only called on runtime instances of asset + { + const FString TemplatePath = FlowAsset->GetTemplateAsset()->GetPathName(); + Message.Append(TEXT(" --- node ")).Append(GetName()).Append(TEXT(", asset ")).Append(FPaths::GetPath(TemplatePath) / FPaths::GetBaseFilename(TemplatePath)); + + return true; + } + + return false; +} +#endif + +#if WITH_EDITOR +EDataValidationResult UFlowNodeBase::ValidateNode() +{ + EDataValidationResult ValidationResult = EDataValidationResult::NotValidated; + + if (GetClass()->IsFunctionImplementedInScript(GET_FUNCTION_NAME_CHECKED(UFlowNodeBase, K2_ValidateNode))) + { + ValidationResult = K2_ValidateNode(); + } + + return ValidationResult; +} +#endif + +bool UFlowNodeBase::TryAddValueToFormatNamedArguments(const FFlowNamedDataPinProperty& NamedDataPinProperty, FFormatNamedArguments& InOutArguments) const +{ + if (NamedDataPinProperty.Name.IsNone() || !NamedDataPinProperty.DataPinValue.IsValid()) + { + return false; + } + + const FFlowDataPinValue& DataPinValue = NamedDataPinProperty.DataPinValue.Get(); + + const FFlowPinTypeName PinTypeName = DataPinValue.GetPinTypeName(); + if (PinTypeName.IsNone()) + { + return false; + } + + const FFlowPinType* PinType = FFlowPinType::LookupPinType(PinTypeName); + if (!PinType) + { + return false; + } + + FFormatArgumentValue FormatValue; + if (PinType->ResolveAndFormatPinValue(*this, NamedDataPinProperty.Name, FormatValue)) + { + InOutArguments.Add(NamedDataPinProperty.Name.ToString(), FormatValue); + return true; + } + + return false; +} + +FFlowDataPinResult UFlowNodeBase::TryResolveDataPin(FName PinName) const +{ + FFlowDataPinResult DataPinResult(EFlowDataPinResolveResult::Success); + + const UFlowNode* FlowNode = GetFlowNodeSelfOrOwner(); + UFlowNode::TFlowPinValueSupplierDataArray PinValueSupplierDatas; + if (!FlowNode->TryGetFlowDataPinSupplierDatasForPinName(PinName, PinValueSupplierDatas)) + { + // If we could not build the PinValueDataSuppliers array, + // then the pin must be disconnected and have no default value available. + DataPinResult.Result = EFlowDataPinResolveResult::FailedWithError; + + LogError(FString::Printf(TEXT("DataPin named '%s' could not be supplied with a value."), *PinName.ToString()), EFlowOnScreenMessageType::Temporary); + + return DataPinResult; + } + + // Iterate over the suppliers in inverse order + for (int32 Index = PinValueSupplierDatas.Num() - 1; Index >= 0; --Index) + { + const FFlowPinValueSupplierData& SupplierData = PinValueSupplierDatas[Index]; + + DataPinResult = SupplierData.PinValueSupplier->TrySupplyDataPin(SupplierData.SupplierPinName); + + if (FlowPinType::IsSuccess(DataPinResult.Result)) + { + return DataPinResult; + } + } + + return DataPinResult; +} + +// #FlowDataPinLegacy +FFlowDataPinResult_Bool UFlowNodeBase::TryResolveDataPinAsBool(const FName& PinName) const +{ + FFlowDataPinResult_Bool BoolResolveResult; + BoolResolveResult.Result = TryResolveDataPinValue(PinName, BoolResolveResult.Value); + return BoolResolveResult; +} + +FFlowDataPinResult_Int UFlowNodeBase::TryResolveDataPinAsInt(const FName& PinName) const +{ + FFlowDataPinResult_Int ResolveResult; + int32 Value = 0; + ResolveResult.Result = TryResolveDataPinValue(PinName, Value); + ResolveResult.Value = Value; + return ResolveResult; +} + +FFlowDataPinResult_Float UFlowNodeBase::TryResolveDataPinAsFloat(const FName& PinName) const +{ + FFlowDataPinResult_Float ResolveResult; + float Value = 0.0f; + ResolveResult.Result = TryResolveDataPinValue(PinName, Value); + ResolveResult.Value = Value; + return ResolveResult; +} + +FFlowDataPinResult_Name UFlowNodeBase::TryResolveDataPinAsName(const FName& PinName) const +{ + FFlowDataPinResult_Name ResolveResult; + ResolveResult.Result = TryResolveDataPinValue(PinName, ResolveResult.Value); + return ResolveResult; +} + +FFlowDataPinResult_String UFlowNodeBase::TryResolveDataPinAsString(const FName& PinName) const +{ + FFlowDataPinResult_String ResolveResult; + ResolveResult.Result = TryResolveDataPinValue(PinName, ResolveResult.Value); + return ResolveResult; +} + +FFlowDataPinResult_Text UFlowNodeBase::TryResolveDataPinAsText(const FName& PinName) const +{ + FFlowDataPinResult_Text ResolveResult; + ResolveResult.Result = TryResolveDataPinValue(PinName, ResolveResult.Value); + return ResolveResult; +} + +FFlowDataPinResult_Enum UFlowNodeBase::TryResolveDataPinAsEnum(const FName& PinName) const +{ + const FFlowDataPinResult DataPinResult = TryResolveDataPin(PinName); + if (!FlowPinType::IsSuccess(DataPinResult.Result)) + { + return FFlowDataPinResult_Enum(DataPinResult.Result); + } + + const FFlowDataPinValue_Enum& Wrapper = DataPinResult.ResultValue.Get(); + + if (Wrapper.Values.IsEmpty()) + { + return FFlowDataPinResult_Enum(EFlowDataPinResolveResult::FailedInsufficientValues); + } + + const FFlowDataPinResult_Enum ResolveResult(Wrapper.Values[0], Wrapper.EnumClass.LoadSynchronous()); + return ResolveResult; +} + +FFlowDataPinResult_Vector UFlowNodeBase::TryResolveDataPinAsVector(const FName& PinName) const +{ + FFlowDataPinResult_Vector ResolveResult; + ResolveResult.Result = TryResolveDataPinValue(PinName, ResolveResult.Value); + return ResolveResult; +} + +FFlowDataPinResult_Rotator UFlowNodeBase::TryResolveDataPinAsRotator(const FName& PinName) const +{ + FFlowDataPinResult_Rotator ResolveResult; + ResolveResult.Result = TryResolveDataPinValue(PinName, ResolveResult.Value); + return ResolveResult; +} + +FFlowDataPinResult_Transform UFlowNodeBase::TryResolveDataPinAsTransform(const FName& PinName) const +{ + FFlowDataPinResult_Transform ResolveResult; + ResolveResult.Result = TryResolveDataPinValue(PinName, ResolveResult.Value); + return ResolveResult; +} + +FFlowDataPinResult_GameplayTag UFlowNodeBase::TryResolveDataPinAsGameplayTag(const FName& PinName) const +{ + FFlowDataPinResult_GameplayTag ResolveResult; + ResolveResult.Result = TryResolveDataPinValue(PinName, ResolveResult.Value); + return ResolveResult; +} + +FFlowDataPinResult_GameplayTagContainer UFlowNodeBase::TryResolveDataPinAsGameplayTagContainer(const FName& PinName) const +{ + FFlowDataPinResult_GameplayTagContainer ResolveResult; + ResolveResult.Result = TryResolveDataPinValue(PinName, ResolveResult.Value); + return ResolveResult; +} + +FFlowDataPinResult_InstancedStruct UFlowNodeBase::TryResolveDataPinAsInstancedStruct(const FName& PinName) const +{ + FFlowDataPinResult_InstancedStruct ResolveResult; + ResolveResult.Result = TryResolveDataPinValue(PinName, ResolveResult.Value); + return ResolveResult; +} + +FFlowDataPinResult_Object UFlowNodeBase::TryResolveDataPinAsObject(const FName& PinName) const +{ + FFlowDataPinResult_Object ResolveResult; + TObjectPtr Value = nullptr; + ResolveResult.Result = TryResolveDataPinValue(PinName, Value); + ResolveResult.Value = Value; + return ResolveResult; +} + +FFlowDataPinResult_Class UFlowNodeBase::TryResolveDataPinAsClass(const FName& PinName) const +{ + FFlowDataPinResult_Class ResolveResult; + TObjectPtr Value = nullptr; + ResolveResult.Result = TryResolveDataPinValue(PinName, Value); + ResolveResult.SetValueFromObjectPtr(Value); + return ResolveResult; +} + +// -- diff --git a/Source/Flow/Private/Nodes/FlowNodeBlueprint.cpp b/Source/Flow/Private/Nodes/FlowNodeBlueprint.cpp index 8d9b7969b..5cb028cb0 100644 --- a/Source/Flow/Private/Nodes/FlowNodeBlueprint.cpp +++ b/Source/Flow/Private/Nodes/FlowNodeBlueprint.cpp @@ -2,6 +2,8 @@ #include "Nodes/FlowNodeBlueprint.h" +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNodeBlueprint) + UFlowNodeBlueprint::UFlowNodeBlueprint(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { diff --git a/Source/Flow/Private/Nodes/FlowPin.cpp b/Source/Flow/Private/Nodes/FlowPin.cpp index 3ae2a42d5..41581cc3c 100644 --- a/Source/Flow/Private/Nodes/FlowPin.cpp +++ b/Source/Flow/Private/Nodes/FlowPin.cpp @@ -1,23 +1,37 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #include "Nodes/FlowPin.h" +#include "FlowLogChannels.h" -#if !UE_BUILD_SHIPPING +#include "GameplayTagContainer.h" +#include "Misc/DateTime.h" +#include "Misc/MessageDialog.h" +#include "StructUtils/InstancedStruct.h" +#include "Types/FlowPinType.h" +#include "Types/FlowPinTypesStandard.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowPin) + +#define LOCTEXT_NAMESPACE "FlowPin" + +////////////////////////////////////////////////////////////////////////// +// Pin Record -FString FPinRecord::NoActivations = TEXT("No activations"); +#if !UE_BUILD_SHIPPING FString FPinRecord::PinActivations = TEXT("Pin activations"); FString FPinRecord::ForcedActivation = TEXT(" (forced activation)"); +FString FPinRecord::PassThroughActivation = TEXT(" (pass-through activation)"); FPinRecord::FPinRecord() : Time(0.0f) , HumanReadableTime(FString()) - , bForcedActivation(false) + , ActivationType(EFlowPinActivationType::Default) { } -FPinRecord::FPinRecord(const double InTime, const bool bInForcedActivation) +FPinRecord::FPinRecord(const double InTime, const EFlowPinActivationType InActivationType) : Time(InTime) - , bForcedActivation(bInForcedActivation) + , ActivationType(InActivationType) { const FDateTime SystemTime(FDateTime::Now()); HumanReadableTime = DoubleDigit(SystemTime.GetHour()) + TEXT(".") @@ -30,5 +44,219 @@ FORCEINLINE FString FPinRecord::DoubleDigit(const int32 Number) { return Number > 9 ? FString::FromInt(Number) : TEXT("0") + FString::FromInt(Number); } +#endif + +////////////////////////////////////////////////////////////////////////// +// Flow Pin + +bool FFlowPin::IsExecPin() const +{ + return PinTypeName == FFlowPinType_Exec::GetPinTypeNameStatic(); +} + +bool FFlowPin::IsExecPinCategory(const FName& PC) +{ + return PC == FFlowPinType_Exec::GetPinTypeNameStatic().Name; +} + +const FName FFlowPin::MetadataKey_SourceForOutputFlowPin = "SourceForOutputFlowPin"; +const FName FFlowPin::MetadataKey_DefaultForInputFlowPin = "DefaultForInputFlowPin"; +const FName FFlowPin::MetadataKey_FlowPinType = "FlowPinType"; + +void FFlowPin::SetPinTypeName(const FFlowPinTypeName& InTypeName) +{ + if (PinTypeName == InTypeName) + { + return; + } + + PinTypeName = InTypeName; +} + +void FFlowPin::TrySetStructSubCategoryObjectFromPinType() +{ + if (PinTypeName == FFlowPinType_Vector::GetPinTypeNameStatic()) + { + PinSubCategoryObject = TBaseStructure::Get(); + } + else if (PinTypeName == FFlowPinType_Rotator::GetPinTypeNameStatic()) + { + PinSubCategoryObject = TBaseStructure::Get(); + } + else if (PinTypeName == FFlowPinType_Transform::GetPinTypeNameStatic()) + { + PinSubCategoryObject = TBaseStructure::Get(); + } + else if (PinTypeName == FFlowPinType_GameplayTag::GetPinTypeNameStatic()) + { + PinSubCategoryObject = TBaseStructure::Get(); + } + else if (PinTypeName == FFlowPinType_GameplayTagContainer::GetPinTypeNameStatic()) + { + PinSubCategoryObject = TBaseStructure::Get(); + } + else if (PinTypeName == FFlowPinType_InstancedStruct::GetPinTypeNameStatic()) + { + PinSubCategoryObject = TBaseStructure::Get(); + } + else if (PinTypeName == FFlowPinType_Enum::GetPinTypeNameStatic()) + { + // Clear the PinSubCategoryObject if it is not an Enum + const UObject* PinSubCategoryObjectPtr = PinSubCategoryObject.Get(); + if (PinSubCategoryObjectPtr && !PinSubCategoryObjectPtr->IsA()) + { + PinSubCategoryObject = nullptr; + } + } + else if (PinTypeName == FFlowPinType_Object::GetPinTypeNameStatic()) + { + // Clear the PinSubCategoryObject if it is not an Object + const UObject* PinSubCategoryObjectPtr = PinSubCategoryObject.Get(); + if (PinSubCategoryObjectPtr && !PinSubCategoryObjectPtr->IsA()) + { + PinSubCategoryObject = nullptr; + } + } + else if (PinTypeName == FFlowPinType_Class::GetPinTypeNameStatic()) + { + // Clear the PinSubCategoryObject if it is not a Class + const UObject* PinSubCategoryObjectPtr = PinSubCategoryObject.Get(); + if (PinSubCategoryObjectPtr && !PinSubCategoryObjectPtr->IsA()) + { + PinSubCategoryObject = nullptr; + } + } + else + { + // Clear the PinSubCategoryObject for all PinTypes that do not use it. + PinSubCategoryObject = nullptr; + } +} + +#if WITH_EDITOR +FEdGraphPinType FFlowPin::BuildEdGraphPinType() const +{ + check(!PinTypeName.Name.IsNone()); + + FEdGraphPinType EdGraphPinType; + EdGraphPinType.PinCategory = PinTypeName.Name; + + // TODO (gtaylor) possible future extension for types, to allow sub categories + EdGraphPinType.PinSubCategory = NAME_None; + + EdGraphPinType.PinSubCategoryObject = PinSubCategoryObject; + EdGraphPinType.ContainerType = ContainerType; + return EdGraphPinType; +} + +void FFlowPin::ConfigureFromEdGraphPin(const FEdGraphPinType& EdGraphPinType) +{ + PinTypeName.Name = EdGraphPinType.PinCategory; + PinSubCategoryObject = EdGraphPinType.PinSubCategoryObject; + ContainerType = EdGraphPinType.ContainerType; +} #endif + +const FFlowPinType* FFlowPin::ResolveFlowPinType() const +{ + // TODO (gtaylor) consider caching this in a mutable? + return FFlowPinType::LookupPinType(PinTypeName); +} + +// #FlowDataPinLegacy +FFlowPinTypeName FFlowPin::GetPinTypeNameForLegacyPinType(EFlowPinType PinType) +{ + FLOW_ASSERT_ENUM_MAX(EFlowPinType, 16); + switch (PinType) + { + case EFlowPinType::Exec: + return FFlowPinType_Exec::GetPinTypeNameStatic(); + case EFlowPinType::Bool: + return FFlowPinType_Bool::GetPinTypeNameStatic(); + case EFlowPinType::Int: + return FFlowPinType_Int::GetPinTypeNameStatic(); + case EFlowPinType::Float: + return FFlowPinType_Float::GetPinTypeNameStatic(); + case EFlowPinType::Name: + return FFlowPinType_Name::GetPinTypeNameStatic(); + case EFlowPinType::String: + return FFlowPinType_String::GetPinTypeNameStatic(); + case EFlowPinType::Text: + return FFlowPinType_Text::GetPinTypeNameStatic(); + case EFlowPinType::Enum: + return FFlowPinType_Enum::GetPinTypeNameStatic(); + case EFlowPinType::Vector: + return FFlowPinType_Vector::GetPinTypeNameStatic(); + case EFlowPinType::Rotator: + return FFlowPinType_Rotator::GetPinTypeNameStatic(); + case EFlowPinType::Transform: + return FFlowPinType_Transform::GetPinTypeNameStatic(); + case EFlowPinType::GameplayTag: + return FFlowPinType_GameplayTag::GetPinTypeNameStatic(); + case EFlowPinType::GameplayTagContainer: + return FFlowPinType_GameplayTagContainer::GetPinTypeNameStatic(); + case EFlowPinType::InstancedStruct: + return FFlowPinType_InstancedStruct::GetPinTypeNameStatic(); + case EFlowPinType::Object: + return FFlowPinType_Object::GetPinTypeNameStatic(); + case EFlowPinType::Class: + return FFlowPinType_Class::GetPinTypeNameStatic(); + default: + return FFlowPinTypeName(); + } +} +// -- + +#if WITH_EDITOR + +FText FFlowPin::BuildHeaderText() const +{ + const FText PinNameToUse = !PinFriendlyName.IsEmpty() ? PinFriendlyName : FText::FromName(PinName); + + if (IsExecPin()) + { + return PinNameToUse; + } + else + { + return FText::Format(LOCTEXT("FlowPinNameAndType", "{0} ({1})"), {PinNameToUse, FText::FromString(PinTypeName.ToString())}); + } +} + +bool FFlowPin::ValidateEnum(const UEnum& EnumType) +{ + // This function copied and adapted from UBlackboardKeyType_Enum::ValidateEnum(), + // because it is inaccessible w/o AIModule and private access + + bool bAllValid = true; + + // Do not test the max value (if present) since it is an internal value and users don't have access to it + const int32 NumEnums = EnumType.ContainsExistingMax() ? EnumType.NumEnums() - 1 : EnumType.NumEnums(); + for (int32 i = 0; i < NumEnums; i++) + { + // Enum data type is uint8 (based on UBlackboardKeyType_Enum::ValidateEnum()) + typedef uint8 FDataType; + + const int64 Value = EnumType.GetValueByIndex(i); + if (Value < std::numeric_limits::min() || Value > std::numeric_limits::max()) + { + UE_LOG(LogFlow, Error, TEXT("'%s' value %lld is outside the range of supported key values for enum [%d, %d].") + , *EnumType.GenerateFullEnumName(*EnumType.GetDisplayNameTextByIndex(i).ToString()) + , Value, std::numeric_limits::min(), std::numeric_limits::max()); + + bAllValid = false; + } + } + + if (!bAllValid) + { + FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("Unsupported enumeration" + , "Specified enumeration contains one or more values outside supported value range for enum keys and can not be used for Flow Data Pins. See log for details.")); + } + + return bAllValid; +} +#endif //WITH_EDITOR + +#undef LOCTEXT_NAMESPACE diff --git a/Source/Flow/Private/Nodes/Graph/FlowNode_BlueprintDataPinSupplierBase.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_BlueprintDataPinSupplierBase.cpp new file mode 100644 index 000000000..eea9b74bd --- /dev/null +++ b/Source/Flow/Private/Nodes/Graph/FlowNode_BlueprintDataPinSupplierBase.cpp @@ -0,0 +1,25 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Nodes/Graph/FlowNode_BlueprintDataPinSupplierBase.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_BlueprintDataPinSupplierBase) + +UFlowNode_BlueprintDataPinSupplierBase::UFlowNode_BlueprintDataPinSupplierBase() +{ +#if WITH_EDITOR + NodeDisplayStyle = FlowNodeStyle::Default; + Category = TEXT("Graph"); +#endif + + AllowedSignalModes = {EFlowSignalMode::Enabled, EFlowSignalMode::Disabled}; +} + +FFlowDataPinResult UFlowNode_BlueprintDataPinSupplierBase::TrySupplyDataPin(FName PinName) const +{ + return BP_TrySupplyDataPin(PinName); +} + +FFlowDataPinResult UFlowNode_BlueprintDataPinSupplierBase::BP_TrySupplyDataPin_Implementation(FName PinName) const +{ + return Super::TrySupplyDataPin(PinName); +} diff --git a/Source/Flow/Private/Nodes/Graph/FlowNode_Checkpoint.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_Checkpoint.cpp new file mode 100644 index 000000000..8a7ccdd67 --- /dev/null +++ b/Source/Flow/Private/Nodes/Graph/FlowNode_Checkpoint.cpp @@ -0,0 +1,41 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Nodes/Graph/FlowNode_Checkpoint.h" +#include "FlowSubsystem.h" + +#include "Kismet/GameplayStatics.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_Checkpoint) + +UFlowNode_Checkpoint::UFlowNode_Checkpoint() + : bUseAsyncSave(false) +{ +#if WITH_EDITOR + Category = TEXT("Graph"); +#endif +} + +void UFlowNode_Checkpoint::ExecuteInput(const FName& PinName) +{ + if (GetFlowSubsystem()) + { + UFlowSaveGame* NewSaveGame = Cast(UGameplayStatics::CreateSaveGameObject(UFlowSaveGame::StaticClass())); + GetFlowSubsystem()->OnGameSaved(NewSaveGame); + + if (bUseAsyncSave) + { + UGameplayStatics::AsyncSaveGameToSlot(NewSaveGame, NewSaveGame->SaveSlotName, 0); + } + else + { + UGameplayStatics::SaveGameToSlot(NewSaveGame, NewSaveGame->SaveSlotName, 0); + } + } + + TriggerFirstOutput(true); +} + +void UFlowNode_Checkpoint::OnLoad_Implementation() +{ + TriggerFirstOutput(true); +} diff --git a/Source/Flow/Private/Nodes/Graph/FlowNode_CustomEventBase.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_CustomEventBase.cpp new file mode 100644 index 000000000..a5445dae5 --- /dev/null +++ b/Source/Flow/Private/Nodes/Graph/FlowNode_CustomEventBase.cpp @@ -0,0 +1,53 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Nodes/Graph/FlowNode_CustomEventBase.h" +#include "FlowSettings.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_CustomEventBase) + +UFlowNode_CustomEventBase::UFlowNode_CustomEventBase() +{ +#if WITH_EDITOR + Category = TEXT("Graph"); + NodeDisplayStyle = FlowNodeStyle::InOut; +#endif + + AllowedSignalModes = {EFlowSignalMode::Enabled, EFlowSignalMode::Disabled}; +} + +void UFlowNode_CustomEventBase::SetEventName(const FName& InEventName) +{ + if (EventName != InEventName) + { + EventName = InEventName; + +#if WITH_EDITOR + // Must reconstruct the visual representation if anything that is included in AdaptiveNodeTitles changes + OnReconstructionRequested.ExecuteIfBound(); +#endif + } +} + +#if WITH_EDITOR + +FString UFlowNode_CustomEventBase::GetNodeDescription() const +{ + if (GetDefault()->bUseAdaptiveNodeTitles) + { + return Super::GetNodeDescription(); + } + + return EventName.ToString(); +} + +EDataValidationResult UFlowNode_CustomEventBase::ValidateNode() +{ + if (EventName.IsNone()) + { + ValidationLog.Error(TEXT("Event Name is empty!"), this); + return EDataValidationResult::Invalid; + } + + return EDataValidationResult::Valid; +} +#endif diff --git a/Source/Flow/Private/Nodes/Graph/FlowNode_CustomInput.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_CustomInput.cpp new file mode 100644 index 000000000..173b482d3 --- /dev/null +++ b/Source/Flow/Private/Nodes/Graph/FlowNode_CustomInput.cpp @@ -0,0 +1,38 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Nodes/Graph/FlowNode_CustomInput.h" +#include "FlowSettings.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_CustomInput) + +#define LOCTEXT_NAMESPACE "FlowNode_CustomInput" + +UFlowNode_CustomInput::UFlowNode_CustomInput() +{ + InputPins.Empty(); +} + +void UFlowNode_CustomInput::ExecuteInput(const FName& PinName) +{ + TriggerFirstOutput(true); +} + +void UFlowNode_CustomInput::PostEditImport() +{ + // Reset EventName after duplicating or copy/pasting + EventName = NAME_None; +} + +#if WITH_EDITOR +FText UFlowNode_CustomInput::K2_GetNodeTitle_Implementation() const +{ + if (!EventName.IsNone() && GetDefault()->bUseAdaptiveNodeTitles) + { + return FText::Format(LOCTEXT("CustomInputTitle", "{0} Input"), {FText::FromString(EventName.ToString())}); + } + + return Super::K2_GetNodeTitle_Implementation(); +} +#endif + +#undef LOCTEXT_NAMESPACE diff --git a/Source/Flow/Private/Nodes/Graph/FlowNode_CustomOutput.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_CustomOutput.cpp new file mode 100644 index 000000000..70cef4361 --- /dev/null +++ b/Source/Flow/Private/Nodes/Graph/FlowNode_CustomOutput.cpp @@ -0,0 +1,66 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Nodes/Graph/FlowNode_CustomOutput.h" +#include "FlowAsset.h" +#include "FlowSettings.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_CustomOutput) + +#define LOCTEXT_NAMESPACE "FlowNode_CustomOutput" + +UFlowNode_CustomOutput::UFlowNode_CustomOutput() +{ + OutputPins.Empty(); +} + +void UFlowNode_CustomOutput::ExecuteInput(const FName& PinName) +{ + UFlowAsset* FlowAsset = GetFlowAsset(); + check(IsValid(FlowAsset)); + + if (EventName.IsNone()) + { + LogWarning(FString::Printf(TEXT("Attempted to trigger a CustomOutput (Node %s, Asset %s), with no EventName"), + *GetName(), + *FlowAsset->GetPathName())); + } + else if (!FlowAsset->TryFindCustomOutputNodeByEventName(EventName)) + { + const TArray OutputNames = FlowAsset->GatherCustomOutputNodeEventNames(); + FString CustomOutputsString; + + for (const FName& OutputName : OutputNames) + { + if (!CustomOutputsString.IsEmpty()) + { + CustomOutputsString += TEXT(", "); + } + + CustomOutputsString += OutputName.ToString(); + } + + LogWarning(FString::Printf(TEXT("Attempted to trigger a CustomOutput (Node %s, Asset %s), with EventName %s, which is not a listed CustomOutput { %s }"), + *GetName(), + *FlowAsset->GetPathName(), + *EventName.ToString(), + *CustomOutputsString)); + } + else + { + FlowAsset->TriggerCustomOutput(EventName); + } +} + +#if WITH_EDITOR +FText UFlowNode_CustomOutput::K2_GetNodeTitle_Implementation() const +{ + if (!EventName.IsNone() && GetDefault()->bUseAdaptiveNodeTitles) + { + return FText::Format(LOCTEXT("CustomOutputTitle", "{0} Output"), {FText::FromString(EventName.ToString())}); + } + + return Super::K2_GetNodeTitle_Implementation(); +} +#endif + +#undef LOCTEXT_NAMESPACE diff --git a/Source/Flow/Private/Nodes/Graph/FlowNode_DefineProperties.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_DefineProperties.cpp new file mode 100644 index 000000000..bf1498a12 --- /dev/null +++ b/Source/Flow/Private/Nodes/Graph/FlowNode_DefineProperties.cpp @@ -0,0 +1,136 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Nodes/Graph/FlowNode_DefineProperties.h" +#include "Types/FlowPinTypesStandard.h" +#include "Types/FlowDataPinValuesStandard.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_DefineProperties) + +UFlowNode_DefineProperties::UFlowNode_DefineProperties() +{ +#if WITH_EDITOR + NodeDisplayStyle = FlowNodeStyle::Terminal; + Category = TEXT("Graph"); +#endif + + InputPins.Empty(); + OutputPins.Empty(); + + AllowedSignalModes = {EFlowSignalMode::Enabled, EFlowSignalMode::Disabled}; +} + +void UFlowNode_DefineProperties::PostLoad() +{ + Super::PostLoad(); + + if (!HasAnyFlags(RF_ArchetypeObject | RF_ClassDefaultObject)) + { + // Migrate the named properties over to the new structs + + for (FFlowNamedDataPinProperty& NamedProperty : NamedProperties) + { + NamedProperty.FixupDataPinProperty(); + } + } +} + +#if WITH_EDITOR +bool UFlowNode_DefineProperties::SupportsContextPins() const +{ + return Super::SupportsContextPins() || !NamedProperties.IsEmpty(); +} + +void UFlowNode_DefineProperties::PostEditChangeChainProperty(FPropertyChangedChainEvent& PropertyChainEvent) +{ + Super::PostEditChangeChainProperty(PropertyChainEvent); + + if (PropertyChainEvent.PropertyChain.Num() == 0) + { + return; + } + + auto& Property = PropertyChainEvent.PropertyChain.GetActiveMemberNode()->GetValue(); + + // The DetailsCustomization for FFlowDataPinValue_Enum isn't being called when using an InstancedStruct + // so we need to call OnEnumNameChanged refresh by hand... + if (PropertyChainEvent.ChangeType == EPropertyChangeType::ValueSet && + Property->GetFName() == GET_MEMBER_NAME_CHECKED(FFlowDataPinOutputProperty_Enum, EnumName)) + { + for (FFlowNamedDataPinProperty& NamedProperty : NamedProperties) + { + if (!NamedProperty.IsValid()) + { + continue; + } + + const FFlowDataPinValue& FlowDataPinProperty = NamedProperty.DataPinValue.Get(); + + if (FlowDataPinProperty.GetPinTypeName() == FFlowPinType_Enum::GetPinTypeNameStatic()) + { + FFlowDataPinValue_Enum& EnumProperty = NamedProperty.DataPinValue.GetMutable(); + EnumProperty.OnEnumNameChanged(); + } + } + } + + constexpr EPropertyChangeType::Type RelevantChangeTypesForReconstructionMask = + EPropertyChangeType::Unspecified | + EPropertyChangeType::ArrayAdd | + EPropertyChangeType::ArrayRemove | + EPropertyChangeType::ArrayClear | + EPropertyChangeType::ValueSet | + EPropertyChangeType::Redirected | + EPropertyChangeType::ArrayMove; + + const uint32 PropertyChangedTypeFlags = (PropertyChainEvent.ChangeType & RelevantChangeTypesForReconstructionMask); + const bool bIsRelevantChangeTypeForReconstruction = PropertyChangedTypeFlags != 0; + const bool bChangedOutputProperties = Property->GetFName() == GET_MEMBER_NAME_CHECKED(UFlowNode_DefineProperties, NamedProperties); + if (bIsRelevantChangeTypeForReconstruction && bChangedOutputProperties) + { + OnReconstructionRequested.ExecuteIfBound(); + } +} +#endif // WITH_EDITOR + +bool UFlowNode_DefineProperties::TryFormatTextWithNamedPropertiesAsParameters(const FText& FormatText, FText& OutFormattedText) const +{ + if (NamedProperties.IsEmpty()) + { + return false; + } + + FFormatNamedArguments Arguments; + for (const FFlowNamedDataPinProperty& NamedProperty : NamedProperties) + { + if (!NamedProperty.Name.IsValid()) + { + LogWarning(TEXT("Could not format text with a nameless named property")); + } + else if (!TryAddValueToFormatNamedArguments(NamedProperty, Arguments)) + { + LogWarning(FString::Printf(TEXT("Could not format text for named property %s"), *NamedProperty.Name.ToString())); + } + } + + OutFormattedText = FText::Format(FormatText, Arguments); + + return true; +} + +#if WITH_EDITOR +void UFlowNode_DefineProperties::OnPostEditEnsureAllNamedPropertiesPinDirection(const FProperty& Property, bool bIsInput) +{ + if (Property.GetFName() == GET_MEMBER_NAME_CHECKED(ThisClass, NamedProperties)) + { + for (FFlowNamedDataPinProperty& NamedProperty : NamedProperties) + { + const UScriptStruct* ScriptStruct = NamedProperty.DataPinValue.GetScriptStruct(); + if (IsValid(ScriptStruct) && ScriptStruct->IsChildOf()) + { + FFlowDataPinValue& Value = NamedProperty.DataPinValue.GetMutable(); + Value.bIsInputPin = bIsInput; + } + } + } +} +#endif \ No newline at end of file diff --git a/Source/Flow/Private/Nodes/Graph/FlowNode_Finish.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_Finish.cpp new file mode 100644 index 000000000..ea34286d0 --- /dev/null +++ b/Source/Flow/Private/Nodes/Graph/FlowNode_Finish.cpp @@ -0,0 +1,22 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Nodes/Graph/FlowNode_Finish.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_Finish) + +UFlowNode_Finish::UFlowNode_Finish() +{ +#if WITH_EDITOR + Category = TEXT("Graph"); + NodeDisplayStyle = FlowNodeStyle::InOut; +#endif + + OutputPins = {}; + AllowedSignalModes = {EFlowSignalMode::Enabled, EFlowSignalMode::Disabled}; +} + +void UFlowNode_Finish::ExecuteInput(const FName& PinName) +{ + // this will call FinishFlow() + Finish(); +} diff --git a/Source/Flow/Private/Nodes/Graph/FlowNode_FormatText.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_FormatText.cpp new file mode 100644 index 000000000..ea6babf0d --- /dev/null +++ b/Source/Flow/Private/Nodes/Graph/FlowNode_FormatText.cpp @@ -0,0 +1,87 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Nodes/Graph/FlowNode_FormatText.h" +#include "Types/FlowPinTypesStandard.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_FormatText) + +#define LOCTEXT_NAMESPACE "FlowNode_FormatText" + +const FName UFlowNode_FormatText::OUTPIN_TextOutput("Formatted Text"); + +UFlowNode_FormatText::UFlowNode_FormatText() +{ +#if WITH_EDITOR + Category = TEXT("Graph"); + NodeDisplayStyle = FlowNodeStyle::Terminal; +#endif + + OutputPins.Add(FFlowPin(OUTPIN_TextOutput, FFlowPinType_Text::GetPinTypeNameStatic())); +} + +FFlowDataPinResult UFlowNode_FormatText::TrySupplyDataPin(FName PinName) const +{ + if (PinName == OUTPIN_TextOutput) + { + FText FormattedText; + const EFlowDataPinResolveResult FormatResult = TryResolveFormattedText(PinName, FormattedText); + + if (FlowPinType::IsSuccess(FormatResult)) + { + return FFlowDataPinResult(FFlowDataPinValue_Text(FormattedText)); + } + else + { + return FFlowDataPinResult(FormatResult); + } + } + + return Super::TrySupplyDataPin(PinName); +} + +EFlowDataPinResolveResult UFlowNode_FormatText::TryResolveFormattedText(const FName& PinName, FText& OutFormattedText) const +{ + FText ResolvedFormatText = FormatText; + const EFlowDataPinResolveResult ResolveResult = TryResolveDataPinValue(GET_MEMBER_NAME_CHECKED(ThisClass, FormatText), ResolvedFormatText); + + if (TryFormatTextWithNamedPropertiesAsParameters(ResolvedFormatText, OutFormattedText)) + { + return EFlowDataPinResolveResult::Success; + } + else + { + LogError(FString::Printf(TEXT("Could not format text '%s' with properties as parameters"), *ResolvedFormatText.ToString()), EFlowOnScreenMessageType::Temporary); + + return EFlowDataPinResolveResult::FailedWithError; + } +} + +#if WITH_EDITOR +void UFlowNode_FormatText::PostEditChangeChainProperty(FPropertyChangedChainEvent& PropertyChainEvent) +{ + const auto& Property = PropertyChainEvent.PropertyChain.GetActiveMemberNode()->GetValue(); + constexpr bool bIsInput = true; + OnPostEditEnsureAllNamedPropertiesPinDirection(*Property, bIsInput); + + Super::PostEditChangeChainProperty(PropertyChainEvent); +} + +void UFlowNode_FormatText::UpdateNodeConfigText_Implementation() +{ + constexpr bool bErrorIfInputPinNotFound = true; + FConnectedPin ConnectedPin; + const bool bIsInputConnected = FindFirstInputPinConnection(GET_MEMBER_NAME_CHECKED(ThisClass, FormatText), bErrorIfInputPinNotFound, ConnectedPin); + + if (bIsInputConnected) + { + SetNodeConfigText(FText::Format(LOCTEXT("FormatTextFromPin", "Format from: {0}"), { FText::FromString(ConnectedPin.PinName.ToString()) })); + } + else + { + SetNodeConfigText(FormatText); + } +} + +#endif + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/Flow/Private/Nodes/Graph/FlowNode_Start.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_Start.cpp new file mode 100644 index 000000000..5b974fa12 --- /dev/null +++ b/Source/Flow/Private/Nodes/Graph/FlowNode_Start.cpp @@ -0,0 +1,60 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Nodes/Graph/FlowNode_Start.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_Start) + +UFlowNode_Start::UFlowNode_Start() +{ +#if WITH_EDITOR + Category = TEXT("Graph"); + NodeDisplayStyle = FlowNodeStyle::InOut; + bCanDelete = bCanDuplicate = false; +#endif + + OutputPins = { UFlowNode::DefaultOutputPin }; +} + +void UFlowNode_Start::ExecuteInput(const FName& PinName) +{ + TriggerFirstOutput(true); +} + +void UFlowNode_Start::SetDataPinValueSupplier(IFlowDataPinValueSupplierInterface* DataPinValueSupplier) +{ + FlowDataPinValueSupplierInterface = Cast(DataPinValueSupplier); +} + +#if WITH_EDITOR + +bool UFlowNode_Start::TryAppendExternalInputPins(TArray& InOutPins) const +{ + // Add pins for all of the Flow DataPin Properties + for (const FFlowNamedDataPinProperty& DataPinProperty : NamedProperties) + { + if (DataPinProperty.IsValid()) + { + InOutPins.AddUnique(DataPinProperty.CreateFlowPin()); + } + } + + return !NamedProperties.IsEmpty(); +} + +#endif // WITH_EDITOR + +FFlowDataPinResult UFlowNode_Start::TrySupplyDataPin(FName PinName) const +{ + if (FlowDataPinValueSupplierInterface) + { + FFlowDataPinResult SuppliedResult = FlowDataPinValueSupplierInterface->TrySupplyDataPin(PinName); + + if (FlowPinType::IsSuccess(SuppliedResult.Result)) + { + return SuppliedResult; + } + } + + return Super::TrySupplyDataPin(PinName); +} + diff --git a/Source/Flow/Private/Nodes/Graph/FlowNode_SubGraph.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_SubGraph.cpp new file mode 100644 index 000000000..3c07698f7 --- /dev/null +++ b/Source/Flow/Private/Nodes/Graph/FlowNode_SubGraph.cpp @@ -0,0 +1,327 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Nodes/Graph/FlowNode_SubGraph.h" + +#include "FlowAsset.h" +#include "FlowSettings.h" +#include "FlowSubsystem.h" +#include "Interfaces/FlowNodeWithExternalDataPinSupplierInterface.h" +#include "Types/FlowAutoDataPinsWorkingData.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_SubGraph) + +#define LOCTEXT_NAMESPACE "FlowNode_SubGraph" + +FFlowPin UFlowNode_SubGraph::StartPin(TEXT("Start")); +FFlowPin UFlowNode_SubGraph::FinishPin(TEXT("Finish")); +const FName UFlowNode_SubGraph::AssetParams_MemberName = GET_MEMBER_NAME_CHECKED(ThisClass, AssetParams); + +UFlowNode_SubGraph::UFlowNode_SubGraph() + : bCanInstanceIdenticalAsset(false) +{ +#if WITH_EDITOR + Category = TEXT("Graph"); + NodeDisplayStyle = FlowNodeStyle::SubGraph; + + AllowedAssignedAssetClasses = {UFlowAsset::StaticClass()}; +#endif + + InputPins = {StartPin}; + OutputPins = {FinishPin}; +} + +bool UFlowNode_SubGraph::CanBeAssetInstanced() const +{ + return !Asset.IsNull() && (bCanInstanceIdenticalAsset || Asset.ToString() != GetFlowAsset()->GetTemplateAsset()->GetPathName()); +} + +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() +{ + if (CanBeAssetInstanced() && GetFlowSubsystem()) + { + GetFlowSubsystem()->RemoveSubFlow(this, EFlowFinishPolicy::Abort); + } +} + +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()) + { + LogError(TEXT("Missing Flow Asset")); + } + else + { + LogError(FString::Printf(TEXT("Asset %s cannot be instance, probably is the same as the asset owning this SubGraph node."), *Asset.ToString())); + } + + Finish(); + return; + } + + if (PinName == TEXT("Start")) + { + if (GetFlowSubsystem()) + { + GetFlowSubsystem()->CreateSubFlow(this); + } + } + else if (!PinName.IsNone()) + { + GetFlowAsset()->TriggerCustomInput_FromSubGraph(this, PinName); + } +} + +void UFlowNode_SubGraph::Cleanup() +{ + if (CanBeAssetInstanced() && GetFlowSubsystem()) + { + GetFlowSubsystem()->RemoveSubFlow(this, EFlowFinishPolicy::Keep); + } + + Super::Cleanup(); +} + +void UFlowNode_SubGraph::ForceFinishNode() +{ + TriggerFirstOutput(true); +} + +void UFlowNode_SubGraph::OnLoad_Implementation() +{ + if (!SavedAssetInstanceName.IsEmpty() && !Asset.IsNull()) + { + GetFlowSubsystem()->LoadSubFlow(this, SavedAssetInstanceName); + SavedAssetInstanceName = FString(); + } +} + +#if WITH_EDITOR + +FText UFlowNode_SubGraph::K2_GetNodeTitle_Implementation() const +{ + if (GetDefault()->bUseAdaptiveNodeTitles && !Asset.IsNull()) + { + return FText::Format(LOCTEXT("SubGraphTitle", "{0}\n{1}"), {Super::K2_GetNodeTitle_Implementation(), FText::FromString(Asset.ToSoftObjectPath().GetAssetName())}); + } + + return Super::K2_GetNodeTitle_Implementation(); +} + +FString UFlowNode_SubGraph::GetNodeDescription() const +{ + if (!GetDefault()->bUseAdaptiveNodeTitles && !Asset.IsNull()) + { + return Asset.ToSoftObjectPath().GetAssetName(); + } + + return Super::GetNodeDescription();; +} + +UObject* UFlowNode_SubGraph::GetAssetToEdit() +{ + return Asset.IsNull() ? nullptr : Asset.LoadSynchronous(); +} + +EDataValidationResult UFlowNode_SubGraph::ValidateNode() +{ + if (Asset.IsNull()) + { + ValidationLog.Error(TEXT("Flow Asset not assigned or invalid!"), this); + return EDataValidationResult::Invalid; + } + + return EDataValidationResult::Valid; +} + +TArray UFlowNode_SubGraph::GetContextInputs() const +{ + TArray ContextInputPins = Super::GetContextInputs(); + + if (!Asset.IsNull()) + { + (void)Asset.LoadSynchronous(); + if (Asset.IsValid()) + { + for (const FName& PinName : Asset->GetCustomInputs()) + { + if (!PinName.IsNone()) + { + ContextInputPins.AddUnique(FFlowPin(PinName)); + } + } + } + } + + return ContextInputPins; +} + +TArray UFlowNode_SubGraph::GetContextOutputs() const +{ + TArray ContextOutputPins = Super::GetContextOutputs(); + + if (!Asset.IsNull()) + { + (void)Asset.LoadSynchronous(); + if (Asset.IsValid()) + { + for (const FName& PinName : Asset->GetCustomOutputs()) + { + if (!PinName.IsNone()) + { + ContextOutputPins.AddUnique(FFlowPin(PinName)); + } + } + } + } + + return ContextOutputPins; +} + +void UFlowNode_SubGraph::AutoGenerateDataPins(FFlowDataPinValueOwner& ValueOwner, FFlowAutoDataPinsWorkingData& InOutWorkingData) +{ + Super::AutoGenerateDataPins(ValueOwner, InOutWorkingData); + + if (Asset.IsNull()) + { + return; + } + + (void)Asset.LoadSynchronous(); + if (!Asset.IsValid()) + { + return; + } + + for (TPair>& Node : Asset->Nodes) + { + if (const IFlowNodeWithExternalDataPinSupplierInterface* ExternalPinSuppliedNode = Cast(Node.Value)) + { + // If subgraph's current Flow Node uses an external data supplier (that will be this subgraph node), + // We need to scrape the external input pins from the node and add them to our auto-generated pins list + + TArray ExternalInputPins; + if (ExternalPinSuppliedNode->TryAppendExternalInputPins(ExternalInputPins)) + { + const int32 NewNum = InOutWorkingData.AutoInputDataPinsNext.Num() + ExternalInputPins.Num(); + InOutWorkingData.AutoInputDataPinsNext.Reserve(NewNum); + + for (const FFlowPin& FlowPin : ExternalInputPins) + { + InOutWorkingData.AutoInputDataPinsNext.Add(FFlowPinSourceData(FlowPin, ValueOwner)); + } + } + } + } +} + +FFlowDataPinResult UFlowNode_SubGraph::TrySupplyDataPin(FName PinName) const +{ + if (PinName == AssetParams_MemberName) + { + // Prevent infinite recursion by sourcing the AssetParams pin directly + // (otherwise, it would attempt to resolve it below and infinitely crash our stack. + // don't ask me how I know). + return Super::TrySupplyDataPin(PinName); + } + + if (!IsInputConnected(PinName)) + { + const bool bHasAssetParams = IsInputConnected(AssetParams_MemberName) || !AssetParams.IsNull(); + if (bHasAssetParams) + { + // If not connected, we can source the value from the asset data params (if available) + TObjectPtr Value = nullptr; + const EFlowDataPinResolveResult ResultEnum = Super::TryResolveDataPinValue(AssetParams_MemberName, Value); + if (FlowPinType::IsSuccess(ResultEnum) && IsValid(Value)) + { + if (const IFlowDataPinValueSupplierInterface* SupplierInterface = Cast(Value)) + { + return SupplierInterface->TrySupplyDataPin(PinName); + } + else + { + LogError(FString::Printf(TEXT("Could not cast object %s to IFlowDataPinValueSupplierInterface! This is unexpected."), *Value->GetName())); + + return FFlowDataPinResult(EFlowDataPinResolveResult::FailedWithError); + } + } + } + } + + // Prefer the standard lookup if the pin is connected + // (or if there is no FlowAssetParams to ask) + return Super::TrySupplyDataPin(PinName); +} + +void UFlowNode_SubGraph::PostLoad() +{ + Super::PostLoad(); + + SubscribeToAssetChanges(); +} + +void UFlowNode_SubGraph::PreEditChange(FProperty* PropertyAboutToChange) +{ + Super::PreEditChange(PropertyAboutToChange); + + if (PropertyAboutToChange->GetFName() == GET_MEMBER_NAME_CHECKED(UFlowNode_SubGraph, Asset)) + { + if (Asset) + { + Asset->OnSubGraphReconstructionRequested.Unbind(); + } + } +} + +void UFlowNode_SubGraph::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + if (PropertyChangedEvent.Property && PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(UFlowNode_SubGraph, Asset)) + { + OnReconstructionRequested.ExecuteIfBound(); + SubscribeToAssetChanges(); + } +} + +void UFlowNode_SubGraph::SubscribeToAssetChanges() +{ + if (Asset) + { + TWeakObjectPtr SelfWeakPtr(this); + Asset->OnSubGraphReconstructionRequested.BindLambda([SelfWeakPtr]() + { + if (SelfWeakPtr.IsValid()) + { + SelfWeakPtr->OnReconstructionRequested.ExecuteIfBound(); + } + }); + } +} +#endif + +#undef LOCTEXT_NAMESPACE diff --git a/Source/Flow/Private/Nodes/Operators/FlowNode_LogicalOR.cpp b/Source/Flow/Private/Nodes/Operators/FlowNode_LogicalOR.cpp deleted file mode 100644 index f814ee709..000000000 --- a/Source/Flow/Private/Nodes/Operators/FlowNode_LogicalOR.cpp +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - -#include "Nodes/Operators/FlowNode_LogicalOR.h" - -UFlowNode_LogicalOR::UFlowNode_LogicalOR(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) -{ -#if WITH_EDITOR - Category = TEXT("Operators"); - NodeStyle = EFlowNodeStyle::Logic; -#endif - - SetNumberedInputPins(0, 1); -} - -void UFlowNode_LogicalOR::ExecuteInput(const FName& PinName) -{ - TriggerFirstOutput(true); -} diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_Branch.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_Branch.cpp new file mode 100644 index 000000000..b826ece4c --- /dev/null +++ b/Source/Flow/Private/Nodes/Route/FlowNode_Branch.cpp @@ -0,0 +1,83 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Nodes/Route/FlowNode_Branch.h" +#include "AddOns/FlowNodeAddOn_PredicateAND.h" +#include "AddOns/FlowNodeAddOn_PredicateOR.h" +#include "FlowSettings.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_Branch) + +#define LOCTEXT_NAMESPACE "FlowNode_Branch" + +const FName UFlowNode_Branch::INPIN_Evaluate = TEXT("Evaluate"); +const FName UFlowNode_Branch::OUTPIN_True = TEXT("True"); +const FName UFlowNode_Branch::OUTPIN_False = TEXT("False"); + +UFlowNode_Branch::UFlowNode_Branch() +{ +#if WITH_EDITOR + Category = TEXT("Route|Logic"); + NodeDisplayStyle = FlowNodeStyle::Logic; +#endif + InputPins.Empty(); + InputPins.Add(FFlowPin(INPIN_Evaluate)); + + OutputPins.Empty(); + OutputPins.Add(FFlowPin(OUTPIN_True)); + OutputPins.Add(FFlowPin(OUTPIN_False)); + + AllowedSignalModes = {EFlowSignalMode::Enabled, EFlowSignalMode::Disabled}; +} + +EFlowAddOnAcceptResult UFlowNode_Branch::AcceptFlowNodeAddOnChild_Implementation(const UFlowNodeAddOn* AddOnTemplate, const TArray& AdditionalAddOnsToAssumeAreChildren) const +{ + if (IFlowPredicateInterface::ImplementsInterfaceSafe(AddOnTemplate)) + { + return EFlowAddOnAcceptResult::TentativeAccept; + } + + return Super::AcceptFlowNodeAddOnChild_Implementation(AddOnTemplate, AdditionalAddOnsToAssumeAreChildren); +} + +void UFlowNode_Branch::ExecuteInput(const FName& PinName) +{ + bool bPassedRootPredicates = false; + FName ResultPinName = OUTPIN_False; + + // Test the root-level IFlowPredicateInterface addons + FLOW_ASSERT_ENUM_MAX(EFlowPredicateCombinationRule, 2); + if (BranchCombinationRule == EFlowPredicateCombinationRule::AND) + { + bPassedRootPredicates = UFlowNodeAddOn_PredicateAND::EvaluatePredicateAND(AddOns); + } + else + { + check(BranchCombinationRule == EFlowPredicateCombinationRule::OR); + + bPassedRootPredicates = UFlowNodeAddOn_PredicateOR::EvaluatePredicateOR(AddOns); + } + + constexpr bool bFinish = true; + if (bPassedRootPredicates) + { + TriggerOutput(OUTPIN_True, bFinish); + } + else + { + TriggerOutput(OUTPIN_False, bFinish); + } +} + +FText UFlowNode_Branch::K2_GetNodeTitle_Implementation() const +{ + FLOW_ASSERT_ENUM_MAX(EFlowPredicateCombinationRule, 2); + if (BranchCombinationRule != EFlowPredicateCombinationRule::AND && + GetDefault()->bUseAdaptiveNodeTitles) + { + return FText::Format(LOCTEXT("BranchTitle", "{0} ({1})"), { Super::K2_GetNodeTitle_Implementation(), UEnum::GetDisplayValueAsText(BranchCombinationRule) }); + } + + return Super::K2_GetNodeTitle_Implementation(); +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_Counter.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_Counter.cpp index 5a4eaf50c..c4e0deab0 100644 --- a/Source/Flow/Private/Nodes/Route/FlowNode_Counter.cpp +++ b/Source/Flow/Private/Nodes/Route/FlowNode_Counter.cpp @@ -2,14 +2,15 @@ #include "Nodes/Route/FlowNode_Counter.h" -UFlowNode_Counter::UFlowNode_Counter(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) - , Goal(2) +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_Counter) + +UFlowNode_Counter::UFlowNode_Counter() + : Goal(2) , CurrentSum(0) { #if WITH_EDITOR Category = TEXT("Route"); - NodeStyle = EFlowNodeStyle::Condition; + NodeDisplayStyle = FlowNodeStyle::Condition; #endif InputPins.Empty(); @@ -63,6 +64,8 @@ void UFlowNode_Counter::ExecuteInput(const FName& PinName) void UFlowNode_Counter::Cleanup() { CurrentSum = 0; + + Super::Cleanup(); } #if WITH_EDITOR diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_CustomInput.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_CustomInput.cpp deleted file mode 100644 index a4f2f6aca..000000000 --- a/Source/Flow/Private/Nodes/Route/FlowNode_CustomInput.cpp +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - -#include "Nodes/Route/FlowNode_CustomInput.h" - -UFlowNode_CustomInput::UFlowNode_CustomInput(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) -{ -#if WITH_EDITOR - Category = TEXT("Route"); - NodeStyle = EFlowNodeStyle::InOut; -#endif - - InputPins.Empty(); -} - -void UFlowNode_CustomInput::ExecuteInput(const FName& PinName) -{ - TriggerFirstOutput(true); -} - -#if WITH_EDITOR -FString UFlowNode_CustomInput::GetNodeDescription() const -{ - return EventName.ToString(); -} -#endif diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_CustomOutput.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_CustomOutput.cpp deleted file mode 100644 index fe32ed2ae..000000000 --- a/Source/Flow/Private/Nodes/Route/FlowNode_CustomOutput.cpp +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - -#include "Nodes/Route/FlowNode_CustomOutput.h" - -#include "FlowAsset.h" -#include "Nodes/Route/FlowNode_SubGraph.h" - -UFlowNode_CustomOutput::UFlowNode_CustomOutput(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) -{ -#if WITH_EDITOR - Category = TEXT("Route"); - NodeStyle = EFlowNodeStyle::InOut; -#endif - - OutputPins.Empty(); -} - -void UFlowNode_CustomOutput::ExecuteInput(const FName& PinName) -{ - if (!EventName.IsNone() && GetFlowAsset()->GetCustomOutputs().Contains(EventName) && GetFlowAsset()->GetNodeOwningThisAssetInstance()) - { - GetFlowAsset()->TriggerCustomOutput(EventName); - } -} - -#if WITH_EDITOR -FString UFlowNode_CustomOutput::GetNodeDescription() const -{ - return EventName.ToString(); -} -#endif diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_ExecutionMultiGate.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_ExecutionMultiGate.cpp index c8ca5016b..fb265c9ff 100644 --- a/Source/Flow/Private/Nodes/Route/FlowNode_ExecutionMultiGate.cpp +++ b/Source/Flow/Private/Nodes/Route/FlowNode_ExecutionMultiGate.cpp @@ -2,13 +2,14 @@ #include "Nodes/Route/FlowNode_ExecutionMultiGate.h" -UFlowNode_ExecutionMultiGate::UFlowNode_ExecutionMultiGate(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) - , StartIndex(INDEX_NONE) +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_ExecutionMultiGate) + +UFlowNode_ExecutionMultiGate::UFlowNode_ExecutionMultiGate() + : StartIndex(INDEX_NONE) { #if WITH_EDITOR Category = TEXT("Route"); - NodeStyle = EFlowNodeStyle::Logic; + NodeDisplayStyle = FlowNodeStyle::Logic; #endif FString ResetPinTooltip = TEXT("Finish work of this node."); @@ -17,6 +18,7 @@ UFlowNode_ExecutionMultiGate::UFlowNode_ExecutionMultiGate(const FObjectInitiali InputPins.Add(FFlowPin(TEXT("Reset"), ResetPinTooltip)); SetNumberedOutputPins(0, 1); + AllowedSignalModes = {EFlowSignalMode::Enabled, EFlowSignalMode::Disabled}; } void UFlowNode_ExecutionMultiGate::ExecuteInput(const FName& PinName) @@ -93,6 +95,8 @@ void UFlowNode_ExecutionMultiGate::Cleanup() { NextOutput = 0; Completed.Reset(); + + Super::Cleanup(); } #if WITH_EDITOR diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_ExecutionSequence.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_ExecutionSequence.cpp index 93afd9dd9..48000b7e9 100644 --- a/Source/Flow/Private/Nodes/Route/FlowNode_ExecutionSequence.cpp +++ b/Source/Flow/Private/Nodes/Route/FlowNode_ExecutionSequence.cpp @@ -2,23 +2,72 @@ #include "Nodes/Route/FlowNode_ExecutionSequence.h" -UFlowNode_ExecutionSequence::UFlowNode_ExecutionSequence(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_ExecutionSequence) + +UFlowNode_ExecutionSequence::UFlowNode_ExecutionSequence() + : bSavePinExecutionState(true) { #if WITH_EDITOR Category = TEXT("Route"); - NodeStyle = EFlowNodeStyle::Logic; + NodeDisplayStyle = FlowNodeStyle::Logic; #endif SetNumberedOutputPins(0, 1); + AllowedSignalModes = {EFlowSignalMode::Enabled, EFlowSignalMode::Disabled}; } void UFlowNode_ExecutionSequence::ExecuteInput(const FName& PinName) +{ + if (bSavePinExecutionState) + { + ExecuteNewConnections(); + } + else + { + for (const FFlowPin& Output : OutputPins) + { + TriggerOutput(Output.PinName, false); + } + + Finish(); + } +} + +void UFlowNode_ExecutionSequence::OnLoad_Implementation() +{ + ExecuteNewConnections(); +} + +void UFlowNode_ExecutionSequence::Cleanup() +{ + ExecutedConnections.Empty(); + + Super::Cleanup(); +} + +void UFlowNode_ExecutionSequence::ExecuteNewConnections() { for (const FFlowPin& Output : OutputPins) { - TriggerOutput(Output.PinName, false); + const FConnectedPin& Connection = GetConnection(Output.PinName); + if (!ExecutedConnections.Contains(Connection.NodeGuid)) + { + ExecutedConnections.Emplace(Connection.NodeGuid); + TriggerOutput(Output.PinName, false); + } } Finish(); } + +#if WITH_EDITOR +FString UFlowNode_ExecutionSequence::GetNodeDescription() const +{ + if (bSavePinExecutionState) + { + return TEXT("Saves pin execution state"); + } + + return Super::GetNodeDescription(); +} +#endif diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_Finish.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_Finish.cpp deleted file mode 100644 index ba6a5e635..000000000 --- a/Source/Flow/Private/Nodes/Route/FlowNode_Finish.cpp +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - -#include "Nodes/Route/FlowNode_Finish.h" - -UFlowNode_Finish::UFlowNode_Finish(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) -{ -#if WITH_EDITOR - Category = TEXT("Route"); - NodeStyle = EFlowNodeStyle::InOut; -#endif - - OutputPins = {}; -} - -void UFlowNode_Finish::ExecuteInput(const FName& PinName) -{ - // this will call FinishFlow() - Finish(); -} diff --git a/Source/Flow/Private/Nodes/Operators/FlowNode_LogicalAND.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_LogicalAND.cpp similarity index 62% rename from Source/Flow/Private/Nodes/Operators/FlowNode_LogicalAND.cpp rename to Source/Flow/Private/Nodes/Route/FlowNode_LogicalAND.cpp index 753bed54a..510327cc5 100644 --- a/Source/Flow/Private/Nodes/Operators/FlowNode_LogicalAND.cpp +++ b/Source/Flow/Private/Nodes/Route/FlowNode_LogicalAND.cpp @@ -1,13 +1,14 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors -#include "Nodes/Operators/FlowNode_LogicalAND.h" +#include "Nodes/Route/FlowNode_LogicalAND.h" -UFlowNode_LogicalAND::UFlowNode_LogicalAND(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_LogicalAND) + +UFlowNode_LogicalAND::UFlowNode_LogicalAND() { #if WITH_EDITOR - Category = TEXT("Operators"); - NodeStyle = EFlowNodeStyle::Logic; + Category = TEXT("Route|Logic"); + NodeDisplayStyle = FlowNodeStyle::Logic; #endif SetNumberedInputPins(0, 1); diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_LogicalOR.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_LogicalOR.cpp new file mode 100644 index 000000000..9dd61985b --- /dev/null +++ b/Source/Flow/Private/Nodes/Route/FlowNode_LogicalOR.cpp @@ -0,0 +1,71 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Nodes/Route/FlowNode_LogicalOR.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_LogicalOR) + +UFlowNode_LogicalOR::UFlowNode_LogicalOR() + : bEnabled(true) + , ExecutionLimit(1) + , ExecutionCount(0) +{ +#if WITH_EDITOR + Category = TEXT("Route|Logic"); + NodeDisplayStyle = FlowNodeStyle::Logic; +#endif + + SetNumberedInputPins(0, 1); + InputPins.Add(FFlowPin(TEXT("Enable"), TEXT("Enabling resets Execution Count"))); + InputPins.Add(FFlowPin(TEXT("Disable"), TEXT("Disabling resets Execution Count"))); +} + +void UFlowNode_LogicalOR::ExecuteInput(const FName& PinName) +{ + if (PinName == TEXT("Enable")) + { + if (!bEnabled) + { + ResetCounter(); + bEnabled = true; + } + return; + } + + if (PinName == TEXT("Disable")) + { + if (bEnabled) + { + bEnabled = false; + Finish(); + } + return; + } + + if (bEnabled && PinName.ToString().IsNumeric()) + { + ExecutionCount++; + if (ExecutionLimit > 0 && ExecutionCount == ExecutionLimit) + { + bEnabled = false; + } + + TriggerFirstOutput(true); + } +} + +void UFlowNode_LogicalOR::ResetCounter() +{ + ExecutionCount = 0; +} + +#if WITH_EDITOR +FString UFlowNode_LogicalOR::GetStatusString() const +{ + if (ExecutionLimit > 1) + { + return FString::Printf(TEXT("ExecutionCount: %d/%d"), ExecutionCount, ExecutionLimit); + } + + return Super::GetStatusString(); +} +#endif diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_Reroute.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_Reroute.cpp index 3f002b6bd..1a6fb2fc2 100644 --- a/Source/Flow/Private/Nodes/Route/FlowNode_Reroute.cpp +++ b/Source/Flow/Private/Nodes/Route/FlowNode_Reroute.cpp @@ -1,16 +1,64 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #include "Nodes/Route/FlowNode_Reroute.h" +#include "FlowAsset.h" -UFlowNode_Reroute::UFlowNode_Reroute(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_Reroute) + +UFlowNode_Reroute::UFlowNode_Reroute() { #if WITH_EDITOR Category = TEXT("Route"); #endif + + AllowedSignalModes = {EFlowSignalMode::Enabled, EFlowSignalMode::Disabled}; } void UFlowNode_Reroute::ExecuteInput(const FName& PinName) { TriggerFirstOutput(true); } + +#if WITH_EDITOR +void UFlowNode_Reroute::ConfigureInputPin(const UFlowNode& ConnectedNode, const FEdGraphPinType& EdGraphPinType) +{ + FFlowPin* InputPin = FindInputPinByName(UFlowNode::DefaultInputPin.PinName); + check(InputPin); + + InputPin->ConfigureFromEdGraphPin(EdGraphPinType); +} + +void UFlowNode_Reroute::ConfigureOutputPin(const UFlowNode& ConnectedNode, const FEdGraphPinType& EdGraphPinType) +{ + FFlowPin* OutputPin = FindOutputPinByName(UFlowNode::DefaultOutputPin.PinName); + check(OutputPin); + + OutputPin->ConfigureFromEdGraphPin(EdGraphPinType); +} +#endif + +FFlowDataPinResult UFlowNode_Reroute::TrySupplyDataPin(FName PinName) const +{ + const FFlowPin* InputPin = FindInputPinByName(UFlowNode::DefaultInputPin.PinName); + if (!InputPin) + { + return FFlowDataPinResult(EFlowDataPinResolveResult::FailedUnknownPin); + } + + FConnectedPin ConnectedPin; + if (!FindFirstInputPinConnection(*InputPin, ConnectedPin)) + { + return FFlowDataPinResult(EFlowDataPinResolveResult::FailedNotConnected); + } + + const UFlowNode* ConnectedFlowNodeSupplier = GetFlowAsset()->GetNode(ConnectedPin.NodeGuid); + if (!IsValid(ConnectedFlowNodeSupplier)) + { + checkf(IsValid(ConnectedFlowNodeSupplier), TEXT("This node should be valid if IsInputConnected returned true")); + + return FFlowDataPinResult(EFlowDataPinResolveResult::FailedNotConnected); + } + + // Hand-off to the connected flow node to supply the value + return ConnectedFlowNodeSupplier->TrySupplyDataPin(ConnectedPin.PinName); +} diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_Start.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_Start.cpp deleted file mode 100644 index 746c01adf..000000000 --- a/Source/Flow/Private/Nodes/Route/FlowNode_Start.cpp +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - -#include "Nodes/Route/FlowNode_Start.h" - -UFlowNode_Start::UFlowNode_Start(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) -{ -#if WITH_EDITOR - Category = TEXT("Route"); - NodeStyle = EFlowNodeStyle::InOut; - bCanDelete = bCanDuplicate = false; -#endif - - InputPins = {}; -} - -void UFlowNode_Start::ExecuteInput(const FName& PinName) -{ - TriggerFirstOutput(true); -} diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_SubGraph.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_SubGraph.cpp deleted file mode 100644 index dede5195c..000000000 --- a/Source/Flow/Private/Nodes/Route/FlowNode_SubGraph.cpp +++ /dev/null @@ -1,191 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - -#include "Nodes/Route/FlowNode_SubGraph.h" - -#include "FlowAsset.h" -#include "FlowSubsystem.h" - -FFlowPin UFlowNode_SubGraph::StartPin(TEXT("Start")); -FFlowPin UFlowNode_SubGraph::FinishPin(TEXT("Finish")); - -UFlowNode_SubGraph::UFlowNode_SubGraph(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) - , bCanInstanceIdenticalAsset(false) -{ -#if WITH_EDITOR - Category = TEXT("Route"); - NodeStyle = EFlowNodeStyle::SubGraph; -#endif - - InputPins = {StartPin}; - OutputPins = {FinishPin}; -} - -bool UFlowNode_SubGraph::CanBeAssetInstanced() const -{ - return !Asset.IsNull() && (bCanInstanceIdenticalAsset || Asset->GetPathName() != GetFlowAsset()->GetTemplateAsset()->GetPathName()); -} - -void UFlowNode_SubGraph::PreloadContent() -{ - if (CanBeAssetInstanced() && GetFlowSubsystem()) - { - GetFlowSubsystem()->CreateSubFlow(this, FString(), true); - } -} - -void UFlowNode_SubGraph::FlushContent() -{ - if (CanBeAssetInstanced() && GetFlowSubsystem()) - { - GetFlowSubsystem()->RemoveSubFlow(this, EFlowFinishPolicy::Abort); - } -} - -void UFlowNode_SubGraph::ExecuteInput(const FName& PinName) -{ - if (CanBeAssetInstanced() == false) - { - if (Asset.IsNull()) - { - LogError(TEXT("Missing Flow Asset")); - } - else - { - LogError(FString::Printf(TEXT("Asset %s cannot be instance, probably is the same as the asset owning this SubGraph node."), *Asset->GetPathName())); - } - - Finish(); - return; - } - - if (PinName == TEXT("Start")) - { - if (GetFlowSubsystem()) - { - GetFlowSubsystem()->CreateSubFlow(this); - } - } - else if (!PinName.IsNone()) - { - GetFlowAsset()->TriggerCustomEvent(this, PinName); - } -} - -void UFlowNode_SubGraph::Cleanup() -{ - if (CanBeAssetInstanced() && GetFlowSubsystem()) - { - GetFlowSubsystem()->RemoveSubFlow(this, EFlowFinishPolicy::Keep); - } -} - -void UFlowNode_SubGraph::ForceFinishNode() -{ - TriggerFirstOutput(true); -} - -void UFlowNode_SubGraph::OnLoad_Implementation() -{ - if (!SavedAssetInstanceName.IsEmpty() && !Asset.IsNull()) - { - GetFlowSubsystem()->LoadSubFlow(this, SavedAssetInstanceName); - SavedAssetInstanceName = FString(); - } -} - -#if WITH_EDITOR -FString UFlowNode_SubGraph::GetNodeDescription() const -{ - return Asset.IsNull() ? FString() : Asset.ToSoftObjectPath().GetAssetName(); -} - -UObject* UFlowNode_SubGraph::GetAssetToEdit() -{ - return Asset.IsNull() ? nullptr : LoadAsset(Asset); -} - -TArray UFlowNode_SubGraph::GetContextInputs() -{ - TArray EventNames; - - if (!Asset.IsNull()) - { - Asset.LoadSynchronous(); - for (const FName& PinName : Asset.Get()->GetCustomInputs()) - { - if (!PinName.IsNone()) - { - EventNames.Emplace(PinName); - } - } - } - - return EventNames; -} - -TArray UFlowNode_SubGraph::GetContextOutputs() -{ - TArray EventNames; - - if (!Asset.IsNull()) - { - Asset.LoadSynchronous(); - for (const FName& PinName : Asset.Get()->GetCustomOutputs()) - { - if (!PinName.IsNone()) - { - EventNames.Emplace(PinName); - } - } - } - - return EventNames; -} - -void UFlowNode_SubGraph::PostLoad() -{ - Super::PostLoad(); - - SubscribeToAssetChanges(); -} - -void UFlowNode_SubGraph::PreEditChange(FProperty* PropertyAboutToChange) -{ - Super::PreEditChange(PropertyAboutToChange); - - if (PropertyAboutToChange->GetFName() == GET_MEMBER_NAME_CHECKED(UFlowNode_SubGraph, Asset)) - { - if (Asset) - { - Asset->OnSubGraphReconstructionRequested.Unbind(); - } - } -} - -void UFlowNode_SubGraph::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) -{ - Super::PostEditChangeProperty(PropertyChangedEvent); - - if (PropertyChangedEvent.Property && PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(UFlowNode_SubGraph, Asset)) - { - OnReconstructionRequested.ExecuteIfBound(); - SubscribeToAssetChanges(); - } -} - -void UFlowNode_SubGraph::SubscribeToAssetChanges() -{ - if (Asset) - { - TWeakObjectPtr SelfWeakPtr(this); - Asset->OnSubGraphReconstructionRequested.BindLambda([SelfWeakPtr]() - { - if (SelfWeakPtr.IsValid()) - { - SelfWeakPtr->OnReconstructionRequested.ExecuteIfBound(); - } - }); - } -} -#endif diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_Switch.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_Switch.cpp new file mode 100644 index 000000000..060757cb3 --- /dev/null +++ b/Source/Flow/Private/Nodes/Route/FlowNode_Switch.cpp @@ -0,0 +1,83 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Nodes/Route/FlowNode_Switch.h" +#include "AddOns/FlowNodeAddOn.h" +#include "FlowSettings.h" +#include "Interfaces/FlowSwitchCaseInterface.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_Switch) + +#define LOCTEXT_NAMESPACE "FlowNode_Switch" + +const FName UFlowNode_Switch::INPIN_Evaluate = TEXT("Evaluate"); +const FName UFlowNode_Switch::OUTPIN_DefaultCase = TEXT("None Passed"); + +UFlowNode_Switch::UFlowNode_Switch() +{ +#if WITH_EDITOR + Category = TEXT("Route|Logic"); + NodeDisplayStyle = FlowNodeStyle::Logic; +#endif + + InputPins.Reset(); + InputPins.Add(FFlowPin(INPIN_Evaluate)); + + OutputPins.Reset(); + OutputPins.Add(FFlowPin(OUTPIN_DefaultCase.ToString(), FString(TEXT("Triggered when no cases pass (during a Switch Evaluate)")))); + + AllowedSignalModes = {EFlowSignalMode::Enabled, EFlowSignalMode::Disabled}; +} + +EFlowAddOnAcceptResult UFlowNode_Switch::AcceptFlowNodeAddOnChild_Implementation(const UFlowNodeAddOn* AddOnTemplate, const TArray& AdditionalAddOnsToAssumeAreChildren) const +{ + if (IFlowSwitchCaseInterface::ImplementsInterfaceSafe(AddOnTemplate)) + { + return EFlowAddOnAcceptResult::TentativeAccept; + } + + return Super::AcceptFlowNodeAddOnChild_Implementation(AddOnTemplate, AdditionalAddOnsToAssumeAreChildren); +} + +void UFlowNode_Switch::ExecuteInput(const FName& PinName) +{ + int32 TriggeringCaseCount = 0; + + // Trigger the IFlowSwitchCaseInterface addons that pass + const EFlowForEachAddOnFunctionReturnValue SwitchCaseResult = + ForEachAddOnForClassConst( + [&TriggeringCaseCount, this](const UFlowNodeAddOn& SwitchCaseAddOn) + { + const IFlowSwitchCaseInterface* SwitchCaseInterface = CastChecked(&SwitchCaseAddOn); + + if (IFlowSwitchCaseInterface::Execute_TryTriggerForCase(&SwitchCaseAddOn)) + { + ++TriggeringCaseCount; + + if (bOnlyTriggerFirstPassingCase) + { + return EFlowForEachAddOnFunctionReturnValue::BreakWithSuccess; + } + } + + return EFlowForEachAddOnFunctionReturnValue::Continue; + }); + + if (TriggeringCaseCount == 0) + { + // Trigger the default case if none of the cases passed + constexpr bool bFinish = true; + TriggerOutput(OUTPIN_DefaultCase, bFinish); + } +} + +FText UFlowNode_Switch::K2_GetNodeTitle_Implementation() const +{ + if (!bOnlyTriggerFirstPassingCase && GetDefault()->bUseAdaptiveNodeTitles) + { + return FText::Format(LOCTEXT("SwitchTitle", "{0} (All Passing)"), { Super::K2_GetNodeTitle_Implementation() }); + } + + return Super::K2_GetNodeTitle_Implementation(); +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_Timer.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_Timer.cpp index d55703707..4e1120c6d 100644 --- a/Source/Flow/Private/Nodes/Route/FlowNode_Timer.cpp +++ b/Source/Flow/Private/Nodes/Route/FlowNode_Timer.cpp @@ -1,21 +1,28 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #include "Nodes/Route/FlowNode_Timer.h" +#include "FlowSettings.h" #include "Engine/World.h" #include "TimerManager.h" -UFlowNode_Timer::UFlowNode_Timer(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) - , CompletionTime(1.0f) - , StepTime(0.0f) - , SumOfSteps(0.0f) - , RemainingCompletionTime(0.0f) - , RemainingStepTime(0.0f) +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNode_Timer) + +#define LOCTEXT_NAMESPACE "FlowNode_Timer" + +FName UFlowNode_Timer::INPIN_CompletionTime; + +UFlowNode_Timer::UFlowNode_Timer() + : CompletionTime(1.0f) + , StepTime(0.0f) + , ResolvedCompletionTime(0.0f) + , SumOfSteps(0.0f) + , RemainingCompletionTime(0.0f) + , RemainingStepTime(0.0f) { #if WITH_EDITOR Category = TEXT("Route"); - NodeStyle = EFlowNodeStyle::Latent; + NodeDisplayStyle = FlowNodeStyle::Latent; #endif InputPins.Add(FFlowPin(TEXT("Skip"))); @@ -25,17 +32,21 @@ UFlowNode_Timer::UFlowNode_Timer(const FObjectInitializer& ObjectInitializer) OutputPins.Add(FFlowPin(TEXT("Completed"))); OutputPins.Add(FFlowPin(TEXT("Step"))); OutputPins.Add(FFlowPin(TEXT("Skipped"))); + + INPIN_CompletionTime = GET_MEMBER_NAME_CHECKED(ThisClass, CompletionTime); } -void UFlowNode_Timer::ExecuteInput(const FName& PinName) +void UFlowNode_Timer::InitializeInstance() { - if (CompletionTime == 0.0f) - { - LogError(TEXT("Invalid Timer settings")); - TriggerOutput(TEXT("Completed"), true); - return; - } + Super::InitializeInstance(); + + // Initialize to the configured value, + // but we will overwrite this with the results of ResolveCompletionTime() when the timer is started + ResolvedCompletionTime = CompletionTime; +} +void UFlowNode_Timer::ExecuteInput(const FName& PinName) +{ if (PinName == TEXT("In")) { if (CompletionTimerHandle.IsValid() || StepTimerHandle.IsValid()) @@ -65,7 +76,15 @@ void UFlowNode_Timer::SetTimer() GetWorld()->GetTimerManager().SetTimer(StepTimerHandle, this, &UFlowNode_Timer::OnStep, StepTime, true); } - GetWorld()->GetTimerManager().SetTimer(CompletionTimerHandle, this, &UFlowNode_Timer::OnCompletion, CompletionTime, false); + ResolvedCompletionTime = ResolveCompletionTime(); + if (ResolvedCompletionTime > UE_KINDA_SMALL_NUMBER) + { + GetWorld()->GetTimerManager().SetTimer(CompletionTimerHandle, this, &UFlowNode_Timer::OnCompletion, ResolvedCompletionTime, false); + } + else + { + GetWorld()->GetTimerManager().SetTimerForNextTick(this, &UFlowNode_Timer::OnCompletion); + } } else { @@ -84,11 +103,20 @@ void UFlowNode_Timer::Restart() SetTimer(); } +float UFlowNode_Timer::ResolveCompletionTime() const +{ + // Get the CompletionTime from either the default (property) or the data pin (if connected) + float ResolvedTime = CompletionTime; + const EFlowDataPinResolveResult TimeResult = TryResolveDataPinValue(INPIN_CompletionTime, ResolvedTime); + + return ResolvedTime; +} + void UFlowNode_Timer::OnStep() { SumOfSteps += StepTime; - if (SumOfSteps >= CompletionTime) + if (SumOfSteps >= ResolvedCompletionTime) { TriggerOutput(TEXT("Completed"), true); } @@ -118,6 +146,8 @@ void UFlowNode_Timer::Cleanup() StepTimerHandle.Invalidate(); SumOfSteps = 0.0f; + + Super::Cleanup(); } void UFlowNode_Timer::OnSave_Implementation() @@ -138,49 +168,86 @@ void UFlowNode_Timer::OnSave_Implementation() void UFlowNode_Timer::OnLoad_Implementation() { - if (RemainingStepTime > 0.0f) + if (RemainingStepTime > 0.0f || RemainingCompletionTime > 0.0f) { - GetWorld()->GetTimerManager().SetTimer(StepTimerHandle, this, &UFlowNode_Timer::OnStep, StepTime, true, - RemainingStepTime); - } + if (RemainingStepTime > 0.0f) + { + GetWorld()->GetTimerManager().SetTimer(StepTimerHandle, this, &UFlowNode_Timer::OnStep, StepTime, true, RemainingStepTime); + } - GetWorld()->GetTimerManager().SetTimer(CompletionTimerHandle, this, &UFlowNode_Timer::OnCompletion, - RemainingCompletionTime, false); + GetWorld()->GetTimerManager().SetTimer(CompletionTimerHandle, this, &UFlowNode_Timer::OnCompletion, RemainingCompletionTime, false); - RemainingStepTime = 0.0f; - RemainingCompletionTime = 0.0f; + RemainingStepTime = 0.0f; + RemainingCompletionTime = 0.0f; + } } #if WITH_EDITOR -FString UFlowNode_Timer::GetNodeDescription() const + +void UFlowNode_Timer::UpdateNodeConfigText_Implementation() { - if (CompletionTime > 0.0f) + constexpr bool bErrorIfInputPinNotFound = false; + const bool bIsInputConnected = IsInputConnected(INPIN_CompletionTime); + + if (bIsInputConnected) { + // CompletionTime will be sourced from the data pin + if (StepTime > 0.0f) { - return FString::SanitizeFloat(CompletionTime, 2).Append(TEXT(", step by ")).Append( - FString::SanitizeFloat(StepTime, 2)); + const FString StepTimeString = FString::Printf(TEXT("%.*f"), 2, StepTime); + + SetNodeConfigText(FText::Format(LOCTEXT("TimerConfigPinWithStep", "Step by {1}"), { FText::FromString(StepTimeString) })); + } + else + { + SetNodeConfigText(FText()); } - return FString::SanitizeFloat(CompletionTime, 2); + return; } - return TEXT("Invalid settings"); + if (CompletionTime > UE_KINDA_SMALL_NUMBER) + { + const FString CompletionTimeString = FString::Printf(TEXT("%.*f"), 2, CompletionTime); + + if (StepTime > 0.0f) + { + const FString StepTimeString = FString::Printf(TEXT("%.*f"), 2, StepTime); + + SetNodeConfigText(FText::Format(LOCTEXT("TimerConfigWithStep", "Time: {0}, step by {1}"), { FText::FromString(CompletionTimeString), FText::FromString(StepTimeString) })); + } + else + { + SetNodeConfigText(FText::Format(LOCTEXT("TimerConfig", "Time: {0}"), { FText::FromString(CompletionTimeString) })); + } + } + else + { + SetNodeConfigText(FText(LOCTEXT("CompletesNextTick", "Completes in next tick"))); + } } FString UFlowNode_Timer::GetStatusString() const { + FString ProgressString; if (StepTime > 0.0f) { - return TEXT("Progress: ") + GetProgressAsString(SumOfSteps); + ProgressString = FString::Printf(TEXT("%.*f"), 2, SumOfSteps); + } + else if (CompletionTimerHandle.IsValid() && GetWorld()) + { + ProgressString = FString::Printf(TEXT("%.*f"), 2, GetWorld()->GetTimerManager().GetTimerElapsed(CompletionTimerHandle)); } - if (CompletionTimerHandle.IsValid() && GetWorld()) + if (!ProgressString.IsEmpty()) { - return TEXT("Progress: ") + GetProgressAsString( - GetWorld()->GetTimerManager().GetTimerElapsed(CompletionTimerHandle)); + return FText::Format(LOCTEXT("ProgressStatus", "Progress: {0}"), { FText::FromString(ProgressString) }).ToString(); } return FString(); } + #endif + +#undef LOCTEXT_NAMESPACE diff --git a/Source/Flow/Private/Nodes/Utils/FlowNode_Checkpoint.cpp b/Source/Flow/Private/Nodes/Utils/FlowNode_Checkpoint.cpp deleted file mode 100644 index 1cdd9e280..000000000 --- a/Source/Flow/Private/Nodes/Utils/FlowNode_Checkpoint.cpp +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - -#include "Nodes/Utils/FlowNode_Checkpoint.h" -#include "FlowSubsystem.h" - -#include "Kismet/GameplayStatics.h" - -UFlowNode_Checkpoint::UFlowNode_Checkpoint(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) -{ -#if WITH_EDITOR - Category = TEXT("Utils"); -#endif -} - -void UFlowNode_Checkpoint::ExecuteInput(const FName& PinName) -{ - if (GetFlowSubsystem()) - { - UFlowSaveGame* NewSaveGame = Cast(UGameplayStatics::CreateSaveGameObject(UFlowSaveGame::StaticClass())); - GetFlowSubsystem()->OnGameSaved(NewSaveGame); - - UGameplayStatics::SaveGameToSlot(NewSaveGame, NewSaveGame->SaveSlotName, 0); - } - - TriggerFirstOutput(true); -} - -void UFlowNode_Checkpoint::OnLoad_Implementation() -{ - TriggerFirstOutput(true); -} diff --git a/Source/Flow/Private/Nodes/Utils/FlowNode_Log.cpp b/Source/Flow/Private/Nodes/Utils/FlowNode_Log.cpp deleted file mode 100644 index 5da89a4e4..000000000 --- a/Source/Flow/Private/Nodes/Utils/FlowNode_Log.cpp +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - -#include "Nodes/Utils/FlowNode_Log.h" -#include "FlowModule.h" - -#include "Engine/Engine.h" - -UFlowNode_Log::UFlowNode_Log(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) - , Message(TEXT("Log!")) - , Verbosity(EFlowLogVerbosity::Warning) - , bPrintToScreen(true) - , Duration(5.0f) - , TextColor(FColor::Yellow) -{ -#if WITH_EDITOR - Category = TEXT("Utils"); -#endif -} - -void UFlowNode_Log::ExecuteInput(const FName& PinName) -{ - switch (Verbosity) - { - case EFlowLogVerbosity::Error: - UE_LOG(LogFlow, Error, TEXT("%s"), *Message); - break; - case EFlowLogVerbosity::Warning: - UE_LOG(LogFlow, Warning, TEXT("%s"), *Message); - break; - case EFlowLogVerbosity::Display: - UE_LOG(LogFlow, Display, TEXT("%s"), *Message); - break; - case EFlowLogVerbosity::Log: - UE_LOG(LogFlow, Log, TEXT("%s"), *Message); - break; - case EFlowLogVerbosity::Verbose: - UE_LOG(LogFlow, Verbose, TEXT("%s"), *Message); - break; - case EFlowLogVerbosity::VeryVerbose: - UE_LOG(LogFlow, VeryVerbose, TEXT("%s"), *Message); - break; - default: ; - } - - if (bPrintToScreen) - { - GEngine->AddOnScreenDebugMessage(-1, Duration, TextColor, Message); - } - - TriggerFirstOutput(true); -} - -#if WITH_EDITOR -FString UFlowNode_Log::GetNodeDescription() const -{ - return Message; -} -#endif diff --git a/Source/Flow/Private/Policies/FlowPinConnectionPolicy.cpp b/Source/Flow/Private/Policies/FlowPinConnectionPolicy.cpp new file mode 100644 index 000000000..02898885f --- /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 000000000..bd8951214 --- /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 000000000..0d25e5636 --- /dev/null +++ b/Source/Flow/Private/Policies/FlowPreloadHelper.cpp @@ -0,0 +1,199 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#include "Policies/FlowPreloadHelper.h" + +#include "AddOns/FlowNodeAddOn.h" +#include "Interfaces/FlowPreloadableInterface.h" +#include "FlowAsset.h" +#include "Policies/FlowPreloadPolicy.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.AddUnique(INPIN_PreloadContent); + OutInputPins.AddUnique(INPIN_FlushContent); +} + +void FFlowPreloadHelper::GetContextOutputs(TArray& OutOutputPins) const +{ + OutOutputPins.AddUnique(OUTPIN_AllPreloadsComplete); +} +#endif diff --git a/Source/Flow/Private/Policies/FlowStandardPreloadPolicies.cpp b/Source/Flow/Private/Policies/FlowStandardPreloadPolicies.cpp new file mode 100644 index 000000000..7fbc15790 --- /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/FlowActorOwnerComponentRef.cpp b/Source/Flow/Private/Types/FlowActorOwnerComponentRef.cpp new file mode 100644 index 000000000..bf6393010 --- /dev/null +++ b/Source/Flow/Private/Types/FlowActorOwnerComponentRef.cpp @@ -0,0 +1,59 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Types/FlowActorOwnerComponentRef.h" +#include "Components/ActorComponent.h" +#include "GameFramework/Actor.h" +#include "Misc/RuntimeErrors.h" +#include "FlowLogChannels.h" + +UActorComponent* FFlowActorOwnerComponentRef::TryResolveComponent(const AActor& InActor, bool bWarnIfFailed) +{ + if (!IsResolved() && IsConfigured()) + { + ResolvedComponent = TryResolveComponentByName(InActor, ComponentName); + + if (bWarnIfFailed && !IsValid(ResolvedComponent)) + { + UE_LOG(LogFlow, Warning, TEXT("Could not resolve component named %s on actor %s"), *ComponentName.ToString(), *InActor.GetName()); + } + } + + return ResolvedComponent; +} + +void FFlowActorOwnerComponentRef::SetResolvedComponentDirect(UActorComponent& Component) +{ + ComponentName = Component.GetFName(); + + ResolvedComponent = &Component; +} + +UActorComponent* FFlowActorOwnerComponentRef::TryResolveComponentByName(const AActor& InActor, const FName& InComponentName) +{ + constexpr bool bIncludeFromChildActors = false; + + UActorComponent* FoundComponent = nullptr; + + // Search for the component (by name) on the given actor + InActor.ForEachComponent( + bIncludeFromChildActors, + [&FoundComponent, &InComponentName](UActorComponent* Component) + { + FString CleanedName = Component->GetName(); + CleanedName.RemoveFromEnd(TEXT("_C")); + + if ((InComponentName == Component->GetFName() || InComponentName == FName(CleanedName)) && + ensureAsRuntimeWarning(FoundComponent == nullptr)) + { + FoundComponent = Component; + } + }); + + return FoundComponent; +} + +bool FFlowActorOwnerComponentRef::IsResolved() const +{ + return IsValid(ResolvedComponent); +} + diff --git a/Source/Flow/Private/Types/FlowAutoDataPinsWorkingData.cpp b/Source/Flow/Private/Types/FlowAutoDataPinsWorkingData.cpp new file mode 100644 index 000000000..747e90824 --- /dev/null +++ b/Source/Flow/Private/Types/FlowAutoDataPinsWorkingData.cpp @@ -0,0 +1,533 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Types/FlowAutoDataPinsWorkingData.h" + +#include "FlowLogChannels.h" +#include "Nodes/FlowNode.h" +#include "Types/FlowDataPinValue.h" +#include "Types/FlowNamedDataPinProperty.h" +#include "Types/FlowStructUtils.h" + +#if WITH_EDITOR + +void FFlowAutoDataPinsWorkingData::Build(UFlowNode& FlowNode, FBuildResult& OutBuildResult) const +{ + OutBuildResult.Reset(); + + // Disambiguation should not mutate the provider-collected arrays; work on locals. + TArray InputPinsLocal = AutoInputDataPinsNext; + TArray OutputPinsLocal = AutoOutputDataPinsNext; + + // Build mapping and apply disambiguation to locals. + AddInputDataPinsToMapAndDisambiguate(InputPinsLocal, OutBuildResult.MapDataPinNameToPropertySource, OutBuildResult.DeferredValuePatches); + AddOutputDataPinsToMapAndDisambiguate(OutputPinsLocal, OutBuildResult.MapDataPinNameToPropertySource, OutBuildResult.DeferredValuePatches); + + BuildNextFlowPinArray(InputPinsLocal, OutBuildResult.AutoInputPins); + BuildNextFlowPinArray(OutputPinsLocal, OutBuildResult.AutoOutputPins); +} + +bool FFlowAutoDataPinsWorkingData::CheckIfProposedPinsMatchPreviousPins(const TArray& PrevPins, const TArray& ProposedPins) +{ + if (PrevPins.Num() != ProposedPins.Num()) + { + return false; + } + + for (int32 Index = 0; Index < PrevPins.Num(); ++Index) + { + if (!PrevPins[Index].DeepIsEqual(ProposedPins[Index])) + { + return false; + } + } + + return true; +} + +bool FFlowAutoDataPinsWorkingData::ArePropertySourcesEqual(const FFlowPinPropertySource& A, const FFlowPinPropertySource& B) +{ + return A.PropertyName == B.PropertyName && A.ValueOwnerIndex == B.ValueOwnerIndex; +} + +bool FFlowAutoDataPinsWorkingData::CheckIfProposedMapMatchesPreviousMap( + const TMap& PrevMap, + const TMap& ProposedMap) +{ + if (PrevMap.Num() != ProposedMap.Num()) + { + return false; + } + + for (const TPair& KVP : PrevMap) + { + const FFlowPinPropertySource* Other = ProposedMap.Find(KVP.Key); + if (!Other || !ArePropertySourcesEqual(KVP.Value, *Other)) + { + return false; + } + } + + return true; +} + +void FFlowAutoDataPinsWorkingData::BuildNextFlowPinArray(const TArray& PinSourceDatas, TArray& OutFlowPins) +{ + OutFlowPins.Reset(); + + for (const FFlowPinSourceData& PinSourceData : PinSourceDatas) + { + OutFlowPins.Add(PinSourceData.FlowPin); + } +} + +void FFlowAutoDataPinsWorkingData::AddFlowDataPinsForClassProperties(FFlowDataPinValueOwner& ValueOwner) +{ + const UObject* ValueOwnerAsObject = ValueOwner.GetValueOwnerAsObject(); + const UClass* Class = ValueOwnerAsObject->GetClass(); + + for (TFieldIterator PropertyIt(Class); PropertyIt; ++PropertyIt) + { + AddFlowDataPinForProperty(*PropertyIt, ValueOwner); + } +} + +void FFlowAutoDataPinsWorkingData::AddFlowDataPinForProperty(FProperty* Property, FFlowDataPinValueOwner& ValueOwner) +{ + bool bIsInputPin = false; + + const FString* AutoPinType = nullptr; + const FString* SourceForOutputFlowPinName = nullptr; + const FString* DefaultForInputFlowPinName = nullptr; + + void* ValueOwnerAsObject = ValueOwner.GetValueOwnerAsObject(); + + FStructProperty* StructProperty = CastField(Property); + const FFlowDataPinValue* DataPinValue = nullptr; + if (StructProperty && StructProperty->Struct) + { + const UScriptStruct* ScriptStruct = StructProperty->Struct; + + if (StructProperty->Struct->IsChildOf()) + { + // Special-case handling for FFlowNamedDataPinProperty + // (it has its own function to create the pin) + FFlowNamedDataPinProperty* NamedPinProperty = FlowStructUtils::CastStructValue(StructProperty, ValueOwnerAsObject); + NamedPinProperty->AutoGenerateDataPinForProperty(ValueOwner, *this); + + return; + } + + AutoPinType = ScriptStruct->FindMetaData(FFlowPin::MetadataKey_FlowPinType); + SourceForOutputFlowPinName = ScriptStruct->FindMetaData(FFlowPin::MetadataKey_SourceForOutputFlowPin); + DefaultForInputFlowPinName = ScriptStruct->FindMetaData(FFlowPin::MetadataKey_DefaultForInputFlowPin); + + // For blueprint use, we allow the Value structs to set input pins via editor-only data + DataPinValue = FlowStructUtils::CastStructValue(StructProperty, ValueOwnerAsObject); + if (DataPinValue) + { + bIsInputPin = DataPinValue->IsInputPin(); + } + } + + if (!AutoPinType) + { + AutoPinType = Property->FindMetaData(FFlowPin::MetadataKey_FlowPinType); + + if (!AutoPinType) + { + return; + } + } + + const FFlowPinType* FlowPinType = FFlowPinType::LookupPinType(FFlowPinTypeName(*AutoPinType)); + if (!FlowPinType) + { + UE_LOG(LogFlow, Error, TEXT("Unknown pin type %s for property %s"), **AutoPinType, *Property->GetName()); + + return; + } + + if (!SourceForOutputFlowPinName) + { + SourceForOutputFlowPinName = Property->FindMetaData(FFlowPin::MetadataKey_SourceForOutputFlowPin); + } + + if (!DefaultForInputFlowPinName) + { + DefaultForInputFlowPinName = Property->FindMetaData(FFlowPin::MetadataKey_DefaultForInputFlowPin); + } + + if (SourceForOutputFlowPinName && DefaultForInputFlowPinName) + { + UE_LOG(LogFlow, Error, TEXT("Error. A property cannot be both a %s and %s"), + *FFlowPin::MetadataKey_SourceForOutputFlowPin.ToString(), + *FFlowPin::MetadataKey_DefaultForInputFlowPin.ToString()); + + return; + } + + bIsInputPin = bIsInputPin || DefaultForInputFlowPinName != nullptr; + + // Default assumption is the pin will be an output pin, unless metadata specifies otherwise + TArray* FlowPinArray = bIsInputPin ? &AutoInputDataPinsNext : &AutoOutputDataPinsNext; + + // Create the new FlowPin + FFlowPin NewFlowPin = FlowPinType->CreateFlowPinFromProperty(*Property, ValueOwnerAsObject); + + // Potentially override the PinFriendlyName if the metadata specified an alternative + if (DefaultForInputFlowPinName) + { + const FString SpecifyInputPinNameString = *DefaultForInputFlowPinName; + if (SpecifyInputPinNameString.Len() > 0) + { + NewFlowPin.PinFriendlyName = FText::FromString(SpecifyInputPinNameString); + } + } + else if (SourceForOutputFlowPinName) + { + const FString SpecifyOutputPinNameString = *SourceForOutputFlowPinName; + if (SpecifyOutputPinNameString.Len() > 0) + { + NewFlowPin.PinFriendlyName = FText::FromString(SpecifyOutputPinNameString); + } + } + + FlowPinArray->Add(FFlowPinSourceData(NewFlowPin, ValueOwner, DataPinValue)); + + // IMPORTANT: wrapper patching is deferred until commit time to avoid dirtying assets on a no-op regen. +} + +void FFlowAutoDataPinsWorkingData::AddPinMappingToMap( + TMap& InOutMap, + const FName& FinalPinName, + const FName& OriginalPinName, + const FFlowDataPinValueOwner& ValueOwner) +{ + if (ValueOwner.IsDefaultValueOwner() && FinalPinName == OriginalPinName) + { + return; + } + + InOutMap.Add(FinalPinName, FFlowPinPropertySource(OriginalPinName, ValueOwner.ValueOwnerIndex)); +} + +void FFlowAutoDataPinsWorkingData::AppendDeferredPatchIfNeeded( + TArray& InOutPatches, + const FFlowPinSourceData& PinSourceData) +{ + if (!PinSourceData.DataPinValue) + { + return; + } + + FDeferredValuePinNamePatch Patch; + Patch.DataPinValue = PinSourceData.DataPinValue; + Patch.NewPinName = PinSourceData.FlowPin.PinName; + InOutPatches.Add(Patch); +} + +bool FFlowAutoDataPinsWorkingData::AreFlowPinTypeSignaturesEquivalent(const FFlowPin& A, const FFlowPin& B) +{ + // Type signature equivalence (matches your requirement for "same type"): + // - ContainerType + // - PinTypeName + // - PinSubCategoryObject + return + A.ContainerType == B.ContainerType && + A.GetPinTypeName() == B.GetPinTypeName() && + A.GetPinSubCategoryObject() == B.GetPinSubCategoryObject(); +} + +void FFlowAutoDataPinsWorkingData::AddInputDataPinsToMapAndDisambiguate( + TArray& InOutAutoInputPinsNext, + TMap& InOutMap, + TArray& InOutDeferredPatches) +{ + if (InOutAutoInputPinsNext.IsEmpty()) + { + return; + } + + // Pass 1: count each base/original name (array order deterministic) + TMap NameCounts; + for (const FFlowPinSourceData& Pin : InOutAutoInputPinsNext) + { + if (!Pin.FlowPin.IsExecPin()) + { + NameCounts.FindOrAdd(Pin.FlowPin.PinName)++; + } + } + + // Determine which base names actually require disambiguation (duplicates + mismatched type signature) + TSet NamesRequiringDisambiguation; + + for (const TPair& KVP : NameCounts) + { + const FName BaseName = KVP.Key; + const int32 Count = KVP.Value; + + if (Count <= 1) + { + continue; + } + + const FFlowPin* FirstTypePin = nullptr; + bool bMismatchFound = false; + + for (const FFlowPinSourceData& Pin : InOutAutoInputPinsNext) + { + if (Pin.FlowPin.IsExecPin() || Pin.FlowPin.PinName != BaseName) + { + continue; + } + + if (!FirstTypePin) + { + FirstTypePin = &Pin.FlowPin; + continue; + } + + if (!AreFlowPinTypeSignaturesEquivalent(*FirstTypePin, Pin.FlowPin)) + { + bMismatchFound = true; + break; + } + } + + if (bMismatchFound) + { + NamesRequiringDisambiguation.Add(BaseName); + } + } + + // Collect names that will remain unchanged (for collision avoidance during renaming) + TSet ReservedFinalNames; + for (const FFlowPinSourceData& Pin : InOutAutoInputPinsNext) + { + if (Pin.FlowPin.IsExecPin()) + { + continue; + } + + const FName BaseName = Pin.FlowPin.PinName; + + // If this base name requires disambiguation, none of its occurrences are reserved + if (NamesRequiringDisambiguation.Contains(BaseName)) + { + continue; + } + + // Only reserve true uniques (Count==1). Duplicate-but-mergeable can keep their base name, + // but reserving it doesn't help us because we won't rename those anyway. + const int32 Count = NameCounts.FindRef(BaseName); + if (Count == 1) + { + ReservedFinalNames.Add(BaseName); + } + } + + // Walk in provider order; map unique and mergeable duplicates, disambiguate mismatch-type duplicates + TSet UsedNames = ReservedFinalNames; + TMap NextLogicalDuplicateIndexByName; + + for (FFlowPinSourceData& Pin : InOutAutoInputPinsNext) + { + if (Pin.FlowPin.IsExecPin()) + { + continue; + } + + const FName OriginalName = Pin.FlowPin.PinName; + + if (!NamesRequiringDisambiguation.Contains(OriginalName)) + { + // No rename required (either unique, or duplicate but mergeable by type signature). + // Preserve name so FlowNode-level "merge by name" remains. + + if (!Pin.ValueOwner.IsDefaultValueOwner()) + { + AddPinMappingToMap(InOutMap, Pin.FlowPin.PinName, Pin.FlowPin.PinName, Pin.ValueOwner); + } + + AppendDeferredPatchIfNeeded(InOutDeferredPatches, Pin); + continue; + } + + // Disambiguate this entry deterministically in encounter order + uint32& NextLogicalIndex = NextLogicalDuplicateIndexByName.FindOrAdd(OriginalName); + if (NextLogicalIndex == 0) + { + NextLogicalIndex = 1; + } + + const uint32 LogicalDuplicateIndex = NextLogicalIndex; + ++NextLogicalIndex; + + DisambiguateDuplicatePin(Pin, UsedNames, LogicalDuplicateIndex, InOutDeferredPatches); + + // IMPORTANT: for disambiguated pins, ALWAYS add a mapping entry (even for default owner), + // because runtime fallback inference assumes "PinName == PropertyName on owner 0". + AddPinMappingToMap(InOutMap, Pin.FlowPin.PinName, OriginalName, Pin.ValueOwner); + } +} + +void FFlowAutoDataPinsWorkingData::AddOutputDataPinsToMapAndDisambiguate( + TArray& InOutAutoOutputPinsNext, + TMap& InOutMap, + TArray& InOutDeferredPatches) +{ + if (InOutAutoOutputPinsNext.IsEmpty()) + { + return; + } + + // Pass 1: count each base/original name (array order deterministic) + TMap NameCounts; + for (const FFlowPinSourceData& Pin : InOutAutoOutputPinsNext) + { + if (!Pin.FlowPin.IsExecPin()) + { + NameCounts.FindOrAdd(Pin.FlowPin.PinName)++; + } + } + + // Collect names that will remain unchanged (for collision avoidance during renaming) + TSet ReservedFinalNames; + for (const FFlowPinSourceData& Pin : InOutAutoOutputPinsNext) + { + if (Pin.FlowPin.IsExecPin()) + { + continue; + } + + const int32 Count = NameCounts.FindRef(Pin.FlowPin.PinName); + if (Count == 1) + { + ReservedFinalNames.Add(Pin.FlowPin.PinName); + } + } + + // Pass 2: walk in array order; assign mapping for unique pins, disambiguate duplicates deterministically in-place + TSet UsedNames = ReservedFinalNames; + TMap NextLogicalDuplicateIndexByName; + + for (FFlowPinSourceData& Pin : InOutAutoOutputPinsNext) + { + if (Pin.FlowPin.IsExecPin()) + { + continue; + } + + const FName OriginalName = Pin.FlowPin.PinName; + const int32 Count = NameCounts.FindRef(OriginalName); + + if (Count <= 1) + { + if (!Pin.ValueOwner.IsDefaultValueOwner()) + { + AddPinMappingToMap( + InOutMap, + Pin.FlowPin.PinName, + Pin.FlowPin.PinName, + Pin.ValueOwner); + } + + AppendDeferredPatchIfNeeded(InOutDeferredPatches, Pin); + continue; + } + + uint32& NextLogicalIndex = NextLogicalDuplicateIndexByName.FindOrAdd(OriginalName); + if (NextLogicalIndex == 0) + { + NextLogicalIndex = 1; + } + + const uint32 LogicalDuplicateIndex = NextLogicalIndex; + ++NextLogicalIndex; + + DisambiguateDuplicatePin(Pin, UsedNames, LogicalDuplicateIndex, InOutDeferredPatches); + + AddPinMappingToMap( + InOutMap, + Pin.FlowPin.PinName, + OriginalName, + Pin.ValueOwner); + } +} + +void FFlowAutoDataPinsWorkingData::DisambiguateDuplicatePin( + FFlowPinSourceData& PinSourceData, + TSet& InOutUsedNames, + uint32 LogicalDuplicateIndex, + TArray& InOutDeferredPatches) +{ + const FName BaseName = PinSourceData.FlowPin.PinName; + + uint32 DisambiguationSuffix = LogicalDuplicateIndex; + while (true) + { + const FName Candidate = FName(FString::Printf(TEXT("%s_%u"), *BaseName.ToString(), DisambiguationSuffix)); + if (!InOutUsedNames.Contains(Candidate)) + { + PinSourceData.FlowPin.PinName = Candidate; + InOutUsedNames.Add(Candidate); + + break; + } + + ++DisambiguationSuffix; + + if (!ensure(DisambiguationSuffix < 1000000u)) + { + UE_LOG(LogFlow, Error, TEXT("Pin name disambiguation failed for %s"), *BaseName.ToString()); + break; + } + } + + ApplyDuplicatePresentation(PinSourceData, LogicalDuplicateIndex); + AppendPinSourceToTooltip(PinSourceData); + + AppendDeferredPatchIfNeeded(InOutDeferredPatches, PinSourceData); +} + +void FFlowAutoDataPinsWorkingData::ApplyDuplicatePresentation( + FFlowPinSourceData& PinSourceData, + uint32 LogicalDuplicateIndex) +{ + const FString FriendlyStr = FString::Printf(TEXT("%s (%u)"), *PinSourceData.FlowPin.PinFriendlyName.ToString(), LogicalDuplicateIndex); + PinSourceData.FlowPin.PinFriendlyName = FText::FromString(FriendlyStr); +} + +void FFlowAutoDataPinsWorkingData::AppendPinSourceToTooltip(FFlowPinSourceData& PinSourceData) +{ + FString& Tooltip = PinSourceData.FlowPin.PinToolTip; + + const FName ValueOwnerName = PinSourceData.ValueOwner.GetValueOwnerName(); + if (!ValueOwnerName.IsNone()) + { + if (!Tooltip.IsEmpty()) + { + Tooltip += TEXT("\n"); + } + + Tooltip += FString::Printf(TEXT("Output of %s"), *ValueOwnerName.ToString()); + } +} + +#endif + +UObject* FFlowDataPinValueOwner::GetValueOwnerAsObject() +{ + return CastChecked(OwnerInterface, ECastCheckedType::NullAllowed); +} + +const UObject* FFlowDataPinValueOwner::GetValueOwnerAsObject() const +{ + return CastChecked(OwnerInterface, ECastCheckedType::NullAllowed); +} + +void FFlowDataPinValueOwnerCollection::AddValueOwner(IFlowDataPinValueOwnerInterface& ValueOwnerInterface) +{ + const int32 ValueOwnerIndex = ValueOwners.Num(); + ValueOwners.Add(FFlowDataPinValueOwner(ValueOwnerInterface, ValueOwnerIndex)); +} \ No newline at end of file diff --git a/Source/Flow/Private/Types/FlowClassUtils.cpp b/Source/Flow/Private/Types/FlowClassUtils.cpp new file mode 100644 index 000000000..acd0c2b42 --- /dev/null +++ b/Source/Flow/Private/Types/FlowClassUtils.cpp @@ -0,0 +1,61 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Types/FlowClassUtils.h" +#include "UObject/Class.h" +#include "UObject/UObjectIterator.h" + +#if WITH_EDITOR +TArray FlowClassUtils::GetClassesFromMetadataString(const FString& MetadataString) +{ + // Adapted from the inaccessible PropertyCustomizationHelpers::GetClassesFromMetadataString + + if (MetadataString.IsEmpty()) + { + return TArray(); + } + + auto FindClass = [](const FString& InClassName) -> UClass* + { + UClass* Class = UClass::TryFindTypeSlow(InClassName, EFindFirstObjectOptions::EnsureIfAmbiguous); + if (!Class) + { + Class = LoadObject(nullptr, *InClassName); + } + return Class; + }; + + TArray ClassNames; + MetadataString.ParseIntoArrayWS(ClassNames, TEXT(","), true); + + TArray Classes; + Classes.Reserve(ClassNames.Num()); + + for (const FString& ClassName : ClassNames) + { + UClass* Class = FindClass(ClassName); + if (!Class) + { + continue; + } + + // If the class is an interface, expand it to be all classes in memory that implement the class. + if (Class->HasAnyClassFlags(CLASS_Interface)) + { + for (TObjectIterator ClassIt; ClassIt; ++ClassIt) + { + UClass* ClassWithInterface = (*ClassIt); + if (ClassWithInterface->ImplementsInterface(Class)) + { + Classes.Add(ClassWithInterface); + } + } + } + else + { + Classes.Add(Class); + } + } + + return Classes; +} +#endif \ No newline at end of file diff --git a/Source/Flow/Private/Types/FlowDataPinBlueprintLibrary.cpp b/Source/Flow/Private/Types/FlowDataPinBlueprintLibrary.cpp new file mode 100644 index 000000000..f756bcbcd --- /dev/null +++ b/Source/Flow/Private/Types/FlowDataPinBlueprintLibrary.cpp @@ -0,0 +1,2273 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Types/FlowDataPinBlueprintLibrary.h" +#include "Types/FlowDataPinValue.h" +#include "Nodes/FlowNodeBase.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowDataPinBlueprintLibrary) + +void UFlowDataPinBlueprintLibrary::ResolveAndExtract_Impl( + UFlowNodeBase* Target, + FName PinName, + EFlowDataPinResolveSimpleResult& SimpleResult, + EFlowDataPinResolveResult& ResultEnum, + auto&& ExtractLambda) +{ + using namespace FlowPinType; + + if (!IsValid(Target)) + { + ResultEnum = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + SimpleResult = ConvertToSimpleResult(ResultEnum); + return; + } + + ResultEnum = ExtractLambda(); + SimpleResult = ConvertToSimpleResult(ResultEnum); +} + +void UFlowDataPinBlueprintLibrary::ResolveAsBool(UFlowNodeBase* Target, const FFlowDataPinValue_Bool& BoolValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, bool& Value, EFlowSingleFromArray SingleMode) +{ + Value = false; + ResolveAndExtract_Impl(Target, BoolValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValue(BoolValue.PropertyPinName, Value, SingleMode); + }); +} + +void UFlowDataPinBlueprintLibrary::ResolveAsBoolArray(UFlowNodeBase* Target, const FFlowDataPinValue_Bool& BoolValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values) +{ + Values.Reset(); + ResolveAndExtract_Impl(Target, BoolValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValues(BoolValue.PropertyPinName, Values); + }); +} + +// Int +void UFlowDataPinBlueprintLibrary::ResolveAsInt(UFlowNodeBase* Target, const FFlowDataPinValue_Int& IntValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, int32& Value, EFlowSingleFromArray SingleMode) +{ + Value = 0; + ResolveAndExtract_Impl(Target, IntValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValue(IntValue.PropertyPinName, Value, SingleMode); + }); +} + +void UFlowDataPinBlueprintLibrary::ResolveAsIntArray(UFlowNodeBase* Target, const FFlowDataPinValue_Int& IntValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values) +{ + Values.Reset(); + ResolveAndExtract_Impl(Target, IntValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValues(IntValue.PropertyPinName, Values); + }); +} + +// Int64 +void UFlowDataPinBlueprintLibrary::ResolveAsInt64(UFlowNodeBase* Target, const FFlowDataPinValue_Int64& Int64Value, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, int64& Value, EFlowSingleFromArray SingleMode) +{ + Value = 0; + ResolveAndExtract_Impl(Target, Int64Value.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValue(Int64Value.PropertyPinName, Value, SingleMode); + }); +} + +void UFlowDataPinBlueprintLibrary::ResolveAsInt64Array(UFlowNodeBase* Target, const FFlowDataPinValue_Int64& Int64Value, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values) +{ + Values.Reset(); + ResolveAndExtract_Impl(Target, Int64Value.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValues(Int64Value.PropertyPinName, Values); + }); +} + +// Float +void UFlowDataPinBlueprintLibrary::ResolveAsFloat(UFlowNodeBase* Target, const FFlowDataPinValue_Float& FloatValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, float& Value, EFlowSingleFromArray SingleMode) +{ + Value = 0.0f; + ResolveAndExtract_Impl(Target, FloatValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValue(FloatValue.PropertyPinName, Value, SingleMode); + }); +} + +void UFlowDataPinBlueprintLibrary::ResolveAsFloatArray(UFlowNodeBase* Target, const FFlowDataPinValue_Float& FloatValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values) +{ + Values.Reset(); + ResolveAndExtract_Impl(Target, FloatValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValues(FloatValue.PropertyPinName, Values); + }); +} + +// Double +void UFlowDataPinBlueprintLibrary::ResolveAsDouble(UFlowNodeBase* Target, const FFlowDataPinValue_Double& DoubleValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, double& Value, EFlowSingleFromArray SingleMode) +{ + Value = 0.0; + ResolveAndExtract_Impl(Target, DoubleValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValue(DoubleValue.PropertyPinName, Value, SingleMode); + }); +} + +void UFlowDataPinBlueprintLibrary::ResolveAsDoubleArray(UFlowNodeBase* Target, const FFlowDataPinValue_Double& DoubleValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values) +{ + Values.Reset(); + ResolveAndExtract_Impl(Target, DoubleValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValues(DoubleValue.PropertyPinName, Values); + }); +} + +// Name +void UFlowDataPinBlueprintLibrary::ResolveAsName(UFlowNodeBase* Target, const FFlowDataPinValue_Name& NameValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, FName& Value, EFlowSingleFromArray SingleMode) +{ + Value = NAME_None; + ResolveAndExtract_Impl(Target, NameValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValue(NameValue.PropertyPinName, Value, SingleMode); + }); +} + +void UFlowDataPinBlueprintLibrary::ResolveAsNameArray(UFlowNodeBase* Target, const FFlowDataPinValue_Name& NameValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values) +{ + Values.Reset(); + ResolveAndExtract_Impl(Target, NameValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValues(NameValue.PropertyPinName, Values); + }); +} + +// String +void UFlowDataPinBlueprintLibrary::ResolveAsString(UFlowNodeBase* Target, const FFlowDataPinValue_String& StringValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, FString& Value, EFlowSingleFromArray SingleMode) +{ + Value = FString(); + ResolveAndExtract_Impl(Target, StringValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValue(StringValue.PropertyPinName, Value, SingleMode); + }); +} + +void UFlowDataPinBlueprintLibrary::ResolveAsStringArray(UFlowNodeBase* Target, const FFlowDataPinValue_String& StringValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values) +{ + Values.Reset(); + ResolveAndExtract_Impl(Target, StringValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValues(StringValue.PropertyPinName, Values); + }); +} + +// Text +void UFlowDataPinBlueprintLibrary::ResolveAsText(UFlowNodeBase* Target, const FFlowDataPinValue_Text& TextValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, FText& Value, EFlowSingleFromArray SingleMode) +{ + Value = FText::GetEmpty(); + ResolveAndExtract_Impl(Target, TextValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValue(TextValue.PropertyPinName, Value, SingleMode); + }); +} + +void UFlowDataPinBlueprintLibrary::ResolveAsTextArray(UFlowNodeBase* Target, const FFlowDataPinValue_Text& TextValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values) +{ + Values.Reset(); + ResolveAndExtract_Impl(Target, TextValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValues(TextValue.PropertyPinName, Values); + }); +} + +// Enum +void UFlowDataPinBlueprintLibrary::ResolveAsEnum(UFlowNodeBase* Target, const FFlowDataPinValue_Enum& EnumValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, uint8& Value, EFlowSingleFromArray SingleMode) +{ + Value = 0; + ResolveAndExtract_Impl(Target, EnumValue.PropertyPinName, Result, ResultEnum, [&]() { + FName ExtractedName; + UEnum* EnumClass = nullptr; + const EFlowDataPinResolveResult ResolveResult = Target->TryResolveDataPinValue(EnumValue.PropertyPinName, ExtractedName, EnumClass, SingleMode); + if (FlowPinType::IsSuccess(ResolveResult) && ensure(IsValid(EnumClass))) + { + const int64 IntValue = EnumClass->GetValueByName(ExtractedName); + Value = static_cast(IntValue); + } + return ResolveResult; + }); +} + +void UFlowDataPinBlueprintLibrary::ResolveAsEnumArray(UFlowNodeBase* Target, const FFlowDataPinValue_Enum& EnumValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values) +{ + Values.Reset(); + ResolveAndExtract_Impl(Target, EnumValue.PropertyPinName, Result, ResultEnum, [&]() { + TArray Names; + UEnum* EnumClass = nullptr; + const EFlowDataPinResolveResult ResolveResult = Target->TryResolveDataPinValues(EnumValue.PropertyPinName, Names, EnumClass); + if (FlowPinType::IsSuccess(ResolveResult) && ensure(IsValid(EnumClass))) + { + Values.Reserve(Names.Num()); + for (const FName& Name : Names) + { + const int64 IntValue = EnumClass->GetValueByName(Name); + Values.Add(static_cast(IntValue)); + } + } + return ResolveResult; + }); +} + +// Vector +void UFlowDataPinBlueprintLibrary::ResolveAsVector(UFlowNodeBase* Target, const FFlowDataPinValue_Vector& VectorValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, FVector& Value, EFlowSingleFromArray SingleMode) +{ + Value = FVector::ZeroVector; + ResolveAndExtract_Impl(Target, VectorValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValue(VectorValue.PropertyPinName, Value, SingleMode); + }); +} + +void UFlowDataPinBlueprintLibrary::ResolveAsVectorArray(UFlowNodeBase* Target, const FFlowDataPinValue_Vector& VectorValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values) +{ + Values.Reset(); + ResolveAndExtract_Impl(Target, VectorValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValues(VectorValue.PropertyPinName, Values); + }); +} + +// Rotator +void UFlowDataPinBlueprintLibrary::ResolveAsRotator(UFlowNodeBase* Target, const FFlowDataPinValue_Rotator& RotatorValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, FRotator& Value, EFlowSingleFromArray SingleMode) +{ + Value = FRotator::ZeroRotator; + ResolveAndExtract_Impl(Target, RotatorValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValue(RotatorValue.PropertyPinName, Value, SingleMode); + }); +} + +void UFlowDataPinBlueprintLibrary::ResolveAsRotatorArray(UFlowNodeBase* Target, const FFlowDataPinValue_Rotator& RotatorValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values) +{ + Values.Reset(); + ResolveAndExtract_Impl(Target, RotatorValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValues(RotatorValue.PropertyPinName, Values); + }); +} + +// Transform +void UFlowDataPinBlueprintLibrary::ResolveAsTransform(UFlowNodeBase* Target, const FFlowDataPinValue_Transform& TransformValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, FTransform& Value, EFlowSingleFromArray SingleMode) +{ + Value = FTransform::Identity; + ResolveAndExtract_Impl(Target, TransformValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValue(TransformValue.PropertyPinName, Value, SingleMode); + }); +} + +void UFlowDataPinBlueprintLibrary::ResolveAsTransformArray(UFlowNodeBase* Target, const FFlowDataPinValue_Transform& TransformValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values) +{ + Values.Reset(); + ResolveAndExtract_Impl(Target, TransformValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValues(TransformValue.PropertyPinName, Values); + }); +} + +// GameplayTag +void UFlowDataPinBlueprintLibrary::ResolveAsGameplayTag(UFlowNodeBase* Target, const FFlowDataPinValue_GameplayTag& GameplayTagValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, FGameplayTag& Value, EFlowSingleFromArray SingleMode) +{ + Value = FGameplayTag(); + ResolveAndExtract_Impl(Target, GameplayTagValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValue(GameplayTagValue.PropertyPinName, Value, SingleMode); + }); +} + +void UFlowDataPinBlueprintLibrary::ResolveAsGameplayTagArray(UFlowNodeBase* Target, const FFlowDataPinValue_GameplayTag& GameplayTagValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values) +{ + Values.Reset(); + ResolveAndExtract_Impl(Target, GameplayTagValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValues(GameplayTagValue.PropertyPinName, Values); + }); +} + +// GameplayTagContainer +void UFlowDataPinBlueprintLibrary::ResolveAsGameplayTagContainer(UFlowNodeBase* Target, const FFlowDataPinValue_GameplayTagContainer& GameplayTagContainerValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, FGameplayTagContainer& Value) +{ + Value = FGameplayTagContainer(); + ResolveAndExtract_Impl(Target, GameplayTagContainerValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValue(GameplayTagContainerValue.PropertyPinName, Value, EFlowSingleFromArray::FirstValue); + }); +} + +// InstancedStruct +void UFlowDataPinBlueprintLibrary::ResolveAsInstancedStruct(UFlowNodeBase* Target, const FFlowDataPinValue_InstancedStruct& InstancedStructValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, FInstancedStruct& Value, EFlowSingleFromArray SingleMode) +{ + Value = FInstancedStruct(); + ResolveAndExtract_Impl(Target, InstancedStructValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValue(InstancedStructValue.PropertyPinName, Value, SingleMode); + }); +} + +void UFlowDataPinBlueprintLibrary::ResolveAsInstancedStructArray(UFlowNodeBase* Target, const FFlowDataPinValue_InstancedStruct& InstancedStructValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values) +{ + Values.Reset(); + ResolveAndExtract_Impl(Target, InstancedStructValue.PropertyPinName, Result, ResultEnum, [&]() { + return Target->TryResolveDataPinValues(InstancedStructValue.PropertyPinName, Values); + }); +} + +// Object +void UFlowDataPinBlueprintLibrary::ResolveAsObject(UFlowNodeBase* Target, const FFlowDataPinValue_Object& ObjectValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, UObject*& Value, EFlowSingleFromArray SingleMode) +{ + Value = nullptr; + ResolveAndExtract_Impl(Target, ObjectValue.PropertyPinName, Result, ResultEnum, [&]() { + TObjectPtr Obj; + const EFlowDataPinResolveResult ResolveResult = Target->TryResolveDataPinValue(ObjectValue.PropertyPinName, Obj, SingleMode); + Value = Obj; + return ResolveResult; + }); +} + +void UFlowDataPinBlueprintLibrary::ResolveAsObjectArray(UFlowNodeBase* Target, const FFlowDataPinValue_Object& ObjectValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values) +{ + Values.Reset(); + ResolveAndExtract_Impl(Target, ObjectValue.PropertyPinName, Result, ResultEnum, [&]() { + TArray> ObjArray; + const EFlowDataPinResolveResult ResolveResult = Target->TryResolveDataPinValues(ObjectValue.PropertyPinName, ObjArray); + Values = ObjArray; + return ResolveResult; + }); +} + +// Class +void UFlowDataPinBlueprintLibrary::ResolveAsClass(UFlowNodeBase* Target, const FFlowDataPinValue_Class& ClassValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, UClass*& Value, EFlowSingleFromArray SingleMode) +{ + Value = nullptr; + ResolveAndExtract_Impl(Target, ClassValue.PropertyPinName, Result, ResultEnum, [&]() { + TObjectPtr ClassObj; + const EFlowDataPinResolveResult ResolveResult = Target->TryResolveDataPinValue(ClassValue.PropertyPinName, ClassObj, SingleMode); + Value = ClassObj; + return ResolveResult; + }); +} + +void UFlowDataPinBlueprintLibrary::ResolveAsClassArray(UFlowNodeBase* Target, const FFlowDataPinValue_Class& ClassValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values) +{ + Values.Reset(); + ResolveAndExtract_Impl(Target, ClassValue.PropertyPinName, Result, ResultEnum, [&]() { + TArray> ClassArray; + const EFlowDataPinResolveResult ResolveResult = Target->TryResolveDataPinValues(ClassValue.PropertyPinName, ClassArray); + Values = ClassArray; + return ResolveResult; + }); +} + +// Bool +bool UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsBool(const FFlowDataPinValue_Bool& BoolValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + bool Extracted = false; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValue(BoolValue.PropertyPinName, Extracted, SingleMode); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Bool failed on pin '%s': %s"), + *BoolValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + + return Extracted; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsBoolArray(const FFlowDataPinValue_Bool& BoolValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + TArray Extracted; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValues(BoolValue.PropertyPinName, Extracted); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Bool Array failed on pin '%s': %s"), + *BoolValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + return TArray(); + } + + return Extracted; +} + +// Int +int32 UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsInt(const FFlowDataPinValue_Int& IntValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + int32 Extracted = 0; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValue(IntValue.PropertyPinName, Extracted, SingleMode); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Int failed on pin '%s': %s"), + *IntValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + + return Extracted; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsIntArray(const FFlowDataPinValue_Int& IntValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + TArray Extracted; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValues(IntValue.PropertyPinName, Extracted); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Int Array failed on pin '%s': %s"), + *IntValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + return TArray(); + } + + return Extracted; +} + +// Int64 +int64 UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsInt64(const FFlowDataPinValue_Int64& Int64Value, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + int64 Extracted = 0; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValue(Int64Value.PropertyPinName, Extracted, SingleMode); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Int64 failed on pin '%s': %s"), + *Int64Value.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + + return Extracted; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsInt64Array(const FFlowDataPinValue_Int64& Int64Value, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + TArray Extracted; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValues(Int64Value.PropertyPinName, Extracted); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Int64 Array failed on pin '%s': %s"), + *Int64Value.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + return TArray(); + } + + return Extracted; +} + +// Float +float UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsFloat(const FFlowDataPinValue_Float& FloatValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + float Extracted = 0.0f; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValue(FloatValue.PropertyPinName, Extracted, SingleMode); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Float failed on pin '%s': %s"), + *FloatValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + + return Extracted; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsFloatArray(const FFlowDataPinValue_Float& FloatValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + TArray Extracted; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValues(FloatValue.PropertyPinName, Extracted); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Float Array failed on pin '%s': %s"), + *FloatValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + return TArray(); + } + + return Extracted; +} + +// Double +double UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsDouble(const FFlowDataPinValue_Double& DoubleValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + double Extracted = 0.0; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValue(DoubleValue.PropertyPinName, Extracted, SingleMode); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Double failed on pin '%s': %s"), + *DoubleValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + + return Extracted; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsDoubleArray(const FFlowDataPinValue_Double& DoubleValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + TArray Extracted; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValues(DoubleValue.PropertyPinName, Extracted); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Double Array failed on pin '%s': %s"), + *DoubleValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + return TArray(); + } + + return Extracted; +} + +// Name +FName UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsName(const FFlowDataPinValue_Name& NameValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + FName Extracted = NAME_None; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValue(NameValue.PropertyPinName, Extracted, SingleMode); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Name failed on pin '%s': %s"), + *NameValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + + return Extracted; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsNameArray(const FFlowDataPinValue_Name& NameValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + TArray Extracted; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValues(NameValue.PropertyPinName, Extracted); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Name Array failed on pin '%s': %s"), + *NameValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + return TArray(); + } + + return Extracted; +} + +// String +FString UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsString(const FFlowDataPinValue_String& StringValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + FString Extracted; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValue(StringValue.PropertyPinName, Extracted, SingleMode); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to String failed on pin '%s': %s"), + *StringValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + + return Extracted; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsStringArray(const FFlowDataPinValue_String& StringValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + TArray Extracted; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValues(StringValue.PropertyPinName, Extracted); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to String Array failed on pin '%s': %s"), + *StringValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + return TArray(); + } + + return Extracted; +} + +// Text +FText UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsText(const FFlowDataPinValue_Text& TextValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + FText Extracted; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValue(TextValue.PropertyPinName, Extracted, SingleMode); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Text failed on pin '%s': %s"), + *TextValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + + return Extracted; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsTextArray(const FFlowDataPinValue_Text& TextValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + TArray Extracted; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValues(TextValue.PropertyPinName, Extracted); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Text Array failed on pin '%s': %s"), + *TextValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + return TArray(); + } + + return Extracted; +} + +// Enum +uint8 UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsEnum(const FFlowDataPinValue_Enum& EnumValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + FName ExtractedName; + UEnum* EnumClass = nullptr; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValue(EnumValue.PropertyPinName, ExtractedName, EnumClass, SingleMode); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Enum failed on pin '%s': %s"), + *EnumValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + return static_cast(INDEX_NONE); + } + + if (ensure(IsValid(EnumClass))) + { + const uint64 ValueInt = EnumClass->GetValueByName(ExtractedName); + return static_cast(ValueInt); + } + + return static_cast(INDEX_NONE); +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsEnumArray(const FFlowDataPinValue_Enum& EnumValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + TArray ExtractedNames; + UEnum* EnumClass = nullptr; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValues(EnumValue.PropertyPinName, ExtractedNames, EnumClass); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Enum Array failed on pin '%s': %s"), + *EnumValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + return TArray(); + } + + if (!ensure(IsValid(EnumClass))) + { + return TArray(); + } + + TArray Result; + Result.Reserve(ExtractedNames.Num()); + for (const FName& Name : ExtractedNames) + { + const uint64 ValueInt = EnumClass->GetValueByName(Name); + Result.Add(static_cast(ValueInt)); + } + + return Result; +} + +// Vector +FVector UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsVector(const FFlowDataPinValue_Vector& VectorValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + FVector Extracted = FVector::ZeroVector; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValue(VectorValue.PropertyPinName, Extracted, SingleMode); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Vector failed on pin '%s': %s"), + *VectorValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + + return Extracted; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsVectorArray(const FFlowDataPinValue_Vector& VectorValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + TArray Extracted; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValues(VectorValue.PropertyPinName, Extracted); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Vector Array failed on pin '%s': %s"), + *VectorValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + return TArray(); + } + + return Extracted; +} + +// Rotator +FRotator UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsRotator(const FFlowDataPinValue_Rotator& RotatorValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + FRotator Extracted = FRotator::ZeroRotator; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValue(RotatorValue.PropertyPinName, Extracted, SingleMode); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Rotator failed on pin '%s': %s"), + *RotatorValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + + return Extracted; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsRotatorArray(const FFlowDataPinValue_Rotator& RotatorValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + TArray Extracted; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValues(RotatorValue.PropertyPinName, Extracted); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Rotator Array failed on pin '%s': %s"), + *RotatorValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + return TArray(); + } + + return Extracted; +} + +// Transform +FTransform UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsTransform(const FFlowDataPinValue_Transform& TransformValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + FTransform Extracted = FTransform::Identity; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValue(TransformValue.PropertyPinName, Extracted, SingleMode); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Transform failed on pin '%s': %s"), + *TransformValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + + return Extracted; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsTransformArray(const FFlowDataPinValue_Transform& TransformValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + TArray Extracted; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValues(TransformValue.PropertyPinName, Extracted); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Transform Array failed on pin '%s': %s"), + *TransformValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + return TArray(); + } + + return Extracted; +} + +// GameplayTag +FGameplayTag UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsGameplayTag(const FFlowDataPinValue_GameplayTag& GameplayTagValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + FGameplayTag Extracted; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValue(GameplayTagValue.PropertyPinName, Extracted, SingleMode); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to GameplayTag failed on pin '%s': %s"), + *GameplayTagValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + + return Extracted; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsGameplayTagArray(const FFlowDataPinValue_GameplayTag& GameplayTagValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + TArray Extracted; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValues(GameplayTagValue.PropertyPinName, Extracted); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to GameplayTag Array failed on pin '%s': %s"), + *GameplayTagValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + return TArray(); + } + + return Extracted; +} + +// GameplayTagContainer +FGameplayTagContainer UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsGameplayTagContainer(const FFlowDataPinValue_GameplayTagContainer& GameplayTagContainerValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + FGameplayTagContainer Extracted; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValue(GameplayTagContainerValue.PropertyPinName, Extracted); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to GameplayTagContainer failed on pin '%s': %s"), + *GameplayTagContainerValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + + return Extracted; +} + +// InstancedStruct +FInstancedStruct UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsInstancedStruct(const FFlowDataPinValue_InstancedStruct& InstancedStructValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + FInstancedStruct Extracted; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValue(InstancedStructValue.PropertyPinName, Extracted, SingleMode); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to InstancedStruct failed on pin '%s': %s"), + *InstancedStructValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + + return Extracted; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsInstancedStructArray(const FFlowDataPinValue_InstancedStruct& InstancedStructValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + TArray Extracted; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValues(InstancedStructValue.PropertyPinName, Extracted); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to InstancedStruct Array failed on pin '%s': %s"), + *InstancedStructValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + return TArray(); + } + + return Extracted; +} + +// Object +UObject* UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsObject(const FFlowDataPinValue_Object& ObjectValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + TObjectPtr Extracted = nullptr; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValue(ObjectValue.PropertyPinName, Extracted, SingleMode); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Object failed on pin '%s': %s"), + *ObjectValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + + return Extracted; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsObjectArray(const FFlowDataPinValue_Object& ObjectValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + TArray> ExtractedTemp; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValues(ObjectValue.PropertyPinName, ExtractedTemp); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Object Array failed on pin '%s': %s"), + *ObjectValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + return TArray(); + } + + TArray Result; + Result.Reserve(ExtractedTemp.Num()); + for (const TObjectPtr& Obj : ExtractedTemp) + { + Result.Add(Obj.Get()); + } + return Result; +} + +// Class +UClass* UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsClass(const FFlowDataPinValue_Class& ClassValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + TObjectPtr Extracted = nullptr; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValue(ClassValue.PropertyPinName, Extracted, SingleMode); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Class failed on pin '%s': %s"), + *ClassValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + + return Extracted; +} + +// Class +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryResolveAsClassArray(const FFlowDataPinValue_Class& ClassValue, const UFlowNodeBase* Target) +{ + using namespace FlowPinType; + + TArray> ExtractedTemp; + EFlowDataPinResolveResult ResolveResult = EFlowDataPinResolveResult::FailedNullFlowNodeBase; + + if (IsValid(Target)) + { + ResolveResult = Target->TryResolveDataPinValues(ClassValue.PropertyPinName, ExtractedTemp); + } + + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("Auto-Resolve to Class Array failed on pin '%s': %s"), + *ClassValue.PropertyPinName.ToString(), + *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + return TArray(); + } + + TArray Result; + Result.Reserve(ExtractedTemp.Num()); + for (const TObjectPtr& ClassPtr : ExtractedTemp) + { + Result.Add(ClassPtr.Get()); + } + + return Result; +} + +bool UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractBool(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + bool ExtractedValue = false; + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + const EFlowDataPinResolveResult ResolveResult = TryExtractValue(DataPinResult, ExtractedValue, SingleMode); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractBool Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValue; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractBoolArray(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + TArray ExtractedValues; + const EFlowDataPinResolveResult ResolveResult = TryExtractValues(DataPinResult, ExtractedValues); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractBoolArray Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValues; +} + +int32 UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractInt(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + int32 ExtractedValue = 0; + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + const EFlowDataPinResolveResult ResolveResult = TryExtractValue(DataPinResult, ExtractedValue, SingleMode); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractInt Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValue; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractIntArray(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + TArray ExtractedValues; + const EFlowDataPinResolveResult ResolveResult = TryExtractValues(DataPinResult, ExtractedValues); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractIntArray Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValues; +} + +int64 UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractInt64(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + int64 ExtractedValue = 0; + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + const EFlowDataPinResolveResult ResolveResult = TryExtractValue(DataPinResult, ExtractedValue, SingleMode); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractInt64 Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValue; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractInt64Array(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + TArray ExtractedValues; + const EFlowDataPinResolveResult ResolveResult = TryExtractValues(DataPinResult, ExtractedValues); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractInt64Array Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValues; +} + +float UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractFloat(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + float ExtractedValue = 0.0f; + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + const EFlowDataPinResolveResult ResolveResult = TryExtractValue(DataPinResult, ExtractedValue, SingleMode); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractFloat Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValue; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractFloatArray(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + TArray ExtractedValues; + const EFlowDataPinResolveResult ResolveResult = TryExtractValues(DataPinResult, ExtractedValues); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractFloatArray Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValues; +} + +double UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractDouble(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + double ExtractedValue = 0.0; + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + const EFlowDataPinResolveResult ResolveResult = TryExtractValue(DataPinResult, ExtractedValue, SingleMode); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractDouble Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValue; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractDoubleArray(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + TArray ExtractedValues; + const EFlowDataPinResolveResult ResolveResult = TryExtractValues(DataPinResult, ExtractedValues); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractDoubleArray Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValues; +} + +FName UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractName(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + FName ExtractedValue = NAME_None; + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + const EFlowDataPinResolveResult ResolveResult = TryExtractValue(DataPinResult, ExtractedValue, SingleMode); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractName Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValue; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractNameArray(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + TArray ExtractedValues; + const EFlowDataPinResolveResult ResolveResult = TryExtractValues(DataPinResult, ExtractedValues); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractNameArray Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValues; +} + +FString UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractString(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + FString ExtractedValue; + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + const EFlowDataPinResolveResult ResolveResult = TryExtractValue(DataPinResult, ExtractedValue, SingleMode); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractString Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValue; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractStringArray(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + TArray ExtractedValues; + const EFlowDataPinResolveResult ResolveResult = TryExtractValues(DataPinResult, ExtractedValues); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractStringArray Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValues; +} + +FText UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractText(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + FText ExtractedValue; + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + const EFlowDataPinResolveResult ResolveResult = TryExtractValue(DataPinResult, ExtractedValue, SingleMode); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractText Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValue; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractTextArray(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + TArray ExtractedValues; + const EFlowDataPinResolveResult ResolveResult = TryExtractValues(DataPinResult, ExtractedValues); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractTextArray Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValues; +} + +uint8 UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractEnum(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + FName ExtractedValueAsName; + UEnum* EnumClass = nullptr; + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + const EFlowDataPinResolveResult ResolveResult = TryExtractValue(DataPinResult, ExtractedValueAsName, EnumClass, SingleMode); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractEnum Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + + if (ensure(IsValid(EnumClass))) + { + const uint64 EnumValueAsInt = EnumClass->GetValueByName(ExtractedValueAsName); + return static_cast(EnumValueAsInt); + } + + return static_cast(INDEX_NONE); +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractEnumArray(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + + TArray ExtractedNames; + UEnum* EnumClass = nullptr; + + const EFlowDataPinResolveResult ResolveResult = TryExtractValues(DataPinResult, ExtractedNames, EnumClass); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractEnumArray Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + return TArray(); + } + + if (!ensure(IsValid(EnumClass))) + { + return TArray(); + } + + TArray ExtractedValues; + ExtractedValues.Reserve(ExtractedNames.Num()); + + for (const FName& Name : ExtractedNames) + { + const uint64 EnumValueAsInt = EnumClass->GetValueByName(Name); + ExtractedValues.Add(static_cast(EnumValueAsInt)); + } + + return ExtractedValues; +} + +FVector UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractVector(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + FVector ExtractedValue = FVector::ZeroVector; + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + const EFlowDataPinResolveResult ResolveResult = TryExtractValue(DataPinResult, ExtractedValue, SingleMode); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractVector Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValue; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractVectorArray(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + TArray ExtractedValues; + const EFlowDataPinResolveResult ResolveResult = TryExtractValues(DataPinResult, ExtractedValues); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractVectorArray Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValues; +} + +FRotator UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractRotator(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + FRotator ExtractedValue = FRotator::ZeroRotator; + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + const EFlowDataPinResolveResult ResolveResult = TryExtractValue(DataPinResult, ExtractedValue, SingleMode); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractRotator Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValue; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractRotatorArray(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + TArray ExtractedValues; + const EFlowDataPinResolveResult ResolveResult = TryExtractValues(DataPinResult, ExtractedValues); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractRotatorArray Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValues; +} + +FTransform UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractTransform(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + FTransform ExtractedValue = FTransform::Identity; + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + const EFlowDataPinResolveResult ResolveResult = TryExtractValue(DataPinResult, ExtractedValue, SingleMode); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractTransform Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValue; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractTransformArray(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + TArray ExtractedValues; + const EFlowDataPinResolveResult ResolveResult = TryExtractValues(DataPinResult, ExtractedValues); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractTransformArray Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValues; +} + +FGameplayTag UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractGameplayTag(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + FGameplayTag ExtractedValue; + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + const EFlowDataPinResolveResult ResolveResult = TryExtractValue(DataPinResult, ExtractedValue, SingleMode); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractGameplayTag Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValue; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractGameplayTagArray(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + TArray ExtractedValues; + const EFlowDataPinResolveResult ResolveResult = TryExtractValues(DataPinResult, ExtractedValues); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractGameplayTagArray Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValues; +} + +FGameplayTagContainer UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractGameplayTagContainer(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + FGameplayTagContainer ExtractedValue; + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + const EFlowDataPinResolveResult ResolveResult = TryExtractValue(DataPinResult, ExtractedValue, SingleMode); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractGameplayTagContainer Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValue; +} + +FInstancedStruct UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractInstancedStruct(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + FInstancedStruct ExtractedValue; + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + const EFlowDataPinResolveResult ResolveResult = TryExtractValue(DataPinResult, ExtractedValue, SingleMode); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractInstancedStruct Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValue; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractInstancedStructArray(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + TArray ExtractedValues; + const EFlowDataPinResolveResult ResolveResult = TryExtractValues(DataPinResult, ExtractedValues); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractInstancedStructArray Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValues; +} + +UObject* UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractObject(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + TObjectPtr ExtractedValue = nullptr; + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + const EFlowDataPinResolveResult ResolveResult = TryExtractValue(DataPinResult, ExtractedValue, SingleMode); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractObject Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValue; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractObjectArray(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + TArray> ExtractedValues; + const EFlowDataPinResolveResult ResolveResult = TryExtractValues(DataPinResult, ExtractedValues); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractObjectArray Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValues; +} + +UClass* UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractClass(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + TObjectPtr ExtractedValue = nullptr; + constexpr EFlowSingleFromArray SingleMode = EFlowSingleFromArray::LastValue; + const EFlowDataPinResolveResult ResolveResult = TryExtractValue(DataPinResult, ExtractedValue, SingleMode); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractClass Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValue; +} + +TArray UFlowDataPinBlueprintLibrary::AutoConvert_TryExtractClassArray(const FFlowDataPinResult& DataPinResult) +{ + using namespace FlowPinType; + TArray> ExtractedValues; + const EFlowDataPinResolveResult ResolveResult = TryExtractValues(DataPinResult, ExtractedValues); + if (!IsSuccess(ResolveResult)) + { + UE_LOG(LogFlow, Error, TEXT("TryExtractClassArray Error: %s"), *UEnum::GetDisplayValueAsText(ResolveResult).ToString()); + } + return ExtractedValues; +} + +bool UFlowDataPinBlueprintLibrary::GetBoolValue(const FFlowDataPinValue_Bool& BoolDataPinValue, EFlowSingleFromArray SingleFromArray) +{ +#if WITH_EDITOR + if (BoolDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetBoolValue on an input pin, use ResolveAsBool instead.")); + } +#endif + const int32 Index = EFlowSingleFromArray_Classifiers::ConvertToIndex(SingleFromArray, BoolDataPinValue.Values.Num()); + if (BoolDataPinValue.Values.IsValidIndex(Index)) + { + return BoolDataPinValue.Values[Index]; + } + UE_LOG(LogFlow, Error, TEXT("Insufficient values in Bool Data Pin Value.")); + return false; +} + +TArray UFlowDataPinBlueprintLibrary::GetBoolValues(FFlowDataPinValue_Bool& BoolDataPinValue) +{ +#if WITH_EDITOR + if (BoolDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetBoolValues on an input pin, use ResolveAsBoolArray instead.")); + } +#endif + return BoolDataPinValue.Values; +} + +// Int +int32 UFlowDataPinBlueprintLibrary::GetIntValue(const FFlowDataPinValue_Int& IntDataPinValue, EFlowSingleFromArray SingleFromArray) +{ +#if WITH_EDITOR + if (IntDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetIntValue on an input pin, use ResolveAsInt instead.")); + } +#endif + const int32 Index = EFlowSingleFromArray_Classifiers::ConvertToIndex(SingleFromArray, IntDataPinValue.Values.Num()); + if (IntDataPinValue.Values.IsValidIndex(Index)) + { + return IntDataPinValue.Values[Index]; + } + UE_LOG(LogFlow, Error, TEXT("Insufficient values in Int Data Pin Value.")); + return 0; +} + +TArray UFlowDataPinBlueprintLibrary::GetIntValues(FFlowDataPinValue_Int& IntDataPinValue) +{ +#if WITH_EDITOR + if (IntDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetIntValues on an input pin, use ResolveAsIntArray instead.")); + } +#endif + return IntDataPinValue.Values; +} + +// Int64 +int64 UFlowDataPinBlueprintLibrary::GetInt64Value(const FFlowDataPinValue_Int64& Int64DataPinValue, EFlowSingleFromArray SingleFromArray) +{ +#if WITH_EDITOR + if (Int64DataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetInt64Value on an input pin, use ResolveAsInt64 instead.")); + } +#endif + const int32 Index = EFlowSingleFromArray_Classifiers::ConvertToIndex(SingleFromArray, Int64DataPinValue.Values.Num()); + if (Int64DataPinValue.Values.IsValidIndex(Index)) + { + return Int64DataPinValue.Values[Index]; + } + UE_LOG(LogFlow, Error, TEXT("Insufficient values in Int64 Data Pin Value.")); + return 0; +} + +TArray UFlowDataPinBlueprintLibrary::GetInt64Values(FFlowDataPinValue_Int64& Int64DataPinValue) +{ +#if WITH_EDITOR + if (Int64DataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetInt64Values on an input pin, use ResolveAsInt64Array instead.")); + } +#endif + return Int64DataPinValue.Values; +} + +// Float +float UFlowDataPinBlueprintLibrary::GetFloatValue(const FFlowDataPinValue_Float& FloatDataPinValue, EFlowSingleFromArray SingleFromArray) +{ +#if WITH_EDITOR + if (FloatDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetFloatValue on an input pin, use ResolveAsFloat instead.")); + } +#endif + const int32 Index = EFlowSingleFromArray_Classifiers::ConvertToIndex(SingleFromArray, FloatDataPinValue.Values.Num()); + if (FloatDataPinValue.Values.IsValidIndex(Index)) + { + return FloatDataPinValue.Values[Index]; + } + UE_LOG(LogFlow, Error, TEXT("Insufficient values in Float Data Pin Value.")); + return 0.f; +} + +TArray UFlowDataPinBlueprintLibrary::GetFloatValues(FFlowDataPinValue_Float& FloatDataPinValue) +{ +#if WITH_EDITOR + if (FloatDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetFloatValues on an input pin, use ResolveAsFloatArray instead.")); + } +#endif + return FloatDataPinValue.Values; +} + +// Double +double UFlowDataPinBlueprintLibrary::GetDoubleValue(const FFlowDataPinValue_Double& DoubleDataPinValue, EFlowSingleFromArray SingleFromArray) +{ +#if WITH_EDITOR + if (DoubleDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetDoubleValue on an input pin, use ResolveAsDouble instead.")); + } +#endif + const int32 Index = EFlowSingleFromArray_Classifiers::ConvertToIndex(SingleFromArray, DoubleDataPinValue.Values.Num()); + if (DoubleDataPinValue.Values.IsValidIndex(Index)) + { + return DoubleDataPinValue.Values[Index]; + } + UE_LOG(LogFlow, Error, TEXT("Insufficient values in Double Data Pin Value.")); + return 0.0; +} + +TArray UFlowDataPinBlueprintLibrary::GetDoubleValues(FFlowDataPinValue_Double& DoubleDataPinValue) +{ +#if WITH_EDITOR + if (DoubleDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetDoubleValues on an input pin, use ResolveAsDoubleArray instead.")); + } +#endif + return DoubleDataPinValue.Values; +} + +// Name +FName UFlowDataPinBlueprintLibrary::GetNameValue(const FFlowDataPinValue_Name& NameDataPinValue, EFlowSingleFromArray SingleFromArray) +{ +#if WITH_EDITOR + if (NameDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetNameValue on an input pin, use ResolveAsName instead.")); + } +#endif + const int32 Index = EFlowSingleFromArray_Classifiers::ConvertToIndex(SingleFromArray, NameDataPinValue.Values.Num()); + if (NameDataPinValue.Values.IsValidIndex(Index)) + { + return NameDataPinValue.Values[Index]; + } + UE_LOG(LogFlow, Error, TEXT("Insufficient values in Name Data Pin Value.")); + return FName(); +} + +TArray UFlowDataPinBlueprintLibrary::GetNameValues(FFlowDataPinValue_Name& NameDataPinValue) +{ +#if WITH_EDITOR + if (NameDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetNameValues on an input pin, use ResolveAsNameArray instead.")); + } +#endif + return NameDataPinValue.Values; +} + +// String +FString UFlowDataPinBlueprintLibrary::GetStringValue(const FFlowDataPinValue_String& StringDataPinValue, EFlowSingleFromArray SingleFromArray) +{ +#if WITH_EDITOR + if (StringDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetStringValue on an input pin, use ResolveAsString instead.")); + } +#endif + const int32 Index = EFlowSingleFromArray_Classifiers::ConvertToIndex(SingleFromArray, StringDataPinValue.Values.Num()); + if (StringDataPinValue.Values.IsValidIndex(Index)) + { + return StringDataPinValue.Values[Index]; + } + UE_LOG(LogFlow, Error, TEXT("Insufficient values in String Data Pin Value.")); + return FString(); +} + +TArray UFlowDataPinBlueprintLibrary::GetStringValues(FFlowDataPinValue_String& StringDataPinValue) +{ +#if WITH_EDITOR + if (StringDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetStringValues on an input pin, use ResolveAsStringArray instead.")); + } +#endif + return StringDataPinValue.Values; +} + +// Text +FText UFlowDataPinBlueprintLibrary::GetTextValue(const FFlowDataPinValue_Text& TextDataPinValue, EFlowSingleFromArray SingleFromArray) +{ +#if WITH_EDITOR + if (TextDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetTextValue on an input pin, use ResolveAsText instead.")); + } +#endif + const int32 Index = EFlowSingleFromArray_Classifiers::ConvertToIndex(SingleFromArray, TextDataPinValue.Values.Num()); + if (TextDataPinValue.Values.IsValidIndex(Index)) + { + return TextDataPinValue.Values[Index]; + } + UE_LOG(LogFlow, Error, TEXT("Insufficient values in Text Data Pin Value.")); + return FText::GetEmpty(); +} + +TArray UFlowDataPinBlueprintLibrary::GetTextValues(FFlowDataPinValue_Text& TextDataPinValue) +{ +#if WITH_EDITOR + if (TextDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetTextValues on an input pin, use ResolveAsTextArray instead.")); + } +#endif + return TextDataPinValue.Values; +} + +// ----- Enum Setters ----- + +void UFlowDataPinBlueprintLibrary::SetEnumValue(uint8 InValue, FFlowDataPinValue_Enum& EnumDataPinValue) +{ + UEnum* EnumClass = EnumDataPinValue.EnumClass.LoadSynchronous(); + if (!IsValid(EnumClass)) + { + UE_LOG(LogFlow, Error, TEXT("SetEnumValue: Null EnumClass")); + return; + } + + const int64 ValueInt64 = static_cast(InValue); + const FName EnumValueName = EnumClass->GetNameByValue(ValueInt64); + if (EnumValueName.IsNone()) + { + UE_LOG(LogFlow, Error, TEXT("SetEnumValue: Could not find enum name for value %d in %s"), InValue, *EnumClass->GetPathName()); + return; + } + + EnumDataPinValue.EnumClass = EnumClass; + EnumDataPinValue.Values = { EnumValueName }; +#if WITH_EDITOR + EnumDataPinValue.MultiType = EFlowDataMultiType::Single; +#endif +} + +void UFlowDataPinBlueprintLibrary::SetEnumValues(const TArray& InValues, FFlowDataPinValue_Enum& EnumDataPinValue) +{ + UEnum* EnumClass = EnumDataPinValue.EnumClass.LoadSynchronous(); + if (!IsValid(EnumClass)) + { + UE_LOG(LogFlow, Error, TEXT("SetEnumValues: Null EnumClass")); + return; + } + + TArray Names; + Names.Reserve(InValues.Num()); + + for (uint8 RawValue : InValues) + { + const int64 ValueInt64 = static_cast(RawValue); + const FName EnumValueName = EnumClass->GetNameByValue(ValueInt64); + if (EnumValueName.IsNone()) + { + UE_LOG(LogFlow, Error, TEXT("SetEnumValues: Could not find enum name for value %d in %s"), RawValue, *EnumClass->GetPathName()); + + // Abort entire set to avoid partial data + return; + } + + Names.Add(EnumValueName); + } + + EnumDataPinValue.EnumClass = EnumClass; + EnumDataPinValue.Values = MoveTemp(Names); +#if WITH_EDITOR + EnumDataPinValue.MultiType = EFlowDataMultiType::Array; +#endif +} + +uint8 UFlowDataPinBlueprintLibrary::GetEnumValue(const FFlowDataPinValue_Enum& EnumDataPinValue, EFlowSingleFromArray SingleFromArray) +{ +#if WITH_EDITOR + if (EnumDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetEnumValue on an input pin, use ResolveAsEnum instead.")); + } +#endif + + UEnum* EnumClass = EnumDataPinValue.EnumClass.LoadSynchronous(); + if (!IsValid(EnumClass)) + { + UE_LOG(LogFlow, Error, TEXT("GetEnumValue: Null EnumClass")); + return 0; + } + + const int32 Index = EFlowSingleFromArray_Classifiers::ConvertToIndex(SingleFromArray, EnumDataPinValue.Values.Num()); + if (!EnumDataPinValue.Values.IsValidIndex(Index)) + { + UE_LOG(LogFlow, Error, TEXT("GetEnumValue: Insufficient values.")); + return 0; + } + + const FName& EnumName = EnumDataPinValue.Values[Index]; + const int32 EnumIndex = EnumClass->GetIndexByName(EnumName); + if (EnumIndex == INDEX_NONE) + { + UE_LOG(LogFlow, Error, TEXT("GetEnumValue: Name '%s' not found in enum %s"), *EnumName.ToString(), *EnumClass->GetPathName()); + return 0; + } + + const int64 RawValue = EnumClass->GetValueByIndex(EnumIndex); + return static_cast(RawValue); +} + +TArray UFlowDataPinBlueprintLibrary::GetEnumValues(const FFlowDataPinValue_Enum& EnumDataPinValue) +{ +#if WITH_EDITOR + if (EnumDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetEnumValues on an input pin, use ResolveAsEnumArray instead.")); + } +#endif + TArray Values; + + UEnum* EnumClass = EnumDataPinValue.EnumClass.LoadSynchronous(); + if (!IsValid(EnumClass)) + { + UE_LOG(LogFlow, Error, TEXT("GetEnumValues: Null EnumClass")); + return Values; + } + + Values.Reserve(EnumDataPinValue.Values.Num()); + for (const FName& EnumName : EnumDataPinValue.Values) + { + const int32 EnumIndex = EnumClass->GetIndexByName(EnumName); + if (EnumIndex == INDEX_NONE) + { + UE_LOG(LogFlow, Error, TEXT("GetEnumValues: Name '%s' not found in enum %s"), *EnumName.ToString(), *EnumClass->GetPathName()); + + // Abort entire set to avoid partial data + return TArray(); + } + const int64 RawValue = EnumClass->GetValueByIndex(EnumIndex); + Values.Add(static_cast(RawValue)); + } + + return Values; +} + +// Vector +FVector UFlowDataPinBlueprintLibrary::GetVectorValue(const FFlowDataPinValue_Vector& VectorDataPinValue, EFlowSingleFromArray SingleFromArray) +{ +#if WITH_EDITOR + if (VectorDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetVectorValue on an input pin, use ResolveAsVector instead.")); + } +#endif + const int32 Index = EFlowSingleFromArray_Classifiers::ConvertToIndex(SingleFromArray, VectorDataPinValue.Values.Num()); + if (VectorDataPinValue.Values.IsValidIndex(Index)) + { + return VectorDataPinValue.Values[Index]; + } + UE_LOG(LogFlow, Error, TEXT("Insufficient values in Vector Data Pin Value.")); + return FVector::ZeroVector; +} + +TArray UFlowDataPinBlueprintLibrary::GetVectorValues(FFlowDataPinValue_Vector& VectorDataPinValue) +{ +#if WITH_EDITOR + if (VectorDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetVectorValues on an input pin, use ResolveAsVectorArray instead.")); + } +#endif + return VectorDataPinValue.Values; +} + +// Rotator +FRotator UFlowDataPinBlueprintLibrary::GetRotatorValue(const FFlowDataPinValue_Rotator& RotatorDataPinValue, EFlowSingleFromArray SingleFromArray) +{ +#if WITH_EDITOR + if (RotatorDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetRotatorValue on an input pin, use ResolveAsRotator instead.")); + } +#endif + const int32 Index = EFlowSingleFromArray_Classifiers::ConvertToIndex(SingleFromArray, RotatorDataPinValue.Values.Num()); + if (RotatorDataPinValue.Values.IsValidIndex(Index)) + { + return RotatorDataPinValue.Values[Index]; + } + UE_LOG(LogFlow, Error, TEXT("Insufficient values in Rotator Data Pin Value.")); + return FRotator::ZeroRotator; +} + +TArray UFlowDataPinBlueprintLibrary::GetRotatorValues(FFlowDataPinValue_Rotator& RotatorDataPinValue) +{ +#if WITH_EDITOR + if (RotatorDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetRotatorValues on an input pin, use ResolveAsRotatorArray instead.")); + } +#endif + return RotatorDataPinValue.Values; +} + +// Transform +FTransform UFlowDataPinBlueprintLibrary::GetTransformValue(const FFlowDataPinValue_Transform& TransformDataPinValue, EFlowSingleFromArray SingleFromArray) +{ +#if WITH_EDITOR + if (TransformDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetTransformValue on an input pin, use ResolveAsTransform instead.")); + } +#endif + const int32 Index = EFlowSingleFromArray_Classifiers::ConvertToIndex(SingleFromArray, TransformDataPinValue.Values.Num()); + if (TransformDataPinValue.Values.IsValidIndex(Index)) + { + return TransformDataPinValue.Values[Index]; + } + UE_LOG(LogFlow, Error, TEXT("Insufficient values in Transform Data Pin Value.")); + return FTransform::Identity; +} + +TArray UFlowDataPinBlueprintLibrary::GetTransformValues(FFlowDataPinValue_Transform& TransformDataPinValue) +{ +#if WITH_EDITOR + if (TransformDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetTransformValues on an input pin, use ResolveAsTransformArray instead.")); + } +#endif + return TransformDataPinValue.Values; +} + +// GameplayTag +FGameplayTag UFlowDataPinBlueprintLibrary::GetGameplayTagValue(const FFlowDataPinValue_GameplayTag& GameplayTagDataPinValue, EFlowSingleFromArray SingleFromArray) +{ +#if WITH_EDITOR + if (GameplayTagDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetGameplayTagValue on an input pin, use ResolveAsGameplayTag instead.")); + } +#endif + const int32 Index = EFlowSingleFromArray_Classifiers::ConvertToIndex(SingleFromArray, GameplayTagDataPinValue.Values.Num()); + if (GameplayTagDataPinValue.Values.IsValidIndex(Index)) + { + return GameplayTagDataPinValue.Values[Index]; + } + UE_LOG(LogFlow, Error, TEXT("Insufficient values in GameplayTag Data Pin Value.")); + return FGameplayTag(); +} + +TArray UFlowDataPinBlueprintLibrary::GetGameplayTagValues(FFlowDataPinValue_GameplayTag& GameplayTagDataPinValue) +{ +#if WITH_EDITOR + if (GameplayTagDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetGameplayTagValues on an input pin, use ResolveAsGameplayTagArray instead.")); + } +#endif + return GameplayTagDataPinValue.Values; +} + +// GameplayTagContainer (scalar only) +FGameplayTagContainer UFlowDataPinBlueprintLibrary::GetGameplayTagContainerValue(const FFlowDataPinValue_GameplayTagContainer& GameplayTagContainerDataPinValue) +{ +#if WITH_EDITOR + if (GameplayTagContainerDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetGameplayTagContainerValue on an input pin, use ResolveAsGameplayTagContainer instead.")); + } +#endif + return GameplayTagContainerDataPinValue.Values; +} + +// InstancedStruct +FInstancedStruct UFlowDataPinBlueprintLibrary::GetInstancedStructValue(const FFlowDataPinValue_InstancedStruct& InstancedStructDataPinValue, EFlowSingleFromArray SingleFromArray) +{ +#if WITH_EDITOR + if (InstancedStructDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetInstancedStructValue on an input pin, use ResolveAsInstancedStruct instead.")); + } +#endif + const int32 Index = EFlowSingleFromArray_Classifiers::ConvertToIndex(SingleFromArray, InstancedStructDataPinValue.Values.Num()); + if (InstancedStructDataPinValue.Values.IsValidIndex(Index)) + { + return InstancedStructDataPinValue.Values[Index]; + } + UE_LOG(LogFlow, Error, TEXT("Insufficient values in InstancedStruct Data Pin Value.")); + return FInstancedStruct(); +} + +TArray UFlowDataPinBlueprintLibrary::GetInstancedStructValues(FFlowDataPinValue_InstancedStruct& InstancedStructDataPinValue) +{ +#if WITH_EDITOR + if (InstancedStructDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetInstancedStructValues on an input pin, use ResolveAsInstancedStructArray instead.")); + } +#endif + return InstancedStructDataPinValue.Values; +} + +// Object +UObject* UFlowDataPinBlueprintLibrary::GetObjectValue(const FFlowDataPinValue_Object& ObjectDataPinValue, EFlowSingleFromArray SingleFromArray) +{ +#if WITH_EDITOR + if (ObjectDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetObjectValue on an input pin, use ResolveAsObject instead.")); + } +#endif + const int32 Index = EFlowSingleFromArray_Classifiers::ConvertToIndex(SingleFromArray, ObjectDataPinValue.Values.Num()); + if (ObjectDataPinValue.Values.IsValidIndex(Index)) + { + return ObjectDataPinValue.Values[Index]; + } + UE_LOG(LogFlow, Error, TEXT("Insufficient values in Object Data Pin Value.")); + return nullptr; +} + +TArray UFlowDataPinBlueprintLibrary::GetObjectValues(FFlowDataPinValue_Object& ObjectDataPinValue) +{ +#if WITH_EDITOR + if (ObjectDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetObjectValues on an input pin, use ResolveAsObjectArray instead.")); + } +#endif + return ObjectDataPinValue.Values; +} + +// Class +FSoftClassPath UFlowDataPinBlueprintLibrary::GetClassValue(const FFlowDataPinValue_Class& ClassDataPinValue, EFlowSingleFromArray SingleFromArray) +{ +#if WITH_EDITOR + if (ClassDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetClassValue on an input pin, use ResolveAsClass instead.")); + } +#endif + const int32 Index = EFlowSingleFromArray_Classifiers::ConvertToIndex(SingleFromArray, ClassDataPinValue.Values.Num()); + if (ClassDataPinValue.Values.IsValidIndex(Index)) + { + return ClassDataPinValue.Values[Index]; + } + UE_LOG(LogFlow, Error, TEXT("Insufficient values in Class Data Pin Value.")); + return FSoftClassPath(); +} + +TArray UFlowDataPinBlueprintLibrary::GetClassValues(FFlowDataPinValue_Class& ClassDataPinValue) +{ +#if WITH_EDITOR + if (ClassDataPinValue.bIsInputPin) + { + UE_LOG(LogFlow, Error, TEXT("Should not call GetClassValues on an input pin, use ResolveAsClassArray instead.")); + } +#endif + return ClassDataPinValue.Values; +} + +FFlowDataPinResult UFlowDataPinBlueprintLibrary::MakeFlowDataPinResult_Bool(const TArray& BoolValues) +{ + FFlowDataPinResult Out{ FFlowDataPinValue_Bool(BoolValues) }; + return Out; +} + +FFlowDataPinResult UFlowDataPinBlueprintLibrary::MakeFlowDataPinResult_Int(const TArray& IntValues) +{ + FFlowDataPinResult Out{ FFlowDataPinValue_Int(IntValues) }; + return Out; +} + +FFlowDataPinResult UFlowDataPinBlueprintLibrary::MakeFlowDataPinResult_Int64(const TArray& Int64Values) +{ + FFlowDataPinResult Out{ FFlowDataPinValue_Int64(Int64Values) }; + return Out; +} + +FFlowDataPinResult UFlowDataPinBlueprintLibrary::MakeFlowDataPinResult_Float(const TArray& FloatValues) +{ + FFlowDataPinResult Out{ FFlowDataPinValue_Float(FloatValues) }; + return Out; +} + +FFlowDataPinResult UFlowDataPinBlueprintLibrary::MakeFlowDataPinResult_Double(const TArray& DoubleValues) +{ + FFlowDataPinResult Out{ FFlowDataPinValue_Double(DoubleValues) }; + return Out; +} + +FFlowDataPinResult UFlowDataPinBlueprintLibrary::MakeFlowDataPinResult_Name(const TArray& NameValues) +{ + FFlowDataPinResult Out{ FFlowDataPinValue_Name(NameValues) }; + return Out; +} + +FFlowDataPinResult UFlowDataPinBlueprintLibrary::MakeFlowDataPinResult_String(const TArray& StringValues) +{ + FFlowDataPinResult Out{ FFlowDataPinValue_String(StringValues) }; + return Out; +} + +FFlowDataPinResult UFlowDataPinBlueprintLibrary::MakeFlowDataPinResult_Text(const TArray& TextValues) +{ + FFlowDataPinResult Out{ FFlowDataPinValue_Text(TextValues) }; + return Out; +} + +FFlowDataPinResult UFlowDataPinBlueprintLibrary::MakeFlowDataPinResult_Enum(const TArray& EnumValues, UEnum* EnumClass) +{ + if (EnumClass) + { + FFlowDataPinResult Out{ FFlowDataPinValue_Enum(*EnumClass, EnumValues) }; + return Out; + } + + return FFlowDataPinResult(EFlowDataPinResolveResult::FailedUnknownEnumValue); +} + +FFlowDataPinResult UFlowDataPinBlueprintLibrary::MakeFlowDataPinResult_Vector(const TArray& VectorValues) +{ + FFlowDataPinResult Out{ FFlowDataPinValue_Vector(VectorValues) }; + return Out; +} + +FFlowDataPinResult UFlowDataPinBlueprintLibrary::MakeFlowDataPinResult_Rotator(const TArray& RotatorValues) +{ + FFlowDataPinResult Out{ FFlowDataPinValue_Rotator(RotatorValues) }; + return Out; +} + +FFlowDataPinResult UFlowDataPinBlueprintLibrary::MakeFlowDataPinResult_Transform(const TArray& TransformValues) +{ + FFlowDataPinResult Out{ FFlowDataPinValue_Transform(TransformValues) }; + return Out; +} + +FFlowDataPinResult UFlowDataPinBlueprintLibrary::MakeFlowDataPinResult_GameplayTag(const TArray& GameplayTagValues) +{ + FFlowDataPinResult Out{ FFlowDataPinValue_GameplayTag(GameplayTagValues) }; + return Out; +} + +FFlowDataPinResult UFlowDataPinBlueprintLibrary::MakeFlowDataPinResult_GameplayTagContainer(FGameplayTagContainer GameplayTagContainerValue) +{ + FFlowDataPinResult Out{ FFlowDataPinValue_GameplayTagContainer(GameplayTagContainerValue) }; + return Out; +} + +FFlowDataPinResult UFlowDataPinBlueprintLibrary::MakeFlowDataPinResult_InstancedStruct(const TArray& InstancedStructValues) +{ + FFlowDataPinResult Out{ FFlowDataPinValue_InstancedStruct(InstancedStructValues) }; + return Out; +} + +FFlowDataPinResult UFlowDataPinBlueprintLibrary::MakeFlowDataPinResult_Object(const TArray& ObjectValues) +{ + FFlowDataPinResult Out{ FFlowDataPinValue_Object(ObjectValues) }; + return Out; +} + +FFlowDataPinResult UFlowDataPinBlueprintLibrary::MakeFlowDataPinResult_Class(const TArray& ClassValues) +{ + FFlowDataPinResult Out{ FFlowDataPinValue_Class(ClassValues) }; + return Out; +} \ No newline at end of file diff --git a/Source/Flow/Private/Types/FlowDataPinProperties.cpp b/Source/Flow/Private/Types/FlowDataPinProperties.cpp new file mode 100644 index 000000000..196672b9e --- /dev/null +++ b/Source/Flow/Private/Types/FlowDataPinProperties.cpp @@ -0,0 +1,39 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +// #FlowDataPinLegacy +#include "Types/FlowDataPinProperties.h" +#include "UObject/Class.h" +#include "UObject/UObjectIterator.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowDataPinProperties) + +FFlowDataPinOutputProperty_Object::FFlowDataPinOutputProperty_Object(UObject* InValue, UClass* InClassFilter) + : Super() +#if WITH_EDITOR + , ClassFilter(InClassFilter) +#endif +{ + const UClass* ObjectClass = IsValid(InValue) ? InValue->GetClass() : nullptr; + if (IsValid(ObjectClass)) + { + const bool bIsInstanced = (ObjectClass->ClassFlags & CLASS_EditInlineNew) != 0; + + if (bIsInstanced) + { + InlineValue = InValue; + ReferenceValue = nullptr; + } + else + { + InlineValue = nullptr; + ReferenceValue = InValue; + } + } + else + { + InlineValue = nullptr; + ReferenceValue = nullptr; + } +} + +// -- diff --git a/Source/Flow/Private/Types/FlowDataPinResults.cpp b/Source/Flow/Private/Types/FlowDataPinResults.cpp new file mode 100644 index 000000000..7f92c5df6 --- /dev/null +++ b/Source/Flow/Private/Types/FlowDataPinResults.cpp @@ -0,0 +1,50 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Types/FlowDataPinResults.h" +#include "Types/FlowDataPinValuesStandard.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowDataPinResults) + +FFlowDataPinResult_Object::FFlowDataPinResult_Object(UObject* InValue) + : Super(EFlowDataPinResolveResult::Success) +{ + SetValueFromObjectPtr(InValue); +} + +// FFlowDataPinResult_Class + +FFlowDataPinResult_Class::FFlowDataPinResult_Class(const FSoftClassPath& InValuePath) + : Super(EFlowDataPinResolveResult::Success) +{ + SetValueFromSoftPath(InValuePath); +} + +FFlowDataPinResult_Class::FFlowDataPinResult_Class(UClass* InValueClass) + : Super(EFlowDataPinResolveResult::Success) +{ + SetValueFromObjectPtr(InValueClass); +} + +void FFlowDataPinResult_Class::SetValueFromSoftPath(const FSoftObjectPath& SoftObjectPath) +{ + const FSoftClassPath SoftClassPath(SoftObjectPath.ToString()); + SetValueSoftClassAndClassPtr(SoftClassPath, SoftClassPath.ResolveClass()); +} + +void FFlowDataPinResult_Class::SetValueSoftClassAndClassPtr(const FSoftClassPath& SoftPath, UClass* ObjectPtr) +{ + ValuePath = SoftPath; + ValueClass = ObjectPtr; +} + +FSoftClassPath FFlowDataPinResult_Class::GetAsSoftClass() const +{ + if (ValuePath.IsValid()) + { + return ValuePath; + } + else + { + return FSoftClassPath(ValueClass); + } +} diff --git a/Source/Flow/Private/Types/FlowDataPinValue.cpp b/Source/Flow/Private/Types/FlowDataPinValue.cpp new file mode 100644 index 000000000..34de3e9f7 --- /dev/null +++ b/Source/Flow/Private/Types/FlowDataPinValue.cpp @@ -0,0 +1,16 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Types/FlowDataPinValue.h" +#include "Types/FlowDataPinResults.h" +#include "Types/FlowDataPinValuesStandard.h" +#include "Types/FlowPinType.h" +#include "Nodes/FlowPin.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowDataPinValue) + +const FString FFlowDataPinValue::StringArraySeparator = TEXT(", "); + +const FFlowPinType* FFlowDataPinValue::LookupPinType() const +{ + return FFlowPinType::LookupPinType(GetPinTypeName()); +} diff --git a/Source/Flow/Private/Types/FlowDataPinValuesStandard.cpp b/Source/Flow/Private/Types/FlowDataPinValuesStandard.cpp new file mode 100644 index 000000000..a31bd141c --- /dev/null +++ b/Source/Flow/Private/Types/FlowDataPinValuesStandard.cpp @@ -0,0 +1,670 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Types/FlowDataPinValuesStandard.h" +#include "Nodes/FlowPin.h" +#include "Types/FlowArray.h" + +#include "GameFramework/Actor.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowDataPinValuesStandard) + +//====================================================================== +// Bool +//====================================================================== +FFlowDataPinValue_Bool::FFlowDataPinValue_Bool(ValueType InValue) + : Values({ InValue }) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Single; +#endif +} + +FFlowDataPinValue_Bool::FFlowDataPinValue_Bool(const TArray& InValues) + : Values(InValues) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Array; +#endif +} + +bool FFlowDataPinValue_Bool::TryConvertValuesToString(FString& OutString) const +{ + OutString = FlowArray::FormatArrayString(Values, + [](ValueType b) + { + return b ? TEXT("true") : TEXT("false"); + }, + StringArraySeparator); + return true; +} + +//====================================================================== +// Int (int32) +//====================================================================== +FFlowDataPinValue_Int::FFlowDataPinValue_Int(ValueType InValue) + : Values({ InValue }) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Single; +#endif +} + +FFlowDataPinValue_Int::FFlowDataPinValue_Int(const TArray& InValues) + : Values(InValues) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Array; +#endif +} + +bool FFlowDataPinValue_Int::TryConvertValuesToString(FString& OutString) const +{ + OutString = FlowArray::FormatArrayString(Values, + [](ValueType Val) + { + return FString::FromInt(Val); + }, + StringArraySeparator); + return true; +} + +//====================================================================== +// Int64 +//====================================================================== +FFlowDataPinValue_Int64::FFlowDataPinValue_Int64(ValueType InValue) + : Values({ InValue }) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Single; +#endif +} + +FFlowDataPinValue_Int64::FFlowDataPinValue_Int64(const TArray& InValues) + : Values(InValues) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Array; +#endif +} + +bool FFlowDataPinValue_Int64::TryConvertValuesToString(FString& OutString) const +{ + OutString = FlowArray::FormatArrayString(Values, + [](ValueType Val) + { + return FString::Printf(TEXT("%lld"), Val); + }, + StringArraySeparator); + return true; +} + +//====================================================================== +// Float +//====================================================================== +FFlowDataPinValue_Float::FFlowDataPinValue_Float(ValueType InValue) + : Values({ InValue }) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Single; +#endif +} + +FFlowDataPinValue_Float::FFlowDataPinValue_Float(const TArray& InValues) + : Values(InValues) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Array; +#endif +} + +bool FFlowDataPinValue_Float::TryConvertValuesToString(FString& OutString) const +{ + OutString = FlowArray::FormatArrayString(Values, + [](ValueType Val) + { + return FString::SanitizeFloat(Val); + }, + StringArraySeparator); + return true; +} + +//====================================================================== +// Double +//====================================================================== +FFlowDataPinValue_Double::FFlowDataPinValue_Double(ValueType InValue) + : Values({ InValue }) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Single; +#endif +} + +FFlowDataPinValue_Double::FFlowDataPinValue_Double(const TArray& InValues) + : Values(InValues) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Array; +#endif +} + +bool FFlowDataPinValue_Double::TryConvertValuesToString(FString& OutString) const +{ + OutString = FlowArray::FormatArrayString(Values, + [](ValueType Val) + { + return FString::SanitizeFloat(Val); + }, + StringArraySeparator); + return true; +} + +//====================================================================== +// Name +//====================================================================== +FFlowDataPinValue_Name::FFlowDataPinValue_Name(const ValueType& InValue) + : Values({ InValue }) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Single; +#endif +} + +FFlowDataPinValue_Name::FFlowDataPinValue_Name(const TArray& InValues) + : Values(InValues) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Array; +#endif +} + +bool FFlowDataPinValue_Name::TryConvertValuesToString(FString& OutString) const +{ + OutString = FlowArray::FormatArrayString(Values, + [](const ValueType& Val) + { + return Val.ToString(); + }, + StringArraySeparator); + return true; +} + +//====================================================================== +// String +//====================================================================== +FFlowDataPinValue_String::FFlowDataPinValue_String(const ValueType& InValue) + : Values({ InValue }) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Single; +#endif +} + +FFlowDataPinValue_String::FFlowDataPinValue_String(const TArray& InValues) + : Values(InValues) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Array; +#endif +} + +bool FFlowDataPinValue_String::TryConvertValuesToString(FString& OutString) const +{ + OutString = FlowArray::FormatArrayString(Values, + [](const ValueType& Val) + { + return Val; + }, + StringArraySeparator); + return true; +} + +//====================================================================== +// Text +//====================================================================== +FFlowDataPinValue_Text::FFlowDataPinValue_Text(const ValueType& InValue) + : Values({ InValue }) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Single; +#endif +} + +FFlowDataPinValue_Text::FFlowDataPinValue_Text(const TArray& InValues) + : Values(InValues) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Array; +#endif +} + +bool FFlowDataPinValue_Text::TryConvertValuesToString(FString& OutString) const +{ + OutString = FlowArray::FormatArrayString(Values, + [](const ValueType& Val) + { + return Val.ToString(); + }, + StringArraySeparator); + return true; +} + +//====================================================================== +// Enum +//====================================================================== +FFlowDataPinValue_Enum::FFlowDataPinValue_Enum(const TSoftObjectPtr& InEnumClass, const ValueType& InValue) + : Values({ InValue }), EnumClass(InEnumClass) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Single; +#endif +} + +FFlowDataPinValue_Enum::FFlowDataPinValue_Enum(const TSoftObjectPtr& InEnumClass, const TArray& InValues) + : Values(InValues), EnumClass(InEnumClass) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Array; +#endif +} + +FFlowDataPinValue_Enum::FFlowDataPinValue_Enum(UEnum& InEnumClass, const TArray& InValues) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Array; +#endif + + EnumClass = &InEnumClass; + Values.Reserve(InValues.Num()); + + for (uint8 RawValue : InValues) + { + const FName EnumValueName = InEnumClass.GetNameByValue(RawValue); + Values.Add(EnumValueName); + } +} + +#if WITH_EDITOR +void FFlowDataPinValue_Enum::OnEnumNameChanged() +{ + if (!EnumName.IsEmpty()) + { + EnumClass = UClass::TryFindTypeSlow(EnumName, EFindFirstObjectOptions::ExactClass); + + if (EnumClass != nullptr && !FFlowPin::ValidateEnum(*EnumClass)) + { + EnumClass = nullptr; + } + } +} +#endif + +UField* FFlowDataPinValue_Enum::GetFieldType() const +{ + return EnumClass.LoadSynchronous(); +} + +bool FFlowDataPinValue_Enum::TryConvertValuesToString(FString& OutString) const +{ + OutString = FlowArray::FormatArrayString(Values, + [](const ValueType& Val) { return Val.ToString(); }, + StringArraySeparator); + return true; +} + +//====================================================================== +// Vector +//====================================================================== +FFlowDataPinValue_Vector::FFlowDataPinValue_Vector(const ValueType& InValue) + : Values({ InValue }) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Single; +#endif +} + +FFlowDataPinValue_Vector::FFlowDataPinValue_Vector(const TArray& InValues) + : Values(InValues) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Array; +#endif +} + +bool FFlowDataPinValue_Vector::TryConvertValuesToString(FString& OutString) const +{ + OutString = FlowArray::FormatArrayString(Values, + [](const ValueType& V) + { + return V.ToCompactString(); + }, + StringArraySeparator); + return true; +} + +//====================================================================== +// Rotator +//====================================================================== +FFlowDataPinValue_Rotator::FFlowDataPinValue_Rotator(const ValueType& InValue) + : Values({ InValue }) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Single; +#endif +} + +FFlowDataPinValue_Rotator::FFlowDataPinValue_Rotator(const TArray& InValues) + : Values(InValues) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Array; +#endif +} + +bool FFlowDataPinValue_Rotator::TryConvertValuesToString(FString& OutString) const +{ + OutString = FlowArray::FormatArrayString(Values, + [](const ValueType& R) + { + return R.ToCompactString(); + }, + StringArraySeparator); + return true; +} + +//====================================================================== +// Transform +//====================================================================== +FFlowDataPinValue_Transform::FFlowDataPinValue_Transform(const ValueType& InValue) + : Values({ InValue }) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Single; +#endif +} + +FFlowDataPinValue_Transform::FFlowDataPinValue_Transform(const TArray& InValues) + : Values(InValues) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Array; +#endif +} + +bool FFlowDataPinValue_Transform::TryConvertValuesToString(FString& OutString) const +{ + OutString = FlowArray::FormatArrayString(Values, + [](const ValueType& T) + { + return T.ToString(); + }, + StringArraySeparator); + return true; +} + +//====================================================================== +// GameplayTag +//====================================================================== +FFlowDataPinValue_GameplayTag::FFlowDataPinValue_GameplayTag(const ValueType& InValue) + : Values({ InValue }) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Single; +#endif +} + +FFlowDataPinValue_GameplayTag::FFlowDataPinValue_GameplayTag(const TArray& InValues) + : Values(InValues) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Array; +#endif +} + +bool FFlowDataPinValue_GameplayTag::TryConvertValuesToString(FString& OutString) const +{ + OutString = FlowArray::FormatArrayString(Values, + [](const ValueType& Tag) + { + return Tag.ToString(); + }, + StringArraySeparator); + return true; +} + +//====================================================================== +// GameplayTagContainer +//====================================================================== +FFlowDataPinValue_GameplayTagContainer::FFlowDataPinValue_GameplayTagContainer(const FGameplayTag& InValue) + : Values({ InValue }) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Single; +#endif +} + +FFlowDataPinValue_GameplayTagContainer::FFlowDataPinValue_GameplayTagContainer(const FGameplayTagContainer& InValues) + : Values(InValues) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Single; +#endif +} + +FFlowDataPinValue_GameplayTagContainer::FFlowDataPinValue_GameplayTagContainer(const TArray& InValues) + : Values(FGameplayTagContainer::CreateFromArray(InValues)) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Array; +#endif +} + +FFlowDataPinValue_GameplayTagContainer::FFlowDataPinValue_GameplayTagContainer(const TArray& InValues) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Array; +#endif + + for (const FGameplayTagContainer& Container : InValues) + { + Values.AppendTags(Container); + } +} + +bool FFlowDataPinValue_GameplayTagContainer::TryConvertValuesToString(FString& OutString) const +{ + OutString = Values.ToStringSimple(); + return true; +} + +//====================================================================== +// InstancedStruct +//====================================================================== +FFlowDataPinValue_InstancedStruct::FFlowDataPinValue_InstancedStruct(const ValueType& InValue) + : Values({ InValue }) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Single; +#endif +} + +FFlowDataPinValue_InstancedStruct::FFlowDataPinValue_InstancedStruct(const TArray& InValues) + : Values(InValues) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Array; +#endif +} + +bool FFlowDataPinValue_InstancedStruct::TryConvertValuesToString(FString& OutString) const +{ + const FInstancedStruct DefaultValue; + + OutString = FlowArray::FormatArrayString( + Values, + [&DefaultValue](const FInstancedStruct& InstancedStruct) + { + FString ExportedString; + + constexpr UObject* ParentObject = nullptr; + constexpr UObject* ExportRootScope = nullptr; + + const bool bExported = InstancedStruct.ExportTextItem( + ExportedString, + DefaultValue, + ParentObject, + PPF_None, + ExportRootScope); + + if (!bExported) + { + // Fallback: just show the contained struct type name (or None) + if (const UScriptStruct* ScriptStruct = InstancedStruct.GetScriptStruct()) + { + return ScriptStruct->GetName(); + } + + return FString(); + } + + return ExportedString; + }, + StringArraySeparator); + + return true; +} + +//====================================================================== +// Object +//====================================================================== +FFlowDataPinValue_Object::FFlowDataPinValue_Object(TObjectPtr InObject, UClass* InClassFilter) +#if WITH_EDITOR + : ClassFilter(InClassFilter) +#endif +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Single; +#endif + Values = { InObject }; +} + +FFlowDataPinValue_Object::FFlowDataPinValue_Object(const TArray>& InObjects, UClass* InClassFilter) +#if WITH_EDITOR + : ClassFilter(InClassFilter) +#endif +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Array; +#endif + Values = InObjects; +} + +FFlowDataPinValue_Object::FFlowDataPinValue_Object(AActor* InActor, UClass* InClassFilter) +#if WITH_EDITOR + : ClassFilter(InClassFilter ? InClassFilter : AActor::StaticClass()) +#endif +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Single; +#endif + Values = { InActor }; +} + +FFlowDataPinValue_Object::FFlowDataPinValue_Object(const TArray& InActors, UClass* InClassFilter) +#if WITH_EDITOR + : ClassFilter(InClassFilter ? InClassFilter : AActor::StaticClass()) +#endif +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Array; +#endif + + Values.Reset(); + Values.Reserve(InActors.Num()); + for (AActor* Actor : InActors) + { + Values.Add(Cast(Actor)); + } +} + +FFlowDataPinValue_Object::FFlowDataPinValue_Object(const TArray& InObjects, UClass* InClassFilter) +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Array; +#endif + Values = InObjects; +} + +bool FFlowDataPinValue_Object::TryConvertValuesToString(FString& OutString) const +{ + OutString = FlowArray::FormatArrayString(Values, + [](UObject* Obj) + { + return Obj ? Obj->GetName() : TEXT("None"); + }, + StringArraySeparator); + return true; +} + +//====================================================================== +// Class +//====================================================================== +FFlowDataPinValue_Class::FFlowDataPinValue_Class(const FSoftClassPath& InPath, UClass* InClassFilter) + : Values({ InPath }) +#if WITH_EDITOR + , ClassFilter(InClassFilter) +#endif +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Single; +#endif +} + +FFlowDataPinValue_Class::FFlowDataPinValue_Class(const TArray& InPaths, UClass* InClassFilter) + : Values(InPaths) +#if WITH_EDITOR + , ClassFilter(InClassFilter) +#endif +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Array; +#endif +} + +FFlowDataPinValue_Class::FFlowDataPinValue_Class(const UClass* InClass, UClass* InClassFilter) + : Values({ FSoftClassPath(InClass) }) +#if WITH_EDITOR + , ClassFilter(InClassFilter) +#endif +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Single; +#endif +} + +FFlowDataPinValue_Class::FFlowDataPinValue_Class(const TArray& InClasses, UClass* InClassFilter) +#if WITH_EDITOR + : ClassFilter(InClassFilter) +#endif +{ +#if WITH_EDITOR + MultiType = EFlowDataMultiType::Array; +#endif + + Values.Reset(); + Values.Reserve(InClasses.Num()); + for (UClass* Class : InClasses) + { + Values.Add(FSoftClassPath(Class)); + } +} + +bool FFlowDataPinValue_Class::TryConvertValuesToString(FString& OutString) const +{ + OutString = FlowArray::FormatArrayString(Values, + [](const FSoftClassPath& Path) + { + return Path.IsValid() ? Path.GetAssetName() : TEXT("None"); + }, + StringArraySeparator); + return true; +} \ No newline at end of file diff --git a/Source/Flow/Private/Types/FlowGameplayTagUtils.cpp b/Source/Flow/Private/Types/FlowGameplayTagUtils.cpp new file mode 100644 index 000000000..d030d2bd5 --- /dev/null +++ b/Source/Flow/Private/Types/FlowGameplayTagUtils.cpp @@ -0,0 +1,81 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Types/FlowGameplayTagUtils.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowGameplayTagUtils) + +bool FFlowGameplayTagRequirements::RequirementsMet(const FGameplayTagContainer& Container) const +{ + const bool bHasRequired = Container.HasAll(RequireTags); + const bool bHasIgnored = Container.HasAny(IgnoreTags); + const bool bMatchQuery = TagQuery.IsEmpty() || TagQuery.Matches(Container); + + return bHasRequired && !bHasIgnored && bMatchQuery; +} + +bool FFlowGameplayTagRequirements::IsEmpty() const +{ + return (RequireTags.Num() == 0 && IgnoreTags.Num() == 0 && TagQuery.IsEmpty()); +} + +FString FFlowGameplayTagRequirements::ToString() const +{ + FString Str; + + if (RequireTags.Num() > 0) + { + Str += FString::Printf(TEXT("require: %s "), *RequireTags.ToStringSimple()); + } + if (IgnoreTags.Num() > 0) + { + Str += FString::Printf(TEXT("ignore: %s "), *IgnoreTags.ToStringSimple()); + } + if (!TagQuery.IsEmpty()) + { + Str += TagQuery.GetDescription(); + } + + return Str; +} + +bool FFlowGameplayTagRequirements::operator==(const FFlowGameplayTagRequirements& Other) const +{ + return RequireTags == Other.RequireTags && IgnoreTags == Other.IgnoreTags && TagQuery == Other.TagQuery; +} + +bool FFlowGameplayTagRequirements::operator!=(const FFlowGameplayTagRequirements& Other) const +{ + return !(*this == Other); +} + +FGameplayTagQuery FFlowGameplayTagRequirements::ConvertTagFieldsToTagQuery() const +{ + const bool bHasRequireTags = !RequireTags.IsEmpty(); + const bool bHasIgnoreTags = !IgnoreTags.IsEmpty(); + + if (!bHasIgnoreTags && !bHasRequireTags) + { + return FGameplayTagQuery{}; + } + + // FGameplayTagContainer::RequirementsMet is HasAll(RequireTags) && !HasAny(IgnoreTags); + FGameplayTagQueryExpression RequiredTagsQueryExpression = FGameplayTagQueryExpression().AllTagsMatch().AddTags(RequireTags); + FGameplayTagQueryExpression IgnoreTagsQueryExpression = FGameplayTagQueryExpression().NoTagsMatch().AddTags(IgnoreTags); + + FGameplayTagQueryExpression RootQueryExpression; + if (bHasRequireTags && bHasIgnoreTags) + { + RootQueryExpression = FGameplayTagQueryExpression().AllExprMatch().AddExpr(RequiredTagsQueryExpression).AddExpr(IgnoreTagsQueryExpression); + } + else if (bHasRequireTags) + { + RootQueryExpression = RequiredTagsQueryExpression; + } + else // bHasIgnoreTags + { + RootQueryExpression = IgnoreTagsQueryExpression; + } + + // Build the expression + return FGameplayTagQuery::BuildQuery(RootQueryExpression); +} diff --git a/Source/Flow/Private/Types/FlowInjectComponentsHelper.cpp b/Source/Flow/Private/Types/FlowInjectComponentsHelper.cpp new file mode 100644 index 000000000..59e7259ff --- /dev/null +++ b/Source/Flow/Private/Types/FlowInjectComponentsHelper.cpp @@ -0,0 +1,100 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Types/FlowInjectComponentsHelper.h" +#include "Components/ActorComponent.h" +#include "GameFramework/Actor.h" +#include "FlowLogChannels.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowInjectComponentsHelper) + +TArray FFlowInjectComponentsHelper::CreateComponentInstancesForActor(AActor& Actor) +{ + TArray ComponentInstances; + + if (ComponentTemplates.IsEmpty() && ComponentClasses.IsEmpty()) + { + return ComponentInstances; + } + + // If the desired component does not already exist, add it to the ActorOwner + for (UActorComponent* ComponentTemplate : ComponentTemplates) + { + if (!IsValid(ComponentTemplate)) + { + UE_LOG(LogFlow, Warning, TEXT("Cannot inject a null component!")); + + continue; + } + + if (UActorComponent* ComponentInstance = TryCreateComponentInstanceForActorFromTemplate(Actor, *ComponentTemplate)) + { + ComponentInstances.Add(ComponentInstance); + } + } + + for (TSubclassOf ComponentClass : ComponentClasses) + { + if (!IsValid(ComponentClass)) + { + UE_LOG(LogFlow, Warning, TEXT("Cannot inject a null component class!")); + + continue; + } + + const FName InstanceBaseName = ComponentClass->GetFName(); + if (UActorComponent* ComponentInstance = TryCreateComponentInstanceForActorFromClass(Actor, ComponentClass, InstanceBaseName)) + { + ComponentInstances.Add(ComponentInstance); + } + } + + return ComponentInstances; +} + +UActorComponent* FFlowInjectComponentsHelper::TryCreateComponentInstanceForActorFromTemplate(AActor& Actor, UActorComponent& ComponentTemplate) +{ + // Following pattern from UGameFrameworkComponentManager::CreateComponentOnInstance() + UClass* ComponentClass = ComponentTemplate.GetClass(); + if (!ComponentClass->GetDefaultObject()->GetIsReplicated() || Actor.GetLocalRole() == ROLE_Authority) + { + const EObjectFlags InstanceFlags = ComponentTemplate.GetFlags() | RF_Transient; + + UActorComponent* ComponentInstance = NewObject(&Actor, ComponentTemplate.GetClass(), ComponentTemplate.GetFName(), InstanceFlags, &ComponentTemplate); + + return ComponentInstance; + } + + return nullptr; +} + +UActorComponent* FFlowInjectComponentsHelper::TryCreateComponentInstanceForActorFromClass(AActor& Actor, TSubclassOf ComponentClass, const FName& InstanceBaseName) +{ + // Following pattern from UGameFrameworkComponentManager::CreateComponentOnInstance() + if (ComponentClass && (!ComponentClass->GetDefaultObject()->GetIsReplicated() || Actor.GetLocalRole() == ROLE_Authority)) + { + const FName UniqueName = MakeUniqueObjectName(&Actor, ComponentClass, InstanceBaseName); + UActorComponent* ComponentInstance = NewObject(&Actor, ComponentClass, UniqueName); + + return ComponentInstance; + } + + return nullptr; +} + +void FFlowInjectComponentsHelper::InjectCreatedComponent(AActor& Actor, UActorComponent& ComponentInstance) +{ + // Following pattern from UGameFrameworkComponentManager::CreateComponentOnInstance() + if (USceneComponent* SceneComponentInstance = Cast(&ComponentInstance)) + { + SceneComponentInstance->SetupAttachment(Actor.GetRootComponent()); + } + + ComponentInstance.RegisterComponent(); +} + +void FFlowInjectComponentsHelper::DestroyInjectedComponent(AActor& Actor, UActorComponent& ComponentInstance) +{ + // Following pattern from UGameFrameworkComponentManager::DestroyInstancedComponent() + ComponentInstance.DestroyComponent(); + ComponentInstance.SetFlags(RF_Transient); +} diff --git a/Source/Flow/Private/Types/FlowInjectComponentsManager.cpp b/Source/Flow/Private/Types/FlowInjectComponentsManager.cpp new file mode 100644 index 000000000..eb4ad7baf --- /dev/null +++ b/Source/Flow/Private/Types/FlowInjectComponentsManager.cpp @@ -0,0 +1,117 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Types/FlowInjectComponentsManager.h" +#include "Types/FlowInjectComponentsHelper.h" +#include "Components/ActorComponent.h" +#include "GameFramework/Actor.h" +#include "FlowLogChannels.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowInjectComponentsManager) + +void UFlowInjectComponentsManager::InitializeRuntime() +{ + check(ActorToComponentsMap.IsEmpty()); +} + +void UFlowInjectComponentsManager::ShutdownRuntime() +{ + if (bRemoveInjectedComponentsWhenDeinitializing) + { + RemoveInjectedComponents(); + } + + ActorToComponentsMap.Empty(); +} + +void UFlowInjectComponentsManager::InjectComponentsOnActor(AActor& Actor, const TArray& ComponentInstances) +{ + for (UActorComponent* ComponentInstance : ComponentInstances) + { + if (IsValid(ComponentInstance)) + { + InjectComponentOnActor(Actor, *ComponentInstance); + } + } +} + +void UFlowInjectComponentsManager::RemoveInjectedComponents() +{ + for (auto& KV : ActorToComponentsMap) + { + AActor* Actor = KV.Key; + const FFlowComponentInstances& Instances = KV.Value; + + if (!IsValid(Actor)) + { + continue; + } + + for (TWeakObjectPtr ComponentInstancePtr : Instances.Components) + { + if (UActorComponent* ComponentInstance = ComponentInstancePtr.Get()) + { + RemoveAndUnregisterComponent(*Actor, *ComponentInstance); + } + } + } +} + +void UFlowInjectComponentsManager::AddAndRegisterComponent(AActor& Actor, UActorComponent& ComponentInstance) +{ + FFlowInjectComponentsHelper::InjectCreatedComponent(Actor, ComponentInstance); + + if (bRemoveInjectedComponentsWhenDeinitializing) + { + // If we will be responsible for removing them later, + // we need to keep track of the spawned components + FFlowComponentInstances& ComponentInstances = ActorToComponentsMap.FindOrAdd(&Actor); + ComponentInstances.Components.Add(&ComponentInstance); + + RegisterOnDestroyedDelegate(Actor); + } +} + +void UFlowInjectComponentsManager::RemoveAndUnregisterComponent(AActor& Actor, UActorComponent& ComponentInstance) +{ + BeforeActorRemovedDelegate.Broadcast(&Actor); + + UnregisterOnDestroyedDelegate(Actor); + + FFlowInjectComponentsHelper::DestroyInjectedComponent(Actor, ComponentInstance); +} + +void UFlowInjectComponentsManager::RegisterOnDestroyedDelegate(AActor& Actor) +{ + Actor.OnDestroyed.AddUniqueDynamic(this, &UFlowInjectComponentsManager::OnActorDestroyed); +} + +void UFlowInjectComponentsManager::UnregisterOnDestroyedDelegate(AActor& Actor) +{ + Actor.OnDestroyed.RemoveDynamic(this, &UFlowInjectComponentsManager::OnActorDestroyed); +} + +void UFlowInjectComponentsManager::RemoveAllInjectedComponentsAndStopMonitoringActor(AActor& Actor) +{ + const FFlowComponentInstances* FoundComponentInstances = ActorToComponentsMap.Find(&Actor); + + if (ensure(FoundComponentInstances)) + { + for (TWeakObjectPtr ComponentInstancePtr : FoundComponentInstances->Components) + { + if (UActorComponent* ComponentInstance = ComponentInstancePtr.Get()) + { + RemoveAndUnregisterComponent(Actor, *ComponentInstance); + } + } + } + + ActorToComponentsMap.Remove(&Actor); +} + +void UFlowInjectComponentsManager::OnActorDestroyed(AActor* DestroyedActor) +{ + if (IsValid(DestroyedActor)) + { + RemoveAllInjectedComponentsAndStopMonitoringActor(*DestroyedActor); + } +} diff --git a/Source/Flow/Private/Types/FlowNamedDataPinProperty.cpp b/Source/Flow/Private/Types/FlowNamedDataPinProperty.cpp new file mode 100644 index 000000000..3f0ecec50 --- /dev/null +++ b/Source/Flow/Private/Types/FlowNamedDataPinProperty.cpp @@ -0,0 +1,57 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Types/FlowNamedDataPinProperty.h" +#include "Types/FlowDataPinPropertyToValueMigration.h" +#include "Types/FlowAutoDataPinsWorkingData.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowNamedDataPinProperty) + +#define LOCTEXT_NAMESPACE "FlowNamedDataPinProperty" + +#if WITH_EDITOR +FFlowPin FFlowNamedDataPinProperty::CreateFlowPin() const +{ + if (const FFlowDataPinValue* FlowDataPinValuePtr = DataPinValue.GetPtr()) + { + if (const FFlowPinType* FlowPinType = FFlowPinType::LookupPinType(FlowDataPinValuePtr->GetPinTypeName())) + { + return FlowPinType->CreateFlowPinFromValueWrapper(Name, *FlowDataPinValuePtr); + } + } + + return FFlowPin(); +} + +FText FFlowNamedDataPinProperty::BuildHeaderText() const +{ + FFlowPinTypeName PinTypeName; + + if (const FFlowDataPinValue* FlowDataPinValuePtr = DataPinValue.GetPtr()) + { + PinTypeName = FlowDataPinValuePtr->GetPinTypeName(); + } + + return FText::Format(LOCTEXT("FlowNamedFFlowDataPinValueHeader", "{0} ({1})"), { FText::FromName(Name), FText::FromString(PinTypeName.ToString()) }); +} + +void FFlowNamedDataPinProperty::AutoGenerateDataPinForProperty(const FFlowDataPinValueOwner& ValueOwner, FFlowAutoDataPinsWorkingData& InOutWorkingData) +{ + if (!IsValid()) + { + return; + } + + FFlowDataPinValue& Value = DataPinValue.GetMutable(); + if (Value.IsInputPin()) + { + InOutWorkingData.AutoInputDataPinsNext.Add(FFlowPinSourceData(CreateFlowPin(), ValueOwner, &Value)); + } + else + { + InOutWorkingData.AutoOutputDataPinsNext.Add(FFlowPinSourceData(CreateFlowPin(), ValueOwner, &Value)); + } +} + +#endif + +#undef LOCTEXT_NAMESPACE diff --git a/Source/Flow/Private/Types/FlowPinType.cpp b/Source/Flow/Private/Types/FlowPinType.cpp new file mode 100644 index 000000000..9eaf4fca5 --- /dev/null +++ b/Source/Flow/Private/Types/FlowPinType.cpp @@ -0,0 +1,101 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Types/FlowPinType.h" +#include "Types/FlowDataPinValue.h" +#include "Types/FlowStructUtils.h" +#include "Nodes/FlowNode.h" +#include "FlowPinSubsystem.h" +#include "FlowLogChannels.h" + +#if WITH_EDITOR +#include "PropertyHandle.h" +#endif + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowPinType) + +const FFlowPinTypeName FFlowPinType::PinTypeNameUnknown = FFlowPinTypeName(FFlowPinTypeNamesStandard::PinTypeNameUnknown); + +const FFlowPinType* FFlowPinType::LookupPinType(const FFlowPinTypeName& PinTypeName) +{ + const FFlowPinType* PinType = UFlowPinSubsystem::Get()->FindPinType(PinTypeName); + + if (!PinType) + { + UE_LOG(LogFlow, Error, TEXT("Could not find pin type %s in FlowPinSubsystem"), *PinTypeName.ToString()); + return nullptr; + } + + return PinType; +} + +bool FFlowPinType::ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const +{ + return false; +} + +bool FFlowPinType::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const +{ + OutResult.Result = EFlowDataPinResolveResult::FailedMismatchedType; + return false; +} + +#if WITH_EDITOR +FFlowPin FFlowPinType::CreateFlowPinFromProperty(const FProperty& Property, void const* InContainer) const +{ + FFlowPin NewFlowPin; + + if (const FFlowDataPinValue* DataPinValue = FlowStructUtils::CastStructValue(&Property, InContainer)) + { + // Create the pin from a FFlowDataPinValue property wrapper + NewFlowPin = CreateFlowPinFromValueWrapper(Property.GetFName(), *DataPinValue); + } + else + { + // Create the pin from a native property + NewFlowPin.PinName = Property.GetFName(); + NewFlowPin.SetPinTypeName(GetPinTypeName()); + + FLOW_ASSERT_ENUM_MAX(EFlowDataMultiType, 2); + if (CastField(&Property)) + { + NewFlowPin.ContainerType = EPinContainerType::Array; + } + + UObject* SubCategoryObject = GetPinSubCategoryObjectFromProperty(&Property, InContainer, DataPinValue); + NewFlowPin.SetPinSubCategoryObject(SubCategoryObject); + } + + // Common property settings for both versions + NewFlowPin.PinFriendlyName = Property.GetDisplayNameText(); + NewFlowPin.PinToolTip = Property.GetToolTipText().ToString(); + + return NewFlowPin; +} + +FFlowPin FFlowPinType::CreateFlowPinFromValueWrapper(const FName& PinName, const FFlowDataPinValue& Wrapper) const +{ + FFlowPin NewFlowPin(PinName); + NewFlowPin.PinFriendlyName = FText::FromName(PinName); + + FLOW_ASSERT_ENUM_MAX(EFlowDataMultiType, 2); + if (Wrapper.IsArray()) + { + NewFlowPin.ContainerType = EPinContainerType::Array; + } + + constexpr const FProperty* Property = nullptr; + constexpr void const* InContainer = nullptr; + UObject* SubCategoryObject = GetPinSubCategoryObjectFromProperty(Property, InContainer, &Wrapper); + NewFlowPin.SetPinSubCategoryObject(SubCategoryObject); + + // Common property settings for both versions + NewFlowPin.SetPinTypeName(GetPinTypeName()); + + return NewFlowPin; +} + +TSharedPtr FFlowPinType::GetValuesHandle(const TSharedRef& FlowDataPinValuePropertyHandle) const +{ + return FlowDataPinValuePropertyHandle.Get().GetChildHandle(TEXT("Values")); +} +#endif \ No newline at end of file diff --git a/Source/Flow/Private/Types/FlowPinTypeNamesStandard.cpp b/Source/Flow/Private/Types/FlowPinTypeNamesStandard.cpp new file mode 100644 index 000000000..775b025ea --- /dev/null +++ b/Source/Flow/Private/Types/FlowPinTypeNamesStandard.cpp @@ -0,0 +1,51 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Types/FlowPinTypeNamesStandard.h" + +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/Private/Types/FlowPinTypesStandard.cpp b/Source/Flow/Private/Types/FlowPinTypesStandard.cpp new file mode 100644 index 000000000..e6d86039a --- /dev/null +++ b/Source/Flow/Private/Types/FlowPinTypesStandard.cpp @@ -0,0 +1,504 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Types/FlowPinTypesStandard.h" +#include "Types/FlowClassUtils.h" +#include "Nodes/FlowNode.h" +#include "Types/FlowDataPinValuesStandard.h" +#include "Types/FlowDataPinResults.h" +#include "Types/FlowPinTypeTemplates.h" +#include "Types/FlowPinTypeNodeTemplates.h" +#include "FlowLogChannels.h" + +#if WITH_EDITOR +#include "EditorClassUtils.h" +#endif + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowPinTypesStandard) + +// NOTE (gtaylor) Beware static initialization order if attempting to use these in static initialization. +// Instead, consider sourcing directly from the FFlowPinTypeNamesStandard's TCHAR form in those cases. +const FFlowPinTypeName FFlowPinType_Exec::PinTypeNameExec = FFlowPinTypeName(FFlowPinTypeNamesStandard::PinTypeNameExec); +const FFlowPinTypeName FFlowPinType_Bool::PinTypeNameBool = FFlowPinTypeName(FFlowPinTypeNamesStandard::PinTypeNameBool); +const FFlowPinTypeName FFlowPinType_Int::PinTypeNameInt = FFlowPinTypeName(FFlowPinTypeNamesStandard::PinTypeNameInt); +const FFlowPinTypeName FFlowPinType_Int64::PinTypeNameInt64 = FFlowPinTypeName(FFlowPinTypeNamesStandard::PinTypeNameInt64); +const FFlowPinTypeName FFlowPinType_Float::PinTypeNameFloat = FFlowPinTypeName(FFlowPinTypeNamesStandard::PinTypeNameFloat); +const FFlowPinTypeName FFlowPinType_Double::PinTypeNameDouble = FFlowPinTypeName(FFlowPinTypeNamesStandard::PinTypeNameDouble); +const FFlowPinTypeName FFlowPinType_Enum::PinTypeNameEnum = FFlowPinTypeName(FFlowPinTypeNamesStandard::PinTypeNameEnum); +const FFlowPinTypeName FFlowPinType_Name::PinTypeNameName = FFlowPinTypeName(FFlowPinTypeNamesStandard::PinTypeNameName); +const FFlowPinTypeName FFlowPinType_String::PinTypeNameString = FFlowPinTypeName(FFlowPinTypeNamesStandard::PinTypeNameString); +const FFlowPinTypeName FFlowPinType_Text::PinTypeNameText = FFlowPinTypeName(FFlowPinTypeNamesStandard::PinTypeNameText); +const FFlowPinTypeName FFlowPinType_Vector::PinTypeNameVector = FFlowPinTypeName(FFlowPinTypeNamesStandard::PinTypeNameVector); +const FFlowPinTypeName FFlowPinType_Rotator::PinTypeNameRotator = FFlowPinTypeName(FFlowPinTypeNamesStandard::PinTypeNameRotator); +const FFlowPinTypeName FFlowPinType_Transform::PinTypeNameTransform = FFlowPinTypeName(FFlowPinTypeNamesStandard::PinTypeNameTransform); +const FFlowPinTypeName FFlowPinType_GameplayTag::PinTypeNameGameplayTag = FFlowPinTypeName(FFlowPinTypeNamesStandard::PinTypeNameGameplayTag); +const FFlowPinTypeName FFlowPinType_GameplayTagContainer::PinTypeNameGameplayTagContainer = FFlowPinTypeName(FFlowPinTypeNamesStandard::PinTypeNameGameplayTagContainer); +const FFlowPinTypeName FFlowPinType_InstancedStruct::PinTypeNameInstancedStruct = FFlowPinTypeName(FFlowPinTypeNamesStandard::PinTypeNameInstancedStruct); +const FFlowPinTypeName FFlowPinType_Object::PinTypeNameObject = FFlowPinTypeName(FFlowPinTypeNamesStandard::PinTypeNameObject); +const FFlowPinTypeName FFlowPinType_Class::PinTypeNameClass = FFlowPinTypeName(FFlowPinTypeNamesStandard::PinTypeNameClass); + +bool FFlowPinType_Bool::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const +{ + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); +} + +bool FFlowPinType_Int::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const +{ + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); +} + +bool FFlowPinType_Int64::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const +{ + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); +} + +bool FFlowPinType_Float::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const +{ + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); +} + +bool FFlowPinType_Double::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const +{ + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); +} + +bool FFlowPinType_Enum::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const +{ + using TFlowPinType = FFlowPinType_Enum; + using TValue = TFlowPinType::ValueType; + using TWrapper = TFlowPinType::WrapperType; + using Traits = FlowPinType::FFlowDataPinValueTraits; + + TInstancedStruct ValueStruct; + const FProperty* FoundProperty = nullptr; + + const IFlowDataPinValueOwnerInterface* PropertyOwnerInterface = CastChecked(&PropertyOwnerObject); + if (!PropertyOwnerInterface->TryFindPropertyByPinName(PropertyName, FoundProperty, ValueStruct)) + { + OutResult.Result = EFlowDataPinResolveResult::FailedUnknownPin; + return false; + } + + if (ValueStruct.IsValid() && ValueStruct.Get().GetPinTypeName() == TFlowPinType::GetPinTypeNameStatic()) + { + OutResult.ResultValue = ValueStruct; + OutResult.Result = EFlowDataPinResolveResult::Success; + return true; + } + + TArray Values; + TSoftObjectPtr EnumClass; + if (FlowPinType::IsSuccess(Traits::ExtractFromProperty(FoundProperty, &PropertyOwnerObject, Values, EnumClass))) + { + OutResult.ResultValue = TInstancedStruct::Make(EnumClass, Values); + OutResult.Result = EFlowDataPinResolveResult::Success; + return true; + } + + OutResult.Result = EFlowDataPinResolveResult::FailedMismatchedType; + return false; +} + +bool FFlowPinType_Name::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const +{ + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); +} + +bool FFlowPinType_String::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const +{ + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); +} + +bool FFlowPinType_Text::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const +{ + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); +} + +bool FFlowPinType_Vector::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const +{ + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); +} + +bool FFlowPinType_Rotator::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const +{ + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); +} + +bool FFlowPinType_Transform::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const +{ + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); +} + +bool FFlowPinType_GameplayTag::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const +{ + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); +} + +bool FFlowPinType_GameplayTagContainer::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const +{ + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); +} + +bool FFlowPinType_InstancedStruct::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const +{ + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); +} + +bool FFlowPinType_Object::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const +{ + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); +} + +bool FFlowPinType_Class::PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const +{ + return FlowPinType::PopulateResultTemplate(PropertyOwnerObject, Node, PropertyName, OutResult); +} + +#if WITH_EDITOR + +UObject* FFlowPinType_Vector::GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const +{ + static UObject* PinSubCategoryObject = TBaseStructure::Get(); + return PinSubCategoryObject; +} + +UObject* FFlowPinType_Rotator::GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const +{ + static UObject* PinSubCategoryObject = TBaseStructure::Get(); + return PinSubCategoryObject; +} + +UObject* FFlowPinType_Transform::GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const +{ + static UObject* PinSubCategoryObject = TBaseStructure::Get(); + return PinSubCategoryObject; +} + +UObject* FFlowPinType_GameplayTag::GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const +{ + static UObject* PinSubCategoryObject = TBaseStructure::Get(); + return PinSubCategoryObject; +} + +UObject* FFlowPinType_GameplayTagContainer::GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const +{ + static UObject* PinSubCategoryObject = TBaseStructure::Get(); + return PinSubCategoryObject; +} + +UObject* FFlowPinType_InstancedStruct::GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const +{ + static UObject* PinSubCategoryObject = TBaseStructure::Get(); + return PinSubCategoryObject; +} + +UObject* FFlowPinType_Enum::GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const +{ + UEnum* EnumClass = nullptr; + if (Wrapper && Wrapper->GetPinTypeName() == FFlowPinType_Enum::GetPinTypeNameStatic()) + { + const FFlowDataPinValue_Enum* EnumWrapper = static_cast(Wrapper); + TSoftObjectPtr EnumClassPtr = EnumWrapper->EnumClass; + EnumClass = EnumClassPtr.LoadSynchronous(); + } + else if (Property) + { + if (const FStructProperty* StructProperty = CastField(Property)) + { + const UStruct* ScriptStruct = FFlowDataPinValue_Enum::StaticStruct(); + if (StructProperty->Struct == ScriptStruct) + { + FFlowDataPinValue_Enum ValueStruct; + StructProperty->GetValue_InContainer(InContainer, &ValueStruct); + EnumClass = ValueStruct.EnumClass.LoadSynchronous(); + } + } + else if (const FEnumProperty* EnumProperty = CastField(Property)) + { + EnumClass = EnumProperty->GetEnum(); + } + else if (const FByteProperty* ByteProperty = CastField(Property)) + { + if (ByteProperty->Enum) + { + EnumClass = ByteProperty->Enum; + } + } + else if (const FArrayProperty* ArrayProperty = CastField(Property)) + { + if (const FEnumProperty* InnerEnumProperty = CastField(ArrayProperty->Inner)) + { + EnumClass = InnerEnumProperty->GetEnum(); + } + else if (const FByteProperty* InnerByteProperty = CastField(ArrayProperty->Inner)) + { + if (InnerByteProperty->Enum) + { + EnumClass = InnerByteProperty->Enum; + } + } + } + } + + return EnumClass; +} + +UClass* FFlowPinType_Object::TryGetObjectClassFromProperty(const FProperty& MetaDataProperty) +{ + if (UClass* MetaClass = FFlowPinType_Object::TryGetMetaClassFromProperty(MetaDataProperty)) + { + return MetaClass; + } + + // FSoftObjectPath can use the "AllowedClasses" to define what classes are allowed for the object. + // Using the "AllowedClasses" metadata tag, but we only support a single class, due to singular return value for this function. + const FString AllowedClassesString = MetaDataProperty.GetMetaData("AllowedClasses"); + const TArray AllowedClasses = FlowClassUtils::GetClassesFromMetadataString(AllowedClassesString); + + if (AllowedClasses.Num() > 1) + { + UE_LOG(LogFlow, Error, TEXT("Only a single AllowedClasses entry is allowed for flow data pin properties (multiple found: %s) for property %s"), *AllowedClassesString, *MetaDataProperty.GetName()); + + return nullptr; + } + + if (AllowedClasses.IsEmpty()) + { + return nullptr; + } + + if (UClass* AllowedClass = AllowedClasses[0]) + { + return AllowedClass; + } + else + { + UE_LOG(LogFlow, Error, TEXT("Could not resolve AllowedClasses '%s' for property %s"), *AllowedClassesString, *MetaDataProperty.GetName()); + } + + return nullptr; +} + +UClass* FFlowPinType_Object::TryGetMetaClassFromProperty(const FProperty& MetaDataProperty) +{ + const FString& MetaClassName = MetaDataProperty.GetMetaData("MetaClass"); + + if (!MetaClassName.IsEmpty()) + { + if (UClass* FoundClass = FEditorClassUtils::GetClassFromString(MetaClassName)) + { + return FoundClass; + } + else + { + UE_LOG(LogFlow, Error, TEXT("Could not resolve MetaClass named %s for property %s"), *MetaClassName, *MetaDataProperty.GetName()); + } + } + + return nullptr; +} + +UObject* FFlowPinType_Object::GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const +{ + UClass* Class = nullptr; + if (Wrapper && Wrapper->GetPinTypeName() == FFlowPinType_Object::GetPinTypeNameStatic()) + { + const FFlowDataPinValue_Object* ObjectWrapper = static_cast(Wrapper); + Class = ObjectWrapper->ClassFilter; + } + else if (Property) + { + if (const FStructProperty* StructProperty = CastField(Property)) + { + const UStruct* ScriptStruct = FFlowDataPinValue_Object::StaticStruct(); + static const UStruct* SoftObjectPathStruct = TBaseStructure::Get(); + if (StructProperty->Struct == ScriptStruct) + { + FFlowDataPinValue_Object ValueStruct; + StructProperty->GetValue_InContainer(InContainer, &ValueStruct); + Class = ValueStruct.ClassFilter; + } + else if (StructProperty->Struct == SoftObjectPathStruct) + { + Class = FFlowPinType_Object::TryGetObjectClassFromProperty(*StructProperty); + } + } + else if (const FObjectProperty* ObjectProperty = CastField(Property)) + { + Class = ObjectProperty->PropertyClass; + } + else if (const FSoftObjectProperty* SoftObjectProperty = CastField(Property)) + { + Class = SoftObjectProperty->PropertyClass; + } + else if (const FWeakObjectProperty* WeakObjectProperty = CastField(Property)) + { + Class = WeakObjectProperty->PropertyClass; + } + else if (const FLazyObjectProperty* LazyObjectProperty = CastField(Property)) + { + Class = LazyObjectProperty->PropertyClass; + } + else if (const FArrayProperty* ArrayProperty = CastField(Property)) + { + if (const FObjectProperty* InnerObjectProperty = CastField(ArrayProperty->Inner)) + { + Class = InnerObjectProperty->PropertyClass; + } + else if (const FSoftObjectProperty* InnerSoftObjectProperty = CastField(ArrayProperty->Inner)) + { + Class = InnerSoftObjectProperty->PropertyClass; + } + else if (const FWeakObjectProperty* InnerWeakObjectProperty = CastField(ArrayProperty->Inner)) + { + Class = InnerWeakObjectProperty->PropertyClass; + } + else if (const FLazyObjectProperty* InnerLazyObjectProperty = CastField(ArrayProperty->Inner)) + { + Class = InnerLazyObjectProperty->PropertyClass; + } + } + } + + return Class; +} + +UObject* FFlowPinType_Class::GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const +{ + UClass* Class = nullptr; + if (Wrapper && Wrapper->GetPinTypeName() == FFlowPinType_Class::GetPinTypeNameStatic()) + { + const FFlowDataPinValue_Class* ClassWrapper = static_cast(Wrapper); + Class = ClassWrapper->ClassFilter; + } + else if (Property) + { + if (const FStructProperty* StructProperty = CastField(Property)) + { + const UStruct* ScriptStruct = FFlowDataPinValue_Class::StaticStruct(); + static const UStruct* SoftClassPathStruct = TBaseStructure::Get(); + if (StructProperty->Struct == ScriptStruct) + { + FFlowDataPinValue_Class ValueStruct; + StructProperty->GetValue_InContainer(InContainer, &ValueStruct); + Class = ValueStruct.ClassFilter; + } + else if (StructProperty->Struct == SoftClassPathStruct) + { + Class = FFlowPinType_Object::TryGetMetaClassFromProperty(*StructProperty); + } + } + else if (const FClassProperty* ClassProperty = CastField(Property)) + { + Class = ClassProperty->MetaClass; + } + else if (const FSoftClassProperty* SoftClassProperty = CastField(Property)) + { + Class = SoftClassProperty->MetaClass; + } + else if (const FArrayProperty* ArrayProperty = CastField(Property)) + { + if (const FClassProperty* InnerClassProperty = CastField(ArrayProperty->Inner)) + { + Class = InnerClassProperty->MetaClass; + } + else if (const FSoftClassProperty* InnerSoftClassProperty = CastField(ArrayProperty->Inner)) + { + Class = InnerSoftClassProperty->MetaClass; + } + } + } + + return Class; +} + +bool FFlowPinType_Exec::ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const +{ + OutValue = FFormatArgumentValue(FText::FromString(TEXT("Exec"))); + return true; +} + +bool FFlowPinType_Bool::ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const +{ + return FlowPinType::ResolveAndFormatArray(Node, PinName, OutValue, [](const bool& Value) { return Value ? TEXT("true") : TEXT("false"); }); +} + +bool FFlowPinType_Int::ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const +{ + return FlowPinType::ResolveAndFormatArray(Node, PinName, OutValue, [](const int32& Value) { return FString::FromInt(Value); }); +} + +bool FFlowPinType_Int64::ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const +{ + return FlowPinType::ResolveAndFormatArray(Node, PinName, OutValue, [](const int64& Value) { return FString::Printf(TEXT("%lld"), Value); }); +} + +bool FFlowPinType_Float::ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const +{ + return FlowPinType::ResolveAndFormatArray(Node, PinName, OutValue, [](const float& Value) { return FString::SanitizeFloat(Value); }); +} + +bool FFlowPinType_Double::ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const +{ + return FlowPinType::ResolveAndFormatArray(Node, PinName, OutValue, [](const double& Value) { return FString::SanitizeFloat(Value); }); +} + +bool FFlowPinType_Enum::ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const +{ + return FlowPinType::ResolveAndFormatArray(Node, PinName, OutValue, [](const FName& Value) { return Value.ToString(); }); +} + +bool FFlowPinType_Name::ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const +{ + return FlowPinType::ResolveAndFormatArray(Node, PinName, OutValue, [](const FName& Value) { return Value.ToString(); }); +} + +bool FFlowPinType_String::ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const +{ + return FlowPinType::ResolveAndFormatArray(Node, PinName, OutValue, [](const FString& Value) { return Value; }); +} + +bool FFlowPinType_Text::ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const +{ + return FlowPinType::ResolveAndFormatArray(Node, PinName, OutValue, [](const FText& Value) { return Value.ToString(); }); +} + +bool FFlowPinType_Vector::ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const +{ + return FlowPinType::ResolveAndFormatArray(Node, PinName, OutValue, [](const FVector& Value) { return Value.ToString(); }); +} + +bool FFlowPinType_Rotator::ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const +{ + return FlowPinType::ResolveAndFormatArray(Node, PinName, OutValue, [](const FRotator& Value) { return Value.ToString(); }); +} + +bool FFlowPinType_Transform::ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const +{ + return FlowPinType::ResolveAndFormatArray(Node, PinName, OutValue, [](const FTransform& Value) { return Value.ToString(); }); +} + +bool FFlowPinType_GameplayTag::ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const +{ + return FlowPinType::ResolveAndFormatArray(Node, PinName, OutValue, [](const FGameplayTag& Value) { return Value.ToString(); }); +} + +bool FFlowPinType_GameplayTagContainer::ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const +{ + return FlowPinType::ResolveAndFormatArray(Node, PinName, OutValue, [](const FGameplayTagContainer& Value) { return Value.ToString(); }); +} + +bool FFlowPinType_InstancedStruct::ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const +{ + return FlowPinType::ResolveAndFormatArray(Node, PinName, OutValue, [](const FInstancedStruct& Value) { return Value.GetScriptStruct() ? Value.GetScriptStruct()->GetName() : TEXT("None"); }); +} + +bool FFlowPinType_Object::ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const +{ + return FlowPinType::ResolveAndFormatArray(Node, PinName, OutValue, [](const UObject* Value) { return Value ? Value->GetName() : TEXT("None"); }); +} + +bool FFlowPinType_Class::ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const +{ + return FlowPinType::ResolveAndFormatArray(Node, PinName, OutValue, [](const UClass* Value) { return Value ? Value->GetName() : TEXT("None"); }); +} +#endif \ No newline at end of file diff --git a/Source/Flow/Public/AddOns/FlowNodeAddOn.h b/Source/Flow/Public/AddOns/FlowNodeAddOn.h new file mode 100644 index 000000000..c1971fa8b --- /dev/null +++ b/Source/Flow/Public/AddOns/FlowNodeAddOn.h @@ -0,0 +1,106 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Nodes/FlowNodeBase.h" +#include "Nodes/FlowPin.h" + +#include "FlowNodeAddOn.generated.h" + +class UFlowNode; + +/** + * A Flow Node AddOn allows user to extend given node instance in the graph with additional logic. + */ +UCLASS(Abstract, MinimalApi, EditInlineNew, Blueprintable) +class UFlowNodeAddOn : public UFlowNodeBase +{ + GENERATED_BODY() + +public: + FLOW_API UFlowNodeAddOn(); + +protected: + /* The Flow Node that contains this AddOn. + * Accessible only when initialized, runtime only. */ + UPROPERTY(Transient) + TObjectPtr FlowNode; + + /* Input pins to add to the owning Flow Node. + * If defined, ExecuteInput will only be executed for these inputs. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "FlowNodeAddOn") + TArray InputPins; + +#if WITH_EDITORONLY_DATA + /* Output pins to add to the owning Flow Node. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "FlowNodeAddOn") + TArray OutputPins; +#endif + +public: + // UFlowNodeBase + + /* AddOns may opt in to be eligible for a given parent. + * - ParentTemplate - the template of the FlowNode or FlowNodeAddOn that is being considered as a potential parent. + * - AdditionalAddOnsToAssumeAreChildren - other AddOns to assume that are already child AddOns for the purposes of this test. + * This list will be populated with the 'other' AddOns in a multi-paste operation in the editor, + * because some paste-targets can only accept a certain mix of addons, so we must know the rest of the set being pasted + * to make the correct decision about whether to allow AddOnTemplate to be added. + * See: https://forums.unrealengine.com/t/default-parameters-with-tarrays/330225 for details on AutoCreateRefTerm. */ + UFUNCTION(BlueprintNativeEvent, BlueprintPure, Category = "FlowNodeAddOn", meta = (AutoCreateRefTerm = AdditionalAddOnsToAssumeAreChildren)) + FLOW_API EFlowAddOnAcceptResult AcceptFlowNodeAddOnParent(const UFlowNodeBase* ParentTemplate, const TArray& AdditionalAddOnsToAssumeAreChildren) const; + + FLOW_API virtual UFlowNode* GetFlowNodeSelfOrOwner() override { return FlowNode; } + FLOW_API virtual bool IsSupportedInputPinName(const FName& PinName) const override; + + FLOW_API virtual void TriggerFirstOutput(const bool bFinish) override; + FLOW_API virtual void TriggerOutput(const FName PinName, const bool bFinish = false, const EFlowPinActivationType ActivationType = EFlowPinActivationType::Default) override; + FLOW_API virtual void Finish() override; + // -- + + // IFlowCoreExecutableInterface + FLOW_API virtual void InitializeInstance() override; + FLOW_API virtual void DeinitializeInstance() override; + // -- + + // UFlowNodeAddOn + + /* The FlowNode that contains this AddOn. + * Accessible only when initialized, runtime only. */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FlowNodeAddon", DisplayName = "Get Flow Node") + FLOW_API UFlowNode* GetFlowNode() const; + + /* Will crawl the hierarchy until it finds a flow node (addons can be attached to other add-ons). */ + FLOW_API UFlowNode* FindOwningFlowNode() const; + // -- + + /* Returns a random seed suitable for this AddOn. + * 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()); } + FLOW_API virtual TArray GetContextInputs() const override; + FLOW_API virtual TArray GetContextOutputs() const override; + // -- + + FLOW_API void RequestReconstructionOnOwningFlowNode() const; + + /* Editor-only method to set the FlowNode for any follow-up operations + * that the addon will need a reliable FlowNode pointer at editor-time */ + void SetFlowNodeForEditor(UFlowNode* FlowNodeOwner) { FlowNode = FlowNodeOwner; } +#endif // WITH_EDITOR + +protected: + void CacheFlowNode(); + +#if WITH_EDITOR + TArray GetPinsForContext(const TArray& Context) const; +#endif +}; diff --git a/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateAND.h b/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateAND.h new file mode 100644 index 000000000..17885f9b8 --- /dev/null +++ b/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateAND.h @@ -0,0 +1,30 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "AddOns/FlowNodeAddOn.h" +#include "Interfaces/FlowPredicateInterface.h" + +#include "FlowNodeAddOn_PredicateAND.generated.h" + +class UFlowNode; + +UCLASS(MinimalApi, NotBlueprintable, meta = (DisplayName = "AND")) +class UFlowNodeAddOn_PredicateAND + : public UFlowNodeAddOn + , public IFlowPredicateInterface +{ + GENERATED_BODY() + +public: + UFlowNodeAddOn_PredicateAND(); + + // UFlowNodeBase + virtual EFlowAddOnAcceptResult AcceptFlowNodeAddOnChild_Implementation(const UFlowNodeAddOn* AddOnTemplate, const TArray& AdditionalAddOnsToAssumeAreChildren) const override; + // -- + + // IFlowPredicateInterface + virtual bool EvaluatePredicate_Implementation() const override; + // -- + + FLOW_API static bool EvaluatePredicateAND(const TArray& AddOns); +}; diff --git a/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateCompareValues.h b/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateCompareValues.h new file mode 100644 index 000000000..b97596d9a --- /dev/null +++ b/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateCompareValues.h @@ -0,0 +1,188 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include + +#include "AddOns/FlowNodeAddOn.h" +#include "Interfaces/FlowPredicateInterface.h" +#include "Types/FlowBranchEnums.h" +#include "Types/FlowNamedDataPinProperty.h" + +#include "FlowNodeAddOn_PredicateCompareValues.generated.h" + +struct FFlowPinConnectionPolicy; + +UCLASS(MinimalApi, NotBlueprintable, meta = (DisplayName = "Compare Values")) +class UFlowNodeAddOn_PredicateCompareValues + : public UFlowNodeAddOn + , public IFlowPredicateInterface +{ + GENERATED_BODY() + +public: + UFlowNodeAddOn_PredicateCompareValues(); + + // IFlowPredicateInterface + virtual bool EvaluatePredicate_Implementation() const override; + // -- + +#if WITH_EDITOR + // UObject + virtual void PostEditChangeChainProperty(FPropertyChangedChainEvent& PropertyChangedEvent) override; + // -- + + // UFlowNodeBase + virtual EDataValidationResult ValidateNode() override; + virtual FText K2_GetNodeTitle_Implementation() const override; + // -- + + /* Utility function for subclasses, if they want to force a named property to be Input or Output. + * Unused in this class. */ + void OnPostEditEnsureAllNamedPropertiesPinDirection(const FProperty& Property, bool bIsInput); +#endif + +protected: + UPROPERTY(EditAnywhere, Category = Configuration, DisplayName = "Operator") + EFlowPredicateCompareOperatorType OperatorType = EFlowPredicateCompareOperatorType::Equal; + + UPROPERTY(EditAnywhere, Category = Configuration) + FFlowNamedDataPinProperty LeftValue; + + UPROPERTY(EditAnywhere, Category = Configuration) + FFlowNamedDataPinProperty RightValue; + + UPROPERTY(EditAnywhere, Category = Configuration) + EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue; + +protected: + // IFlowDataPinValueOwnerInterface + virtual bool TryFindPropertyByPinName( + const FName& PinName, + const FProperty*& OutFoundProperty, + TInstancedStruct& OutFoundInstancedStruct) const override; + // -- + + // Operator classifiers + bool IsEqualityOp() const; + bool IsArithmeticOp() const; + + /* Compatibility check by pin type names. */ + static bool AreComparablePinTypes( + const FFlowPinConnectionPolicy& PinConnectionPolicy, + const FName& LeftPinTypeName, + const FName& RightPinTypeName); + + // Domain classifiers + 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 IsBoolTypeName(const FName& TypeName); + static bool IsVectorTypeName(const FName& TypeName); + static bool IsRotatorTypeName(const FName& TypeName); + static bool IsTransformTypeName(const FName& TypeName); + static bool IsObjectTypeName(const FName& TypeName); + static bool IsClassTypeName(const FName& TypeName); + static bool IsInstancedStructTypeName(const FName& TypeName); + + // ----------------------------------------------------------------------- + // Domain equality comparisons + // (these return true if they successfully compared; equality result is via out param) + // ----------------------------------------------------------------------- + + /* 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==. */ + template > + bool TryCheckResolvedValuesEqual(bool& bOutIsEqual, const TCHAR* ErrorLabel, ComparatorFn Comparator = {}) const; + + // Domain comparisons that need special handling beyond simple resolve-and-compare + 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. */ + bool TryCheckFallbackStringEqual(bool& bOutIsEqual) const; + + // Numeric comparisons support full operator set + bool TryCompareAsDouble() const; + bool TryCompareAsInt64() const; + + // Comparison helpers + bool CompareDoubleUsingOperator(double LeftValueAsDouble, double RightValueAsDouble) const; + bool CompareInt64UsingOperator(int64 LeftValueAsInt64, int64 RightValueAsInt64) const; + + /* Helper for equality-only type blocks in EvaluatePredicate. + * Guards against arithmetic operators, calls CompareFunc, applies Equal/NotEqual flip. + * Returns the final predicate result, or false on error. */ + bool EvaluateEqualityBlock(const TCHAR* TypeLabel, const TFunctionRef CompareFunc) const; + + // These are the DataPinNamedProperty property names + // (ie, the name of the property itself, eg "LeftValue") + const FName& GetLeftValuePropertyName() const; + const FName& GetRightValuePropertyName() const; + + /* This is the value as-authored by the node author in their graph. */ + FORCEINLINE const FName& GetAuthoredValueName(const FFlowNamedDataPinProperty& NamedDataPinProperty) const { return NamedDataPinProperty.Name; } + + /* This is the authored value after being disambiguated (for duplicates). + * Example: how it is presented and indexed on the owning Flow Node. */ + FORCEINLINE const FName& GetDisambiguatedValueName(const FFlowNamedDataPinProperty& NamedDataPinProperty) const { return NamedDataPinProperty.DataPinValue.Get().PropertyPinName; } + +private: + + /* Cached type names for the current evaluation, to avoid repeated TInstancedStruct::Get() calls. + * Only valid during a single call to EvaluatePredicate_Implementation or ValidateNode. */ + struct FCachedTypeNames + { + FName LeftTypeName; + FName RightTypeName; + bool bIsValid = false; + + void Reset() { bIsValid = false; } + }; + + /* Populate cached type names from the current LeftValue/RightValue. + * Returns false (and logs error) if either value is not configured. */ + bool CacheTypeNames(FCachedTypeNames& OutCache) const; +}; + +// ----------------------------------------------------------------------- +// Template implementations +// ----------------------------------------------------------------------- + +template +bool UFlowNodeAddOn_PredicateCompareValues::TryCheckResolvedValuesEqual(bool& bOutIsEqual, const TCHAR* ErrorLabel, ComparatorFn Comparator) const +{ + typename TFlowPinType::ValueType LeftResolved{}; + { + const EFlowDataPinResolveResult ResolveResult = + TryResolveDataPinValue(GetDisambiguatedValueName(LeftValue), LeftResolved, SingleFromArray); + + if (!FlowPinType::IsSuccess(ResolveResult)) + { + LogError(FString::Printf(TEXT("Failed to resolve LeftValue as %s."), ErrorLabel)); + return false; + } + } + + typename TFlowPinType::ValueType RightResolved{}; + { + const EFlowDataPinResolveResult ResolveResult = + TryResolveDataPinValue(GetDisambiguatedValueName(RightValue), RightResolved, SingleFromArray); + + if (!FlowPinType::IsSuccess(ResolveResult)) + { + LogError(FString::Printf(TEXT("Failed to resolve RightValue as %s."), ErrorLabel)); + return false; + } + } + + bOutIsEqual = Comparator(LeftResolved, RightResolved); + return true; +} diff --git a/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateNOT.h b/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateNOT.h new file mode 100644 index 000000000..4d3da6347 --- /dev/null +++ b/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateNOT.h @@ -0,0 +1,28 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "AddOns/FlowNodeAddOn.h" +#include "Interfaces/FlowPredicateInterface.h" + +#include "FlowNodeAddOn_PredicateNOT.generated.h" + +class UFlowNode; + +UCLASS(MinimalApi, NotBlueprintable, meta = (DisplayName = "NOT")) +class UFlowNodeAddOn_PredicateNOT + : public UFlowNodeAddOn + , public IFlowPredicateInterface +{ + GENERATED_BODY() + +public: + UFlowNodeAddOn_PredicateNOT(); + + // UFlowNodeBase + virtual EFlowAddOnAcceptResult AcceptFlowNodeAddOnChild_Implementation(const UFlowNodeAddOn* AddOnTemplate, const TArray& AdditionalAddOnsToAssumeAreChildren) const override; + // -- + + // IFlowPredicateInterface + virtual bool EvaluatePredicate_Implementation() const override; + // -- +}; diff --git a/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateOR.h b/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateOR.h new file mode 100644 index 000000000..dcd1dff30 --- /dev/null +++ b/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateOR.h @@ -0,0 +1,30 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "AddOns/FlowNodeAddOn.h" +#include "Interfaces/FlowPredicateInterface.h" + +#include "FlowNodeAddOn_PredicateOR.generated.h" + +class UFlowNode; + +UCLASS(MinimalApi, NotBlueprintable, meta = (DisplayName = "OR")) +class UFlowNodeAddOn_PredicateOR + : public UFlowNodeAddOn + , public IFlowPredicateInterface +{ + GENERATED_BODY() + +public: + UFlowNodeAddOn_PredicateOR(); + + // UFlowNodeBase + virtual EFlowAddOnAcceptResult AcceptFlowNodeAddOnChild_Implementation(const UFlowNodeAddOn* AddOnTemplate, const TArray& AdditionalAddOnsToAssumeAreChildren) const override; + // -- + + // IFlowPredicateInterface + virtual bool EvaluatePredicate_Implementation() const override; + // -- + + FLOW_API static bool EvaluatePredicateOR(const TArray& AddOns); +}; diff --git a/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateRequireGameplayTags.h b/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateRequireGameplayTags.h new file mode 100644 index 000000000..c38f6c4f6 --- /dev/null +++ b/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateRequireGameplayTags.h @@ -0,0 +1,50 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "AddOns/FlowNodeAddOn.h" +#include "Interfaces/FlowPredicateInterface.h" +#include "Types/FlowGameplayTagUtils.h" + +#include "FlowNodeAddOn_PredicateRequireGameplayTags.generated.h" + +UCLASS(MinimalApi, NotBlueprintable, meta = (DisplayName = "Require Gameplay Tags")) +class UFlowNodeAddOn_PredicateRequireGameplayTags + : public UFlowNodeAddOn + , public IFlowPredicateInterface +{ + GENERATED_BODY() + +public: + + UFlowNodeAddOn_PredicateRequireGameplayTags(); + + // IFlowPredicateInterface + virtual bool EvaluatePredicate_Implementation() const override; + // -- + + // UFlowNodeBase + virtual void UpdateNodeConfigText_Implementation() override; + // -- + +#if WITH_EDITOR + // UObject Interface + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; + // -- + + // UFlowNodeBase + void OnEditorPinConnectionsChanged(const TArray& Changes) override; + // -- +#endif + + bool TryGetTagsToCheckFromDataPin(FGameplayTagContainer& TagsToCheckValue) const; + +public: + + /* DataPin input for the Gameplay Tag or Tag Container to test with the Requirements. */ + UPROPERTY(EditAnywhere, Category = Configuration, meta = (DefaultForInputFlowPin, FlowPinType = "GameplayTagContainer")) + FGameplayTagContainer Tags; + + /* Requirements to evaluate the Test Tags with. */ + UPROPERTY(EditAnywhere, Category = Configuration) + FFlowGameplayTagRequirements Requirements; +}; diff --git a/Source/Flow/Public/AddOns/FlowNodeAddOn_SwitchCase.h b/Source/Flow/Public/AddOns/FlowNodeAddOn_SwitchCase.h new file mode 100644 index 000000000..52868cf13 --- /dev/null +++ b/Source/Flow/Public/AddOns/FlowNodeAddOn_SwitchCase.h @@ -0,0 +1,57 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "AddOns/FlowNodeAddOn.h" +#include "Interfaces/FlowSwitchCaseInterface.h" +#include "Types/FlowBranchEnums.h" +#include "FlowNodeAddOn_SwitchCase.generated.h" + +class UFlowNode; + +UCLASS(MinimalApi, Blueprintable, meta = (DisplayName = "Case")) +class UFlowNodeAddOn_SwitchCase + : public UFlowNodeAddOn + , public IFlowSwitchCaseInterface +{ + GENERATED_BODY() + +public: + + /* The output pin for this Switch Case, if it passes. */ + UPROPERTY(EditAnywhere, Category = "Switch") + FName CaseName; + + /* The output pin for this Switch Case, if it passes. */ + UPROPERTY() + mutable FName OutputPinName; + + /* For root-level predicates on this Switch Case, do we treat them as an "AND" (all must pass) or an "OR" (at least one must pass)? */ + UPROPERTY(EditAnywhere, Category = "Switch", DisplayName = "Root Combination Rule") + EFlowPredicateCombinationRule BranchCombinationRule = EFlowPredicateCombinationRule::AND; + + /* The base PinName for the Switch Case output(s). */ + static const FName DefaultCaseName; + +public: + UFlowNodeAddOn_SwitchCase(); + + // UFlowNodeBase + virtual EFlowAddOnAcceptResult AcceptFlowNodeAddOnChild_Implementation(const UFlowNodeAddOn* AddOnTemplate, const TArray& AdditionalAddOnsToAssumeAreChildren) const override; + virtual FText K2_GetNodeTitle_Implementation() const override; + // -- + +#if WITH_EDITOR + // UObject + virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override; + // -- + + // IFlowContextPinSupplierInterface + virtual bool SupportsContextPins() const override { return true; } + virtual TArray GetContextOutputs() const override; + // -- +#endif + + // IFlowSwitchCaseInterface + virtual bool TryTriggerForCase_Implementation() const override; + // -- +}; diff --git a/Source/Flow/Public/Asset/FlowAssetParams.h b/Source/Flow/Public/Asset/FlowAssetParams.h new file mode 100644 index 000000000..5ce19f2dc --- /dev/null +++ b/Source/Flow/Public/Asset/FlowAssetParams.h @@ -0,0 +1,109 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Engine/DataAsset.h" +#include "Types/FlowNamedDataPinProperty.h" +#include "Interfaces/FlowDataPinValueOwnerInterface.h" +#include "Interfaces/FlowDataPinValueSupplierInterface.h" +#include "Interfaces/FlowAssetProviderInterface.h" +#include "Asset/FlowAssetParamsTypes.h" + +#include "FlowAssetParams.generated.h" + +class UFlowAsset; + +/** +* Data asset for storing Flow Graph Start node parameters, supporting external configuration. +* This is considered experimental at the moment. +*/ +UCLASS(BlueprintType) +class FLOW_API UFlowAssetParams + : public UDataAsset + , public IFlowAssetProviderInterface + , public IFlowDataPinValueOwnerInterface + , public IFlowDataPinValueSupplierInterface +{ + GENERATED_BODY() + +public: +#if WITH_EDITORONLY_DATA + /* Reference to the associated Flow Asset. */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = FlowAssetParams) + TSoftObjectPtr OwnerFlowAsset; + + /* Reference to the "Parent" params object to inherit from (if any). */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = FlowAssetParams) + FFlowAssetParamsPtr ParentParams; + + /* Array of properties synchronized with the Start node (local adds/overrides; effective flattened via ReconcilePropertiesWithParentParams). */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = FlowAssetParams, meta = (EditFixedSize)) + TArray Properties; +#endif + + UPROPERTY() + TMap> PropertyMap; + +public: + // UObject interface +#if WITH_EDITOR + virtual void PostLoad() override; + virtual void PreSaveRoot(FObjectPreSaveRootContext ObjectSaveContext) override; +#endif + virtual void Serialize(FArchive& Ar) override; + // -- + + // IFlowDataPinValueSupplierInterface + virtual bool CanSupplyDataPinValues() const override; + virtual FFlowDataPinResult TrySupplyDataPin(FName PinName) const override; + // -- + + // IFlowAssetProviderInterface + virtual UFlowAsset* ProvideFlowAsset() const override; + // -- + +#if WITH_EDITOR + // UObject interface + virtual EDataValidationResult IsDataValid(FDataValidationContext& Context) const override; + // -- + + /* Generates properties from the associated Start node or updates Start node from params. */ + EFlowReconcilePropertiesResult ReconcilePropertiesWithStartNode( + const FDateTime& FlowAssetLastSaveTimeStamp, + const TSoftObjectPtr& InOwnerFlowAsset, + TArray& MutablePropertiesFromStartNode); + + /* Updates properties from ParentParams, handling inheritance and name enforcement. */ + EFlowReconcilePropertiesResult ReconcilePropertiesWithParentParams(); + + void ConfigureFlowAssetParams(TSoftObjectPtr OwnerAsset, TSoftObjectPtr InParentParams, const TArray& InProperties); + + // IFlowDataPinValueOwnerInterface + virtual bool CanModifyFlowDataPinType() const override; + virtual bool ShowFlowDataPinValueInputPinCheckbox() const override; + virtual bool ShowFlowDataPinValueClassFilter(const FFlowDataPinValue* Value) const override; + virtual bool CanEditFlowDataPinValueClassFilter(const FFlowDataPinValue* Value) const override; + virtual void SetFlowDataPinValuesRebuildDelegate(FSimpleDelegate InDelegate) override + { + FlowDataPinValuesRebuildDelegate = InDelegate; + } + + virtual void RequestFlowDataPinValuesDetailsRebuild() override + { + if (FlowDataPinValuesRebuildDelegate.IsBound()) + { + FlowDataPinValuesRebuildDelegate.Execute(); + } + } + +private: + FSimpleDelegate FlowDataPinValuesRebuildDelegate; + // -- + +protected: + + EFlowReconcilePropertiesResult CheckForParentCycle() const; + + void ModifyAndRebuildPropertiesMap(); + void RebuildPropertiesMap(); +#endif +}; \ No newline at end of file diff --git a/Source/Flow/Public/Asset/FlowAssetParamsTypes.h b/Source/Flow/Public/Asset/FlowAssetParamsTypes.h new file mode 100644 index 000000000..604a09594 --- /dev/null +++ b/Source/Flow/Public/Asset/FlowAssetParamsTypes.h @@ -0,0 +1,70 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Types/FlowEnumUtils.h" +#include "UObject/SoftObjectPtr.h" + +#include "FlowAssetParamsTypes.generated.h" + +class UFlowAssetParams; + +/** + * Result of reconciling Flow Asset Params with Start node or SuperParams. + */ +UENUM(BlueprintType) +enum class EFlowReconcilePropertiesResult : uint8 +{ + NoChanges, + + ParamsPropertiesUpdated, + AssetPropertyValuesUpdated, + + Error_InvalidAsset, + Error_PropertyCountMismatch, + Error_PropertyTypeMismatch, + Error_CyclicInheritance, + Error_UnloadableParent, + + Max UMETA(Hidden), + Invalid UMETA(Hidden), + Min = 0 UMETA(Hidden), + + SuccessFirst = NoChanges UMETA(Hidden), + SuccessLast = AssetPropertyValuesUpdated UMETA(Hidden), + + ModifiedFirst = ParamsPropertiesUpdated UMETA(Hidden), + ModifiedLast = AssetPropertyValuesUpdated UMETA(Hidden), + + ErrorFirst = Error_InvalidAsset UMETA(Hidden), + ErrorLast = Error_UnloadableParent UMETA(Hidden), +}; +FLOW_ENUM_RANGE_VALUES(EFlowReconcilePropertiesResult) + +namespace EFlowReconcilePropertiesResult_Classifiers +{ + FORCEINLINE bool IsSuccessResult(const EFlowReconcilePropertiesResult Result) { return FLOW_IS_ENUM_IN_SUBRANGE(Result, EFlowReconcilePropertiesResult::Success); } + FORCEINLINE bool IsModifiedResult(const EFlowReconcilePropertiesResult Result) { return FLOW_IS_ENUM_IN_SUBRANGE(Result, EFlowReconcilePropertiesResult::Modified); } + FORCEINLINE bool IsErrorResult(const EFlowReconcilePropertiesResult Result) { return FLOW_IS_ENUM_IN_SUBRANGE(Result, EFlowReconcilePropertiesResult::Error); } +} + +/** + * Wrapper for TSoftObjectPtr to enable editor customization. + * + * Supported metadata tags + * ShowCreateNew: Should we show the "Create New" button? + * HideChildParams: When showing a chooser, should we hide "Child" params or not? (Child params have a non-null ParentParams) + */ +USTRUCT(BlueprintType) +struct FLOW_API FFlowAssetParamsPtr +{ + GENERATED_BODY() + + FFlowAssetParamsPtr() = default; + explicit FFlowAssetParamsPtr(const TSoftObjectPtr InAssetParamsPtr) : AssetPtr(InAssetParamsPtr) { } + + UFlowAssetParams* ResolveFlowAssetParams() const; + + /* Reference to the Flow Asset Params. */ + UPROPERTY(EditAnywhere, Category = FlowAssetParams, meta = (EditAssetInline)) + TSoftObjectPtr AssetPtr; +}; diff --git a/Source/Flow/Public/Asset/FlowAssetParamsUtils.h b/Source/Flow/Public/Asset/FlowAssetParamsUtils.h new file mode 100644 index 000000000..a89149d69 --- /dev/null +++ b/Source/Flow/Public/Asset/FlowAssetParamsUtils.h @@ -0,0 +1,65 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Misc/DateTime.h" +#include "Asset/FlowAssetParamsTypes.h" + +#include "FlowAssetParamsUtils.generated.h" + +class UObject; +class UFlowAssetParams; +struct FFlowNamedDataPinProperty; + +/** +* Utility functions for Flow Asset Params reconciliation and validation. +*/ +USTRUCT() +struct FLOW_API FFlowAssetParamsUtils +{ + GENERATED_BODY() + +#if WITH_EDITOR + static FDateTime GetLastSavedTimestampForObject(const UObject* Object); + + static EFlowReconcilePropertiesResult CheckPropertiesMatch( + const TArray& PropertiesA, + const TArray& PropertiesB); + + static const FFlowNamedDataPinProperty* FindPropertyByGuid( + const TArray& Props, + const FGuid& Guid); + + static FFlowNamedDataPinProperty* FindPropertyByGuid( + TArray& Props, + const FGuid& Guid); + + static bool ArePropertyArraysEqual( + const TArray& A, + const TArray& B); + + static bool ArePropertiesEqual( + const FFlowNamedDataPinProperty& A, + const FFlowNamedDataPinProperty& B); + + /** + * Create Flow Asset Params asset from a parent params asset. + * - Creates the new asset in the same folder as the parent + * - Uses parent's asset name as the base for unique name generation (ParentName, ParentName_1, ...) + * - Copies OwnerFlowAsset + Properties and sets ParentParams to the provided parent + * - Runs ReconcilePropertiesWithParentParams (cycle detection, flattened inheritance, etc.) + * - Attempts source control checkout/add + * - Saves the new package + * - Registers and syncs to Content Browser + * + * @param ParentParams The parent params asset to inherit from. Must be valid. + * @param bShowDialogs If true, errors are surfaced via modal dialogs as well as logs. + * @param OutOptionalFailureReason If provided, filled with a human-readable error message on failure. + * @return The created child params asset or nullptr on failure. + */ + static UFlowAssetParams* CreateChildParamsAsset(UFlowAssetParams& ParentParams, const bool bShowDialogs = true, FText* OutOptionalFailureReason = nullptr); + +protected: + static void FailCreateChild(const FText& Reason, const bool bShowDialogs, FText* OutOptionalFailureReason); + +#endif +}; diff --git a/Source/Flow/Public/Asset/FlowDeferredTransitionScope.h b/Source/Flow/Public/Asset/FlowDeferredTransitionScope.h new file mode 100644 index 000000000..bc465e078 --- /dev/null +++ b/Source/Flow/Public/Asset/FlowDeferredTransitionScope.h @@ -0,0 +1,34 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Misc/Guid.h" + +#include "Nodes/FlowPin.h" + +class UFlowAsset; + +struct FFlowDeferredTriggerInput +{ + FGuid NodeGuid; + FName PinName; + FConnectedPin FromPin; +}; + +struct FLOW_API FFlowDeferredTransitionScope +{ +public: + void EnqueueDeferredTrigger(const FFlowDeferredTriggerInput& Entry); + bool TryFlushDeferredTriggers(UFlowAsset& OwningFlowAsset); + + void CloseScope() { bIsOpen = false; } + bool IsOpen() const { return bIsOpen; } + + const TArray& GetDeferredTriggers() const { return DeferredTriggers; } + +protected: + /* Deferred triggers for this scope. */ + TArray DeferredTriggers; + + /* Is currently accepting new deferred triggers. */ + bool bIsOpen = true; +}; diff --git a/Source/Flow/Public/FlowAsset.h b/Source/Flow/Public/FlowAsset.h index f0e3c0363..fa1048b08 100644 --- a/Source/Flow/Public/FlowAsset.h +++ b/Source/Flow/Public/FlowAsset.h @@ -1,285 +1,529 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once -#include "CoreMinimal.h" #include "FlowSave.h" #include "FlowTypes.h" +#include "Asset/FlowAssetParamsTypes.h" +#include "Asset/FlowDeferredTransitionScope.h" +#include "Nodes/FlowNode.h" + +#if WITH_EDITOR +#include "FlowMessageLog.h" +#endif + +#include "StructUtils/InstancedStruct.h" +#include "Templates/SharedPointer.h" +#include "UObject/ObjectKey.h" + #include "FlowAsset.generated.h" -class UFlowNode; +class UFlowNode_CustomOutput; class UFlowNode_CustomInput; -class UFlowNode_Start; class UFlowNode_SubGraph; class UFlowSubsystem; +struct FFlowPreloadPolicy; +struct FFlowPinConnectionPolicy; class UEdGraph; class UEdGraphNode; -class UFlowAsset; - -#if WITH_EDITOR - -/** Interface for calling the graph editor methods */ -class FLOW_API IFlowGraphInterface -{ -public: - IFlowGraphInterface() {} - virtual ~IFlowGraphInterface() {} - virtual void OnInputTriggered(UEdGraphNode* GraphNode, const int32 Index) const {} - virtual void OnOutputTriggered(UEdGraphNode* GraphNode, const int32 Index) const {} -}; - -DECLARE_DELEGATE(FFlowAssetEvent); +#if !UE_BUILD_SHIPPING +DECLARE_DELEGATE(FFlowGraphEvent); +DECLARE_DELEGATE_TwoParams(FFlowSignalEvent, UFlowNode* /*FlowNode*/, const FName& /*PinName*/); #endif /** - * Single asset containing flow nodes. + * Asset containing Flow nodes organized as non-linear graph. */ UCLASS(BlueprintType, hideCategories = Object) class FLOW_API UFlowAsset : public UObject { GENERATED_UCLASS_BODY() +public: friend class UFlowNode; friend class UFlowNode_CustomOutput; friend class UFlowNode_SubGraph; friend class UFlowSubsystem; friend class FFlowAssetDetails; + friend class FFlowNode_SubGraphDetails; friend class UFlowGraphSchema; UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Flow Asset") FGuid AssetGuid; - // Set it to False, if this asset is instantiated as Root Flow for owner that doesn't live in the world - // This allow to SaveGame support works properly, if owner of Root Flow would be Game Instance or its subsystem + /* Set it to False, if this asset is instantiated as Root Flow for owner that doesn't live in the world. + * This allows to SaveGame support works properly, if owner of Root Flow would be Game Instance or its subsystem. */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Flow Asset") bool bWorldBound; - + ////////////////////////////////////////////////////////////////////////// -// Graph +// Graph (editor-only) +public: #if WITH_EDITOR +public: friend class UFlowGraph; // UObject static void AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector); virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; virtual void PostDuplicate(bool bDuplicateForPIE) override; - virtual EDataValidationResult IsDataValid(TArray& ValidationErrors) override; + virtual void PostLoad() override; + virtual void PreSaveRoot(FObjectPreSaveRootContext ObjectSaveContext) override; // -- -#endif +#endif - // IFlowGraphInterface #if WITH_EDITORONLY_DATA +public: + FSimpleDelegate OnDetailsRefreshRequested; + + static FString ValidationError_NodeClassNotAllowed; + static FString ValidationError_AddOnNodeClassNotAllowed; + static FString ValidationError_NullNodeInstance; + static FString ValidationError_NullAddOnNodeInstance; + private: UPROPERTY() - UEdGraph* FlowGraph; - - static TSharedPtr FlowGraphInterface; + TObjectPtr FlowGraph; #endif -public: #if WITH_EDITOR - UEdGraph* GetGraph() const { return FlowGraph; }; +public: + void SetupForEditing(); + + UEdGraph* GetGraph() const { return FlowGraph; } + + virtual EDataValidationResult ValidateAsset(FFlowMessageLog& MessageLog); + + /* Returns whether the node class is allowed in this flow asset. */ + bool IsNodeOrAddOnClassAllowed(const UClass* FlowNodeClass, FText* OutOptionalFailureReason = nullptr) const; + + virtual TSubclassOf GetDefaultFlowAssetForSubgraphs() const { return GetClass(); } - static void SetFlowGraphInterface(TSharedPtr InFlowAssetEditor); - static TSharedPtr GetFlowGraphInterface() { return FlowGraphInterface; }; +protected: + bool CanFlowNodeClassBeUsedByFlowAsset(const UClass& FlowNodeClass) const; + bool CanFlowAssetUseFlowNodeClass(const UClass& FlowNodeClass) const; + bool CanFlowAssetReferenceFlowNode(const UClass& FlowNodeClass, FText* OutOptionalFailureReason = nullptr) const; + + bool IsFlowNodeClassInAllowedClasses(const UClass& FlowNodeClass, const TSubclassOf& RequiredAncestor = nullptr) const; + bool IsFlowNodeClassInDeniedClasses(const UClass& FlowNodeClass) const; + +private: + /* Recursively validates the given addon and its children. */ + void ValidateAddOnTree(UFlowNodeAddOn& AddOn, FFlowMessageLog& MessageLog); #endif - // -- ////////////////////////////////////////////////////////////////////////// // Nodes protected: - TArray> AllowedNodeClasses; - TArray> DeniedNodeClasses; + TArray> AllowedNodeClasses; + TArray> DeniedNodeClasses; + + TArray> AllowedInSubgraphNodeClasses; + TArray> DeniedInSubgraphNodeClasses; bool bStartNodePlacedAsGhostNode; private: UPROPERTY() - TMap Nodes; + TMap> Nodes; + +public: +#if WITH_EDITOR + FFlowGraphEvent OnSubGraphReconstructionRequested; + + UFlowNode* CreateNode(const UClass* NodeClass, UEdGraphNode* GraphNode); + + void RegisterNode(const FGuid& NewGuid, UFlowNode* NewNode); + void UnregisterNode(const FGuid& NodeGuid); + + /* Processes nodes and updates pin connections from the graph to the UFlowNode (processes all nodes in the graph if passed nullptr). */ + void HarvestNodeConnections(UFlowNode* TargetNode = nullptr); + + static bool TryGetDefaultForInputPinName(const FStructProperty& StructProperty, const void* Container, FString& OutString); +#endif - /** - * Custom Inputs define custom entry points in graph, it's similar to blueprint Custom Events - * Sub Graph node using this Flow Asset will generate context Input Pin for every valid Event name on this list - */ +public: + const TMap& GetNodes() const { return ObjectPtrDecay(Nodes); } + TArray GetAllNodes() const; + + UFlowNode* GetNode(const FGuid& Guid) const { return Nodes.FindRef(Guid); } + + template + T* GetNode(const FGuid& Guid) const + { + static_assert(TPointerIsConvertibleFromTo::Value, "'T' template parameter to GetNode must be derived from UFlowNode"); + + if (UFlowNode* Node = Nodes.FindRef(Guid)) + { + return Cast(Node); + } + + return nullptr; + } + + UFUNCTION(BlueprintPure, Category = "FlowAsset", meta = (DeterminesOutputType = "FlowNodeClass")) + TArray GetNodesInExecutionOrder(UFlowNode* FirstIteratedNode, const TSubclassOf FlowNodeClass) const; + + template + void GetNodesInExecutionOrder(UFlowNode* FirstIteratedNode, TArray& OutNodes) const + { + static_assert(TPointerIsConvertibleFromTo::Value, "'T' template parameter to GetNodesInExecutionOrder must be derived from UFlowNode"); + + if (FirstIteratedNode) + { + TSet> IteratedNodes; + GetNodesInExecutionOrder_Recursive(FirstIteratedNode, IteratedNodes, OutNodes); + } + } + +protected: + template + void GetNodesInExecutionOrder_Recursive(UFlowNode* Node, TSet>& IteratedNodes, TArray& OutNodes) const + { + IteratedNodes.Add(Node); + + if (T* NodeOfRequiredType = Cast(Node)) + { + OutNodes.Emplace(NodeOfRequiredType); + } + + for (UFlowNode* ConnectedNode : Node->GatherConnectedNodes()) + { + if (ConnectedNode && !IteratedNodes.Contains(ConnectedNode)) + { + GetNodesInExecutionOrder_Recursive(ConnectedNode, IteratedNodes, OutNodes); + } + } + } + +public: + UFUNCTION(BlueprintPure, Category = "FlowAsset") + virtual UFlowNode* GetDefaultEntryNode() const; + +////////////////////////////////////////////////////////////////////////// +// Custom Inputs/Outputs + +#if WITH_EDITORONLY_DATA +protected: + /* Custom Inputs define custom entry points in graph, it's similar to blueprint Custom Events. + * Sub Graph node using this Flow Asset will generate context Input Pin for every valid Event name on this list. */ UPROPERTY(EditAnywhere, Category = "Sub Graph") TArray CustomInputs; - /** - * Custom Outputs define custom graph outputs, this allow to send signals to the parent graph while executing this graph - * Sub Graph node using this Flow Asset will generate context Output Pin for every valid Event name on this list - */ + /* Custom Outputs define custom graph outputs, this allows to send signals to the parent graph while executing this graph. + * Sub Graph node using this Flow Asset will generate context Output Pin for every valid Event name on this list. */ UPROPERTY(EditAnywhere, Category = "Sub Graph") TArray CustomOutputs; +#endif + +public: + /* Gathers all the nodes that are connected to the Start & Custom Inputs of the flow graph. */ + TArray GatherNodesConnectedToAllInputs() const; + + UFlowNode_CustomInput* TryFindCustomInputNodeByEventName(const FName& EventName) const; + UFlowNode_CustomOutput* TryFindCustomOutputNodeByEventName(const FName& EventName) const; + + TArray GatherCustomInputNodeEventNames() const; + TArray GatherCustomOutputNodeEventNames() const; -public: #if WITH_EDITOR - FFlowAssetEvent OnSubGraphReconstructionRequested; + const TArray& GetCustomInputs() const { return CustomInputs; } + const TArray& GetCustomOutputs() const { return CustomOutputs; } - UFlowNode* CreateNode(const UClass* NodeClass, UEdGraphNode* GraphNode); +protected: + void AddCustomInput(const FName& EventName); + void RemoveCustomInput(const FName& EventName); - void RegisterNode(const FGuid& NewGuid, UFlowNode* NewNode); - void UnregisterNode(const FGuid& NodeGuid); + void AddCustomOutput(const FName& EventName); + void RemoveCustomOutput(const FName& EventName); +#endif + +////////////////////////////////////////////////////////////////////////// +// Pin connections - // Processes all nodes and creates map of all pin connections - void HarvestNodeConnections(); +protected: + /* Policy for UFlowGraphSchema (and others) to use to enforce pin connectivity. + * Also used at runtime by predicates (e.g., CompareValues) for type classification queries. */ + UPROPERTY(VisibleAnywhere, AdvancedDisplay, Category = PinConnection) + TInstancedStruct PinConnectionPolicy; + +public: +#if WITH_EDITOR + /* Override these functions to set up unique policy(ies) for a UFlowAsset subclass */ + virtual void InitializePinConnectionPolicy(); #endif - UFlowNode* GetNode(const FGuid& Guid) const; - TMap GetNodes() const { return Nodes; } + const FFlowPinConnectionPolicy& GetPinConnectionPolicy() const; + + /* Return all other Pins connected to the passed Pin. */ + TArray GatherPinsConnectedToPin(const FConnectedPin& Pin) const; + +////////////////////////////////////////////////////////////////////////// +// FlowAssetParams support (Start node params for a Flow graph) + + /* Default parameters asset for this Flow Asset (optional). */ + UPROPERTY(EditAnywhere, Category = FlowAssetParams, meta = (ShowCreateNew, HideChildParams)) + FFlowAssetParamsPtr BaseAssetParams; + +#if WITH_EDITOR + /* Generates a new params asset from the Start node. */ + UFlowAssetParams* GenerateParamsFromStartNode(); - TArray GetCustomInputs() const { return CustomInputs; } - TArray GetCustomOutputs() const { return CustomOutputs; } + /* Generates the FlowAssetParams name for the 'base' (root) asset, used when creating the params asset. */ + virtual FString GenerateParamsAssetName() const; + +protected: + + void ReconcileBaseAssetParams(const FDateTime& AssetLastSavedTimestamp); +#endif ////////////////////////////////////////////////////////////////////////// // Instances of the template asset private: - // Original object holds references to instances + /* Original object holds references to instances. */ UPROPERTY(Transient) - TArray ActiveInstances; + TArray> ActiveInstances; #if WITH_EDITORONLY_DATA - TWeakObjectPtr InspectedInstance; + TWeakObjectPtr InspectedInstance; + + /* Message log for storing runtime errors/notes/warnings that will only last until the next game run. + * Log lives in the asset template, so it can be inspected after ending the PIE. */ + TSharedPtr RuntimeLog; #endif public: void AddInstance(UFlowAsset* Instance); int32 RemoveInstance(UFlowAsset* Instance); + TConstArrayView> GetActiveInstances() const { return ActiveInstances; } void ClearInstances(); int32 GetInstancesNum() const { return ActiveInstances.Num(); } #if WITH_EDITOR - void GetInstanceDisplayNames(TArray>& OutDisplayNames) const; - - void SetInspectedInstance(const FName& NewInspectedInstanceName); - UFlowAsset* GetInspectedInstance() const { return InspectedInstance.IsValid() ? InspectedInstance.Get() : nullptr; } + void SetInspectedInstance(TWeakObjectPtr NewInspectedInstance); + const UFlowAsset* GetInspectedInstance() const { return InspectedInstance.IsValid() ? InspectedInstance.Get() : nullptr; } DECLARE_EVENT(UFlowAsset, FRefreshDebuggerEvent); + FRefreshDebuggerEvent& OnDebuggerRefresh() { return RefreshDebuggerEvent; } FRefreshDebuggerEvent RefreshDebuggerEvent; + DECLARE_EVENT_TwoParams(UFlowAsset, FRuntimeMessageEvent, const UFlowAsset*, const TSharedRef&); + + FRuntimeMessageEvent& OnRuntimeMessageAdded() { return RuntimeMessageEvent; } + FRuntimeMessageEvent RuntimeMessageEvent; + private: - void BroadcastDebuggerRefresh() const { RefreshDebuggerEvent.Broadcast(); } + void BroadcastDebuggerRefresh() const; + void BroadcastRuntimeMessageAdded(const TSharedRef& Message) const; #endif ////////////////////////////////////////////////////////////////////////// // Executing asset instance -private: +protected: UPROPERTY() - UFlowAsset* TemplateAsset; + TObjectPtr TemplateAsset; - // Object that spawned Root Flow instance, i.e. World Settings or Player Controller - // This pointer is passed to child instances: Flow Asset instances created by the SubGraph nodes + /* Object that spawned Root Flow instance, i.e. World Settings or Player Controller. + * This pointer is passed to child instances: Flow Asset instances created by the SubGraph nodes. */ TWeakObjectPtr Owner; - // SubGraph node that created this Flow Asset instance + /* SubGraph node that created this Flow Asset instance. */ TWeakObjectPtr NodeOwningThisAssetInstance; - // Flow Asset instances created by SubGraph nodes placed in the current graph + /* Flow Asset instances created by SubGraph nodes placed in the current graph. */ TMap, TWeakObjectPtr> ActiveSubGraphs; - // Execution of the graph always starts from this node, there can be only one StartNode in the graph + /* Optional entry points to the graph, similar to blueprint Custom Events. + * Contains nodes only if it is initialized instance (see InitializeInstance, IsInstanceInitialized), empty otherwise. */ UPROPERTY() - UFlowNode_Start* StartNode; + TSet> CustomInputNodes; - // Optional entry points to the graph, similar to blueprint Custom Events + /* Nodes that have any work left, not marked as Finished yet. */ UPROPERTY() - TSet CustomInputNodes; + TArray> ActiveNodes; + /* All nodes active in the past, done their work. */ UPROPERTY() - TSet PreloadedNodes; - - // Nodes that have any work left, not marked as Finished yet - UPROPERTY() - TArray ActiveNodes; - - // All nodes active in the past, done their work - UPROPERTY() - TArray RecordedNodes; + TArray> RecordedNodes; + UPROPERTY(Transient) EFlowFinishPolicy FinishPolicy; public: - void InitializeInstance(const TWeakObjectPtr InOwner, UFlowAsset* InTemplateAsset); + virtual void InitializeInstance(const TWeakObjectPtr InOwner, UFlowAsset& InTemplateAsset); + virtual void DeinitializeInstance(); + bool IsInstanceInitialized() const { return IsValid(TemplateAsset); } UFlowAsset* GetTemplateAsset() const { return TemplateAsset; } - - // Object that spawned Root Flow instance, i.e. World Settings or Player Controller - // This pointer is passed to child instances: Flow Asset instances created by the SubGraph nodes + + /* Object that spawned Root Flow instance, i.e. World Settings or Player Controller. + * This pointer is passed to child instances: Flow Asset instances created by the SubGraph nodes. */ UFUNCTION(BlueprintPure, Category = "Flow") UObject* GetOwner() const { return Owner.Get(); } template - TWeakObjectPtr GetOwner() const + TWeakObjectPtr GetOwner() const { return Owner.IsValid() ? Cast(Owner) : nullptr; } - virtual void PreloadNodes(); + /* Returns the Owner as an Actor, or if Owner is a Component, return its Owner as an Actor. */ + UFUNCTION(BlueprintPure, Category = "Flow") + AActor* TryFindActorOwner() const; virtual void PreStartFlow(); - virtual void StartFlow(); - - virtual void FinishFlow(const EFlowFinishPolicy InFinishPolicy); - - // Get Flow Asset instance created by the given SubGraph node - TWeakObjectPtr GetFlowInstance(UFlowNode_SubGraph* SubGraphNode) const; + virtual void StartFlow(IFlowDataPinValueSupplierInterface* DataPinValueSupplier = nullptr); + bool HasStartedFlow() const; -private: - void TriggerCustomEvent(UFlowNode_SubGraph* Node, const FName& EventName) const; - void TriggerCustomOutput(const FName& EventName) const; - - void TriggerInput(const FGuid& NodeGuid, const FName& PinName); - - void FinishNode(UFlowNode* Node); +protected: + virtual void FinishNode(UFlowNode* Node); void ResetNodes(); + +public: + virtual void FinishFlow(const EFlowFinishPolicy InFinishPolicy, const bool bRemoveInstance = true); public: UFlowSubsystem* GetFlowSubsystem() const; - FName GetDisplayName() const; UFlowNode_SubGraph* GetNodeOwningThisAssetInstance() const; - UFlowAsset* GetMasterInstance() const; + UFlowAsset* GetParentInstance() const; + + /* Get Flow Asset instance created by the given SubGraph node. */ + TWeakObjectPtr GetFlowInstance(UFlowNode_SubGraph* SubGraphNode) const; - // Are there any active nodes? + /* Are there any active nodes? */ UFUNCTION(BlueprintPure, Category = "Flow") bool IsActive() const { return ActiveNodes.Num() > 0; } - // Returns nodes that have any work left, not marked as Finished yet + /* Returns nodes that have any work left, not marked as Finished yet. */ UFUNCTION(BlueprintPure, Category = "Flow") - TArray GetActiveNodes() const { return ActiveNodes; } + const TArray& GetActiveNodes() const { return ActiveNodes; } - // Returns nodes active in the past, done their work + /* Returns nodes active in the past, done their work. */ UFUNCTION(BlueprintPure, Category = "Flow") - TArray GetRecordedNodes() const { return RecordedNodes; } + const TArray& GetRecordedNodes() const { return RecordedNodes; } ////////////////////////////////////////////////////////////////////////// -// SaveGame +// Preload policy + +protected: + /* Policy controlling when nodes implementing IFlowPreloadableInterface preload and flush their content. + * Initialized from UFlowSettings defaults. Override InitializePreloadPolicy() in a subclass to set a unique policy. */ + UPROPERTY(VisibleAnywhere, AdvancedDisplay, Category = Preload) + TInstancedStruct PreloadPolicy; + + /* Override these functions to set up unique policy(ies) for a UFlowAsset subclass. */ + virtual void InitializePreloadPolicy(); + +public: + const FFlowPreloadPolicy& GetPreloadPolicy() const; + +////////////////////////////////////////////////////////////////////////// +// Trigger Input + +#if !UE_BUILD_SHIPPING +public: + FFlowSignalEvent OnPinTriggered; +#endif + +protected: + /* Stack of active deferred transition scopes (innermost = top). + * Stored as TSharedPtr so callers can safely cache a reference to a specific scope + * without it being invalidated by array reallocations/resizes during nested triggers. */ + TArray> DeferredTransitionScopes; + +public: + void TriggerCustomInput(const FName& EventName, IFlowDataPinValueSupplierInterface* DataPinValueSupplier = nullptr); + + void TriggerCustomInput_FromSubGraph(UFlowNode_SubGraph* Node, const FName& EventName) const; + void TriggerCustomOutput(const FName& EventName); + + /* todo: Extend FromPin through to Node level Trigger functions. */ + virtual void TriggerInput(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin); + +protected: + /* Trigger the node directly (no deferral, no new scope). */ + void TriggerInputDirect(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin); + + /* Allow subclasses to disable the standard defer trigger mechanism */ + virtual bool ShouldDeferTriggers() const; + +protected: + void EnqueueDeferredTrigger(const FGuid& NodeGuid, const FName& PinName, const FConnectedPin& FromPin); + TSharedPtr PushDeferredTransitionScope(); + void PopDeferredTransitionScope(const TSharedPtr& Scope); + + bool TryFlushAndRemoveDeferredTransitionScope(const TSharedPtr& Scope); + +public: + /* Try to flush (and clear) all Deferred Trigger scopes. + * Can fail to flush all if a FFlowExecutionGate causes a new halt. */ + bool TryFlushAllDeferredTriggerScopes(); + + /* Clear (do not trigger) any remaining deferred transitions (for shutdown cases). */ + void ClearAllDeferredTriggerScopes(); +protected: + void CancelAndWarnForUnflushedDeferredTriggers(); + + /* Returns a shared pointer to the current top (innermost) deferred transition scope, + * or nullptr if there is no active scope. Safe to cache and use later. */ + TSharedPtr GetTopDeferredTransitionScope() const; + +////////////////////////////////////////////////////////////////////////// +// Expected Owner Class support + +protected: + /* Expects to be owned (at runtime) by an object with this class (or one of its subclasses). + * If the class is an AActor, and the Flow Asset is owned by a component, it will consider the component's owner for the AActor. */ + UPROPERTY(EditAnywhere, Category = "Flow") + TSubclassOf ExpectedOwnerClass; + +public: + UClass* GetExpectedOwnerClass() const { return ExpectedOwnerClass; } + +////////////////////////////////////////////////////////////////////////// +// SaveGame support + +public: UFUNCTION(BlueprintCallable, Category = "SaveGame") FFlowAssetSaveData SaveInstance(TArray& SavedFlowInstances); UFUNCTION(BlueprintCallable, Category = "SaveGame") void LoadInstance(const FFlowAssetSaveData& AssetRecord); -private: - void OnActivationStateLoaded(UFlowNode* Node); - protected: + virtual void OnActivationStateLoaded(UFlowNode* Node); + UFUNCTION(BlueprintNativeEvent, Category = "SaveGame") void OnSave(); - + UFUNCTION(BlueprintNativeEvent, Category = "SaveGame") void OnLoad(); -public: +public: UFUNCTION(BlueprintNativeEvent, Category = "SaveGame") - bool IsBoundToWorld(); -}; + bool IsBoundToWorld() const; + +////////////////////////////////////////////////////////////////////////// +// Utils + +#if WITH_EDITOR +public: + void LogError(const FString& MessageToLog, const UFlowNodeBase* Node) const; + void LogWarning(const FString& MessageToLog, const UFlowNodeBase* Node) const; + void LogNote(const FString& MessageToLog, const UFlowNodeBase* Node) const; + +private: + /* Shared implementation for LogError/LogWarning/LogNote to avoid code duplication. */ + void LogRuntimeMessage(EMessageSeverity::Type Severity, const FString& MessageToLog, const UFlowNodeBase* Node) const; +#endif +}; \ No newline at end of file diff --git a/Source/Flow/Public/FlowComponent.h b/Source/Flow/Public/FlowComponent.h index abc83704a..bdd6b0b86 100644 --- a/Source/Flow/Public/FlowComponent.h +++ b/Source/Flow/Public/FlowComponent.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Components/ActorComponent.h" @@ -7,6 +6,8 @@ #include "FlowSave.h" #include "FlowTypes.h" +#include "Interfaces/FlowAssetProviderInterface.h" +#include "Asset/FlowAssetParamsTypes.h" #include "FlowComponent.generated.h" class UFlowAsset; @@ -41,7 +42,7 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FFlowComponentDynamicNotify, class * Base component of Flow System - makes possible to communicate between Actor, Flow Subsystem and Flow Graphs */ UCLASS(Blueprintable, meta = (BlueprintSpawnableComponent)) -class FLOW_API UFlowComponent : public UActorComponent +class FLOW_API UFlowComponent : public UActorComponent, public IFlowAssetProviderInterface { GENERATED_UCLASS_BODY() @@ -52,18 +53,9 @@ class FLOW_API UFlowComponent : public UActorComponent ////////////////////////////////////////////////////////////////////////// // Identity Tags - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Flow") + UPROPERTY(EditAnywhere, BlueprintReadOnly, ReplicatedUsing = OnRep_IdentityTags, Category = "Flow") FGameplayTagContainer IdentityTags; -private: - // Used to replicate tags added during gameplay - UPROPERTY(ReplicatedUsing = OnRep_AddedIdentityTags) - FGameplayTagContainer AddedIdentityTags; - - // Used to replicate tags removed during gameplay - UPROPERTY(ReplicatedUsing = OnRep_RemovedIdentityTags) - FGameplayTagContainer RemovedIdentityTags; - public: virtual void BeginPlay() override; virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; @@ -80,12 +72,14 @@ class FLOW_API UFlowComponent : public UActorComponent UFUNCTION(BlueprintCallable, Category = "Flow") void RemoveIdentityTags(FGameplayTagContainer Tags, const EFlowNetMode NetMode = EFlowNetMode::Authority); -private: - UFUNCTION() - void OnRep_AddedIdentityTags(); +protected: + void RegisterWithFlowSubsystem(); + void UnregisterWithFlowSubsystem(); + virtual void BeginRootFlow(bool bComponentLoadedFromSaveGame); +private: UFUNCTION() - void OnRep_RemovedIdentityTags(); + void OnRep_IdentityTags(const FGameplayTagContainer& PreviousTags); public: UPROPERTY(BlueprintAssignable, Category = "Flow") @@ -104,20 +98,20 @@ class FLOW_API UFlowComponent : public UActorComponent // Component sending Notify Tags to Flow Graph, or any other listener private: - // Stores only recently sent tags + /* Stores only recently sent tags. */ UPROPERTY(ReplicatedUsing = OnRep_SentNotifyTags) FGameplayTagContainer RecentlySentNotifyTags; public: - FGameplayTagContainer GetRecentlySentNotifyTags() const { return RecentlySentNotifyTags; } + const FGameplayTagContainer& GetRecentlySentNotifyTags() const { return RecentlySentNotifyTags; } - // Send single notification from the actor to Flow graphs - // If set on server, it always going to be replicated to clients + /* Send single notification from the actor to Flow graphs. + * If set on server, it's always going to be replicated to clients. */ UFUNCTION(BlueprintCallable, Category = "Flow") void NotifyGraph(const FGameplayTag NotifyTag, const EFlowNetMode NetMode = EFlowNetMode::Authority); - // Send multiple notifications at once - from the actor to Flow graphs - // If set on server, it always going to be replicated to clients + /* Send multiple notifications at once - from the actor to Flow graphs. + * If set on server, it's always going to be replicated to clients. */ UFUNCTION(BlueprintCallable, Category = "Flow") void BulkNotifyGraph(const FGameplayTagContainer NotifyTags, const EFlowNetMode NetMode = EFlowNetMode::Authority); @@ -132,11 +126,12 @@ class FLOW_API UFlowComponent : public UActorComponent // Component receiving Notify Tags from Flow Graph private: - // Stores only recently replicated tags + /* Stores only recently replicated tags. */ UPROPERTY(ReplicatedUsing = OnRep_NotifyTagsFromGraph) FGameplayTagContainer NotifyTagsFromGraph; public: + UFUNCTION(BlueprintCallable, Category = "Flow") virtual void NotifyFromGraph(const FGameplayTagContainer& NotifyTags, const EFlowNetMode NetMode = EFlowNetMode::Authority); private: @@ -144,7 +139,7 @@ class FLOW_API UFlowComponent : public UActorComponent void OnRep_NotifyTagsFromGraph(); public: - // Receive notification from Flow graph or another Flow Component + /* Receive notification from Flow graph or another Flow Component. */ UPROPERTY(BlueprintAssignable, Category = "Flow") FFlowComponentDynamicNotify ReceiveNotify; @@ -152,12 +147,12 @@ class FLOW_API UFlowComponent : public UActorComponent // Sending Notify Tags between Flow components private: - // Stores only recently replicated tags + /* Stores only recently replicated tags. */ UPROPERTY(ReplicatedUsing = OnRep_NotifyTagsFromAnotherComponent) TArray NotifyTagsFromAnotherComponent; public: - // Send notification to another actor containing Flow Component + /* Send notification to another actor containing Flow Component. */ UFUNCTION(BlueprintCallable, Category = "Flow") virtual void NotifyActor(const FGameplayTag ActorTag, const FGameplayTag NotifyTag, const EFlowNetMode NetMode = EFlowNetMode::Authority); @@ -169,33 +164,37 @@ class FLOW_API UFlowComponent : public UActorComponent // Root Flow public: - // Asset that might instantiated as "Root Flow" + /* Asset that might be instantiated as "Root Flow". */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RootFlow") - UFlowAsset* RootFlow; + TObjectPtr RootFlow; - // If true, component will start Root Flow on Begin Play + /* Flow Asset Params to use as the data pin value supplier for the Root Flow.*/ + UPROPERTY(EditAnywhere, Category = "RootFlow") + FFlowAssetParamsPtr RootFlowParams; + + /* If true, component will start Root Flow on Begin Play. */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "RootFlow") bool bAutoStartRootFlow; - // Networking mode for creating this Root Flow + /* Networking mode for creating this Root Flow. */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "RootFlow") EFlowNetMode RootFlowMode; - // If false, another Root Flow instance won't be created from this component, if this Flow Asset is already instantiated + /* If false, another Root Flow instance won't be created from this component, if this Flow Asset is already instantiated. */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "RootFlow") bool bAllowMultipleInstances; UPROPERTY(SaveGame) FString SavedAssetInstanceName; - - // This will instantiate Flow Asset assigned on this component. - // Created Flow Asset instance will be a "root flow", as additional Flow Assets can be instantiated via Sub Graph node + + /* This will instantiate Flow Asset assigned on this component. + * Created Flow Asset instance will be a "root flow", as additional Flow Assets can be instantiated via Sub Graph node. */ UFUNCTION(BlueprintCallable, Category = "RootFlow") - void StartRootFlow(); + virtual void StartRootFlow(); // This will destroy instantiated Flow Asset - created from asset assigned on this component. UFUNCTION(BlueprintCallable, Category = "RootFlow") - void FinishRootFlow(UFlowAsset* TemplateAsset, const EFlowFinishPolicy FinishPolicy); + virtual void FinishRootFlow(UFlowAsset* TemplateAsset, const EFlowFinishPolicy FinishPolicy); UFUNCTION(BlueprintPure, Category = "FlowSubsystem") TSet GetRootInstances(const UObject* Owner) const; @@ -203,9 +202,34 @@ class FLOW_API UFlowComponent : public UActorComponent UFUNCTION(BlueprintPure, Category = "RootFlow", meta = (DeprecatedFunction, DeprecationMessage="Use GetRootInstances() instead.")) UFlowAsset* GetRootFlowInstance() const; + // IFlowAssetProviderInterface + virtual UFlowAsset* ProvideFlowAsset() const override { return RootFlow; } + // -- + +////////////////////////////////////////////////////////////////////////// +// Custom Input and Output events + +public: + /* This will trigger a specific CustomInput on this component's root flow. */ + UFUNCTION(BlueprintCallable, Category = "RootFlow") + void TriggerRootFlowCustomInput(const FName& EventName) const; + + /* Called when a Root flow asset triggers a CustomOutput. */ + UFUNCTION(BlueprintImplementableEvent, DisplayName = "OnRootFlowCustomEvent") + void BP_OnRootFlowCustomEvent(UFlowAsset* RootFlowInstance, const FName& EventName); + + virtual void OnRootFlowCustomEvent(UFlowAsset* RootFlowInstance, const FName& EventName) {} + + // UFlowAsset-only access + void DispatchRootFlowCustomEvent(UFlowAsset* RootFlowInstance, const FName& EventName); + // --- + ////////////////////////////////////////////////////////////////////////// // SaveGame +public: + virtual bool CanSave() const { return true; } + UFUNCTION(BlueprintCallable, Category = "SaveGame") virtual void SaveRootFlow(TArray& SavedFlowInstances); @@ -216,7 +240,7 @@ class FLOW_API UFlowComponent : public UActorComponent FFlowComponentSaveData SaveInstance(); UFUNCTION(BlueprintCallable, Category = "SaveGame") - bool LoadInstance(); + bool LoadInstance(const UFlowSubsystem* FlowSubsystem); protected: UFUNCTION(BlueprintNativeEvent, Category = "SaveGame") @@ -231,4 +255,4 @@ class FLOW_API UFlowComponent : public UActorComponent public: UFlowSubsystem* GetFlowSubsystem() const; bool IsFlowNetMode(const EFlowNetMode NetMode) const; -}; +}; \ No newline at end of file diff --git a/Source/Flow/Public/FlowExecutableActorComponent.h b/Source/Flow/Public/FlowExecutableActorComponent.h new file mode 100644 index 000000000..fa71c56a7 --- /dev/null +++ b/Source/Flow/Public/FlowExecutableActorComponent.h @@ -0,0 +1,60 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Components/ActorComponent.h" +#include "Interfaces/FlowContextPinSupplierInterface.h" +#include "Interfaces/FlowCoreExecutableInterface.h" +#include "Interfaces/FlowDataPinValueOwnerInterface.h" +#include "Interfaces/FlowExternalExecutableInterface.h" + +#include "FlowExecutableActorComponent.generated.h" + +/** + * A base class for blueprint components that are expected to be executed from an ExecuteComponent flow node. + * Provides the support for FFlowDataPinValue subclasses, so that blueprint components (that derive from this) + * can have their pins be automatically discovered and supplied. + */ +UCLASS(Abstract, Blueprintable, EditInlineNew, DisplayName = "Flow Executable Actor Component", hidecategories = (Tags, Activation, Cooking, AssetUserData, Navigation)) +class FLOW_API UFlowExecutableActorComponent + : public UActorComponent + , public IFlowContextPinSupplierInterface + , public IFlowCoreExecutableInterface + , public IFlowDataPinValueOwnerInterface + , public IFlowExternalExecutableInterface +{ + GENERATED_BODY() + +private: + FSimpleDelegate FlowDataPinValuesRebuildDelegate; + +protected: + /* FlowNodeBase that will execute this component in the FlowGraph on our behalf. */ + UPROPERTY(Transient, BlueprintReadOnly, Category = DataPins) + TObjectPtr FlowNodeProxy; + +public: + + // IFlowContextPinSupplierInterface + virtual bool K2_SupportsContextPins_Implementation() const override { return true; } + // -- + +#if WITH_EDITOR + + // IFlowDataPinValueOwnerInterface + virtual bool CanModifyFlowDataPinType() const override; + virtual bool ShowFlowDataPinValueInputPinCheckbox() const override; + virtual bool ShowFlowDataPinValueClassFilter(const FFlowDataPinValue* Value) const override; + virtual bool CanEditFlowDataPinValueClassFilter(const FFlowDataPinValue* Value) const override; + virtual void SetFlowDataPinValuesRebuildDelegate(FSimpleDelegate InDelegate) override; + virtual void RequestFlowDataPinValuesDetailsRebuild() override; + // -- +#endif //WITH_EDITOR + + // IFlowExternalExecutableInterface + virtual void PreActivateExternalFlowExecutable(UFlowNodeBase& FlowNodeBase) override; + // -- + +protected: + + FORCEINLINE bool IsDefaultObject() const { return HasAnyFlags(RF_ClassDefaultObject); } +}; \ No newline at end of file diff --git a/Source/Flow/Public/FlowLogChannels.h b/Source/Flow/Public/FlowLogChannels.h new file mode 100644 index 000000000..68c554028 --- /dev/null +++ b/Source/Flow/Public/FlowLogChannels.h @@ -0,0 +1,6 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Logging/LogMacros.h" + +FLOW_API DECLARE_LOG_CATEGORY_EXTERN(LogFlow, Log, All); diff --git a/Source/Flow/Public/FlowMessageLog.h b/Source/Flow/Public/FlowMessageLog.h new file mode 100644 index 000000000..07ff9e9f1 --- /dev/null +++ b/Source/Flow/Public/FlowMessageLog.h @@ -0,0 +1,100 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#if WITH_EDITOR +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "Logging/TokenizedMessage.h" +#include "Misc/UObjectToken.h" + +class UFlowAsset; +class UFlowNodeBase; + +/** + * Message Log token that links to an element in Flow Graph + */ +class FLOW_API FFlowGraphToken : public IMessageToken +{ +private: + const TWeakObjectPtr GraphNode; + const FEdGraphPinReference GraphPin; + + explicit FFlowGraphToken(const UFlowAsset* InFlowAsset); + explicit FFlowGraphToken(const UFlowNodeBase* InFlowNodeBase); + explicit FFlowGraphToken(const UEdGraphNode* InGraphNode, const UEdGraphPin* InPin); + +public: + /** Factory method, tokens can only be constructed as shared refs */ + static TSharedPtr Create(const UFlowAsset* InFlowAsset, FTokenizedMessage& Message); + static TSharedPtr Create(const UFlowNodeBase* InFlowNodeBase, FTokenizedMessage& Message); + static TSharedPtr Create(const UEdGraphNode* InGraphNode, FTokenizedMessage& Message); + static TSharedPtr Create(const UEdGraphPin* InPin, FTokenizedMessage& Message); + + const UEdGraphNode* GetGraphNode() const { return GraphNode.Get(); } + const UEdGraphPin* GetPin() const { return GraphPin.Get(); } + + // IMessageToken + virtual EMessageToken::Type GetType() const override + { + return EMessageToken::EdGraph; + } +}; + +/** + * List of Message Log lines + */ +class FLOW_API FFlowMessageLog +{ +public: + static const FName LogName; + TArray> Messages; + +public: + FFlowMessageLog() + { + } + + template + TSharedRef Error(const TCHAR* Format, T* Object) + { + TSharedRef Message = FTokenizedMessage::Create(EMessageSeverity::Error); + AddMessage(NAME_None, Format, Message, Object); + return Message; + } + + template + TSharedRef Warning(const TCHAR* Format, T* Object) + { + TSharedRef Message = FTokenizedMessage::Create(EMessageSeverity::Warning); + AddMessage(NAME_None, Format, Message, Object); + return Message; + } + + template + TSharedRef Note(const TCHAR* Format, T* Object) + { + TSharedRef Message = FTokenizedMessage::Create(EMessageSeverity::Info); + AddMessage(NAME_None, Format, Message, Object); + return Message; + } + +protected: + template + void AddMessage(const FName MessageID, const TCHAR* Format, TSharedRef& Message, T* Object) + { + Message->SetIdentifier(MessageID); + + if (Object) + { + if (const TSharedPtr Token = FFlowGraphToken::Create(Object, Message.Get())) + { + Message->SetMessageLink(FUObjectToken::Create(Object)); + } + } + + Message.Get().AddToken(FTextToken::Create(FText::FromString(Format))); + Messages.Add(Message); + } +}; + +#endif // WITH_EDITOR diff --git a/Source/Flow/Public/FlowModule.h b/Source/Flow/Public/FlowModule.h index 9244e2be4..b3ebd81e1 100644 --- a/Source/Flow/Public/FlowModule.h +++ b/Source/Flow/Public/FlowModule.h @@ -1,12 +1,8 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once -#include "Logging/LogMacros.h" #include "Modules/ModuleInterface.h" -DECLARE_LOG_CATEGORY_EXTERN(LogFlow, Log, All) - class FFlowModule final : public IModuleInterface { public: diff --git a/Source/Flow/Public/FlowPinSubsystem.h b/Source/Flow/Public/FlowPinSubsystem.h new file mode 100644 index 000000000..fcaffa4b1 --- /dev/null +++ b/Source/Flow/Public/FlowPinSubsystem.h @@ -0,0 +1,61 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Subsystems/EngineSubsystem.h" +#include "StructUtils/InstancedStruct.h" +#include "Templates/UnrealTypeTraits.h" + +#include "Types/FlowPinType.h" +#include "Types/FlowPinTypeName.h" +#include "FlowPinSubsystem.generated.h" + +UCLASS(MinimalApi) +class UFlowPinSubsystem : public UEngineSubsystem +{ + GENERATED_BODY() + +protected: + UPROPERTY(Transient) + TMap> PinTypes; + +public: + FLOW_API static UFlowPinSubsystem* Get(); + + virtual bool ShouldCreateSubsystem(UObject* Outer) const override; + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + + template + void RegisterPinType() + { + TInstancedStruct PinType; + PinType.InitializeAs(); + RegisterPinType(TPinType::GetPinTypeNameStatic(), PinType); + } + FLOW_API void RegisterPinType(const FFlowPinTypeName& TypeName, const TInstancedStruct& PinType); + + template + void UnregisterPinType() + { + UnregisterPinType(TPinType::GetPinTypeNameStatic()); + } + FLOW_API void UnregisterPinType(const FFlowPinTypeName& TypeName); + + template + const TPinType* FindPinType(const FFlowPinTypeName& TypeName) const + { + static_assert(TIsDerivedFrom::IsDerived, "TPinType must be derived from FFlowPinType"); + + if (const TInstancedStruct* Found = PinTypes.Find(TypeName)) + { + return Found->GetPtr(); + } + + return nullptr; + } + + FLOW_API TArray GetPinTypeNames() const; + +protected: + void UnregisterAllPinTypes(); +}; \ No newline at end of file diff --git a/Source/Flow/Public/FlowSave.h b/Source/Flow/Public/FlowSave.h index fc9e939ef..b5ea9e30a 100644 --- a/Source/Flow/Public/FlowSave.h +++ b/Source/Flow/Public/FlowSave.h @@ -1,10 +1,7 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once -#include "CoreMinimal.h" #include "GameFramework/SaveGame.h" -#include "Serialization/BufferArchive.h" #include "Serialization/ObjectAndNameAsStringProxyArchive.h" #include "FlowSave.generated.h" @@ -70,7 +67,7 @@ struct FLOW_API FFlowComponentSaveData struct FLOW_API FFlowArchive : public FObjectAndNameAsStringProxyArchive { - FFlowArchive(FArchive& InInnerArchive) : FObjectAndNameAsStringProxyArchive(InInnerArchive, true) + explicit FFlowArchive(FArchive& InInnerArchive) : FObjectAndNameAsStringProxyArchive(InInnerArchive, true) { ArIsSaveGame = true; } diff --git a/Source/Flow/Public/FlowSettings.h b/Source/Flow/Public/FlowSettings.h index 475c11c09..8d3d0a0d9 100644 --- a/Source/Flow/Public/FlowSettings.h +++ b/Source/Flow/Public/FlowSettings.h @@ -1,32 +1,80 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Engine/DeveloperSettings.h" -#include "Templates/SubclassOf.h" +#include "StructUtils/InstancedStruct.h" +#include "UObject/SoftObjectPath.h" #include "FlowSettings.generated.h" -class UFlowNode; +struct FFlowPinConnectionPolicy; +struct FFlowPreloadPolicy; /** - * + * Mostly runtime settings of the Flow Graph. */ UCLASS(Config = Game, defaultconfig, meta = (DisplayName = "Flow")) -class UFlowSettings final : public UDeveloperSettings +class FLOW_API UFlowSettings : public UDeveloperSettings { GENERATED_UCLASS_BODY() - static UFlowSettings* Get() { return CastChecked(UFlowSettings::StaticClass()->GetDefaultObject()); } +public: + /* 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") + bool bDeferTriggeredOutputsWhileTriggering; + + /* If enabled, runtime logs will be added when a flow node signal mode is set to Disabled. */ + UPROPERTY(Config, EditAnywhere, Category = "Flow") + bool bLogOnSignalDisabled; - // Set if to False, if you don't want to create client-side Flow Graphs - // And you don't access to the Flow Component registry on clients + /* If enabled, runtime logs will be added when a flow node signal mode is set to Pass-through. */ + UPROPERTY(Config, EditAnywhere, Category = "Flow") + bool bLogOnSignalPassthrough; + + /* Set if to False, if you don't want to create client-side Flow Graphs. + * And you don't access to the Flow Component registry on clients. */ UPROPERTY(Config, EditAnywhere, Category = "Networking") bool bCreateFlowSubsystemOnClients; - // How many nodes of given class should be preloaded with the Flow Asset instance? - UPROPERTY(Config, EditAnywhere, Category = "Preload") - TMap, int32> DefaultPreloadDepth; - + /* Adjust the Titles for FlowNodes to be more expressive than default + * by incorporating data that would otherwise go in the Description. */ + UPROPERTY(EditAnywhere, config, Category = "Nodes") + bool bUseAdaptiveNodeTitles; + +#if WITH_EDITOR + DECLARE_DELEGATE(FFlowSettingsEvent); + FFlowSettingsEvent OnAdaptiveNodeTitlesChanged; +#endif + + /* Default class to use as a FlowAsset's "ExpectedOwnerClass". */ + UPROPERTY(EditAnywhere, Config, Category = "Nodes") + FSoftClassPath DefaultExpectedOwnerClass; + UPROPERTY(Config, EditAnywhere, Category = "SaveSystem") bool bWarnAboutMissingIdentityTags; + +public: + UClass* GetDefaultExpectedOwnerClass() const; + +#if WITH_EDITORONLY_DATA + virtual FName GetCategoryName() const override { return FName("Flow Graph"); } + virtual FText GetSectionText() const override { return INVTEXT("Settings"); } +#endif }; diff --git a/Source/Flow/Public/FlowSubsystem.h b/Source/Flow/Public/FlowSubsystem.h index 3315609dd..ea68d8921 100644 --- a/Source/Flow/Public/FlowSubsystem.h +++ b/Source/Flow/Public/FlowSubsystem.h @@ -1,8 +1,6 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once -#include "Engine/StreamableManager.h" #include "GameFramework/Actor.h" #include "GameplayTagContainer.h" #include "Subsystems/GameInstanceSubsystem.h" @@ -10,13 +8,14 @@ #include "FlowComponent.h" #include "FlowSubsystem.generated.h" -class UFlowAsset; -class UFlowNode_SubGraph; +class IFlowDataPinValueSupplierInterface; DECLARE_DYNAMIC_MULTICAST_DELEGATE(FSimpleFlowEvent); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSimpleFlowComponentEvent, UFlowComponent*, Component); DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FTaggedFlowComponentEvent, UFlowComponent*, Component, const FGameplayTagContainer&, Tags); +DECLARE_DELEGATE_OneParam(FNativeFlowAssetEvent, class UFlowAsset*); + /** * Flow Subsystem * - manages lifetime of Flow Graphs @@ -31,42 +30,49 @@ class FLOW_API UFlowSubsystem : public UGameInstanceSubsystem public: UFlowSubsystem(); -private: friend class UFlowAsset; friend class UFlowComponent; friend class UFlowNode_SubGraph; + virtual bool ShouldCreateSubsystem(UObject* Outer) const override; + virtual UWorld* GetWorld() const override; + + virtual void Deinitialize() override; + +////////////////////////////////////////////////////////////////////////// +// Lifetime cycle of Flow Asset instances + +protected: /* All asset templates with active instances */ UPROPERTY() - TArray InstancedTemplates; + TArray> InstancedTemplates; /* Assets instanced by object from another system, i.e. World Settings or Player Controller */ UPROPERTY() - TMap> RootInstances; + TMap, TWeakObjectPtr> RootInstances; /* Assets instanced by Sub Graph nodes */ UPROPERTY() - TMap InstancedSubFlows; - - FStreamableManager Streamable; - -protected: - UPROPERTY() - UFlowSaveGame* LoadedSaveGame; + TMap, TObjectPtr> InstancedSubFlows; +#if !UE_BUILD_SHIPPING public: - virtual bool ShouldCreateSubsystem(UObject* Outer) const override; + /* Called after creating the first instance of given Flow Asset */ + static FNativeFlowAssetEvent OnInstancedTemplateAdded; - virtual void Initialize(FSubsystemCollectionBase& Collection) override; - virtual void Deinitialize() override; + /* Called just before removing the last instance of given Flow Asset */ + static FNativeFlowAssetEvent OnInstancedTemplateRemoved; +#endif +public: + UFUNCTION(BlueprintCallable, Category = "FlowSubsystem") virtual void AbortActiveFlows(); /* Start the root Flow, graph that will eventually instantiate next Flow Graphs through the SubGraph node */ UFUNCTION(BlueprintCallable, Category = "FlowSubsystem", meta = (DefaultToSelf = "Owner")) - virtual void StartRootFlow(UObject* Owner, UFlowAsset* FlowAsset, const bool bAllowMultipleInstances = true); + virtual void StartRootFlow(UObject* Owner, UFlowAsset* FlowAsset, const TScriptInterface DataPinValueSupplier, const bool bAllowMultipleInstances = true); - virtual UFlowAsset* CreateRootFlow(UObject* Owner, UFlowAsset* FlowAsset, const bool bAllowMultipleInstances = true); + virtual UFlowAsset* CreateRootFlow(UObject* Owner, UFlowAsset* FlowAsset, const bool bAllowMultipleInstances = true, const FString& NewInstanceName = FString()); /* Finish Policy value is read by Flow Node * Nodes have opportunity to terminate themselves differently if Flow Graph has been aborted @@ -81,11 +87,23 @@ class FLOW_API UFlowSubsystem : public UGameInstanceSubsystem virtual void FinishAllRootFlows(UObject* Owner, const EFlowFinishPolicy FinishPolicy); protected: - UFlowAsset* CreateSubFlow(UFlowNode_SubGraph* SubGraphNode, const FString SavedInstanceName = FString(), const bool bPreloading = false); + UFlowAsset* CreateSubFlow(UFlowNode_SubGraph* SubGraphNode, const FString& SavedInstanceName = FString(), const bool bPreloading = false); void RemoveSubFlow(UFlowNode_SubGraph* SubGraphNode, const EFlowFinishPolicy FinishPolicy); - UFlowAsset* CreateFlowInstance(const TWeakObjectPtr Owner, TSoftObjectPtr FlowAsset, FString NewInstanceName = FString()); - void RemoveInstancedTemplate(UFlowAsset* Template); +public: + UFlowAsset* CreateFlowInstance(const TWeakObjectPtr Owner, UFlowAsset* LoadedFlowAsset, FString NewInstanceName = FString()); + +protected: + virtual void AddInstancedTemplate(UFlowAsset* Template); + virtual void RemoveInstancedTemplate(UFlowAsset* Template); + +public: + /* Try to flush (and clear) all Deferred Trigger scopes. + * (can fail to flush all if a FFlowExecutionGate causes a new halt) */ + bool TryFlushAllDeferredTriggerScopes() const; + + /* Clear (do not trigger) any remaining deferred transitions. (for shutdown cases) */ + void ClearAllDeferredTriggerScopes(); public: /* Returns all assets instanced by object from another system like World Settings */ @@ -101,32 +119,49 @@ class FLOW_API UFlowSubsystem : public UGameInstanceSubsystem /* Returns assets instanced by Sub Graph nodes */ UFUNCTION(BlueprintPure, Category = "FlowSubsystem") - TMap GetInstancedSubFlows() const { return InstancedSubFlows; } + const TMap& GetInstancedSubFlows() const { return ObjectPtrDecay(InstancedSubFlows); } - virtual UWorld* GetWorld() const override; ////////////////////////////////////////////////////////////////////////// -// SaveGame +// SaveGame support + +protected: + UPROPERTY(Transient) + TObjectPtr LoadedSaveGame; +public: UPROPERTY(BlueprintAssignable, Category = "FlowSubsystem") FSimpleFlowEvent OnSaveGame; UFUNCTION(BlueprintCallable, Category = "FlowSubsystem") virtual void OnGameSaved(UFlowSaveGame* SaveGame); + virtual void OnGameSaved(TArray& FlowComponents, TArray& FlowInstances); + UFUNCTION(BlueprintCallable, Category = "FlowSubsystem") virtual void OnGameLoaded(UFlowSaveGame* SaveGame); - virtual void LoadRootFlow(UObject* Owner, UFlowAsset* FlowAsset, const FString& SavedAssetInstanceName); + virtual void OnGameLoaded(TArray& FlowComponents, TArray& FlowInstances); + + UFUNCTION(BlueprintCallable, Category = "FlowSubsystem") + virtual void LoadRootFlow(UObject* Owner, UFlowAsset* FlowAsset, const FString& SavedAssetInstanceName, const bool bAllowMultipleInstances); + + UFUNCTION(BlueprintCallable, Category = "FlowSubsystem") virtual void LoadSubFlow(UFlowNode_SubGraph* SubGraphNode, const FString& SavedAssetInstanceName); UFUNCTION(BlueprintPure, Category = "FlowSubsystem") UFlowSaveGame* GetLoadedSaveGame() const { return LoadedSaveGame; } + virtual const FFlowComponentSaveData* GetLoadedComponentRecord(const UFlowComponent* Component) const; + virtual const FFlowAssetSaveData* GetLoadedAssetRecord(const UObject* Owner, const UFlowAsset* Asset, const FString& SavedAssetInstanceName) const; + + UFUNCTION(BlueprintCallable, Category = "FlowSubsystem") + virtual void ClearLoadedSaveGame(); + ////////////////////////////////////////////////////////////////////////// // Component Registry -private: +protected: /* All the Flow Components currently existing in the world */ TMultiMap> FlowComponentRegistry; @@ -415,4 +450,4 @@ class FLOW_API UFlowSubsystem : public UGameInstanceSubsystem private: void FindComponents(const FGameplayTag& Tag, const bool bExactMatch, TArray>& OutComponents) const; void FindComponents(const FGameplayTagContainer& Tags, const EGameplayContainerMatchType MatchType, const bool bExactMatch, TSet>& OutComponents) const; -}; +}; \ No newline at end of file diff --git a/Source/Flow/Public/FlowTags.h b/Source/Flow/Public/FlowTags.h new file mode 100644 index 000000000..829ddba20 --- /dev/null +++ b/Source/Flow/Public/FlowTags.h @@ -0,0 +1,27 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "NativeGameplayTags.h" + +namespace FlowNodeStyle +{ + FLOW_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(CategoryName); + FLOW_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Custom); + + FLOW_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Node); + FLOW_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Default); + FLOW_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Condition); + FLOW_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Deprecated); + FLOW_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Developer); + FLOW_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(InOut); + FLOW_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Latent); + FLOW_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Logic); + FLOW_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(SubGraph); + FLOW_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Terminal); + + FLOW_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(AddOn); + FLOW_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(AddOn_PerSpawnedActor); + FLOW_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(AddOn_Predicate); + FLOW_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(AddOn_Predicate_Composite); + FLOW_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(AddOn_SwitchCase); +} diff --git a/Source/Flow/Public/FlowTypes.h b/Source/Flow/Public/FlowTypes.h index 11dec7977..9b51d6c56 100644 --- a/Source/Flow/Public/FlowTypes.h +++ b/Source/Flow/Public/FlowTypes.h @@ -1,23 +1,29 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "GameplayTagContainer.h" +#include "Types/FlowEnumUtils.h" -#include "FlowSave.h" #include "FlowTypes.generated.h" #if WITH_EDITORONLY_DATA UENUM(BlueprintType) enum class EFlowNodeStyle : uint8 { + // Deprecated EFlowNodeStyle enum (use NodeDisplayStyle tag instead) Condition, Default, InOut UMETA(Hidden), Latent, Logic, - SubGraph UMETA(Hidden) + SubGraph UMETA(Hidden), + Custom, + + Max UMETA(Hidden), + Invalid UMETA(Hidden), + Min = 0 UMETA(Hidden), }; +FLOW_ENUM_RANGE_VALUES(EFlowNodeStyle) #endif UENUM(BlueprintType) @@ -26,12 +32,26 @@ enum class EFlowNodeState : uint8 NeverActivated, Active, Completed, - Aborted + Aborted, + + Max UMETA(Hidden), + Invalid UMETA(Hidden), + Min = 0 UMETA(Hidden), + + // State subrange for states that count as "Finished" + FinishedFirst = Completed UMETA(Hidden), + FinishedLast = Aborted UMETA(Hidden), }; +FLOW_ENUM_RANGE_VALUES(EFlowNodeState) + +namespace EFlowNodeState_Classifiers +{ + FORCEINLINE bool IsFinishedState(EFlowNodeState State) { return FLOW_IS_ENUM_IN_SUBRANGE(State, EFlowNodeState::Finished); } +} -// Finish Policy value is read by Flow Node -// Nodes have opportunity to terminate themselves differently if Flow Graph has been aborted -// Example: Spawn node might despawn all actors if Flow Graph is aborted, not completed +/* Finish Policy value is read by Flow Node + * Nodes have opportunity to terminate themselves differently if Flow Graph has been aborted + * Example: Spawn node might despawn all actors if Flow Graph is aborted, not completed */ UENUM(BlueprintType) enum class EFlowFinishPolicy : uint8 { @@ -39,6 +59,14 @@ enum class EFlowFinishPolicy : uint8 Abort }; +UENUM(BlueprintType) +enum class EFlowSignalMode : uint8 +{ + Enabled UMETA(ToolTip = "Default state, node is fully executed."), + Disabled UMETA(ToolTip = "No logic executed, any Input Pin activation is ignored. Node instantly enters a deactivated state."), + PassThrough UMETA(ToolTip = "Internal node logic not executed. All connected outputs are triggered, node finishes its work.") +}; + UENUM(BlueprintType) enum class EFlowNetMode : uint8 { @@ -82,5 +110,77 @@ UENUM(BlueprintType) enum class EFlowOnScreenMessageType : uint8 { Temporary, - Permanent + Permanent, + Disabled +}; + +UENUM(BlueprintType) +enum class EFlowAddOnAcceptResult : uint8 +{ + // Note that these enum values are ordered by priority, where greater numerical values are higher priority + // (see CombineFlowAddOnAcceptResult) + + // No result from the current operation + Undetermined, + + // Accept, if all other conditions are met + TentativeAccept, + + // Reject the AddOn outright, regardless if previously TentativelyAccept-ed + Reject, + + Max UMETA(Hidden), + Invalid UMETA(Hidden), + Min = Undetermined UMETA(Hidden), +}; +FLOW_ENUM_RANGE_VALUES(EFlowAddOnAcceptResult); + +FORCEINLINE_DEBUGGABLE EFlowAddOnAcceptResult CombineFlowAddOnAcceptResult(EFlowAddOnAcceptResult Result0, EFlowAddOnAcceptResult Result1) +{ + const FlowEnum::safe_underlying_type::type Result0AsInt = FlowEnum::ToInt(Result0); + const FlowEnum::safe_underlying_type::type Result1AsInt = FlowEnum::ToInt(Result1); + + // Prioritize the higher numerical value enum value + return static_cast(FMath::Max(Result0AsInt, Result1AsInt)); +} + +UENUM() +enum class EFlowForEachAddOnFunctionReturnValue : int8 +{ + // Continue iterating the ForEach loop + Continue, + + // Break out of the ForEach loop, with a "Success" result (whatever that means to the TFunction) + BreakWithSuccess, + + // Break out of the ForEach loop, with a "Failure" return (whatever that means to the TFunction) + BreakWithFailure, + + Max UMETA(Hidden), + Invalid = -1 UMETA(Hidden), + Min = 0 UMETA(Hidden), + + ContinueForEachFirst = Continue UMETA(Hidden), + ContinueForEachLast = Continue UMETA(Hidden), +}; +FLOW_ENUM_RANGE_VALUES(EFlowForEachAddOnFunctionReturnValue); + +namespace EFlowForEachAddOnFunctionReturnValue_Classifiers +{ + FORCEINLINE bool ShouldContinueForEach(EFlowForEachAddOnFunctionReturnValue Result) { return FLOW_IS_ENUM_IN_SUBRANGE(Result, EFlowForEachAddOnFunctionReturnValue::ContinueForEach); } +} + +UENUM() +enum class EFlowForEachAddOnChildRule : int8 +{ + // Apply the Function to all child addons (and children of addons, etc.) + AllChildren, + + // Apply the Function to immediate child addons only (do not apply to their children) + ImmediateChildrenOnly, + + Max UMETA(Hidden), + Invalid = -1 UMETA(Hidden), + Min = 0 UMETA(Hidden), }; +FLOW_ENUM_RANGE_VALUES(EFlowForEachAddOnChildRule); diff --git a/Source/Flow/Public/FlowWorldSettings.h b/Source/Flow/Public/FlowWorldSettings.h index b6fb3dffd..44d125a41 100644 --- a/Source/Flow/Public/FlowWorldSettings.h +++ b/Source/Flow/Public/FlowWorldSettings.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "GameFramework/WorldSettings.h" @@ -17,17 +16,8 @@ class FLOW_API AFlowWorldSettings : public AWorldSettings private: UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "Flow", meta = (AllowPrivateAccess = "true")) - UFlowComponent* FlowComponent; + TObjectPtr FlowComponent; public: UFlowComponent* GetFlowComponent() const { return FlowComponent; } - - virtual void PostLoad() override; - virtual void PostInitializeComponents() override; - -private: - bool IsValidInstance() const; - - UPROPERTY() - class UFlowAsset* FlowAsset_DEPRECATED; }; diff --git a/Source/Flow/Public/Interfaces/FlowAssetProviderInterface.h b/Source/Flow/Public/Interfaces/FlowAssetProviderInterface.h new file mode 100644 index 000000000..6cfde6454 --- /dev/null +++ b/Source/Flow/Public/Interfaces/FlowAssetProviderInterface.h @@ -0,0 +1,29 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/Interface.h" + +#include "FlowAssetProviderInterface.generated.h" + +class UFlowAsset; + +/** + * Interface to define a UFlowAsset provider. + * This is used for filtering in FFlowAssetParamsPtrCustomization. + */ +UINTERFACE(MinimalAPI, Blueprintable, DisplayName = "Flow Asset Provider Interface") +class UFlowAssetProviderInterface : public UInterface +{ + GENERATED_BODY() +}; + +class FLOW_API IFlowAssetProviderInterface +{ + GENERATED_BODY() + +public: + /* Provide a FlowAsset for use in FFlowAssetParamsPtr resolution. */ + UFUNCTION(BlueprintImplementableEvent, Category = FlowAssetParams, DisplayName = "ProvideFlowAsset") + UFlowAsset* K2_ProvideFlowAsset() const; + virtual UFlowAsset* ProvideFlowAsset() const; +}; diff --git a/Source/Flow/Public/Interfaces/FlowContextPinSupplierInterface.h b/Source/Flow/Public/Interfaces/FlowContextPinSupplierInterface.h new file mode 100644 index 000000000..45764be3c --- /dev/null +++ b/Source/Flow/Public/Interfaces/FlowContextPinSupplierInterface.h @@ -0,0 +1,54 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/Interface.h" + +#include "Nodes/FlowPin.h" +#include "FlowContextPinSupplierInterface.generated.h" + +/** + * A flow element (UFlowNode, UFlowNodeAddOn, etc.) that may supply context pins. + * "Context Pins" are those that can be dynamically added/removed to a FlowNode by property + * settings on the flow node, by subobjects, etc. + */ +UINTERFACE(MinimalAPI, Blueprintable, DisplayName = "Flow ContextPin Supplier Interface") +class UFlowContextPinSupplierInterface : public UInterface +{ + GENERATED_BODY() +}; + +class FLOW_API IFlowContextPinSupplierInterface +{ + GENERATED_BODY() + +public: + +#if WITH_EDITOR + /* Be careful, enabling it might cause loading gigabytes of data as nodes would load all related data (i.e. Level Sequences). */ + virtual bool CanRefreshContextPinsOnLoad() const { return false; } +#endif + + UFUNCTION(BlueprintNativeEvent, Category = "FlowNode In-Editor Functions", DisplayName = "SupportsContextPins", meta = (DevelopmentOnly)) + bool K2_SupportsContextPins() const; + virtual bool K2_SupportsContextPins_Implementation() const; + +#if WITH_EDITOR + /* Note: This method can only be called by native implementors of the interface, so we still have to manually handle and check + * classes that only implement the interface in Blueprint. */ + virtual bool SupportsContextPins() const { return Execute_K2_SupportsContextPins(Cast(this)); } +#endif + + UFUNCTION(BlueprintNativeEvent, Category = "FlowNode In-Editor Functions", DisplayName = "GetContextInputs", meta = (DevelopmentOnly)) + TArray K2_GetContextInputs() const; + virtual TArray K2_GetContextInputs_Implementation() const; +#if WITH_EDITOR + virtual TArray GetContextInputs() const { return Execute_K2_GetContextInputs(Cast(this)); } +#endif + + UFUNCTION(BlueprintNativeEvent, Category = "FlowNode In-Editor Functions", DisplayName = "GetContextOutputs", meta = (DevelopmentOnly)) + TArray K2_GetContextOutputs() const; + virtual TArray K2_GetContextOutputs_Implementation() const; +#if WITH_EDITOR + virtual TArray GetContextOutputs() const { return Execute_K2_GetContextOutputs(Cast(this)); } +#endif +}; diff --git a/Source/Flow/Public/Interfaces/FlowCoreExecutableInterface.h b/Source/Flow/Public/Interfaces/FlowCoreExecutableInterface.h new file mode 100644 index 000000000..28a0a8e01 --- /dev/null +++ b/Source/Flow/Public/Interfaces/FlowCoreExecutableInterface.h @@ -0,0 +1,53 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/Interface.h" + +#include "FlowCoreExecutableInterface.generated.h" + +/** + * Implemented by objects that can execute within a Flow Graph. + * Example: UFlowNode and UFlowNodeAddOn subclasses implement this. + */ +UINTERFACE(MinimalAPI, Blueprintable, DisplayName = "Flow Core Executable Interface") +class UFlowCoreExecutableInterface : public UInterface +{ + GENERATED_BODY() +}; + +class FLOW_API IFlowCoreExecutableInterface +{ + GENERATED_BODY() + +public: + /* Method called just after creating the node instance, while initializing the Flow Asset instance. + * This happens before executing graph, only called during gameplay. */ + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", DisplayName = "Initialize Instance") + void K2_InitializeInstance(); + virtual void InitializeInstance() { Execute_K2_InitializeInstance(Cast(this)); } + + /* Event called from UMKTFlowNode::DeinitializeInstance(). */ + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", DisplayName = "Deinitialize Instance") + void K2_DeinitializeInstance(); + virtual void DeinitializeInstance() { Execute_K2_DeinitializeInstance(Cast(this)); } + + /* Called immediately before the first input is triggered. */ + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", DisplayName = "OnActivate") + void K2_OnActivate(); + virtual void OnActivate() { Execute_K2_OnActivate(Cast(this)); } + + /* Event called after node finished the work. */ + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", DisplayName = "Cleanup") + void K2_Cleanup(); + virtual void Cleanup() { Execute_K2_Cleanup(Cast(this)); } + + /* Define what happens when node is terminated from the outside. */ + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", DisplayName = "Force Finish Node") + void K2_ForceFinishNode(); + virtual void ForceFinishNode() { Execute_K2_ForceFinishNode(Cast(this)); } + + /* Event reacting on triggering Input pin. */ + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", DisplayName = "Execute Input") + void K2_ExecuteInput(const FName& PinName); + virtual void ExecuteInput(const FName& PinName) { Execute_K2_ExecuteInput(Cast(this), PinName); } +}; diff --git a/Source/Flow/Public/Interfaces/FlowDataPinPropertyProviderInterface.h b/Source/Flow/Public/Interfaces/FlowDataPinPropertyProviderInterface.h new file mode 100644 index 000000000..2a0338d90 --- /dev/null +++ b/Source/Flow/Public/Interfaces/FlowDataPinPropertyProviderInterface.h @@ -0,0 +1,28 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "StructUtils/InstancedStruct.h" +#include "UObject/Interface.h" + +#include "FlowDataPinPropertyProviderInterface.generated.h" + +struct FFlowDataPinValue; + +/** + * Interface to define a FFlowDataPinValue provider. + * This is used in plumbing data in the AI Flow extension plugin into the Flow Data Pins framework. + */ +UINTERFACE(MinimalAPI, NotBlueprintable) +class UFlowDataPinPropertyProviderInterface : public UInterface +{ + GENERATED_BODY() +}; + +class FLOW_API IFlowDataPinPropertyProviderInterface +{ + GENERATED_BODY() + +public: + /* Provide a FFlowDataPinValue (instancedStruct) for the creation of data pins and supplying their values. */ + virtual bool TryProvideFlowDataPinProperty(TInstancedStruct& OutFlowDataPinProperty) const = 0; +}; diff --git a/Source/Flow/Public/Interfaces/FlowDataPinValueOwnerInterface.h b/Source/Flow/Public/Interfaces/FlowDataPinValueOwnerInterface.h new file mode 100644 index 000000000..6980e726c --- /dev/null +++ b/Source/Flow/Public/Interfaces/FlowDataPinValueOwnerInterface.h @@ -0,0 +1,61 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/Interface.h" +#include "Delegates/Delegate.h" +#include "StructUtils/InstancedStruct.h" + +#include "FlowDataPinValueOwnerInterface.generated.h" + +struct FFlowDataPinValue; +struct FFlowDataPinValueOwner; +struct FFlowAutoDataPinsWorkingData; + +UINTERFACE(NotBlueprintable) +class FLOW_API UFlowDataPinValueOwnerInterface : public UInterface +{ + GENERATED_BODY() +}; + +class FLOW_API IFlowDataPinValueOwnerInterface +{ + GENERATED_BODY() + +public: +#if WITH_EDITOR + /* Determines if the pin's type properties (bIsInputPin, MultiType) can be modified. */ + virtual bool CanModifyFlowDataPinType() const { return true; } + + /* Determines if the bIsInputPin checkbox should be visible in the Details panel. */ + virtual bool ShowFlowDataPinValueInputPinCheckbox() const { return true; } + + /* Should the ClassFilter or EnumClass row be visible? */ + virtual bool ShowFlowDataPinValueClassFilter(const FFlowDataPinValue* Value) const { return true; } + + /* Base policy for whether the ClassFilter / Enum source can be edited. */ + virtual bool CanEditFlowDataPinValueClassFilter(const FFlowDataPinValue* Value) const { return true; } + + /* Set the delegate that forces a layout rebuild (provided by owner detail customization). */ + virtual void SetFlowDataPinValuesRebuildDelegate(FSimpleDelegate InDelegate) {} + + /* Request a details rebuild (executes delegate if bound). */ + virtual void RequestFlowDataPinValuesDetailsRebuild() {} + + /* Automatically generate data pins from FFlowDataPinValue and FFlowNamedDataPinProperty structs. */ + virtual void AutoGenerateDataPins(FFlowDataPinValueOwner& ValueOwner, FFlowAutoDataPinsWorkingData& InOutWorkingData); +#endif + + /* Advanced helper for TrySupplyDataPin, which can be overridden in subclasses to provide alternate sourcing for properties. + * If returns true, either OutFoundProperty or OutFoundInstancedStruct is expected to carry the property value. + * This function is used for cases like DefineProperties, Start, and blackboard lookup nodes. */ + virtual bool TryFindPropertyByPinName( + const FName& PinName, + const FProperty*& OutFoundProperty, + TInstancedStruct& OutFoundInstancedStruct) const; + + static bool TryFindPropertyByPinName_Static( + const UObject& PropertyOwnerObject, + const FName& PinName, + const FProperty*& OutFoundProperty, + TInstancedStruct& OutFoundInstancedStruct); +}; diff --git a/Source/Flow/Public/Interfaces/FlowDataPinValueSupplierInterface.h b/Source/Flow/Public/Interfaces/FlowDataPinValueSupplierInterface.h new file mode 100644 index 000000000..4efff617f --- /dev/null +++ b/Source/Flow/Public/Interfaces/FlowDataPinValueSupplierInterface.h @@ -0,0 +1,30 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/Interface.h" + +#include "Types/FlowDataPinResults.h" +#include "FlowDataPinValueSupplierInterface.generated.h" + +/** + * Interface to define a Flow Data Pin value supplier. This is generally a UFlowNode subclass, + * but we may support external suppliers that are not flow nodes in the future. + * Example: for supplying configuration values for the root graph. + */ +UINTERFACE(MinimalAPI, NotBlueprintable, DisplayName = "Flow Data Pin Value Supplier Interface") +class UFlowDataPinValueSupplierInterface : public UInterface +{ + GENERATED_BODY() +}; + +class FLOW_API IFlowDataPinValueSupplierInterface +{ + GENERATED_BODY() + +public: + /* Can this node actually supply Data Pin values? + * Implementers of this interface will need to use their own logic to answer this question. */ + virtual bool CanSupplyDataPinValues() const { return true; } + + virtual FFlowDataPinResult TrySupplyDataPin(FName PinName) const { return FFlowDataPinResult(); } +}; diff --git a/Source/Flow/Public/Interfaces/FlowExecutionGate.h b/Source/Flow/Public/Interfaces/FlowExecutionGate.h new file mode 100644 index 000000000..dc3240cb0 --- /dev/null +++ b/Source/Flow/Public/Interfaces/FlowExecutionGate.h @@ -0,0 +1,35 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/Interface.h" + +class UFlowAsset; + +/** + * Implemented by a debugger/runtime system (in another module) that can halt Flow execution. + * Flow runtime queries this through FFlowExecutionGate without depending on the debugger module. + */ +class FLOW_API IFlowExecutionGate +{ +public: + virtual ~IFlowExecutionGate() = default; + + /* Return true when Flow execution should be halted globally. */ + virtual bool IsFlowExecutionHalted() const = 0; +}; + +/** + * Global registry + minimal deferred-execution queue for Flow runtime. + */ +class FLOW_API FFlowExecutionGate +{ +public: + static void SetGate(IFlowExecutionGate* InGate); + static IFlowExecutionGate* GetGate(); + + /* True if a gate exists and it currently wants Flow execution halted. */ + static bool IsHalted(); + +private: + static IFlowExecutionGate* Gate; +}; \ No newline at end of file diff --git a/Source/Flow/Public/Interfaces/FlowExternalExecutableInterface.h b/Source/Flow/Public/Interfaces/FlowExternalExecutableInterface.h new file mode 100644 index 000000000..42bddc2a5 --- /dev/null +++ b/Source/Flow/Public/Interfaces/FlowExternalExecutableInterface.h @@ -0,0 +1,31 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/Interface.h" +#include "UObject/ScriptInterface.h" + +#include "FlowExternalExecutableInterface.generated.h" + +class UFlowNodeBase; + +/** + * Implemented by external objects that can execute within a Flow Graph via a FlowNode or FlowNodeAddOn proxy. + */ +UINTERFACE(MinimalAPI, Blueprintable, DisplayName = "Flow External Executable Interface") +class UFlowExternalExecutableInterface : public UInterface +{ + GENERATED_BODY() +}; + +class FLOW_API IFlowExternalExecutableInterface +{ + GENERATED_BODY() + +public: + /* Called immediately prior to OnActivate() to set the native proxy that is executing the + * external executable object in the flow graph. This is primarily done so that the external element has a + * handle to call TriggerOutput() and Finish() when it has completed its work. */ + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", DisplayName = "PreActivateExternalFlowExecutable") + void K2_PreActivateExternalFlowExecutable(const UFlowNodeBase* FlowNodeBase); + virtual void PreActivateExternalFlowExecutable(UFlowNodeBase& FlowNodeBase); +}; diff --git a/Source/Flow/Public/Interfaces/FlowNamedPropertiesSupplierInterface.h b/Source/Flow/Public/Interfaces/FlowNamedPropertiesSupplierInterface.h new file mode 100644 index 000000000..5ae2c46fc --- /dev/null +++ b/Source/Flow/Public/Interfaces/FlowNamedPropertiesSupplierInterface.h @@ -0,0 +1,29 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/Interface.h" + +#include "FlowNamedPropertiesSupplierInterface.generated.h" + +struct FFlowNamedDataPinProperty; + +UINTERFACE(Blueprintable) +class FLOW_API UFlowNamedPropertiesSupplierInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Interface for Flow nodes that supply named properties, such as Start or DefineProperties nodes. + */ +class FLOW_API IFlowNamedPropertiesSupplierInterface +{ + GENERATED_BODY() + +public: + + /* Returns the array of named properties defined by this node. */ + virtual TArray& GetMutableNamedProperties() = 0; + const TArray& GetNamedProperties() const + { return const_cast(this)->GetMutableNamedProperties(); } +}; diff --git a/Source/Flow/Public/Interfaces/FlowNodeWithExternalDataPinSupplierInterface.h b/Source/Flow/Public/Interfaces/FlowNodeWithExternalDataPinSupplierInterface.h new file mode 100644 index 000000000..10f86c2a4 --- /dev/null +++ b/Source/Flow/Public/Interfaces/FlowNodeWithExternalDataPinSupplierInterface.h @@ -0,0 +1,34 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/Interface.h" +#include "FlowNodeWithExternalDataPinSupplierInterface.generated.h" + +class IFlowDataPinValueSupplierInterface; +struct FFlowPin; + +/** + * Interface for special flow node types that support an external data pin supplier. + * The primary (only?) implementing node is UFlowNode_Start, which supplies its pin data externally from + * either the SubGraph that instanced the graph that is being started. + */ +UINTERFACE(MinimalAPI, NotBlueprintable, DisplayName = "Flow Node With External Data Pin Value Supplier Interface") +class UFlowNodeWithExternalDataPinSupplierInterface : public UInterface +{ + GENERATED_BODY() +}; + +class FLOW_API IFlowNodeWithExternalDataPinSupplierInterface +{ + GENERATED_BODY() + +public: + /* Set the external DataPinValueSupplier for this node to use. */ + virtual void SetDataPinValueSupplier(IFlowDataPinValueSupplierInterface* DataPinValueSupplier) = 0; + + /* Append the external InputPins for the external supplier to include in its own pins (eg, UFlowNode_Subgraph). */ + virtual bool TryAppendExternalInputPins(TArray& InOutPins) const { return false; } + + /* Get the IFlowDataPinValueSupplierInterface for the external supplier for this node. */ + virtual IFlowDataPinValueSupplierInterface* GetExternalDataPinSupplier() const = 0; +}; diff --git a/Source/Flow/Public/Interfaces/FlowPredicateInterface.h b/Source/Flow/Public/Interfaces/FlowPredicateInterface.h new file mode 100644 index 000000000..a3a5a2be3 --- /dev/null +++ b/Source/Flow/Public/Interfaces/FlowPredicateInterface.h @@ -0,0 +1,28 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/Interface.h" +#include "FlowPredicateInterface.generated.h" + +class UFlowNodeAddOn; + +/** + * Predicate interface for AddOns. + */ +UINTERFACE(MinimalAPI, BlueprintType, Blueprintable, DisplayName = "Flow Predicate Interface") +class UFlowPredicateInterface : public UInterface +{ + GENERATED_BODY() +}; + +class FLOW_API IFlowPredicateInterface +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintNativeEvent) + bool EvaluatePredicate() const; + virtual bool EvaluatePredicate_Implementation() const { return true; } + + static bool ImplementsInterfaceSafe(const UFlowNodeAddOn* AddOnTemplate); +}; diff --git a/Source/Flow/Public/Interfaces/FlowPreloadableInterface.h b/Source/Flow/Public/Interfaces/FlowPreloadableInterface.h new file mode 100644 index 000000000..0a8c2f709 --- /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/Interfaces/FlowSwitchCaseInterface.h b/Source/Flow/Public/Interfaces/FlowSwitchCaseInterface.h new file mode 100644 index 000000000..3561dbc8f --- /dev/null +++ b/Source/Flow/Public/Interfaces/FlowSwitchCaseInterface.h @@ -0,0 +1,31 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/Interface.h" +#include "Templates/SubclassOf.h" + +#include "FlowSwitchCaseInterface.generated.h" + +class UFlowNodeAddOn; + +/** + * 'Case' AddOn for the Switch node. + */ +UINTERFACE(MinimalAPI, BlueprintType, Blueprintable, DisplayName = "Flow Switch Case Interface") +class UFlowSwitchCaseInterface : public UInterface +{ + GENERATED_BODY() +}; + +class FLOW_API IFlowSwitchCaseInterface +{ + GENERATED_BODY() + +public: + + UFUNCTION(BlueprintNativeEvent) + bool TryTriggerForCase() const; + virtual bool TryTriggerForCase_Implementation() const { return true; } + + static bool ImplementsInterfaceSafe(const UFlowNodeAddOn* AddOnTemplate); +}; diff --git a/Source/Flow/Public/LevelSequence/FlowLevelSequenceActor.h b/Source/Flow/Public/LevelSequence/FlowLevelSequenceActor.h index c84cc538e..99c6d5cf3 100644 --- a/Source/Flow/Public/LevelSequence/FlowLevelSequenceActor.h +++ b/Source/Flow/Public/LevelSequence/FlowLevelSequenceActor.h @@ -1,31 +1,30 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "LevelSequenceActor.h" #include "FlowLevelSequenceActor.generated.h" +class ULevelSequence; + /** - * Custom ALevelSequenceActor is needed to override ULevelSequencePlayer class + * Custom ALevelSequenceActor is needed to override ULevelSequencePlayer class. */ UCLASS(hideCategories=(Rendering, Physics, LOD, Activation, Input)) class FLOW_API AFlowLevelSequenceActor : public ALevelSequenceActor { GENERATED_UCLASS_BODY() -protected: +protected: + UPROPERTY(ReplicatedUsing = OnRep_ReplicatedLevelSequenceAsset) + TObjectPtr ReplicatedLevelSequenceAsset; + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; - + public: + void SetPlaybackSettings(FMovieSceneSequencePlaybackSettings NewPlaybackSettings); void SetReplicatedLevelSequenceAsset(ULevelSequence* Asset); - UFUNCTION(NetMulticast, Reliable) - void RPC_InitializePlayer(); - protected: - UPROPERTY(ReplicatedUsing = OnRep_ReplicatedLevelSequenceAsset) - TObjectPtr ReplicatedLevelSequenceAsset; - UFUNCTION() void OnRep_ReplicatedLevelSequenceAsset(); }; diff --git a/Source/Flow/Public/LevelSequence/FlowLevelSequencePlayer.h b/Source/Flow/Public/LevelSequence/FlowLevelSequencePlayer.h index 8818a9786..038125d14 100644 --- a/Source/Flow/Public/LevelSequence/FlowLevelSequencePlayer.h +++ b/Source/Flow/Public/LevelSequence/FlowLevelSequencePlayer.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "LevelSequencePlayer.h" @@ -8,25 +7,33 @@ class UFlowNode; /** - * Custom ULevelSequencePlayer allows for binding Flow Nodes to Level Sequence events + * Custom ULevelSequencePlayer allows for binding Flow Nodes to Level Sequence events. */ UCLASS() class FLOW_API UFlowLevelSequencePlayer : public ULevelSequencePlayer { - GENERATED_UCLASS_BODY() + GENERATED_UCLASS_BODY() private: - // most likely this is a UFlowNode_PlayLevelSequence or its child - UPROPERTY() - UFlowNode* FlowEventReceiver; + /* Most likely this is a UFlowNode_PlayLevelSequence or its child. */ + UPROPERTY() + TObjectPtr FlowEventReceiver; public: - // variant of ULevelSequencePlayer::CreateLevelSequencePlayer - static UFlowLevelSequencePlayer* CreateFlowLevelSequencePlayer(UObject* WorldContextObject, ULevelSequence* LevelSequence, FMovieSceneSequencePlaybackSettings Settings, FLevelSequenceCameraSettings CameraSettings, AActor* TransformOriginActor, bool bReplicates, ALevelSequenceActor*& OutActor); + /* Variant of ULevelSequencePlayer::CreateLevelSequencePlayer. */ + static UFlowLevelSequencePlayer* CreateFlowLevelSequencePlayer( + const UObject* WorldContextObject, + ULevelSequence* LevelSequence, + FMovieSceneSequencePlaybackSettings Settings, + FLevelSequenceCameraSettings CameraSettings, + AActor* TransformOriginActor, + const bool bReplicates, + const bool bAlwaysRelevant, + ALevelSequenceActor*& OutActor); void SetFlowEventReceiver(UFlowNode* FlowNode) { FlowEventReceiver = FlowNode; } // IMovieScenePlayer virtual TArray GetEventContexts() const override; - // -- + // -- }; diff --git a/Source/Flow/Public/MovieScene/MovieSceneFlowRepeaterSection.h b/Source/Flow/Public/MovieScene/MovieSceneFlowRepeaterSection.h index 3642103f6..007f76f1c 100644 --- a/Source/Flow/Public/MovieScene/MovieSceneFlowRepeaterSection.h +++ b/Source/Flow/Public/MovieScene/MovieSceneFlowRepeaterSection.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "MovieSceneFlowSectionBase.h" @@ -18,7 +17,7 @@ class FLOW_API UMovieSceneFlowRepeaterSection : public UMovieSceneFlowSectionBas virtual TArrayView GetAllEntryPoints() override { return MakeArrayView(&EventName, 1); } #endif - /** The event that should be triggered each time this section is evaluated */ + /* The event that should be triggered each time this section is evaluated. */ UPROPERTY(EditAnywhere, Category = "Flow") FString EventName; }; diff --git a/Source/Flow/Public/MovieScene/MovieSceneFlowSectionBase.h b/Source/Flow/Public/MovieScene/MovieSceneFlowSectionBase.h index 6ea290bc2..93d0ac15c 100644 --- a/Source/Flow/Public/MovieScene/MovieSceneFlowSectionBase.h +++ b/Source/Flow/Public/MovieScene/MovieSceneFlowSectionBase.h @@ -1,12 +1,11 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "MovieSceneSection.h" #include "MovieSceneFlowSectionBase.generated.h" /** - * Base class for flow sections + * Base class for flow sections. */ UCLASS() class FLOW_API UMovieSceneFlowSectionBase : public UMovieSceneSection diff --git a/Source/Flow/Public/MovieScene/MovieSceneFlowTemplate.h b/Source/Flow/Public/MovieScene/MovieSceneFlowTemplate.h index 47ff27af4..827a4c29b 100644 --- a/Source/Flow/Public/MovieScene/MovieSceneFlowTemplate.h +++ b/Source/Flow/Public/MovieScene/MovieSceneFlowTemplate.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Evaluation/MovieSceneEvalTemplate.h" diff --git a/Source/Flow/Public/MovieScene/MovieSceneFlowTrack.h b/Source/Flow/Public/MovieScene/MovieSceneFlowTrack.h index 3b7f921ef..bc08d8aa9 100644 --- a/Source/Flow/Public/MovieScene/MovieSceneFlowTrack.h +++ b/Source/Flow/Public/MovieScene/MovieSceneFlowTrack.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Tracks/MovieSceneEventTrack.h" @@ -42,25 +41,26 @@ class FLOW_API UMovieSceneFlowTrack virtual void PostCompile(FMovieSceneEvaluationTrack& Track, const FMovieSceneTrackCompilerArgs& Args) const override; virtual bool SupportsMultipleRows() const override { return true; } virtual FMovieSceneTrackSegmentBlenderPtr GetTrackSegmentBlender() const override; + // -- #if WITH_EDITOR virtual FText GetDefaultDisplayName() const override; #endif - /** If events should be fired when passed playing the sequence forwards. */ + /* If events should be fired when passed playing the sequence forwards. */ UPROPERTY(EditAnywhere, Category=TrackEvent) uint32 bFireEventsWhenForwards:1; - /** If events should be fired when passed playing the sequence backwards. */ + /* If events should be fired when passed playing the sequence backwards. */ UPROPERTY(EditAnywhere, Category=TrackEvent) uint32 bFireEventsWhenBackwards:1; - /** Defines where in the evaluation to trigger events */ + /* Defines where in the evaluation to trigger events. */ UPROPERTY(EditAnywhere, Category=TrackEvent) EFireEventsAtPosition EventPosition; private: - /** The track's sections. */ + /* The track's sections. */ UPROPERTY() - TArray Sections; + TArray> Sections; }; diff --git a/Source/Flow/Public/MovieScene/MovieSceneFlowTriggerSection.h b/Source/Flow/Public/MovieScene/MovieSceneFlowTriggerSection.h index 1650cd373..0023c61fb 100644 --- a/Source/Flow/Public/MovieScene/MovieSceneFlowTriggerSection.h +++ b/Source/Flow/Public/MovieScene/MovieSceneFlowTriggerSection.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Channels/MovieSceneStringChannel.h" @@ -22,7 +21,7 @@ class FLOW_API UMovieSceneFlowTriggerSection : public UMovieSceneFlowSectionBase virtual TArrayView GetAllEntryPoints() override { return StringChannel.GetData().GetValues(); } #endif - /** The channel that defines this section's timed events */ + /* The channel that defines this section's timed. */ UPROPERTY() FMovieSceneStringChannel StringChannel; }; diff --git a/Source/Flow/Public/Nodes/World/FlowNode_ComponentObserver.h b/Source/Flow/Public/Nodes/Actor/FlowNode_ComponentObserver.h similarity index 78% rename from Source/Flow/Public/Nodes/World/FlowNode_ComponentObserver.h rename to Source/Flow/Public/Nodes/Actor/FlowNode_ComponentObserver.h index 32a7254a8..9adbef409 100644 --- a/Source/Flow/Public/Nodes/World/FlowNode_ComponentObserver.h +++ b/Source/Flow/Public/Nodes/Actor/FlowNode_ComponentObserver.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "GameplayTagContainer.h" @@ -10,13 +9,16 @@ class UFlowComponent; /** - * Base class for nodes operating on actors with the Flow Component - * Such nodes usually wait until a specific action occurs in the actor + * Base class for nodes operating on actors with the Flow Component. + * Such nodes usually wait until a specific action occurs in the actor. */ UCLASS(Abstract, NotBlueprintable) class FLOW_API UFlowNode_ComponentObserver : public UFlowNode { - GENERATED_UCLASS_BODY() + GENERATED_BODY() + +public: + UFlowNode_ComponentObserver(); friend class FFlowNode_ComponentObserverDetails; @@ -24,25 +26,23 @@ class FLOW_API UFlowNode_ComponentObserver : public UFlowNode UPROPERTY(EditAnywhere, Category = "ObservedComponent") FGameplayTagContainer IdentityTags; - // Container A: Identity Tags in Flow Component - // Container B: Identity Tags listed above + /* Container A: Identity Tags in Flow Component. + * Container B: Identity Tags listed above. */ UPROPERTY(EditAnywhere, Category = "ObservedComponent") EFlowTagContainerMatchType IdentityMatchType; - // This node will become Completed, if Success Limit > 0 and Success Count reaches this limit - // Set this to zero, if you'd like receive events indefinitely (node would finish work only if explicitly Stopped) + /* This node will become Completed, if Success Limit > 0 and Success Count reaches this limit. + * Set this to zero, if you'd like receive events indefinitely (node would finish work only if explicitly Stopped). */ UPROPERTY(EditAnywhere, Category = "Lifetime", meta = (ClampMin = 0)) int32 SuccessLimit; - // This node will become Completed, if Success Limit > 0 and Success Count reaches this limit + /* This node will become Completed, if Success Limit > 0 and Success Count reaches this limit. */ UPROPERTY(VisibleAnywhere, Category = "Lifetime", SaveGame) int32 SuccessCount; TMap, TWeakObjectPtr> RegisteredActors; protected: - virtual void PostLoad() override; - virtual void ExecuteInput(const FName& PinName) override; virtual void OnLoad_Implementation() override; @@ -72,10 +72,8 @@ class FLOW_API UFlowNode_ComponentObserver : public UFlowNode #if WITH_EDITOR public: virtual FString GetNodeDescription() const override; + virtual EDataValidationResult ValidateNode() override; + virtual FString GetStatusString() const override; #endif - -private: - UPROPERTY() - FGameplayTag IdentityTag_DEPRECATED; }; diff --git a/Source/Flow/Public/Nodes/Actor/FlowNode_ExecuteComponent.h b/Source/Flow/Public/Nodes/Actor/FlowNode_ExecuteComponent.h new file mode 100644 index 000000000..30773ba4b --- /dev/null +++ b/Source/Flow/Public/Nodes/Actor/FlowNode_ExecuteComponent.h @@ -0,0 +1,136 @@ +// 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" + +#include "FlowNode_ExecuteComponent.generated.h" + +class UFlowInjectComponentsManager; + +UENUM() +enum class EExecuteComponentSource : uint8 +{ + Undetermined, + + BindToExisting, + InjectFromTemplate, + InjectFromClass, + + Max UMETA(Hidden), + Invalid UMETA(Hidden), + Min = 0, + + UsesInjectManagerFirst = InjectFromTemplate UMETA(Hidden), + UsesInjectManagerLast = InjectFromClass UMETA(Hidden), +}; +FLOW_ENUM_RANGE_VALUES(EExecuteComponentSource) + +namespace EExecuteComponentSource_Classifiers +{ + FORCEINLINE bool DoesComponentSourceUseInjectManager(EExecuteComponentSource Source) { return FLOW_IS_ENUM_IN_SUBRANGE(Source, EExecuteComponentSource::UsesInjectManager); } +} + +/** + * 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 + , public IFlowPreloadableInterface +{ + GENERATED_BODY() + +public: + UFlowNode_ExecuteComponent(); + + // IFlowCoreExecutableInterface + virtual void InitializeInstance() override; + virtual void DeinitializeInstance() 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; + // -- + + // UFlowNode + virtual void GatherDataPinValueOwnerCollection(FFlowDataPinValueOwnerCollection& ValueOwnerCollection) const override; + // -- + +#if WITH_EDITOR + // IFlowContextPinSupplierInterface + virtual bool SupportsContextPins() const override { return true; } + virtual TArray GetContextInputs() const override; + virtual TArray GetContextOutputs() const override; + // -- + + // UObject + virtual void PostLoad() override; + virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override; + // -- + + // UFlowNode + virtual FText K2_GetNodeTitle_Implementation() const override; + virtual EDataValidationResult ValidateNode() override; + + virtual FString GetStatusString() const override; + // -- +#endif // WITH_EDITOR + +protected: +#if WITH_EDITOR + void RefreshPins(); + const UActorComponent* TryGetExpectedComponent() const; + + void RefreshComponentSource(); +#endif // WITH_EDITOR + + bool TryInjectComponent(); + + const UActorComponent* GetResolvedOrExpectedComponent() const; + + UActorComponent* TryResolveComponent(); + UActorComponent* GetResolvedComponent() const; + TSubclassOf TryGetExpectedActorOwnerClass() const; + +protected: + /* Executable Component (by name) on the expected Flow owning Actor. + * The component must implement the IFlowExecutableComponentInterface. */ + UPROPERTY(EditAnywhere, Category = "Flow Executable Component", meta = (DisplayName = "Component to Execute", MustImplement = "/Script/Flow.FlowCoreExecutableInterface,/Script/Flow.FlowExternalExecutableInterface", EditConditionHides, EditCondition = "ComponentSource == EExecuteComponentSource::BindToExisting || ComponentSource == EExecuteComponentSource::Undetermined")) + FFlowActorOwnerComponentRef ComponentRef; + + /* Component (template) to inject on the spawned actor, may be configured inline. */ + UPROPERTY(EditAnywhere, Instanced, Category = Configuration, DisplayName = "Inject & Execute Component (from Template)", meta = (MustImplement = "/Script/Flow.FlowCoreExecutableInterface,/Script/Flow.FlowExternalExecutableInterface", EditConditionHides, EditCondition = "ComponentSource == EExecuteComponentSource::InjectFromTemplate || ComponentSource == EExecuteComponentSource::Undetermined")) + TObjectPtr ComponentTemplate = nullptr; + + /* Component (class) to inject on the spawned actor. */ + UPROPERTY(EditAnywhere, Category = Configuration, DisplayName = "Inject & Execute Component (by Class)", meta = (MustImplement = "/Script/Flow.FlowCoreExecutableInterface,/Script/Flow.FlowExternalExecutableInterface", EditConditionHides, EditCondition = "ComponentSource == EExecuteComponentSource::InjectFromClass || ComponentSource == EExecuteComponentSource::Undetermined")) + TSubclassOf ComponentClass = nullptr; + + /* Manager object to inject and remove components from the Flow owning Actor. */ + UPROPERTY(Transient) + TObjectPtr InjectComponentsManager = nullptr; + + /* Look for the component (by class) on the Actor and re-use it (rather than injecting) if the component already exists. */ + UPROPERTY(EditAnywhere, Category = Configuration, DisplayName = "Re-use existing component if found", meta = (EditConditionHides, EditCondition = "ComponentSource == EExecuteComponentSource::InjectFromClass")) + bool bReuseExistingComponent = true; + + /* Allow injecting the component, if it cannot be found on the Actor. */ + UPROPERTY(EditAnywhere, Category = Configuration, DisplayName = "Allow injecting component", meta = (EditConditionHides, EditCondition = "ComponentSource == EExecuteComponentSource::InjectFromClass && bReuseExistingComponent")) + bool bAllowInjectComponent = true; + + /* Inject component(s) onto the owning Actor. */ + UPROPERTY() + EExecuteComponentSource ComponentSource = EExecuteComponentSource::Undetermined; +}; diff --git a/Source/Flow/Public/Nodes/World/FlowNode_NotifyActor.h b/Source/Flow/Public/Nodes/Actor/FlowNode_NotifyActor.h similarity index 55% rename from Source/Flow/Public/Nodes/World/FlowNode_NotifyActor.h rename to Source/Flow/Public/Nodes/Actor/FlowNode_NotifyActor.h index f637cd27e..c1ba8291e 100644 --- a/Source/Flow/Public/Nodes/World/FlowNode_NotifyActor.h +++ b/Source/Flow/Public/Nodes/Actor/FlowNode_NotifyActor.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "GameplayTagContainer.h" @@ -8,37 +7,41 @@ #include "FlowNode_NotifyActor.generated.h" /** - * Finds all Flow Components with matching Identity Tag and calls ReceiveNotify event on these components + * Finds all Flow Components with matching Identity Tag and calls ReceiveNotify event on these components. */ -UCLASS(NotBlueprintable, meta = (DisplayName = "Notify Actor")) +UCLASS(NotBlueprintable, meta = (DisplayName = "Notify Actor", Keywords = "event")) class FLOW_API UFlowNode_NotifyActor : public UFlowNode { - GENERATED_UCLASS_BODY() + GENERATED_BODY() + +public: + UFlowNode_NotifyActor(); protected: UPROPERTY(EditAnywhere, Category = "Notify") FGameplayTagContainer IdentityTags; + UPROPERTY(EditAnywhere, Category = "Notify") + EGameplayContainerMatchType MatchType; + /** + * If true, identity tags must be an exact match. + * Be careful, setting this to false may be very expensive, as the + * search cost is proportional to the number of registered Gameplay Tags! + */ + UPROPERTY(EditAnywhere, Category = "Notify") + bool bExactMatch; + UPROPERTY(EditAnywhere, Category = "Notify") FGameplayTagContainer NotifyTags; UPROPERTY(EditAnywhere, Category = "Notify") EFlowNetMode NetMode; - virtual void PostLoad() override; - virtual void ExecuteInput(const FName& PinName) override; #if WITH_EDITOR public: virtual FString GetNodeDescription() const override; + virtual EDataValidationResult ValidateNode() override; #endif - -private: - UPROPERTY() - FGameplayTag IdentityTag_DEPRECATED; - - UPROPERTY() - FGameplayTag NotifyTag_DEPRECATED; - }; diff --git a/Source/Flow/Public/Nodes/World/FlowNode_OnActorRegistered.h b/Source/Flow/Public/Nodes/Actor/FlowNode_OnActorRegistered.h similarity index 74% rename from Source/Flow/Public/Nodes/World/FlowNode_OnActorRegistered.h rename to Source/Flow/Public/Nodes/Actor/FlowNode_OnActorRegistered.h index 59882d377..8e31068c2 100644 --- a/Source/Flow/Public/Nodes/World/FlowNode_OnActorRegistered.h +++ b/Source/Flow/Public/Nodes/Actor/FlowNode_OnActorRegistered.h @@ -1,20 +1,20 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once -#include "Nodes/World/FlowNode_ComponentObserver.h" +#include "Nodes/Actor/FlowNode_ComponentObserver.h" #include "FlowNode_OnActorRegistered.generated.h" /** - * Triggers output when Flow Component with matching Identity Tag appears in the world + * Triggers output when Flow Component with matching Identity Tag appears in the world. */ -UCLASS(NotBlueprintable, meta = (DisplayName = "On Actor Registered")) +UCLASS(NotBlueprintable, meta = (DisplayName = "On Actor Registered", Keywords = "bind")) class FLOW_API UFlowNode_OnActorRegistered : public UFlowNode_ComponentObserver { - GENERATED_UCLASS_BODY() + GENERATED_BODY() + +public: + UFlowNode_OnActorRegistered(); protected: - virtual void ExecuteInput(const FName& PinName) override; - virtual void ObserveActor(TWeakObjectPtr Actor, TWeakObjectPtr Component) override; }; diff --git a/Source/Flow/Public/Nodes/World/FlowNode_OnActorUnregistered.h b/Source/Flow/Public/Nodes/Actor/FlowNode_OnActorUnregistered.h similarity index 77% rename from Source/Flow/Public/Nodes/World/FlowNode_OnActorUnregistered.h rename to Source/Flow/Public/Nodes/Actor/FlowNode_OnActorUnregistered.h index fccad314e..a54ed498c 100644 --- a/Source/Flow/Public/Nodes/World/FlowNode_OnActorUnregistered.h +++ b/Source/Flow/Public/Nodes/Actor/FlowNode_OnActorUnregistered.h @@ -1,21 +1,21 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once -#include "Nodes/World/FlowNode_ComponentObserver.h" +#include "Nodes/Actor/FlowNode_ComponentObserver.h" #include "FlowNode_OnActorUnregistered.generated.h" /** - * Triggers output when Flow Component with matching Identity Tag disappears from the world + * Triggers output when Flow Component with matching Identity Tag disappears from the world. */ -UCLASS(NotBlueprintable, meta = (DisplayName = "On Actor Unregistered")) +UCLASS(NotBlueprintable, meta = (DisplayName = "On Actor Unregistered", Keywords = "unbind")) class FLOW_API UFlowNode_OnActorUnregistered : public UFlowNode_ComponentObserver { - GENERATED_UCLASS_BODY() + GENERATED_BODY() -protected: - virtual void ExecuteInput(const FName& PinName) override; +public: + UFlowNode_OnActorUnregistered(); +protected: virtual void ObserveActor(TWeakObjectPtr Actor, TWeakObjectPtr Component) override; virtual void ForgetActor(TWeakObjectPtr Actor, TWeakObjectPtr Component) override; }; diff --git a/Source/Flow/Public/Nodes/World/FlowNode_OnNotifyFromActor.h b/Source/Flow/Public/Nodes/Actor/FlowNode_OnNotifyFromActor.h similarity index 65% rename from Source/Flow/Public/Nodes/World/FlowNode_OnNotifyFromActor.h rename to Source/Flow/Public/Nodes/Actor/FlowNode_OnNotifyFromActor.h index 3cab19a33..0a8709be1 100644 --- a/Source/Flow/Public/Nodes/World/FlowNode_OnNotifyFromActor.h +++ b/Source/Flow/Public/Nodes/Actor/FlowNode_OnNotifyFromActor.h @@ -1,31 +1,29 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once -#include "Nodes/World/FlowNode_ComponentObserver.h" +#include "Nodes/Actor/FlowNode_ComponentObserver.h" #include "FlowNode_OnNotifyFromActor.generated.h" /** - * Triggers output when Flow Component with matching Identity Tag calls NotifyGraph function with matching Notify Tag + * Triggers output when Flow Component with matching Identity Tag calls NotifyGraph function with matching Notify Tag. */ UCLASS(NotBlueprintable, meta = (DisplayName = "On Notify From Actor")) class FLOW_API UFlowNode_OnNotifyFromActor : public UFlowNode_ComponentObserver { - GENERATED_UCLASS_BODY() + GENERATED_BODY() + +public: + UFlowNode_OnNotifyFromActor(); protected: UPROPERTY(EditAnywhere, Category = "Notify") FGameplayTagContainer NotifyTags; - // If true, node will check given Notify Tag is present in the Recently Sent Notify Tags - // This might be helpful in multiplayer, if client-side Flow Node started work after server sent the notify + /* If true, node will check given Notify Tag is present in the Recently Sent Notify Tags. + * This might be helpful in multiplayer, if client-side Flow Node started work after server sent the Notify. */ UPROPERTY(EditAnywhere, Category = "Notify") bool bRetroactive; - virtual void PostLoad() override; - - virtual void ExecuteInput(const FName& PinName) override; - virtual void ObserveActor(TWeakObjectPtr Actor, TWeakObjectPtr Component) override; virtual void ForgetActor(TWeakObjectPtr Actor, TWeakObjectPtr Component) override; @@ -35,8 +33,4 @@ class FLOW_API UFlowNode_OnNotifyFromActor : public UFlowNode_ComponentObserver public: virtual FString GetNodeDescription() const override; #endif - -private: - UPROPERTY() - FGameplayTag NotifyTag_DEPRECATED; }; diff --git a/Source/Flow/Public/Nodes/World/FlowNode_PlayLevelSequence.h b/Source/Flow/Public/Nodes/Actor/FlowNode_PlayLevelSequence.h similarity index 65% rename from Source/Flow/Public/Nodes/World/FlowNode_PlayLevelSequence.h rename to Source/Flow/Public/Nodes/Actor/FlowNode_PlayLevelSequence.h index ac526fe1c..02eaa1b58 100644 --- a/Source/Flow/Public/Nodes/World/FlowNode_PlayLevelSequence.h +++ b/Source/Flow/Public/Nodes/Actor/FlowNode_PlayLevelSequence.h @@ -1,11 +1,12 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "EngineDefines.h" +#include "Engine/StreamableManager.h" #include "LevelSequencePlayer.h" #include "MovieSceneSequencePlayer.h" +#include "Interfaces/FlowPreloadableInterface.h" #include "Nodes/FlowNode.h" #include "FlowNode_PlayLevelSequence.generated.h" @@ -21,11 +22,18 @@ 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_UCLASS_BODY() + GENERATED_BODY() + +public: + UFlowNode_PlayLevelSequence(); + friend struct FFlowTrackExecutionToken; +public: static FFlowNodeLevelSequenceEvent OnPlaybackStarted; static FFlowNodeLevelSequenceEvent OnPlaybackCompleted; @@ -38,31 +46,36 @@ class FLOW_API UFlowNode_PlayLevelSequence : public UFlowNode UPROPERTY(EditAnywhere, Category = "Sequence") bool bPlayReverse; - UPROPERTY(EditAnywhere, Category = "Sequence") - bool bReplicates; - UPROPERTY(EditAnywhere, Category = "Sequence") FLevelSequenceCameraSettings CameraSettings; - // Level Sequence playback can be moved to any place in the world by applying Transform Origin - // Enabling this option will use actor that created Root Flow instance, i.e. World Settings or Player Controller - // https://docs.unrealengine.com/5.0/en-US/creating-level-sequences-with-dynamic-transforms-in-unreal-engine/ + /* Level Sequence playback can be moved to any place in the world by applying Transform Origin. + * Enabling this option will use actor that created Root Flow instance, i.e. World Settings or Player Controller/ + * See https://docs.unrealengine.com/5.0/en-US/creating-level-sequences-with-dynamic-transforms-in-unreal-engine/ */ UPROPERTY(EditAnywhere, Category = "Sequence") bool bUseGraphOwnerAsTransformOrigin; - - // if True, Play Rate will by multiplied by Custom Time Dilation - // Enabling this option will use Custom Time Dilation from actor that created Root Flow instance, i.e. World Settings or Player Controller + + /* If true, playback of this level sequence on the server will be synchronized across other clients. */ + UPROPERTY(EditAnywhere, Category = "Sequence") + bool bReplicates; + + /* Always relevant for network (overrides bOnlyRelevantToOwner). */ + UPROPERTY(EditAnywhere, Category = "Sequence") + bool bAlwaysRelevant; + + /* If True, Play Rate will by multiplied by Custom Time Dilation. + * Enabling this option will use Custom Time Dilation from actor that created Root Flow instance, i.e. World Settings or Player Controller. */ UPROPERTY(EditAnywhere, Category = "Sequence") bool bApplyOwnerTimeDilation; protected: UPROPERTY() - ULevelSequence* LoadedSequence; + TObjectPtr LoadedSequence; UPROPERTY() - UFlowLevelSequencePlayer* SequencePlayer; + TObjectPtr SequencePlayer; - // Play Rate set by the user in PlaybackSettings + /* Play Rate set by the user in PlaybackSettings. */ float CachedPlayRate; UPROPERTY(SaveGame) @@ -74,16 +87,24 @@ class FLOW_API UFlowNode_PlayLevelSequence : public UFlowNode UPROPERTY(SaveGame) float TimeDilation; + FStreamableManager StreamableManager; + + TSharedPtr PreloadHandle; + public: #if WITH_EDITOR + // IFlowContextPinSupplierInterface virtual bool SupportsContextPins() const override { return true; } - virtual TArray GetContextOutputs() override; + virtual TArray GetContextOutputs() const override; + // -- 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(); @@ -115,6 +136,8 @@ class FLOW_API UFlowNode_PlayLevelSequence : public UFlowNode #if WITH_EDITOR virtual FString GetNodeDescription() const override; + virtual EDataValidationResult ValidateNode() override; + virtual FString GetStatusString() const override; virtual UObject* GetAssetToEdit() override; #endif diff --git a/Source/Flow/Public/Nodes/Utils/FlowNode_Log.h b/Source/Flow/Public/Nodes/Developer/FlowNode_Log.h similarity index 57% rename from Source/Flow/Public/Nodes/Utils/FlowNode_Log.h rename to Source/Flow/Public/Nodes/Developer/FlowNode_Log.h index 5c74fcc5b..9f0495786 100644 --- a/Source/Flow/Public/Nodes/Utils/FlowNode_Log.h +++ b/Source/Flow/Public/Nodes/Developer/FlowNode_Log.h @@ -1,11 +1,12 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once -#include "Nodes/FlowNode.h" +#include "Nodes/Graph/FlowNode_DefineProperties.h" #include "FlowNode_Log.generated.h" -// Variant of ELogVerbosity +/** + * Variant of ELogVerbosity. + */ UENUM(BlueprintType) enum class EFlowLogVerbosity : uint8 { @@ -21,13 +22,18 @@ enum class EFlowLogVerbosity : uint8 * Adds message to log * Optionally shows message on screen */ -UCLASS(NotBlueprintable, meta = (DisplayName = "Log")) -class FLOW_API UFlowNode_Log : public UFlowNode +UCLASS(NotBlueprintable, meta = (DisplayName = "Log", Keywords = "print")) +class FLOW_API UFlowNode_Log : public UFlowNode_DefineProperties { - GENERATED_UCLASS_BODY() - + GENERATED_BODY() + +public: + UFlowNode_Log(); + private: - UPROPERTY(EditAnywhere, Category = "Flow") + /* The message to write to the log. + * If the Message input pin is not connected to another source. */ + UPROPERTY(EditAnywhere, Category = "Flow", meta = (DefaultForInputFlowPin, FlowPinType = String)) FString Message; UPROPERTY(EditAnywhere, Category = "Flow") @@ -36,17 +42,30 @@ class FLOW_API UFlowNode_Log : public UFlowNode UPROPERTY(EditAnywhere, Category = "Flow") bool bPrintToScreen; - UPROPERTY(EditAnywhere, Category = "Flow", meta = (EditCondition = "bPrintToScreen")) + UPROPERTY(EditAnywhere, Category = "Flow", meta = (EditCondition = "bPrintToScreen", EditConditionHides)) float Duration; - UPROPERTY(EditAnywhere, Category = "Flow", meta = (EditCondition = "bPrintToScreen")) + UPROPERTY(EditAnywhere, Category = "Flow", meta = (EditCondition = "bPrintToScreen", EditConditionHides)) FColor TextColor; protected: + // IFlowCoreExecutableInterface virtual void ExecuteInput(const FName& PinName) override; + // -- #if WITH_EDITOR public: - virtual FString GetNodeDescription() const override; + // UObject + virtual void PostEditChangeChainProperty(FPropertyChangedChainEvent& PropertyChangedEvent) override; + // -- + + // UFlowNodeBase + virtual void OnEditorPinConnectionsChanged(const TArray& Changes) override; + // -- + + virtual void UpdateNodeConfigText_Implementation() override; #endif + +public: + EFlowLogVerbosity GetVerbosity() const { return Verbosity; } }; diff --git a/Source/Flow/Public/Nodes/FlowNode.h b/Source/Flow/Public/Nodes/FlowNode.h index f0d20c63b..4e9bbb8a3 100644 --- a/Source/Flow/Public/Nodes/FlowNode.h +++ b/Source/Flow/Public/Nodes/FlowNode.h @@ -1,112 +1,118 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once -#include "CoreMinimal.h" #include "EdGraph/EdGraphNode.h" -#include "Engine/StreamableManager.h" #include "GameplayTagContainer.h" +#include "StructUtils/InstancedStruct.h" +#include "UObject/TextProperty.h" #include "VisualLogger/VisualLoggerDebugSnapshotInterface.h" +#include "FlowNodeBase.h" #include "FlowTypes.h" +#include "Interfaces/FlowDataPinValueSupplierInterface.h" #include "Nodes/FlowPin.h" +#include "Types/FlowArray.h" +#include "Types/FlowAutoDataPinsWorkingData.h" +#include "Types/FlowPinConnectionChange.h" #include "FlowNode.generated.h" -class UFlowAsset; -class UFlowSubsystem; - -#if WITH_EDITOR -DECLARE_DELEGATE(FFlowNodeEvent); -#endif +struct FFlowNodeSaveData; +struct FFlowPreloadHelper; /** * A Flow Node is UObject-based node designed to handle entire gameplay feature within single node. */ UCLASS(Abstract, Blueprintable, HideCategories = Object) -class FLOW_API UFlowNode : public UObject, public IVisualLoggerDebugSnapshotInterface +class FLOW_API UFlowNode : public UFlowNodeBase + , public IFlowDataPinValueSupplierInterface + , public IVisualLoggerDebugSnapshotInterface { - GENERATED_UCLASS_BODY() + GENERATED_BODY() + +public: + UFlowNode(); friend class SFlowGraphNode; friend class UFlowAsset; friend class UFlowGraphNode; - friend class UFlowGraphSchema; + friend class UFlowNodeAddOn; friend class SFlowInputPinHandle; friend class SFlowOutputPinHandle; ////////////////////////////////////////////////////////////////////////// // Node -private: - UPROPERTY() - UEdGraphNode* GraphNode; - #if WITH_EDITORONLY_DATA + protected: UPROPERTY() - FString Category; - - UPROPERTY(EditDefaultsOnly, Category = "FlowNode") - EFlowNodeStyle NodeStyle; + TArray> AllowedAssetClasses; - uint8 bCanDelete : 1; - uint8 bCanDuplicate : 1; - - UPROPERTY(EditDefaultsOnly, Category = "FlowNode") - bool bNodeDeprecated; - - // If this node is deprecated, it might be replaced by another node - UPROPERTY(EditDefaultsOnly, Category = "FlowNode") - TSubclassOf ReplacedBy; + UPROPERTY() + TArray> DeniedAssetClasses; +#endif public: - FFlowNodeEvent OnReconstructionRequested; + // UFlowNodeBase + virtual UFlowNode* GetFlowNodeSelfOrOwner() override { return this; } + virtual bool IsSupportedInputPinName(const FName& PinName) const override; + // -- + +#if WITH_EDITOR + /* Set up UFlowNodeBase when being opened for edit in the editor. */ + virtual void SetupForEditing(UEdGraphNode& EdGraphNode) override; + + /** + * Editor-only: ensure any editor-time parent pointers are correctly set for this node and any child AddOns. + * Goal: AddOns always have a valid FlowNode pointer while being edited (creation/paste/undo/reconstruct/open). + * Safe to call repeatedly. + */ + virtual void EnsureAddOnFlowNodePointersForEditor(); #endif public: -#if WITH_EDITOR // UObject - virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; virtual void PostLoad() override; // -- - // Opportunity to update node's data before UFlowGraphNode would call ReconstructNode() - virtual void FixNode(UEdGraphNode* NewGraph); +#if WITH_EDITOR + // UObject + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; + // -- #endif - UEdGraphNode* GetGraphNode() const { return GraphNode; } + /* Inherits Guid after graph node. */ + UPROPERTY() + FGuid NodeGuid; -#if WITH_EDITOR - void SetGraphNode(UEdGraphNode* NewGraph); +public: + UFUNCTION(BlueprintCallable, Category = "FlowNode") + void SetGuid(const FGuid& NewGuid) { NodeGuid = NewGuid; } - virtual FString GetNodeCategory() const; - virtual FText GetNodeTitle() const; - virtual FText GetNodeToolTip() const; - - // This method allows to have different for every node instance, i.e. Red if node represents enemy, Green if node represents a friend - virtual bool GetDynamicTitleColor(FLinearColor& OutColor) const { return false; } + UFUNCTION(BlueprintPure, Category = "FlowNode") + const FGuid& GetGuid() const { return NodeGuid; } - EFlowNodeStyle GetNodeStyle() const { return NodeStyle; } + /* Returns a random seed suitable for this flow node, + * by default based on the node Guid, + * but may be overridden in subclasses to supply some other value. */ + virtual int32 GetRandomSeed() const override { return GetTypeHash(NodeGuid); } - // Short summary of node's content - displayed over node as NodeInfoPopup - virtual FString GetNodeDescription() const; -#endif + virtual const UFlowNode* GetParentNode() const override + { + return UFlowNodeBase::GetFlowNodeSelfOrOwner(); + } + +public: + virtual bool CanFinishGraph() const { return K2_CanFinishGraph(); } protected: - // Short summary of node's content - displayed over node as NodeInfoPopup - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "GetNodeDescription")) - FString K2_GetNodeDescription() const; + UPROPERTY(EditDefaultsOnly, Category = "FlowNode") + TArray AllowedSignalModes; - // Inherits Guid after graph node + /* If enabled, signal will pass through node without calling ExecuteInput(). + * Designed to handle patching already released games. */ UPROPERTY() - FGuid NodeGuid; - -public: - void SetGuid(const FGuid NewGuid) { NodeGuid = NewGuid; } - FGuid GetGuid() const { return NodeGuid; } - - UFUNCTION(BlueprintPure, Category = "FlowNode") - UFlowAsset* GetFlowAsset() const; + EFlowSignalMode SignalMode; ////////////////////////////////////////////////////////////////////////// // All created pins (default, class-specific and added by user) @@ -116,23 +122,34 @@ class FLOW_API UFlowNode : public UObject, public IVisualLoggerDebugSnapshotInte static FFlowPin DefaultOutputPin; protected: - // Class-specific and user-added inputs + /* Class-specific and user-added inputs. */ UPROPERTY(EditDefaultsOnly, Category = "FlowNode") TArray InputPins; - // Class-specific and user-added outputs + /* Class-specific and user-added outputs. */ UPROPERTY(EditDefaultsOnly, Category = "FlowNode") TArray OutputPins; - void AddInputPins(TArray PinNames); - void AddOutputPins(TArray PinNames); + void AddInputPins(const TArray& Pins); + void AddOutputPins(const TArray& Pins); + +#if WITH_EDITOR + /* Utility function to rebuild a pin array in editor (either InputPins or OutputPins, passed as InOutPins) + * returns true if the InOutPins array was rebuilt. */ + bool RebuildPinArray(const TArray& NewPinNames, TArray& InOutPins, const FFlowPin& DefaultPin); + bool RebuildPinArray(const TArray& NewPins, TArray& InOutPins, const FFlowPin& DefaultPin); +#endif - // always use default range for nodes with user-created outputs i.e. Execution Sequence + /* Always use default range for nodes with user-created outputs i.e. Execution Sequence. */ void SetNumberedInputPins(const uint8 FirstNumber = 0, const uint8 LastNumber = 1); void SetNumberedOutputPins(const uint8 FirstNumber = 0, const uint8 LastNumber = 1); - TArray GetInputPins() const { return InputPins; } - TArray GetOutputPins() const { return OutputPins; } + uint8 CountNumberedInputs() const; + uint8 CountNumberedOutputs() const; + +public: + const TArray& GetInputPins() const { return InputPins; } + const TArray& GetOutputPins() const { return OutputPins; } UFUNCTION(BlueprintPure, Category = "FlowNode") TArray GetInputNames() const; @@ -140,206 +157,320 @@ class FLOW_API UFlowNode : public UObject, public IVisualLoggerDebugSnapshotInte UFUNCTION(BlueprintPure, Category = "FlowNode") TArray GetOutputNames() const; -public: #if WITH_EDITOR - virtual bool SupportsContextPins() const { return false; } - - // Be careful, enabling it might cause loading gigabytes of data as nodes would load all related data (i.e. Level Sequences) - virtual bool CanRefreshContextPinsOnLoad() const { return false; } - - virtual TArray GetContextInputs() { return TArray(); } - virtual TArray GetContextOutputs() { return TArray(); } + // IFlowContextPinSupplierInterface + virtual bool SupportsContextPins() const override; + virtual TArray GetContextInputs() const override; + virtual TArray GetContextOutputs() const override; + // -- virtual bool CanUserAddInput() const; virtual bool CanUserAddOutput() const; - void RemoveUserInput(); - void RemoveUserOutput(); + void RemoveUserInput(const FName& PinName); + void RemoveUserOutput(const FName& PinName); #endif protected: - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "CanUserAddInput")) + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "Can Finish Graph")) + bool K2_CanFinishGraph() const; + + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "Can User Add Input")) bool K2_CanUserAddInput() const; - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "CanUserAddOutput")) + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "Can User Add Output")) bool K2_CanUserAddOutput() const; ////////////////////////////////////////////////////////////////////////// // Connections to other nodes -private: - // Map outputs to the connected node and input pin +protected: + /* Map input/outputs to the connected node and input pin. */ UPROPERTY() TMap Connections; public: - void SetConnections(const TMap& InConnections) { Connections = InConnections; } +#if WITH_EDITOR + void SetConnections(const TMap& InConnections); +#endif + FConnectedPin GetConnection(const FName OutputName) const { return Connections.FindRef(OutputName); } - TSet GetConnectedNodes() const; UFUNCTION(BlueprintPure, Category= "FlowNode") - bool IsInputConnected(const FName& PinName) const; + TSet GatherConnectedNodes() const; + + FName GetPinConnectedToNode(const FGuid& OtherNodeGuid); + + UFUNCTION(BlueprintPure, Category= "FlowNode") + bool IsInputConnected(const FName& PinName, bool bErrorIfPinNotFound = true) const; UFUNCTION(BlueprintPure, Category= "FlowNode") - bool IsOutputConnected(const FName& PinName) const; + bool IsOutputConnected(const FName& PinName, bool bErrorIfPinNotFound = true) const; + + // Preferred signatures for: + // - exec output pins + // - data input pins + // ... otherwise use the array signatures below + bool FindFirstInputPinConnection(const FName& PinName, bool bErrorIfPinNotFound, FConnectedPin& FirstConnectedPin) const; + bool FindFirstOutputPinConnection(const FName& PinName, bool bErrorIfPinNotFound, FConnectedPin& FirstConnectedPin) const; + bool FindFirstInputPinConnection(const FFlowPin& FlowPin, FConnectedPin& FirstConnectedPin) const; + bool FindFirstOutputPinConnection(const FFlowPin& FlowPin, FConnectedPin& FirstConnectedPin) const; + + // Preferred signatures for: + // - exec input pins + // - data output pins + // - cases where you do not need the connection info (with ConnectedPins == nullptr) + // ... otherwise use the non-array signatures above + bool FindInputPinConnections(const FName& PinName, bool bErrorIfPinNotFound, TArray* ConnectedPins = nullptr) const; + bool FindOutputPinConnections(const FName& PinName, bool bErrorIfPinNotFound, TArray* ConnectedPins = nullptr) const; + bool FindInputPinConnections(const FFlowPin& FlowPin, TArray* ConnectedPins = nullptr) const; + bool FindOutputPinConnections(const FFlowPin& FlowPin, TArray* ConnectedPins = nullptr) const; + + FFlowPin* FindInputPinByName(const FName& PinName); + FFlowPin* FindOutputPinByName(const FName& PinName); + const FFlowPin* FindInputPinByName(const FName& PinName) const { return const_cast(this)->FindInputPinByName(PinName); } + const FFlowPin* FindOutputPinByName(const FName& PinName) const { return const_cast(this)->FindOutputPinByName(PinName); } static void RecursiveFindNodesByClass(UFlowNode* Node, const TSubclassOf Class, uint8 Depth, TArray& OutNodes); +protected: + /* Slow and fast lookup functions, based on whether we are proactively caching the connections for quick lookup + * in the Connections array (by PinCategory). */ + bool FindConnectedNodeForPinCached(const FName& FlowPinName, FConnectedPin& ConnectedPin) const; + bool FindConnectedNodeForPinUncached(const FName& FlowPinName, TArray* ConnectedPins = nullptr) const; + + /* Helper templates for Find*PinConnection* functions */ + template + bool FindFirstPinConnection(const FFlowPin& FlowPin, const TArray& FlowPinArray, FConnectedPin& FirstConnectedPin) const; + template + bool FindPinConnections(const FFlowPin& FlowPin, const TArray& FlowPinArray, TArray* ConnectedPins) const; + + /* Return all connections to a Pin this Node knows about. + * Connections are only stored on one of the Nodes they connect depending on pin type. + * As such, this function may not return anything even if the Node is connected to the Pin. + * Use UFlowAsset::GetAllPinsConnectedToPin() to do a guaranteed find of all Connections. */ + TArray GetKnownConnectionsToPin(const FConnectedPin& Pin) const; + +#if WITH_EDITOR + static void BuildConnectionChangeList( + const UFlowAsset& FlowAsset, + const TMap& OldConnections, + const TMap& NewConnections, + TArray& OutChanges); + + /* Broadcasts OnEditorPinConnectionsChanged to this node and all AddOns */ + void BroadcastEditorPinConnectionsChanged(const TArray& Changes); +#endif + +////////////////////////////////////////////////////////////////////////// +// Data Pins + +public: + using TFlowPinValueSupplierDataArray = FlowArray::TInlineArray; + + /* Map for PinName to Property supplier for non-trivial data pin property lookups. + * Non-trivial means a different pin name from its property source, or a non-zero property owner object index. + * See TryGatherPropertyOwnersAndPopulateResult(). */ + UPROPERTY() + TMap MapDataPinNameToPropertySource; + +#if WITH_EDITORONLY_DATA +protected: + UPROPERTY(VisibleDefaultsOnly, AdvancedDisplay, Category = "FlowNode", meta = (GetByRef)) + TArray AutoInputDataPins; + + UPROPERTY(VisibleDefaultsOnly, AdvancedDisplay, Category = "FlowNode", meta = (GetByRef)) + TArray AutoOutputDataPins; +#endif + +#if WITH_EDITOR +public: + bool TryUpdateAutoDataPins(); +#endif + + // IFlowDataPinValueSupplierInterface +public: + virtual FFlowDataPinResult TrySupplyDataPin(FName PinName) const override; + +protected: + /* Helper for TryGetFlowDataPinSupplierDatasForPinName(). */ + void TryAddSupplierDataToArray(FFlowPinValueSupplierData& InOutSupplierData, TFlowPinValueSupplierDataArray& InOutPinValueSupplierDatas) const; + +public: + /* Advanced helper for TrySupplyDataPin, which can be overridden in subclasses to provide additional or replacement object(s) + * for sourcing the properties for the given pin name. These objects will have PopulateResult called on them. + * This function is used for cases like ExecuteComponent. */ + virtual void GatherDataPinValueOwnerCollection(FFlowDataPinValueOwnerCollection& ValueOwnerCollection) const; + + bool TryGatherPropertyOwnersAndPopulateResult( + const FName& PinName, + const FFlowPinType& DataPinType, + const FFlowPin& FlowPin, + FFlowDataPinResult& OutSuppliedResult) const; + + bool TryGetFlowDataPinSupplierDatasForPinName(const FName& PinName, TFlowPinValueSupplierDataArray& InOutPinValueSupplierDatas) const; + + // #FlowDataPinLegacy +public: + void FixupDataPinTypes(); + +protected: + static void FixupDataPinTypesForArray(TArray& MutableDataPinArray); + static void FixupDataPinTypesForPin(FFlowPin& MutableDataPin); + // -- + ////////////////////////////////////////////////////////////////////////// // Debugger + protected: static FString MissingIdentityTag; static FString MissingNotifyTag; static FString MissingClass; static FString NoActorsFound; +#if WITH_EDITOR + +protected: + virtual EDataValidationResult ValidateNode() override; + void ValidateFlowPinArrayIsUnique(const TArray& FlowPins, TSet& InOutUniquePinNames, EDataValidationResult& InOutResult); +#endif + ////////////////////////////////////////////////////////////////////////// // Executing node instance public: - bool bPreloaded; + // IFlowCoreExecutableInterface + virtual void InitializeInstance() override; + virtual void DeinitializeInstance() override; -protected: - FStreamableManager StreamableManager; + virtual void OnActivate() override; + virtual void Cleanup() override; + virtual void ExecuteInput(const FName& PinName) override; + // -- +protected: UPROPERTY(SaveGame) EFlowNodeState ActivationState; -public: +public: EFlowNodeState GetActivationState() const { return ActivationState; } - + bool HasFinished() const { return EFlowNodeState_Classifiers::IsFinishedState(ActivationState); } + #if !UE_BUILD_SHIPPING -private: + +protected: TMap> InputRecords; TMap> OutputRecords; #endif -public: - UFUNCTION(BlueprintPure, Category = "FlowNode") - UFlowSubsystem* GetFlowSubsystem() const; - - virtual UWorld* GetWorld() const override; +protected: + /* Trigger execution of input pin. */ + void TriggerInput(const FName& PinName, const EFlowPinActivationType ActivationType = EFlowPinActivationType::Default); protected: - // Method called just after creating the node instance, while initializing the Flow Asset instance - // This happens before executing graph, only called during gameplay - virtual void InitializeInstance(); + void Deactivate(); - // Event called just after creating the node instance, while initializing the Flow Asset instance - // This happens before executing graph, only called during gameplay - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "InitInstance")) - void K2_InitializeInstance(); +public: + virtual void TriggerFirstOutput(const bool bFinish) override; + virtual void TriggerOutput(FName PinName, const bool bFinish = false, const EFlowPinActivationType ActivationType = EFlowPinActivationType::Default) override; + virtual void Finish() override; + +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(); -protected: - virtual void PreloadContent(); - virtual void FlushContent(); + /* Returns true if this node's content is currently preloaded. */ + bool IsContentPreloaded() const; - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "PreloadContent")) - void K2_PreloadContent(); + /* 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(); - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "FlushContent")) - void K2_FlushContent(); +protected: + /* Instanced preload helper allocated at InitializeInstance for nodes implementing IFlowPreloadableInterface. + * Remains uninitialized (invalid) for non-preloadable nodes. */ + UPROPERTY(Transient) + TInstancedStruct PreloadHelper; - // Trigger execution of input pin - void TriggerInput(const FName& PinName, const bool bForcedActivation = false); + bool TryInitializePreloadHelper(); + void DeinitializePreloadHelper(); - // Method reacting on triggering Input pin - virtual void ExecuteInput(const FName& PinName); + /* Forwards PinName to the PreloadHelper if one exists. Returns true if the helper consumed the pin. */ + bool DispatchExecuteInputToPreloadHelper(const FName& PinName); - // Event reacting on triggering Input pin - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "ExecuteInput")) - void K2_ExecuteInput(const FName& PinName); +////////////////////////////////////////////////////////////////////////// +// SaveGame support - // Simply trigger the first Output Pin, convenient to use if node has only one output +public: UFUNCTION(BlueprintCallable, Category = "FlowNode") - void TriggerFirstOutput(const bool bFinish); - - UFUNCTION(BlueprintCallable, Category = "FlowNode", meta = (HidePin = "bForcedActivation")) - void TriggerOutput(const FName& PinName, const bool bFinish = false, const bool bForcedActivation = false); - - void TriggerOutput(const FString& PinName, const bool bFinish = false); - void TriggerOutput(const FText& PinName, const bool bFinish = false); - void TriggerOutput(const TCHAR* PinName, const bool bFinish = false); - - UFUNCTION(BlueprintCallable, Category = "FlowNode", meta = (HidePin = "bForcedActivation")) - void TriggerOutputPin(const FFlowOutputPinHandle Pin, const bool bFinish = false, const bool bForcedActivation = false); + void SaveInstance(FFlowNodeSaveData& NodeRecord); - // Finish execution of node, it will call Cleanup UFUNCTION(BlueprintCallable, Category = "FlowNode") - void Finish(); - - void Deactivate(); + void LoadInstance(const FFlowNodeSaveData& NodeRecord); - // Method called after node finished the work - virtual void Cleanup(); +protected: + UFUNCTION(BlueprintNativeEvent, Category = "FlowNode") + void OnSave(); - // Event called after node finished the work - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "Cleanup")) - void K2_Cleanup(); + UFUNCTION(BlueprintNativeEvent, Category = "FlowNode") + void OnLoad(); -public: - // Define what happens when node is terminated from the outside - virtual void ForceFinishNode(); + UFUNCTION(BlueprintNativeEvent, Category = "FlowNode") + void OnPassThrough(); -protected: - // Define what happens when node is terminated from the outside - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "ForceFinishNode")) - void K2_ForceFinishNode(); + UFUNCTION(BlueprintNativeEvent, Category = "FlowNode") + bool ShouldSave(); -private: - void ResetRecords(); +////////////////////////////////////////////////////////////////////////// +// Utils -#if WITH_EDITOR public: +#if WITH_EDITOR UFlowNode* GetInspectedInstance() const; TMap GetWireRecords() const; TArray GetPinRecords(const FName& PinName, const EEdGraphPinDirection PinDirection) const; +#endif - // Information displayed while node is working - displayed over node as NodeInfoPopup - virtual FString GetStatusString() const; + /* Information displayed while node is working - displayed over node as NodeInfoPopup. */ + FString GetStatusStringForNodeAndAddOns() const; + +#if WITH_EDITOR virtual bool GetStatusBackgroundColor(FLinearColor& OutColor) const; +#endif +protected: + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "Get Status Background Color")) + bool K2_GetStatusBackgroundColor(FLinearColor& OutColor) const; + +#if WITH_EDITOR + +public: virtual FString GetAssetPath(); virtual UObject* GetAssetToEdit(); virtual AActor* GetActorToFocus(); #endif protected: - // Information displayed while node is working - displayed over node as NodeInfoPopup - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "GetStatusString")) - FString K2_GetStatusString() const; - - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "GetStatusBackgroundColor")) - bool K2_GetStatusBackgroundColor(FLinearColor& OutColor) const; - - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "GetAssetPath")) + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "Get Asset Path")) FString K2_GetAssetPath(); - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "GetAssetToEdit")) + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "Get Asset To Edit")) UObject* K2_GetAssetToEdit(); - UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "GetActorToFocus")) + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "Get Actor To Focus")) AActor* K2_GetActorToFocus(); - template - T* LoadAsset(TSoftObjectPtr AssetPtr) - { - ensure(!AssetPtr.IsNull()); - - if (AssetPtr.IsPending()) - { - const FSoftObjectPath& AssetRef = AssetPtr.ToSoftObjectPath(); - AssetPtr = Cast(StreamableManager.LoadSynchronous(AssetRef, false)); - } - - return Cast(AssetPtr.Get()); - } - public: UFUNCTION(BlueprintPure, Category = "FlowNode") static FString GetIdentityTagDescription(const FGameplayTag& Tag); @@ -355,27 +486,4 @@ class FLOW_API UFlowNode : public UObject, public IVisualLoggerDebugSnapshotInte UFUNCTION(BlueprintPure, Category = "FlowNode") static FString GetProgressAsString(float Value); - - UFUNCTION(BlueprintCallable, Category = "FlowNode") - void LogError(FString Message, const EFlowOnScreenMessageType OnScreenMessageType = EFlowOnScreenMessageType::Permanent) const; - - UFUNCTION(BlueprintCallable, Category = "FlowNode") - void SaveInstance(FFlowNodeSaveData& NodeRecord); - - UFUNCTION(BlueprintCallable, Category = "FlowNode") - void LoadInstance(const FFlowNodeSaveData& NodeRecord); - -protected: - UFUNCTION(BlueprintNativeEvent, Category = "FlowNode") - void OnSave(); - - UFUNCTION(BlueprintNativeEvent, Category = "FlowNode") - void OnLoad(); - -private: - UPROPERTY() - TArray InputNames_DEPRECATED; - - UPROPERTY() - TArray OutputNames_DEPRECATED; }; diff --git a/Source/Flow/Public/Nodes/FlowNodeAddOnBlueprint.h b/Source/Flow/Public/Nodes/FlowNodeAddOnBlueprint.h new file mode 100644 index 000000000..9f4cf0f1b --- /dev/null +++ b/Source/Flow/Public/Nodes/FlowNodeAddOnBlueprint.h @@ -0,0 +1,23 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "CoreMinimal.h" +#include "Engine/Blueprint.h" +#include "FlowNodeAddOnBlueprint.generated.h" + +/** + * Flow Node AddOn Blueprint class + */ +UCLASS(BlueprintType) +class FLOW_API UFlowNodeAddOnBlueprint : public UBlueprint +{ + GENERATED_UCLASS_BODY() + +#if WITH_EDITOR + // UBlueprint + virtual bool SupportedByDefaultBlueprintFactory() const override { return false; } + + virtual bool SupportsDelegates() const override { return false; } + // -- +#endif +}; diff --git a/Source/Flow/Public/Nodes/FlowNodeBase.h b/Source/Flow/Public/Nodes/FlowNodeBase.h new file mode 100644 index 000000000..c3c1a4d02 --- /dev/null +++ b/Source/Flow/Public/Nodes/FlowNodeBase.h @@ -0,0 +1,621 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Runtime/Engine/Public/DrawDebugHelpers.h" +#include "Templates/SubclassOf.h" + +#include "Interfaces/FlowCoreExecutableInterface.h" +#include "Interfaces/FlowContextPinSupplierInterface.h" +#include "Interfaces/FlowDataPinValueOwnerInterface.h" +#include "FlowMessageLog.h" +#include "FlowTags.h" // used by subclasses +#include "FlowTypes.h" +#include "Types/FlowDataPinResults.h" +#include "Types/FlowPinConnectionChange.h" +#include "Types/FlowPinTypeTemplates.h" + +#include "FlowNodeBase.generated.h" + +class UFlowAsset; +class UFlowNode; +class UFlowNodeAddOn; +class UFlowSubsystem; +class UEdGraphNode; +class IFlowDataPinValueSupplierInterface; +struct FFlowAutoDataPinsWorkingData; +struct FFlowNamedDataPinProperty; +struct FFlowPin; +struct FFlowPinType; + +#if WITH_EDITORONLY_DATA +DECLARE_DELEGATE(FFlowNodeEvent); +#endif + +/** + * Describes an overlay icon to display on a flow node in the editor. + */ +USTRUCT() +struct FLOW_API FFlowNodeOverlayIcon +{ + GENERATED_BODY() + + FFlowNodeOverlayIcon() = default; + + explicit FFlowNodeOverlayIcon(const FName& InBrushName, const FVector2D& InOffset = FVector2D::ZeroVector, const FName& InStyleSetName = NAME_None) + : BrushName(InBrushName) + , Offset(InOffset) + , StyleSetName(InStyleSetName) + { + } + + /* Name of the brush to use for the icon */ + UPROPERTY() + FName BrushName = NAME_None; + + /* Offset from the top-left corner of the node (position X moves right, positive Y moves down) */ + UPROPERTY() + FVector2D Offset = FVector2D::ZeroVector; + + /* Name of the StyleSet that contains your brush. If left empty Flow will first search the default Flow StyleSet and then the default Unreal StyleSet */ + UPROPERTY() + FName StyleSetName = NAME_None; +}; + +typedef TFunction FConstFlowNodeAddOnFunction; +typedef TFunction FFlowNodeAddOnFunction; + +/** + * Supplier + PinName (in that supplier) for a Flow Data Pin value. + */ +struct FFlowPinValueSupplierData +{ + FName SupplierPinName; + const IFlowDataPinValueSupplierInterface* PinValueSupplier = nullptr; +}; + +/** + * The base class for UFlowNode and UFlowNodeAddOn, with their shared functionality + */ +UCLASS(Abstract, BlueprintType, HideCategories = Object) +class FLOW_API UFlowNodeBase + : public UObject + , public IFlowCoreExecutableInterface + , public IFlowContextPinSupplierInterface + , public IFlowDataPinValueOwnerInterface +{ + GENERATED_BODY() + +public: + UFlowNodeBase(); + + friend class SFlowGraphNode; + friend class UFlowAsset; + friend class UFlowGraphNode; + friend class UFlowGraphSchema; + +////////////////////////////////////////////////////////////////////////// +// Node + +public: + // UObject + virtual UWorld* GetWorld() const override; + // -- + + // IFlowCoreExecutableInterface + virtual void InitializeInstance() override; + virtual void DeinitializeInstance() override; + + virtual void OnActivate() override; + virtual void ExecuteInput(const FName& PinName) override; + + virtual void ForceFinishNode() override; + virtual void Cleanup() override; + // -- + + /* Dispatcher for ExecuteInput to ensure the AddOns get their ExecuteInput calls even if the node/addon. */ + void ExecuteInputForSelfAndAddOns(const FName& PinName); + + /* Finish execution of node, it will call Cleanup. */ + UFUNCTION(BlueprintCallable, Category = "FlowNode") + virtual void Finish() PURE_VIRTUAL(Finish) + + /* Simply trigger the first Output Pin, convenient to use if node has only one output. */ + UFUNCTION(BlueprintCallable, Category = "FlowNode") + virtual void TriggerFirstOutput(const bool bFinish) PURE_VIRTUAL(TriggerFirstOutput) + + /* Cause a specific output to be triggered (by PinName). */ + UFUNCTION(BlueprintCallable, Category = "FlowNode", meta = (HidePin = "ActivationType")) + virtual void TriggerOutput(const FName PinName, const bool bFinish = false, const EFlowPinActivationType ActivationType = EFlowPinActivationType::Default) PURE_VIRTUAL(TriggerOutput) + + /* TriggerOutput convenience aliases. */ + void TriggerOutput(const FString& PinName, const bool bFinish = false); + void TriggerOutput(const FText& PinName, const bool bFinish = false); + void TriggerOutput(const TCHAR* PinName, const bool bFinish = false); + + /* Cause a specific output to be triggered (by PinHandle). */ + UFUNCTION(BlueprintCallable, Category = "FlowNode", meta = (HidePin = "ActivationType")) + virtual void TriggerOutputPin(const FFlowOutputPinHandle Pin, const bool bFinish = false, const EFlowPinActivationType ActivationType = EFlowPinActivationType::Default); + + /* Returns a random seed suitable for this flow node base. */ + UFUNCTION(BlueprintPure, Category = "FlowNode") + virtual int32 GetRandomSeed() const PURE_VIRTUAL(GetRandomSeed, return 0;); + + /* Returns the owning top-level Flow node. */ + virtual const UFlowNode* GetParentNode() const PURE_VIRTUAL(GetParentNode, return nullptr;); + +////////////////////////////////////////////////////////////////////////// +// Pins + +public: + static const FFlowPin* FindFlowPinByName(const FName& PinName, const TArray& FlowPins); + static FFlowPin* FindFlowPinByName(const FName& PinName, TArray& FlowPins); + virtual bool IsSupportedInputPinName(const FName& PinName) const PURE_VIRTUAL(IsSupportedInputPinName, return true;); + +#if WITH_EDITOR +public: + // IFlowContextPinSupplierInterface + virtual bool SupportsContextPins() const override { return IFlowContextPinSupplierInterface::SupportsContextPins(); } + virtual TArray GetContextInputs() const override; + virtual TArray GetContextOutputs() const override; + // -- +#endif + + /** Called in the editor when this node's pin connections change. */ + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", DisplayName = "On Editor Pin Connections Changed") + void K2_OnEditorPinConnectionsChanged(const TArray& Changes); + virtual void OnEditorPinConnectionsChanged(const TArray& Changes) { K2_OnEditorPinConnectionsChanged(Changes); } + +////////////////////////////////////////////////////////////////////////// +// Owners + +public: + UFUNCTION(BlueprintPure, Category = "FlowNode") + UFlowAsset* GetFlowAsset() const; + + const UFlowNode* GetFlowNodeSelfOrOwner() const; + virtual UFlowNode* GetFlowNodeSelfOrOwner() PURE_VIRTUAL(GetFlowNodeSelfOrOwner, return nullptr;); + + UFUNCTION(BlueprintPure, Category = "FlowNode") + UFlowSubsystem* GetFlowSubsystem() const; + + /* Gets the Owning Actor for this Node's RootFlow. + * If the immediate parent is an UActorComponent, it will get that Component's actor. */ + UFUNCTION(BlueprintCallable, Category = "FlowNode") + AActor* TryGetRootFlowActorOwner() const; + + /* Gets the Owning Object for this Node's RootFlow. */ + UFUNCTION(BlueprintCallable, Category = "FlowNode") + UObject* TryGetRootFlowObjectOwner() const; + + static TArray BuildFlowNodeBaseAncestorChain(UFlowNodeBase& FromFlowNodeBase, bool bIncludeFromFlowNodeBase); + +////////////////////////////////////////////////////////////////////////// +// AddOn support + +protected: + /* Flow Node AddOn attachments. */ + UPROPERTY(BlueprintReadOnly, Instanced, Category = "FlowNode") + TArray> AddOns; + +protected: + /* FlowNodes and AddOns may determine which AddOns are eligible to be their children. + * - AddOnTemplate - the template of the FlowNodeAddOn that is being considered to be added as a child. + * - AdditionalAddOnsToAssumeAreChildren - other AddOns to assume that are already child AddOns for the purposes of checking is AddOnTemplate is allowed. + * This list will be populated with the 'other' AddOns in a multi-paste operation in the editor, + * because some paste-targets can only accept a certain mix of addons, so we must know the rest of the set being pasted + * to make the correct decision about whether to allow AddOnTemplate to be added. + * See https://forums.unrealengine.com/t/default-parameters-with-tarrays/330225 for details on AutoCreateRefTerm. */ + UFUNCTION(BlueprintNativeEvent, BlueprintPure, Category = "FlowNode", meta = (AutoCreateRefTerm = AdditionalAddOnsToAssumeAreChildren)) + EFlowAddOnAcceptResult AcceptFlowNodeAddOnChild(const UFlowNodeAddOn* AddOnTemplate, const TArray& AdditionalAddOnsToAssumeAreChildren) const; + +public: + virtual const TArray& GetFlowNodeAddOnChildren() const { return AddOns; } + +#if WITH_EDITOR + virtual TArray& GetFlowNodeAddOnChildrenByEditor() { return MutableView(AddOns); } + EFlowAddOnAcceptResult CheckAcceptFlowNodeAddOnChild(const UFlowNodeAddOn* AddOnTemplate, const TArray& AdditionalAddOnsToAssumeAreChildren) const; +#endif + + bool IsClassOrImplementsInterface(const UClass& InterfaceOrClass) const + { + // InterfaceOrClass can either be the AddOn's UClass (or its superclass) + // or an interface (the UClass version) that its UClass implements + return IsA(&InterfaceOrClass) || GetClass()->ImplementsInterface(&InterfaceOrClass); + } + + template + bool IsClassOrImplementsInterface() const + { + return IsClassOrImplementsInterface(*TInterfaceOrClass::StaticClass()); + } + + /** + * Call a function for all of this object's AddOns (recursively iterating AddOns inside AddOn). + */ + + EFlowForEachAddOnFunctionReturnValue ForEachAddOnConst(const FConstFlowNodeAddOnFunction& Function, EFlowForEachAddOnChildRule AddOnChildRule = EFlowForEachAddOnChildRule::AllChildren) const; + EFlowForEachAddOnFunctionReturnValue ForEachAddOn(const FFlowNodeAddOnFunction& Function, EFlowForEachAddOnChildRule AddOnChildRule = EFlowForEachAddOnChildRule::AllChildren) const; + + template + EFlowForEachAddOnFunctionReturnValue ForEachAddOnForClassConst(const FConstFlowNodeAddOnFunction Function) const + { + return ForEachAddOnForClassConst(*TInterfaceOrClass::StaticClass(), Function, TAddOnChildRule); + } + + EFlowForEachAddOnFunctionReturnValue ForEachAddOnForClassConst(const UClass& InterfaceOrClass, const FConstFlowNodeAddOnFunction& Function, EFlowForEachAddOnChildRule AddOnChildRule = EFlowForEachAddOnChildRule::AllChildren) const; + + template + EFlowForEachAddOnFunctionReturnValue ForEachAddOnForClass(const FFlowNodeAddOnFunction Function) const + { + return ForEachAddOnForClass(*TInterfaceOrClass::StaticClass(), Function, TAddOnChildRule); + } + + EFlowForEachAddOnFunctionReturnValue ForEachAddOnForClass(const UClass& InterfaceOrClass, const FFlowNodeAddOnFunction& Function, EFlowForEachAddOnChildRule AddOnChildRule = EFlowForEachAddOnChildRule::AllChildren) const; + +public: + +////////////////////////////////////////////////////////////////////////// +// Data Pins + + // IFlowDataPinValueOwnerInterface +#if WITH_EDITOR +public: + virtual bool CanModifyFlowDataPinType() const override; + virtual bool ShowFlowDataPinValueInputPinCheckbox() const override; + virtual bool ShowFlowDataPinValueClassFilter(const FFlowDataPinValue* Value) const override; + virtual bool CanEditFlowDataPinValueClassFilter(const FFlowDataPinValue* Value) const override; + virtual void SetFlowDataPinValuesRebuildDelegate(FSimpleDelegate InDelegate) override + { + FlowDataPinValuesRebuildDelegate = InDelegate; + } + virtual void RequestFlowDataPinValuesDetailsRebuild() override + { + if (FlowDataPinValuesRebuildDelegate.IsBound()) + { + FlowDataPinValuesRebuildDelegate.Execute(); + } + } +private: + FSimpleDelegate FlowDataPinValuesRebuildDelegate; +protected: + // Helpers for IFlowDataPinValueOwnerInterface + bool IsPlacedInFlowAsset() const; + bool IsFlowNamedPropertiesSupplier() const; +#endif + // -- + +private: + UFUNCTION(BlueprintPure, Category = DataPins, DisplayName = "Resolve DataPin By Name") + FFlowDataPinResult TryResolveDataPin(FName PinName) const; + +public: + /* Generic single-value resolve & extractor. */ + template + EFlowDataPinResolveResult TryResolveDataPinValue(const FName& PinName, typename TFlowPinType::ValueType& OutValue, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue) const; + + /* Generic array-value resolve & extractor. */ + template + EFlowDataPinResolveResult TryResolveDataPinValues(const FName& PinName, TArray& OutValues) const; + + /* Special-case single-value resolve & extractor for native enums. */ + template requires std::is_enum_v + EFlowDataPinResolveResult TryResolveDataPinValue(const FName& PinName, TEnumType& OutValue, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue) const; + + /* Special-case array-value resolve & extractor for native enums. */ + template requires std::is_enum_v + EFlowDataPinResolveResult TryResolveDataPinValues(const FName& PinName, TArray& OutValues) const; + + /* Special-case single-value resolve & extractor for enums (as FName values). */ + template + EFlowDataPinResolveResult TryResolveDataPinValue(const FName& PinName, FName& OutEnumValue, UEnum*& OutEnumClass, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue) const; + + /* Special-case array-value resolve & extractor for enums (as FName values). */ + template + EFlowDataPinResolveResult TryResolveDataPinValues(const FName& PinName, TArray& OutEnumValues, UEnum*& OutEnumClass) const; + +public: + + // #FlowDataPinLegacy + UFUNCTION(BlueprintCallable, Category = DataPins, DisplayName = "Try Resolve DataPin As Bool", meta = (DeprecatedFunction, DeprecationMessage = "Use TryResolveDataPin (in blueprint) or TryResolveDataPinValue(s) (in code) instead")) + FFlowDataPinResult_Bool TryResolveDataPinAsBool(const FName& PinName) const; + + UFUNCTION(BlueprintCallable, Category = DataPins, DisplayName = "Try Resolve DataPin As Int", meta = (DeprecatedFunction, DeprecationMessage = "Use TryResolveDataPin (in blueprint) or TryResolveDataPinValue(s) (in code) instead")) + FFlowDataPinResult_Int TryResolveDataPinAsInt(const FName& PinName) const; + + UFUNCTION(BlueprintCallable, Category = DataPins, DisplayName = "Try Resolve DataPin As Float", meta = (DeprecatedFunction, DeprecationMessage = "Use TryResolveDataPin (in blueprint) or TryResolveDataPinValue(s) (in code) instead")) + FFlowDataPinResult_Float TryResolveDataPinAsFloat(const FName& PinName) const; + + UFUNCTION(BlueprintCallable, Category = DataPins, DisplayName = "Try Resolve DataPin As Name", meta = (DeprecatedFunction, DeprecationMessage = "Use TryResolveDataPin (in blueprint) or TryResolveDataPinValue(s) (in code) instead")) + FFlowDataPinResult_Name TryResolveDataPinAsName(const FName& PinName) const; + + UFUNCTION(BlueprintCallable, Category = DataPins, DisplayName = "Try Resolve DataPin As String", meta = (DeprecatedFunction, DeprecationMessage = "Use TryResolveDataPin (in blueprint) or TryResolveDataPinValue(s) (in code) instead")) + FFlowDataPinResult_String TryResolveDataPinAsString(const FName& PinName) const; + + UFUNCTION(BlueprintCallable, Category = DataPins, DisplayName = "Try Resolve DataPin As Text", meta = (DeprecatedFunction, DeprecationMessage = "Use TryResolveDataPin (in blueprint) or TryResolveDataPinValue(s) (in code) instead")) + FFlowDataPinResult_Text TryResolveDataPinAsText(const FName& PinName) const; + + UFUNCTION(BlueprintCallable, Category = DataPins, DisplayName = "Try Resolve DataPin As Enum", meta = (DeprecatedFunction, DeprecationMessage = "Use TryResolveDataPin (in blueprint) or TryResolveDataPinValue(s) (in code) instead")) + FFlowDataPinResult_Enum TryResolveDataPinAsEnum(const FName& PinName) const; + + UFUNCTION(BlueprintCallable, Category = DataPins, DisplayName = "Try Resolve DataPin As Vector", meta = (DeprecatedFunction, DeprecationMessage = "Use TryResolveDataPin (in blueprint) or TryResolveDataPinValue(s) (in code) instead")) + FFlowDataPinResult_Vector TryResolveDataPinAsVector(const FName& PinName) const; + + UFUNCTION(BlueprintCallable, Category = DataPins, DisplayName = "Try Resolve DataPin As Rotator", meta = (DeprecatedFunction, DeprecationMessage = "Use TryResolveDataPin (in blueprint) or TryResolveDataPinValue(s) (in code) instead")) + FFlowDataPinResult_Rotator TryResolveDataPinAsRotator(const FName& PinName) const; + + UFUNCTION(BlueprintCallable, Category = DataPins, DisplayName = "Try Resolve DataPin As Transform", meta = (DeprecatedFunction, DeprecationMessage = "Use TryResolveDataPin (in blueprint) or TryResolveDataPinValue(s) (in code) instead")) + FFlowDataPinResult_Transform TryResolveDataPinAsTransform(const FName& PinName) const; + + UFUNCTION(BlueprintCallable, Category = DataPins, DisplayName = "Try Resolve DataPin As GameplayTag", meta = (DeprecatedFunction, DeprecationMessage = "Use TryResolveDataPin (in blueprint) or TryResolveDataPinValue(s) (in code) instead")) + FFlowDataPinResult_GameplayTag TryResolveDataPinAsGameplayTag(const FName& PinName) const; + + UFUNCTION(BlueprintCallable, Category = DataPins, DisplayName = "Try Resolve DataPin As GameplayTagContainer", meta = (DeprecatedFunction, DeprecationMessage = "Use TryResolveDataPin (in blueprint) or TryResolveDataPinValue(s) (in code) instead")) + FFlowDataPinResult_GameplayTagContainer TryResolveDataPinAsGameplayTagContainer(const FName& PinName) const; + + UFUNCTION(BlueprintCallable, Category = DataPins, DisplayName = "Try Resolve DataPin As InstancedStruct", meta = (DeprecatedFunction, DeprecationMessage = "Use TryResolveDataPin (in blueprint) or TryResolveDataPinValue(s) (in code) instead")) + FFlowDataPinResult_InstancedStruct TryResolveDataPinAsInstancedStruct(const FName& PinName) const; + + UFUNCTION(BlueprintCallable, Category = DataPins, DisplayName = "Try Resolve DataPin As Object", meta = (DeprecatedFunction, DeprecationMessage = "Use TryResolveDataPin (in blueprint) or TryResolveDataPinValue(s) (in code) instead")) + FFlowDataPinResult_Object TryResolveDataPinAsObject(const FName& PinName) const; + + UFUNCTION(BlueprintCallable, Category = DataPins, DisplayName = "Try Resolve DataPin As Class", meta = (DeprecatedFunction, DeprecationMessage = "Use TryResolveDataPin (in blueprint) or TryResolveDataPinValue(s) (in code) instead")) + FFlowDataPinResult_Class TryResolveDataPinAsClass(const FName& PinName) const; + // -- + +protected: + bool TryAddValueToFormatNamedArguments(const FFlowNamedDataPinProperty& NamedDataPinProperty, FFormatNamedArguments& InOutArguments) const; + +////////////////////////////////////////////////////////////////////////// +// Editor + +#if WITH_EDITORONLY_DATA +protected: + UPROPERTY() + TObjectPtr GraphNode; + + UPROPERTY(EditDefaultsOnly, Category = "FlowNode") + uint8 bDisplayNodeTitleWithoutPrefix : 1; + + uint8 bCanDelete : 1 ; + uint8 bCanDuplicate : 1; + + UPROPERTY(EditDefaultsOnly, Category = "FlowNode") + bool bNodeDeprecated; + + /* If this node is deprecated, it might be replaced by another node. */ + UPROPERTY(EditDefaultsOnly, Category = "FlowNode") + TSubclassOf ReplacedBy; + + FFlowNodeEvent OnReconstructionRequested; + FFlowNodeEvent OnAddOnRequestedParentReconstruction; +#endif // WITH_EDITORONLY_DATA + +#if WITH_EDITOR +public: + virtual void PostLoad() override; + + void SetGraphNode(UEdGraphNode* NewGraphNode); + UEdGraphNode* GetGraphNode() const { return GraphNode; } + + void SetCanDelete(const bool CanDelete); + + /* Set up UFlowNodeBase when being opened for edit in the editor. */ + virtual void SetupForEditing(UEdGraphNode& EdGraphNode); + + /* Opportunity to update node's data before UFlowGraphNode would call ReconstructNode(). */ + virtual void FixNode(UEdGraphNode* NewGraphNode); + + // UObject + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; + // -- + + void RequestReconstruction() const { (void) OnReconstructionRequested.ExecuteIfBound(); }; + + /* Used when import graph from another asset. */ + virtual void PostImport() {} +#endif + +public: + /* Called by owning FlowNode to add to its Status String. */ + virtual FString GetStatusString() const; + +protected: + /* Information displayed while node is working - displayed over node as NodeInfoPopup. */ + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "Get Status String")) + FString K2_GetStatusString() const; + +#if WITH_EDITORONLY_DATA +protected: + UPROPERTY() + FString Category; + + UPROPERTY(EditDefaultsOnly, Category = "FlowNode", meta = (Categories = "Flow.NodeStyle")) + FGameplayTag NodeDisplayStyle; + + /* Deprecated NodeStyle, replaced by NodeDisplayStyle. */ + UPROPERTY(meta = (DeprecatedProperty, DeprecationMessage = "Use the NodeDisplayStyle instead.")) + EFlowNodeStyle NodeStyle; + + /* Set Node Style to custom to use your own color for this node (if using Flow.NodeStyle.Custom). */ + UPROPERTY(EditDefaultsOnly, Category = "FlowNode", DisplayName = "Custom Node Color") + FLinearColor NodeColor; + + /* Optional developer-facing text to explain the configuration of this node when viewed in the editor. + * May be authored or set procedurally via UpdateNodeConfigText and SetNodeConfigText. */ + UPROPERTY(EditDefaultsOnly, AdvancedDisplay, Category = "FlowNode") + FText DevNodeConfigText; +#endif // WITH_EDITORONLY_DATA + +#if WITH_EDITOR +public: + /* WARNING! Call UFlowGraphSettings::GetNodeCategoryForNode() instead! */ + virtual FString GetNodeCategory() const; + + const FGameplayTag& GetNodeDisplayStyle() const { return NodeDisplayStyle; } + + /* This method allows to have different for every node instance, i.e. Red if node represents enemy, Green if node represents a friend. */ + virtual bool GetDynamicTitleColor(FLinearColor& OutColor) const; + + virtual FText GetNodeTitle() const { return K2_GetNodeTitle(); } + virtual FText GetNodeToolTip() const { return K2_GetNodeToolTip(); } + + FText GetGeneratedDisplayName() const; + + /** + * Returns overlay icons to display on this node instance in the editor. + * Icons are positioned relative to the top-left corner of the node. + * @param OutOverlayIcons Brush and positioning details of each icon to overlay on the node. + * @param WidgetSize The size of the Node in the editor. Useful for determining offset position values for each overlay icon. + */ + virtual void GetOverlayIcons(TArray& OutOverlayIcons, const FVector2f& WidgetSize) const {}; + + /** + * Gets details to draw an optional corner icon on the node. + * If this function returns true and valid Brush details are given then the corresponding icon will be displayed centered on the top-right of the node. + * @param OutBrushName The Brush name of the icon to display. + * @param OutStyleSetName The StyleSet name of the icon to display. If NAME_None is set we will first search the default Flow StyleSet and then the default Unreal StyleSet. + * @return Returns true if the Node wants to display an icon in the top-right corner. + */ + virtual bool GetCornerIcon(FName& OutBrushName, FName& OutStyleSetName) const { return false; } + +protected: + void EnsureNodeDisplayStyle(); +#endif // WITH_EDITOR + +public: + UFUNCTION(BlueprintNativeEvent, Category = "FlowNode") + FText K2_GetNodeTitle() const; + + UFUNCTION(BlueprintNativeEvent, Category = "FlowNode") + FText K2_GetNodeToolTip() const; + + UFUNCTION(BlueprintPure, Category = "FlowNode") + virtual FText GetNodeConfigText() const; + +protected: + /* Set the editor-only Config Text. + * For displaying config info on the Node in the flow graph, ignored in non-editor builds. */ + UFUNCTION(BlueprintCallable, Category = "FlowNode") + void SetNodeConfigText(const FText& NodeConfigText); + + /* Called whenever a property change event occurs on this flow node object, + * giving the implementor a chance to update their NodeConfigText (via SetNodeConfigText). */ + UFUNCTION(BlueprintNativeEvent, Category = "FlowNode") + void UpdateNodeConfigText(); + +////////////////////////////////////////////////////////////////////////// +// Debug support + +#if WITH_EDITORONLY_DATA +protected: + FFlowMessageLog ValidationLog; +#endif + +#if WITH_EDITOR +public: + /* Short summary of node's content - displayed over node as NodeInfoPopup. */ + virtual FString GetNodeDescription() const; + + /* Complex summary of node's content including its addons. */ + FString GetAddOnDescriptions() const; +#endif + +protected: + /* Short summary of node's content - displayed over node as NodeInfoPopup. */ + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode", meta = (DisplayName = "Get Node Description")) + FString K2_GetNodeDescription() const; + +public: + UFUNCTION(BlueprintCallable, Category = "FlowNode", meta = (DevelopmentOnly)) + void LogError(FString Message, const EFlowOnScreenMessageType OnScreenMessageType = EFlowOnScreenMessageType::Permanent) const; + + UFUNCTION(BlueprintCallable, Category = "FlowNode", meta = (DevelopmentOnly)) + void LogWarning(FString Message) const; + + UFUNCTION(BlueprintCallable, Category = "FlowNode", meta = (DevelopmentOnly)) + void LogNote(FString Message) const; + + UFUNCTION(BlueprintCallable, Category = "FlowNode", meta = (DevelopmentOnly)) + void LogVerbose(FString Message) const; + +#if !NO_LOGGING || UE_ENABLE_DEBUG_DRAWING +protected: + bool BuildMessage(FString& Message) const; +#endif + +#if WITH_EDITOR + virtual EDataValidationResult ValidateNode(); +#endif + + /* Optional validation override for Blueprints. */ + UFUNCTION(BlueprintImplementableEvent, Category = "FlowNode|Validation", meta = (DisplayName = "Validate Node", DevelopmentOnly)) + EDataValidationResult K2_ValidateNode(); + + /* Log validation error (editor-only). */ + UFUNCTION(BlueprintCallable, Category = "FlowNode|Validation", meta = (DevelopmentOnly)) + void LogValidationError(const FString& Message); + + /* Log validation warning (editor-only). */ + UFUNCTION(BlueprintCallable, Category = "FlowNode|Validation", meta = (DevelopmentOnly)) + void LogValidationWarning(const FString& Message); + + /* Log validation note (editor-only). */ + UFUNCTION(BlueprintCallable, Category = "FlowNode|Validation", meta = (DevelopmentOnly)) + void LogValidationNote(const FString& Message); +}; + +/** + * Templates & inline implementations + */ + +template +EFlowDataPinResolveResult UFlowNodeBase::TryResolveDataPinValue(const FName& PinName, typename TFlowPinType::ValueType& OutValue, EFlowSingleFromArray SingleFromArray /*= EFlowSingleFromArray::LastValue*/) const +{ + const FFlowDataPinResult DataPinResult = TryResolveDataPin(PinName); + return FlowPinType::TryExtractValue(DataPinResult, OutValue, SingleFromArray); +} + +template +EFlowDataPinResolveResult UFlowNodeBase::TryResolveDataPinValues(const FName& PinName, TArray& OutValues) const +{ + const FFlowDataPinResult DataPinResult = TryResolveDataPin(PinName); + return FlowPinType::TryExtractValues(DataPinResult, OutValues); +} + +template +EFlowDataPinResolveResult UFlowNodeBase::TryResolveDataPinValue(const FName& PinName, FName& OutEnumValue, UEnum*& OutEnumClass, EFlowSingleFromArray SingleFromArray /*= EFlowSingleFromArray::LastValue*/) const +{ + const FFlowDataPinResult DataPinResult = TryResolveDataPin(PinName); + if (!FlowPinType::IsSuccess(DataPinResult.Result)) + { + return DataPinResult.Result; + } + + return FlowPinType::TryExtractValue(DataPinResult, OutEnumValue, OutEnumClass, SingleFromArray); +} + +template +EFlowDataPinResolveResult UFlowNodeBase::TryResolveDataPinValues(const FName& PinName, TArray& OutEnumValues, UEnum*& OutEnumClass) const +{ + const FFlowDataPinResult DataPinResult = TryResolveDataPin(PinName); + if (!FlowPinType::IsSuccess(DataPinResult.Result)) + { + return DataPinResult.Result; + } + + return FlowPinType::TryExtractValues(DataPinResult, OutEnumValues, OutEnumClass); +} + +template requires std::is_enum_v +EFlowDataPinResolveResult UFlowNodeBase::TryResolveDataPinValue(const FName& PinName, TEnumType& OutValue, EFlowSingleFromArray SingleFromArray /*= EFlowSingleFromArray::LastValue*/) const +{ + const FFlowDataPinResult DataPinResult = TryResolveDataPin(PinName); + return FlowPinType::TryExtractValue(DataPinResult, OutValue, SingleFromArray); +} + +template requires std::is_enum_v +EFlowDataPinResolveResult UFlowNodeBase::TryResolveDataPinValues(const FName& PinName, TArray& OutValues) const +{ + const FFlowDataPinResult DataPinResult = TryResolveDataPin(PinName); + return FlowPinType::TryExtractValues(DataPinResult, OutValues); +} diff --git a/Source/Flow/Public/Nodes/FlowNodeBlueprint.h b/Source/Flow/Public/Nodes/FlowNodeBlueprint.h index 32f82c4ba..89652b451 100644 --- a/Source/Flow/Public/Nodes/FlowNodeBlueprint.h +++ b/Source/Flow/Public/Nodes/FlowNodeBlueprint.h @@ -1,15 +1,12 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once -#include "CoreMinimal.h" #include "Engine/Blueprint.h" #include "FlowNodeBlueprint.generated.h" /** - * A specialized blueprint class required for customizing Asset Type Actions + * Flow Node Blueprint class */ - UCLASS(BlueprintType) class FLOW_API UFlowNodeBlueprint : public UBlueprint { @@ -18,10 +15,7 @@ class FLOW_API UFlowNodeBlueprint : public UBlueprint #if WITH_EDITOR // UBlueprint virtual bool SupportedByDefaultBlueprintFactory() const override { return false; } - virtual bool SupportsDelegates() const override { return false; } - virtual bool SupportsEventGraphs() const override { return false; } - virtual bool SupportsAnimLayers() const override { return false; } // -- #endif }; diff --git a/Source/Flow/Public/Nodes/FlowPin.h b/Source/Flow/Public/Nodes/FlowPin.h index 42186bfc9..3d5f91edb 100644 --- a/Source/Flow/Public/Nodes/FlowPin.h +++ b/Source/Flow/Public/Nodes/FlowPin.h @@ -1,79 +1,143 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once +#include "Types/FlowPinEnums.h" +#include "Types/FlowPinTypeName.h" + +#include "Templates/SubclassOf.h" +#include "UObject/ObjectMacros.h" +#include "Types/FlowPinTypeNamesStandard.h" +#include "EdGraph/EdGraphPin.h" + #include "FlowPin.generated.h" -USTRUCT() +class UEnum; +class UClass; +class UObject; +class IPropertyHandle; +struct FFlowPinType; + +USTRUCT(BlueprintType, meta = (HasNativeMake = "/Script/Flow.FlowDataPinBlueprintLibrary.MakeStruct", HasNativeBreak = "/Script/Flow.FlowDataPinBlueprintLibrary.BreakStruct")) struct FLOW_API FFlowPin { GENERATED_BODY() - // A logical name, used during execution of pin - UPROPERTY(EditDefaultsOnly, Category = "FlowPin") + /* A logical name, used during execution of pin. */ + UPROPERTY(EditDefaultsOnly, Category = FlowPin) FName PinName; - // An optional Display Name, you can use it to override PinName without the need to update graph connections - UPROPERTY(EditDefaultsOnly, Category = "FlowPin") + /* An optional Display Name, you can use it to override PinName without the need to update graph connections. */ + UPROPERTY(EditDefaultsOnly, Category = FlowPin) FText PinFriendlyName; - UPROPERTY(EditDefaultsOnly, Category = "FlowPin") + UPROPERTY(EditDefaultsOnly, Category = FlowPin) FString PinToolTip; + /* Deprecated PinType, use PinTypeName instead (all standard names are defined in FFlowPinTypeNamesStandard). */ + UPROPERTY(Meta = (DeprecatedProperty, DeprecationMessage = "Use PinTypeName instead")) + EFlowPinType PinType = EFlowPinType::Invalid; + + /* Only supporting None (Single) or Array for now(tm) for data pins via EFlowMultiType. */ + UPROPERTY() + EPinContainerType ContainerType = EPinContainerType::None; + +protected: + UPROPERTY() + FFlowPinTypeName PinTypeName = FFlowPinTypeName(FFlowPinTypeNamesStandard::PinTypeNameExec); + + /* Sub-category object + * Used to identify the struct or class type for some PinCategories. */ + UPROPERTY() + TWeakObjectPtr PinSubCategoryObject; + +public: FFlowPin() : PinName(NAME_None) { } - FFlowPin(const FName& InPinName) + FFlowPin(const FFlowPin& InFlowPin) = default; + FFlowPin(FFlowPin&& InFlowPin) = default; + FFlowPin& operator =(FFlowPin&& InFlowPin) = default; + FFlowPin& operator =(const FFlowPin& InFlowPin) = default; + + explicit FFlowPin(const FName& InPinName) : PinName(InPinName) { } - FFlowPin(const FString& InPinName) + explicit FFlowPin(const FString& InPinName) : PinName(*InPinName) { } - FFlowPin(const FText& InPinName) + explicit FFlowPin(const FText& InPinName) : PinName(*InPinName.ToString()) { } - FFlowPin(const TCHAR* InPinName) + explicit FFlowPin(const TCHAR* InPinName) : PinName(FName(InPinName)) { } - FFlowPin(const uint8& InPinName) + explicit FFlowPin(const uint8& InPinName) : PinName(FName(*FString::FromInt(InPinName))) { } - FFlowPin(const int32& InPinName) + explicit FFlowPin(const int32& InPinName) : PinName(FName(*FString::FromInt(InPinName))) { } - FFlowPin(const FStringView InPinName, const FText& InPinFriendlyName) + explicit FFlowPin(const FStringView InPinName, const FText& InPinFriendlyName) : PinName(InPinName) , PinFriendlyName(InPinFriendlyName) { } - FFlowPin(const FStringView InPinName, const FString& InPinTooltip) + explicit FFlowPin(const FStringView InPinName, const FString& InPinTooltip) : PinName(InPinName) , PinToolTip(InPinTooltip) { } - FFlowPin(const FStringView InPinName, const FText& InPinFriendlyName, const FString& InPinTooltip) + explicit FFlowPin(const FStringView InPinName, const FText& InPinFriendlyName, const FString& InPinTooltip) + : PinName(InPinName) + , PinFriendlyName(InPinFriendlyName) + , PinToolTip(InPinTooltip) + { + } + + explicit FFlowPin(const FName& InPinName, const FText& InPinFriendlyName) + : PinName(InPinName) + , PinFriendlyName(InPinFriendlyName) + { + } + + explicit FFlowPin(const FName& InPinName, const FText& InPinFriendlyName, const FString& InPinTooltip) : PinName(InPinName) , PinFriendlyName(InPinFriendlyName) , PinToolTip(InPinTooltip) { } + explicit FFlowPin(const FName& InPinName, const FText& InPinFriendlyName, const FFlowPinTypeName& InTypeName, UObject* OptionalSubCategoryObject = nullptr) + : PinName(InPinName) + , PinFriendlyName(InPinFriendlyName) + { + SetPinTypeName(InTypeName); + SetPinSubCategoryObject(OptionalSubCategoryObject); + } + + explicit FFlowPin(const FName& InPinName, const FFlowPinTypeName& InTypeName, UObject* OptionalSubCategoryObject = nullptr) + : PinName(InPinName) + { + SetPinTypeName(InTypeName); + SetPinSubCategoryObject(OptionalSubCategoryObject); + } + FORCEINLINE bool IsValid() const { return !PinName.IsNone(); @@ -99,10 +163,96 @@ struct FLOW_API FFlowPin return PinName != Other; } + bool DeepIsEqual(const FFlowPin& Other) const + { + // Do a deep pin match (not a simple name-only match), to check if the pins are exactly equal + return + PinName == Other.PinName && + PinFriendlyName.EqualTo(Other.PinFriendlyName) && + PinToolTip == Other.PinToolTip && + ContainerType == Other.ContainerType && + PinTypeName == Other.PinTypeName && + PinSubCategoryObject == Other.PinSubCategoryObject; + } + friend uint32 GetTypeHash(const FFlowPin& FlowPin) { return GetTypeHash(FlowPin.PinName); } + +public: + +#if WITH_EDITOR + FText BuildHeaderText() const; + + static bool ValidateEnum(const UEnum& EnumType); + + FEdGraphPinType BuildEdGraphPinType() const; + void ConfigureFromEdGraphPin(const FEdGraphPinType& EdGraphPinType); +#endif + + void SetPinTypeName(const FFlowPinTypeName& InTypeName); + const FFlowPinTypeName& GetPinTypeName() const { return PinTypeName; } + const FFlowPinType* ResolveFlowPinType() const; + void SetPinSubCategoryObject(UObject* Object) { PinSubCategoryObject = Object; } + static FFlowPinTypeName GetPinTypeNameForLegacyPinType(EFlowPinType PinType); + + const TWeakObjectPtr& GetPinSubCategoryObject() const { return PinSubCategoryObject; } + + // FFlowPin instance signatures for "trait" functions + bool IsExecPin() const; + static bool IsExecPinCategory(const FName& PC); + FORCEINLINE bool IsDataPin() const { return !IsExecPin(); } + // -- + + // + + /** + * Metadata keys for properties that bind and auto-generate Data Pins. + */ + + /* SourceForOutputFlowPin + * May be used on a non-FFlowDataPinProperty within a UFlowNode to bind the + * output data pin to use the property as its source. + * + * If a string value is given, it is interpreted as the Data Pin's name, + * otherwise, the property's DisplayName (or lacking that, its authored name) + * will be assumed to also be the Pin's name. */ + static const FName MetadataKey_SourceForOutputFlowPin; + + /* DefaultForInputFlowPin + * May be used on a non-FFlowDataPinProperty within a UFlowNode to bind the + * Input data pin to use the property as its default value. + * + * If the input pin IS NOT connected to another node, then the bound property + * value will be supplied as a default. + * + * If the input pin IS connected to another node, then the connected node's supplied + * value will be used instead of the default from the bound property. + * + * If a string value is given, it is interpreted as the Data Pin's name, + * otherwise, the property's DisplayName (or lacking that, its authored name) + * will be assumed to also be the Pin's name. */ + static const FName MetadataKey_DefaultForInputFlowPin; + + /* FlowPinType + * May be used on either a property (within a UFlowNode) or a USTRUCT declaration for + * a FFlowDataPinProperty subclass. + * + * If used on a property, then it indicates that a data pin of the given type should be auto-generated, + * and bound to the property. May be used in conjunction with SourceForOutputFlowPin or DefaultForInputFlowPin + * (but not both) to determine how the property binding is to be applied (as input default or output supply source) + * + * If used on a FFlowDataPinProperty struct declaration, then it defines the type of pin + * that should be auto-generated when the struct is used as a property in a UFlowNode. + * + * The string value of the metadata should exactly match a value in EFlowPinType. */ + static const FName MetadataKey_FlowPinType; + // -- + +protected: + + void TrySetStructSubCategoryObjectFromPinType(); }; USTRUCT() @@ -110,7 +260,7 @@ struct FLOW_API FFlowPinHandle { GENERATED_BODY() - // Update SFlowPinHandleBase code if this property name would be ever changed + /* Update SFlowPinHandleBase code if this property name would be ever changed. */ UPROPERTY() FName PinName; @@ -140,7 +290,9 @@ struct FLOW_API FFlowOutputPinHandle : public FFlowPinHandle } }; -// Processing Flow Nodes creates map of connected pins +/** + * Processing Flow Nodes creates map of connected pins. + */ USTRUCT() struct FLOW_API FConnectedPin { @@ -180,20 +332,30 @@ struct FLOW_API FConnectedPin } }; -// Every time pin is activated, we record it and display this data while user hovers mouse over pin +UENUM(BlueprintType) +enum class EFlowPinActivationType : uint8 +{ + Default, + Forced, + PassThrough +}; + +/** + * Every time pin is activated, we record it and display this data while user hovers mouse over pin. + */ #if !UE_BUILD_SHIPPING struct FLOW_API FPinRecord { double Time; FString HumanReadableTime; - bool bForcedActivation; + EFlowPinActivationType ActivationType; - static FString NoActivations; static FString PinActivations; static FString ForcedActivation; + static FString PassThroughActivation; FPinRecord(); - FPinRecord(const double InTime, const bool bInForcedActivation); + FPinRecord(const double InTime, const EFlowPinActivationType InActivationType); private: FORCEINLINE static FString DoubleDigit(const int32 Number); diff --git a/Source/Flow/Public/Nodes/Graph/FlowNode_BlueprintDataPinSupplierBase.h b/Source/Flow/Public/Nodes/Graph/FlowNode_BlueprintDataPinSupplierBase.h new file mode 100644 index 000000000..02e5a08bf --- /dev/null +++ b/Source/Flow/Public/Nodes/Graph/FlowNode_BlueprintDataPinSupplierBase.h @@ -0,0 +1,31 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Nodes/FlowNode.h" +#include "FlowNode_BlueprintDataPinSupplierBase.generated.h" + +/** + * FlowNode to give an event to blueprint for supplying data pin values on-demand. + */ +UCLASS(Abstract, Blueprintable, meta = (DisplayName = "Blueprint Data-Pin Supplier base")) +class FLOW_API UFlowNode_BlueprintDataPinSupplierBase : public UFlowNode +{ + GENERATED_BODY() + +public: + UFlowNode_BlueprintDataPinSupplierBase(); + + // IFlowDataPinValueSupplierInterface + virtual FFlowDataPinResult TrySupplyDataPin(FName PinName) const override; + // -- + + /* Blueprint signature for TrySupplyDataPin override. */ + UFUNCTION(BlueprintNativeEvent, Category = DataPins, DisplayName = "Try Supply DataPin") + FFlowDataPinResult BP_TrySupplyDataPin(FName PinName) const; + + /* Blueprint access for the 'standard' implementation of TrySupplyDataPin + * For cases where they want to override some pins, but maybe not all, they can have the BP + * override call this version to handle any cases it doesn't want to handle. */ + UFUNCTION(BlueprintPure, Category = DataPins, DisplayName = "Try Supply DataPin (standard implementation)") + FFlowDataPinResult BP_Super_TrySupplyDataPin(FName PinName) const { return Super::TrySupplyDataPin(PinName); } +}; diff --git a/Source/Flow/Public/Nodes/Graph/FlowNode_Checkpoint.h b/Source/Flow/Public/Nodes/Graph/FlowNode_Checkpoint.h new file mode 100644 index 000000000..b8faaf953 --- /dev/null +++ b/Source/Flow/Public/Nodes/Graph/FlowNode_Checkpoint.h @@ -0,0 +1,28 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Nodes/FlowNode.h" +#include "FlowNode_Checkpoint.generated.h" + +/** + * Save the state of the game to the save file. + * It's recommended to replace this with a game-specific variant and this node to UFlowGraphSettings::NodesHiddenFromPalette. + */ +UCLASS(NotBlueprintable, Config = Game, defaultconfig, meta = (DisplayName = "Checkpoint", Keywords = "autosave, save")) +class FLOW_API UFlowNode_Checkpoint final : public UFlowNode +{ + GENERATED_BODY() + +public: + UFlowNode_Checkpoint(); + +protected: + /* Change setting by editing DefaultGame.ini. + * [/Script/Flow.FlowNode_Checkpoint] + * bUseAsyncSave=True */ + UPROPERTY(VisibleAnywhere, Config, Category = "Checkpoint") + bool bUseAsyncSave; + + virtual void ExecuteInput(const FName& PinName) override; + virtual void OnLoad_Implementation() override; +}; diff --git a/Source/Flow/Public/Nodes/Graph/FlowNode_CustomEventBase.h b/Source/Flow/Public/Nodes/Graph/FlowNode_CustomEventBase.h new file mode 100644 index 000000000..3d35db322 --- /dev/null +++ b/Source/Flow/Public/Nodes/Graph/FlowNode_CustomEventBase.h @@ -0,0 +1,31 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Nodes/FlowNode.h" +#include "FlowNode_CustomEventBase.generated.h" + +/** + * Base class for nodes used to receive/send events between graphs. + */ +UCLASS(Abstract, NotBlueprintable) +class FLOW_API UFlowNode_CustomEventBase : public UFlowNode +{ + GENERATED_BODY() + +public: + UFlowNode_CustomEventBase(); + +protected: + UPROPERTY() + FName EventName; + +public: + void SetEventName(const FName& InEventName); + const FName& GetEventName() const { return EventName; } + +#if WITH_EDITOR +public: + virtual FString GetNodeDescription() const override; + virtual EDataValidationResult ValidateNode() override; +#endif +}; diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_CustomInput.h b/Source/Flow/Public/Nodes/Graph/FlowNode_CustomInput.h similarity index 51% rename from Source/Flow/Public/Nodes/Route/FlowNode_CustomInput.h rename to Source/Flow/Public/Nodes/Graph/FlowNode_CustomInput.h index be86208af..df0612c3c 100644 --- a/Source/Flow/Public/Nodes/Route/FlowNode_CustomInput.h +++ b/Source/Flow/Public/Nodes/Graph/FlowNode_CustomInput.h @@ -1,26 +1,30 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once -#include "Nodes/FlowNode.h" +#include "FlowNode_CustomEventBase.h" #include "FlowNode_CustomInput.generated.h" /** - * Triggers output upon activation of Input (matching this EventName) on the SubGraph node containing this graph + * Triggers output upon activation of Input (matching this EventName) on the SubGraph node containing this graph. */ UCLASS(NotBlueprintable, meta = (DisplayName = "Custom Input")) -class FLOW_API UFlowNode_CustomInput : public UFlowNode +class FLOW_API UFlowNode_CustomInput : public UFlowNode_CustomEventBase { - GENERATED_UCLASS_BODY() + GENERATED_BODY() + +public: + UFlowNode_CustomInput(); - UPROPERTY() - FName EventName; + friend class UFlowAsset; protected: virtual void ExecuteInput(const FName& PinName) override; +public: + virtual void PostEditImport() override; + #if WITH_EDITOR public: - virtual FString GetNodeDescription() const override; + virtual FText K2_GetNodeTitle_Implementation() const override; #endif }; diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_CustomOutput.h b/Source/Flow/Public/Nodes/Graph/FlowNode_CustomOutput.h similarity index 55% rename from Source/Flow/Public/Nodes/Route/FlowNode_CustomOutput.h rename to Source/Flow/Public/Nodes/Graph/FlowNode_CustomOutput.h index 9e430b267..a5c87c638 100644 --- a/Source/Flow/Public/Nodes/Route/FlowNode_CustomOutput.h +++ b/Source/Flow/Public/Nodes/Graph/FlowNode_CustomOutput.h @@ -1,26 +1,25 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once -#include "Nodes/FlowNode.h" +#include "FlowNode_CustomEventBase.h" #include "FlowNode_CustomOutput.generated.h" /** - * Triggers output on SubGraph node containing this graph - * Triggered output name matches EventName selected on this node + * Triggers output on SubGraph node containing this graph. + * Triggered output name matches EventName selected on this node. */ UCLASS(NotBlueprintable, meta = (DisplayName = "Custom Output")) -class FLOW_API UFlowNode_CustomOutput final : public UFlowNode +class FLOW_API UFlowNode_CustomOutput final : public UFlowNode_CustomEventBase { - GENERATED_UCLASS_BODY() - - UPROPERTY() - FName EventName; + GENERATED_BODY() +public: + UFlowNode_CustomOutput(); + protected: virtual void ExecuteInput(const FName& PinName) override; #if WITH_EDITOR - virtual FString GetNodeDescription() const override; + virtual FText K2_GetNodeTitle_Implementation() const override; #endif }; diff --git a/Source/Flow/Public/Nodes/Graph/FlowNode_DefineProperties.h b/Source/Flow/Public/Nodes/Graph/FlowNode_DefineProperties.h new file mode 100644 index 000000000..e452d6178 --- /dev/null +++ b/Source/Flow/Public/Nodes/Graph/FlowNode_DefineProperties.h @@ -0,0 +1,54 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Interfaces/FlowNamedPropertiesSupplierInterface.h" +#include "Nodes/FlowNode.h" +#include "Types/FlowNamedDataPinProperty.h" + +#include "FlowNode_DefineProperties.generated.h" + +/** + * FlowNode to define data pin property literals for use connecting to data pin inputs in a flow graph. + */ +UCLASS(Blueprintable, meta = (DisplayName = "Define Properties")) +class FLOW_API UFlowNode_DefineProperties + : public UFlowNode + , public IFlowNamedPropertiesSupplierInterface +{ + GENERATED_BODY() + +public: + UFlowNode_DefineProperties(); + +protected: + /* Instance-defined properties. + * These will auto-generate a matching pin that is bound to its property as its data source. */ + UPROPERTY(EditAnywhere, Category = "Configuration", DisplayName = Properties) + TArray NamedProperties; + +public: + virtual void PostLoad() override; + +#if WITH_EDITOR + // IFlowContextPinSupplierInterface + virtual bool SupportsContextPins() const override; + // -- + + // UObject + virtual void PostEditChangeChainProperty(FPropertyChangedChainEvent& PropertyChangedEvent) override; + // -- +#endif + + // IFlowNamedPropertiesSupplierInterface + virtual TArray& GetMutableNamedProperties() override { return NamedProperties; } + // -- + + bool TryFormatTextWithNamedPropertiesAsParameters(const FText& FormatText, FText& OutFormattedText) const; + +protected: +#if WITH_EDITOR + /* Utility function for subclasses, if they want to force a named property to be Input or Output. + * Unused in this class. */ + void OnPostEditEnsureAllNamedPropertiesPinDirection(const FProperty& Property, bool bIsInput); +#endif +}; diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_Finish.h b/Source/Flow/Public/Nodes/Graph/FlowNode_Finish.h similarity index 62% rename from Source/Flow/Public/Nodes/Route/FlowNode_Finish.h rename to Source/Flow/Public/Nodes/Graph/FlowNode_Finish.h index d5d4c59af..b4458ebac 100644 --- a/Source/Flow/Public/Nodes/Route/FlowNode_Finish.h +++ b/Source/Flow/Public/Nodes/Graph/FlowNode_Finish.h @@ -1,19 +1,22 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Nodes/FlowNode.h" #include "FlowNode_Finish.generated.h" /** - * Finish execution of this Flow Asset - * All active nodes and sub graphs will be deactivated + * Finish execution of this Flow Asset. + * All active nodes and sub graphs will be deactivated. */ UCLASS(NotBlueprintable, meta = (DisplayName = "Finish")) class FLOW_API UFlowNode_Finish : public UFlowNode { - GENERATED_UCLASS_BODY() + GENERATED_BODY() + +public: + UFlowNode_Finish(); protected: + virtual bool CanFinishGraph() const override { return true; } virtual void ExecuteInput(const FName& PinName) override; }; diff --git a/Source/Flow/Public/Nodes/Graph/FlowNode_FormatText.h b/Source/Flow/Public/Nodes/Graph/FlowNode_FormatText.h new file mode 100644 index 000000000..d018ab36b --- /dev/null +++ b/Source/Flow/Public/Nodes/Graph/FlowNode_FormatText.h @@ -0,0 +1,44 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Nodes/Graph/FlowNode_DefineProperties.h" +#include "FlowNode_FormatText.generated.h" + +/** + * Formats a text string using the standard UE FText formatting system. + * using input pins as parameters and the output is delivered to OUTPIN_TextOutput. + */ +UCLASS(NotBlueprintable, meta = (DisplayName = "Format Text", Keywords = "print")) +class FLOW_API UFlowNode_FormatText : public UFlowNode_DefineProperties +{ + GENERATED_BODY() + +public: + UFlowNode_FormatText(); + +private: + /* Format text string. + * Uses standard Unreal "FText" formatting: eg, {PinName} will refer to input called PinName. + * Note: complex types are exported "ToString" and InstancedStruct is not supported. */ + UPROPERTY(EditAnywhere, Category = "Flow", meta = (DefaultForInputFlowPin, FlowPinType = Text)) + FText FormatText; + +#if WITH_EDITOR +public: + // UObject + virtual void PostEditChangeChainProperty(FPropertyChangedChainEvent& PropertyChangedEvent) override; + // -- + + virtual void UpdateNodeConfigText_Implementation() override; +#endif + +protected: + EFlowDataPinResolveResult TryResolveFormattedText(const FName& PinName, FText& OutFormattedText) const; + +public: + // IFlowDataPinValueSupplierInterface + virtual FFlowDataPinResult TrySupplyDataPin(FName PinName) const override; + // -- + + static const FName OUTPIN_TextOutput; +}; diff --git a/Source/Flow/Public/Nodes/Graph/FlowNode_Start.h b/Source/Flow/Public/Nodes/Graph/FlowNode_Start.h new file mode 100644 index 000000000..67bc84294 --- /dev/null +++ b/Source/Flow/Public/Nodes/Graph/FlowNode_Start.h @@ -0,0 +1,45 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Nodes/Graph/FlowNode_DefineProperties.h" +#include "Interfaces/FlowNodeWithExternalDataPinSupplierInterface.h" +#include "FlowNode_Start.generated.h" + +/** + * Execution of the graph always starts from this node. + */ +UCLASS(NotBlueprintable, NotPlaceable, meta = (DisplayName = "Start")) +class FLOW_API UFlowNode_Start + : public UFlowNode_DefineProperties + , public IFlowNodeWithExternalDataPinSupplierInterface +{ + GENERATED_BODY() + +public: + UFlowNode_Start(); + + friend class UFlowAsset; + +protected: + /* External DataPin Value Supplier. + * Example: the UFlowNode_SubGraph that instanced this Start node's flow asset. */ + UPROPERTY(Transient) + TScriptInterface FlowDataPinValueSupplierInterface; + +public: + // IFlowCoreExecutableInterface + virtual void ExecuteInput(const FName& PinName) override; + // -- + + // IFlowNodeWithExternalDataPinSupplierInterface + virtual void SetDataPinValueSupplier(IFlowDataPinValueSupplierInterface* DataPinValueSupplier) override; + virtual IFlowDataPinValueSupplierInterface* GetExternalDataPinSupplier() const override { return FlowDataPinValueSupplierInterface.GetInterface(); } +#if WITH_EDITOR + virtual bool TryAppendExternalInputPins(TArray& InOutPins) const override; +#endif + // -- + + // IFlowDataPinValueSupplierInterface + virtual FFlowDataPinResult TrySupplyDataPin(FName PinName) const override; + // -- +}; diff --git a/Source/Flow/Public/Nodes/Graph/FlowNode_SubGraph.h b/Source/Flow/Public/Nodes/Graph/FlowNode_SubGraph.h new file mode 100644 index 000000000..5d7dfaf62 --- /dev/null +++ b/Source/Flow/Public/Nodes/Graph/FlowNode_SubGraph.h @@ -0,0 +1,108 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Interfaces/FlowPreloadableInterface.h" +#include "Nodes/FlowNode.h" +#include "FlowNode_SubGraph.generated.h" + +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 + , public IFlowPreloadableInterface +{ + GENERATED_BODY() + +public: + UFlowNode_SubGraph(); + + friend class UFlowAsset; + friend class FFlowNode_SubGraphDetails; + friend class UFlowSubsystem; + + static FFlowPin StartPin; + static FFlowPin FinishPin; + +private: + UPROPERTY(EditAnywhere, Category = "Graph") + TSoftObjectPtr Asset; + + /* Flow Asset Params to use as the data pin value supplier for the Asset. */ + UPROPERTY(EditAnywhere, Category = "Graph", meta = (DefaultForInputFlowPin, FlowPinType = "Object")) + TSoftObjectPtr AssetParams; + + /* Allow to create instance of the same Flow Asset as the asset containing this node. + * Enabling it may cause an infinite loop, if graph would keep creating copies of itself. */ + UPROPERTY(EditAnywhere, Category = "Graph") + bool bCanInstanceIdenticalAsset; + + UPROPERTY(SaveGame) + FString SavedAssetInstanceName; + +protected: + virtual bool CanBeAssetInstanced() const; + + // IFlowPreloadableInterface + virtual EFlowPreloadResult PreloadContent() override; + virtual void FlushContent() override; + // -- + + virtual void ExecuteInput(const FName& PinName) override; + virtual void Cleanup() override; + +public: + virtual void ForceFinishNode() override; + +protected: + virtual void OnLoad_Implementation() override; + +#if WITH_EDITORONLY_DATA + +protected: + /* All the classes allowed to be used as assets on this subgraph node. */ + UPROPERTY() + TArray> AllowedAssignedAssetClasses; + + /* All the classes disallowed to be used as assets on this subgraph node. */ + UPROPERTY() + TArray> DeniedAssignedAssetClasses; +#endif + +#if WITH_EDITOR + +public: + virtual FText K2_GetNodeTitle_Implementation() const override; + virtual FString GetNodeDescription() const override; + virtual UObject* GetAssetToEdit() override; + virtual EDataValidationResult ValidateNode() override; + + // IFlowContextPinSupplierInterface + virtual bool SupportsContextPins() const override { return true; } + virtual TArray GetContextInputs() const override; + virtual TArray GetContextOutputs() const override; + // -- + + // IFlowDataPinValueOwnerInterface + virtual void AutoGenerateDataPins(FFlowDataPinValueOwner& ValueOwner, FFlowAutoDataPinsWorkingData& InOutWorkingData) override; + // -- + + // IFlowDataPinValueSupplierInterface + virtual FFlowDataPinResult TrySupplyDataPin(FName PinName) const override; + // -- + + // UObject + virtual void PostLoad() override; + virtual void PreEditChange(FProperty* PropertyAboutToChange) override; + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; + // -- + +private: + void SubscribeToAssetChanges(); +#endif + + static const FName AssetParams_MemberName; +}; diff --git a/Source/Flow/Public/Nodes/Operators/FlowNode_LogicalOR.h b/Source/Flow/Public/Nodes/Operators/FlowNode_LogicalOR.h deleted file mode 100644 index 75828dab9..000000000 --- a/Source/Flow/Public/Nodes/Operators/FlowNode_LogicalOR.h +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - -#pragma once - -#include "Nodes/FlowNode.h" -#include "FlowNode_LogicalOR.generated.h" - -/** - * Logical OR - * Output will be triggered only once - */ -UCLASS(NotBlueprintable, meta = (DisplayName = "OR")) -class FLOW_API UFlowNode_LogicalOR final : public UFlowNode -{ - GENERATED_UCLASS_BODY() - -#if WITH_EDITOR - virtual bool CanUserAddInput() const override { return true; } -#endif - -protected: - virtual void ExecuteInput(const FName& PinName) override; -}; diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_Branch.h b/Source/Flow/Public/Nodes/Route/FlowNode_Branch.h new file mode 100644 index 000000000..900fc0541 --- /dev/null +++ b/Source/Flow/Public/Nodes/Route/FlowNode_Branch.h @@ -0,0 +1,35 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Nodes/FlowNode.h" +#include "Types/FlowBranchEnums.h" +#include "FlowNode_Branch.generated.h" + +/** + * FEvaluates its AddOns that implement the IFlowPredicateInterface to determine the output pin to trigger. + */ +UCLASS(MinimalApi, NotBlueprintable, meta = (DisplayName = "Branch")) +class UFlowNode_Branch : public UFlowNode +{ + GENERATED_BODY() + +public: + UFlowNode_Branch(); + + /* For root-level predicates on this branch, do we treat them as an "AND" (all must pass) or an "OR" (at least one must pass)? */ + UPROPERTY(EditAnywhere, Category = "Branch", DisplayName = "Root Combination Rule") + EFlowPredicateCombinationRule BranchCombinationRule = EFlowPredicateCombinationRule::AND; + +public: + // UFlowNodeBase + virtual EFlowAddOnAcceptResult AcceptFlowNodeAddOnChild_Implementation(const UFlowNodeAddOn* AddOnTemplate, const TArray& AdditionalAddOnsToAssumeAreChildren) const override; + virtual FText K2_GetNodeTitle_Implementation() const override; + // -- + + /* Event reacting on triggering Input pin. */ + virtual void ExecuteInput(const FName& PinName) override; + + static const FName INPIN_Evaluate; + static const FName OUTPIN_True; + static const FName OUTPIN_False; +}; diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_Counter.h b/Source/Flow/Public/Nodes/Route/FlowNode_Counter.h index 8346814d5..46e2d5e14 100644 --- a/Source/Flow/Public/Nodes/Route/FlowNode_Counter.h +++ b/Source/Flow/Public/Nodes/Route/FlowNode_Counter.h @@ -1,17 +1,19 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Nodes/FlowNode.h" #include "FlowNode_Counter.generated.h" /** - * Counts how many times signal entered this node + * Counts how many times signal entered this node. */ UCLASS(NotBlueprintable, meta = (DisplayName = "Counter")) class FLOW_API UFlowNode_Counter final : public UFlowNode { - GENERATED_UCLASS_BODY() + GENERATED_BODY() + +public: + UFlowNode_Counter(); protected: UPROPERTY(EditAnywhere, Category = "Counter", meta = (ClampMin = 2)) diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_ExecutionMultiGate.h b/Source/Flow/Public/Nodes/Route/FlowNode_ExecutionMultiGate.h index da039c221..bd6631cc8 100644 --- a/Source/Flow/Public/Nodes/Route/FlowNode_ExecutionMultiGate.h +++ b/Source/Flow/Public/Nodes/Route/FlowNode_ExecutionMultiGate.h @@ -1,23 +1,26 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Nodes/FlowNode.h" #include "FlowNode_ExecutionMultiGate.generated.h" /** - * Executes a series of pins in order + * Executes a series of pins in order. */ -UCLASS(NotBlueprintable, meta = (DisplayName = "Multi Gate")) +UCLASS(NotBlueprintable, meta = (DisplayName = "Multi Gate", Keywords = "series, loop, random")) class FLOW_API UFlowNode_ExecutionMultiGate final : public UFlowNode { - GENERATED_UCLASS_BODY() + GENERATED_BODY() + +public: + UFlowNode_ExecutionMultiGate(); +protected: UPROPERTY(EditAnywhere, Category = "MultiGate") bool bRandom; - // Allow executing output pins again, without triggering Reset pin - // If set to False, every output pin can be triggered only once + /* Allow executing output pins again, without triggering Reset pin. + * If set to False, every output pin can be triggered only once/ */ UPROPERTY(EditAnywhere, Category = "MultiGate") bool bLoop; diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_ExecutionSequence.h b/Source/Flow/Public/Nodes/Route/FlowNode_ExecutionSequence.h index bda6e8e57..0abcd4f45 100644 --- a/Source/Flow/Public/Nodes/Route/FlowNode_ExecutionSequence.h +++ b/Source/Flow/Public/Nodes/Route/FlowNode_ExecutionSequence.h @@ -1,22 +1,50 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Nodes/FlowNode.h" #include "FlowNode_ExecutionSequence.generated.h" /** - * Executes all outputs sequentially + * Executes all outputs sequentially. */ UCLASS(NotBlueprintable, meta = (DisplayName = "Sequence")) class FLOW_API UFlowNode_ExecutionSequence final : public UFlowNode { - GENERATED_UCLASS_BODY() + GENERATED_BODY() + +public: + UFlowNode_ExecutionSequence(); + +protected: + /** + * If enabled and the graph is saved during gameplay, this node + * tracks and saves which pins it has executed. + * + * If you add new connections or replace old connections with + * different nodes, this node will detect the changes. If during gameplay + * you load an old save game which had different connections, this node + * will automatically execute the updated connections you created. + */ + UPROPERTY(EditAnywhere, Category = "Sequence") + bool bSavePinExecutionState; + + UPROPERTY(SaveGame) + TSet ExecutedConnections; +public: #if WITH_EDITOR virtual bool CanUserAddOutput() const override { return true; } #endif protected: virtual void ExecuteInput(const FName& PinName) override; + virtual void OnLoad_Implementation() override; + virtual void Cleanup() override; + + void ExecuteNewConnections(); + +#if WITH_EDITOR +public: + virtual FString GetNodeDescription() const override; +#endif }; diff --git a/Source/Flow/Public/Nodes/Operators/FlowNode_LogicalAND.h b/Source/Flow/Public/Nodes/Route/FlowNode_LogicalAND.h similarity index 73% rename from Source/Flow/Public/Nodes/Operators/FlowNode_LogicalAND.h rename to Source/Flow/Public/Nodes/Route/FlowNode_LogicalAND.h index b646bf773..61d63b986 100644 --- a/Source/Flow/Public/Nodes/Operators/FlowNode_LogicalAND.h +++ b/Source/Flow/Public/Nodes/Route/FlowNode_LogicalAND.h @@ -1,18 +1,20 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Nodes/FlowNode.h" #include "FlowNode_LogicalAND.generated.h" /** - * Logical AND - * Output will be triggered only once + * Logical AND. + * Output will be triggered only once. */ -UCLASS(NotBlueprintable, meta = (DisplayName = "AND")) +UCLASS(NotBlueprintable, meta = (DisplayName = "AND", Keywords = "&")) class FLOW_API UFlowNode_LogicalAND final : public UFlowNode { - GENERATED_UCLASS_BODY() + GENERATED_BODY() + +public: + UFlowNode_LogicalAND(); private: UPROPERTY(SaveGame) diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_LogicalOR.h b/Source/Flow/Public/Nodes/Route/FlowNode_LogicalOR.h new file mode 100644 index 000000000..a59f29088 --- /dev/null +++ b/Source/Flow/Public/Nodes/Route/FlowNode_LogicalOR.h @@ -0,0 +1,45 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Nodes/FlowNode.h" +#include "FlowNode_LogicalOR.generated.h" + +/** + * Logical OR. + * Output will be triggered only once. + */ +UCLASS(NotBlueprintable, meta = (DisplayName = "OR", Keywords = "|")) +class FLOW_API UFlowNode_LogicalOR final : public UFlowNode +{ + GENERATED_BODY() + +public: + UFlowNode_LogicalOR(); + +protected: + UPROPERTY(EditAnywhere, Category = "Lifetime", SaveGame) + bool bEnabled; + + /* This node will become Blocked (not executed anymore), if Execution Limit > 0 and Execution Count reaches this limit. + * Set this to zero, if you'd like fire output indefinitely. */ + UPROPERTY(EditAnywhere, Category = "Lifetime", meta = (ClampMin = 0)) + int32 ExecutionLimit; + + /* This node will become Blocked (not executed anymore), if Execution Limit > 0 and Execution Count reaches this limit. */ + UPROPERTY(VisibleAnywhere, Category = "Lifetime", SaveGame) + int32 ExecutionCount; + +#if WITH_EDITOR +public: + virtual bool CanUserAddInput() const override { return true; } +#endif + +protected: + virtual void ExecuteInput(const FName& PinName) override; + + void ResetCounter(); + +#if WITH_EDITOR + virtual FString GetStatusString() const override; +#endif +}; diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_Reroute.h b/Source/Flow/Public/Nodes/Route/FlowNode_Reroute.h index 3556dbe91..8196b20d5 100644 --- a/Source/Flow/Public/Nodes/Route/FlowNode_Reroute.h +++ b/Source/Flow/Public/Nodes/Route/FlowNode_Reroute.h @@ -1,18 +1,33 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Nodes/FlowNode.h" #include "FlowNode_Reroute.generated.h" /** - * Reroute + * Reroute. */ UCLASS(NotBlueprintable, meta = (DisplayName = "Reroute")) class FLOW_API UFlowNode_Reroute final : public UFlowNode { - GENERATED_UCLASS_BODY() + GENERATED_BODY() +public: + UFlowNode_Reroute(); + protected: + // IFlowCoreExecutableInterface virtual void ExecuteInput(const FName& PinName) override; + // -- + + // IFlowDataPinValueSupplierInterface + virtual FFlowDataPinResult TrySupplyDataPin(FName PinName) const override; + // -- + +public: +#if WITH_EDITOR + // For configuration from connecting pins via UFlowGraphNode_Reroute + void ConfigureInputPin(const UFlowNode& ConnectedNode, const FEdGraphPinType& EdGraphPinType); + void ConfigureOutputPin(const UFlowNode& ConnectedNode, const FEdGraphPinType& EdGraphPinType); +#endif }; diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_Start.h b/Source/Flow/Public/Nodes/Route/FlowNode_Start.h deleted file mode 100644 index 9a0a09525..000000000 --- a/Source/Flow/Public/Nodes/Route/FlowNode_Start.h +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - -#pragma once - -#include "Nodes/FlowNode.h" -#include "FlowNode_Start.generated.h" - -/** - * Execution of the graph always starts from this node - */ -UCLASS(NotBlueprintable, NotPlaceable, meta = (DisplayName = "Start")) -class FLOW_API UFlowNode_Start : public UFlowNode -{ - GENERATED_UCLASS_BODY() - -protected: - virtual void ExecuteInput(const FName& PinName) override; -}; diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_SubGraph.h b/Source/Flow/Public/Nodes/Route/FlowNode_SubGraph.h deleted file mode 100644 index 522540f25..000000000 --- a/Source/Flow/Public/Nodes/Route/FlowNode_SubGraph.h +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - -#pragma once - -#include "Nodes/FlowNode.h" -#include "FlowNode_SubGraph.generated.h" - -/** - * Creates instance of provided Flow Asset and starts its execution - */ -UCLASS(NotBlueprintable, meta = (DisplayName = "Sub Graph")) -class FLOW_API UFlowNode_SubGraph : public UFlowNode -{ - GENERATED_UCLASS_BODY() - - friend class UFlowAsset; - friend class UFlowSubsystem; - - static FFlowPin StartPin; - static FFlowPin FinishPin; - -private: - UPROPERTY(EditAnywhere, Category = "Graph") - TSoftObjectPtr Asset; - - /* - * Allow to create instance of the same Flow Asset as the asset containing this node - * Enabling it may cause an infinite loop, if graph would keep creating copies of itself - */ - UPROPERTY(EditAnywhere, Category = "Graph") - bool bCanInstanceIdenticalAsset; - - UPROPERTY(SaveGame) - FString SavedAssetInstanceName; - -protected: - virtual bool CanBeAssetInstanced() const; - - virtual void PreloadContent() override; - virtual void FlushContent() override; - - virtual void ExecuteInput(const FName& PinName) override; - virtual void Cleanup() override; - -public: - virtual void ForceFinishNode() override; - -protected: - virtual void OnLoad_Implementation() override; - -public: -#if WITH_EDITOR - virtual FString GetNodeDescription() const override; - virtual UObject* GetAssetToEdit() override; - - virtual bool SupportsContextPins() const override { return true; } - - virtual TArray GetContextInputs() override; - virtual TArray GetContextOutputs() override; - - // UObject - virtual void PostLoad() override; - virtual void PreEditChange(FProperty* PropertyAboutToChange) override; - virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; - // -- - -private: - void SubscribeToAssetChanges(); -#endif -}; diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_Switch.h b/Source/Flow/Public/Nodes/Route/FlowNode_Switch.h new file mode 100644 index 000000000..f2a42cbb0 --- /dev/null +++ b/Source/Flow/Public/Nodes/Route/FlowNode_Switch.h @@ -0,0 +1,35 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Nodes/FlowNode.h" +#include "FlowNode_Switch.generated.h" + +/** + * Similar to a Branch flow node, provides a "Switch" style logic (ie, C/C++), + * where cases are evaluated and triggered if their predicates pass. + * By default, only the first passing case is triggered (see bOnlyTriggerFirstPassingCase). + */ +UCLASS(MinimalApi, NotBlueprintable, meta = (DisplayName = "Switch")) +class UFlowNode_Switch : public UFlowNode +{ + GENERATED_BODY() + +public: + UFlowNode_Switch(); + + /* Only trigger the switch output for the first passing case during a single Evaluate + * (if false, all passing cases will trigger) */ + UPROPERTY(EditAnywhere, Category = "Switch") + bool bOnlyTriggerFirstPassingCase = true; + + // UFlowNodeBase + virtual EFlowAddOnAcceptResult AcceptFlowNodeAddOnChild_Implementation(const UFlowNodeAddOn* AddOnTemplate, const TArray& AdditionalAddOnsToAssumeAreChildren) const override; + virtual FText K2_GetNodeTitle_Implementation() const override; + // -- + + /* Event reacting on triggering Input pin. */ + virtual void ExecuteInput(const FName& PinName) override; + + static const FName INPIN_Evaluate; + static const FName OUTPIN_DefaultCase; +}; diff --git a/Source/Flow/Public/Nodes/Route/FlowNode_Timer.h b/Source/Flow/Public/Nodes/Route/FlowNode_Timer.h index 0d9154ad2..c67a551cc 100644 --- a/Source/Flow/Public/Nodes/Route/FlowNode_Timer.h +++ b/Source/Flow/Public/Nodes/Route/FlowNode_Timer.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Engine/EngineTypes.h" @@ -7,25 +6,34 @@ #include "FlowNode_Timer.generated.h" /** - * Triggers outputs after time elapsed + * Triggers outputs after time elapsed. */ -UCLASS(NotBlueprintable, meta = (DisplayName = "Timer")) +UCLASS(NotBlueprintable, meta = (DisplayName = "Timer", Keywords = "delay, step, tick")) class FLOW_API UFlowNode_Timer : public UFlowNode { - GENERATED_UCLASS_BODY() + GENERATED_BODY() + +public: + UFlowNode_Timer(); protected: - UPROPERTY(EditAnywhere, Category = "Timer", meta = (ClampMin = 0.0f)) + /* If the value is closer to 0, Timer will complete in next tick. */ + UPROPERTY(EditAnywhere, Category = "Timer", meta = (ClampMin = 0.0f, DefaultForInputFlowPin, FlowPinType = Float)) float CompletionTime; - // this allows to trigger other nodes multiple times before completing the Timer + /* This allows to trigger other nodes multiple times before completing the Timer. */ UPROPERTY(EditAnywhere, Category = "Timer", meta = (ClampMin = 0.0f)) float StepTime; + static FName INPIN_CompletionTime; + private: FTimerHandle CompletionTimerHandle; FTimerHandle StepTimerHandle; + UPROPERTY(SaveGame) + float ResolvedCompletionTime; + UPROPERTY(SaveGame) float SumOfSteps; @@ -36,10 +44,13 @@ class FLOW_API UFlowNode_Timer : public UFlowNode float RemainingStepTime; protected: + virtual void InitializeInstance() override; virtual void ExecuteInput(const FName& PinName) override; virtual void SetTimer(); virtual void Restart(); + + float ResolveCompletionTime() const; private: UFUNCTION() @@ -55,7 +66,10 @@ class FLOW_API UFlowNode_Timer : public UFlowNode virtual void OnLoad_Implementation() override; #if WITH_EDITOR - virtual FString GetNodeDescription() const override; +public: + virtual void UpdateNodeConfigText_Implementation() override; + +protected: virtual FString GetStatusString() const override; #endif }; diff --git a/Source/Flow/Public/Nodes/Utils/FlowNode_Checkpoint.h b/Source/Flow/Public/Nodes/Utils/FlowNode_Checkpoint.h deleted file mode 100644 index cc2b4dac8..000000000 --- a/Source/Flow/Public/Nodes/Utils/FlowNode_Checkpoint.h +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - -#pragma once - -#include "Nodes/FlowNode.h" -#include "FlowNode_Checkpoint.generated.h" - -/** - * Save the state of the game to the save file - * It's recommended to replace this with game-specific variant and this node to UFlowGraphSettings::HiddenNodes - */ -UCLASS(NotBlueprintable, meta = (DisplayName = "Checkpoint")) -class FLOW_API UFlowNode_Checkpoint final : public UFlowNode -{ - GENERATED_UCLASS_BODY() - -protected: - virtual void ExecuteInput(const FName& PinName) override; - virtual void OnLoad_Implementation() override; -}; diff --git a/Source/Flow/Public/Policies/FlowPinConnectionPolicy.h b/Source/Flow/Public/Policies/FlowPinConnectionPolicy.h new file mode 100644 index 000000000..40701f245 --- /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/Policies/FlowPinTypeMatchPolicy.h b/Source/Flow/Public/Policies/FlowPinTypeMatchPolicy.h new file mode 100644 index 000000000..307157ec0 --- /dev/null +++ b/Source/Flow/Public/Policies/FlowPinTypeMatchPolicy.h @@ -0,0 +1,43 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "FlowPinTypeMatchPolicy.generated.h" + +UENUM(meta = (BitFlags)) +enum class EFlowPinTypeMatchRules : uint32 +{ + None = 0, + + RequirePinCategoryMatch = 1 << 0, + RequirePinCategoryMemberReferenceMatch = 1 << 1, + RequireContainerTypeMatch = 1 << 2, + RequirePinSubCategoryObjectMatch = 1 << 3, + AllowSubCategoryObjectSubclasses = 1 << 4, + AllowSubCategoryObjectSameLayout = 1 << 5, + SameLayoutMustMatchPropertyNames = 1 << 6, + + /* The "Standard" PinType matching rules (applies to most types). */ + StandardPinTypeMatchRulesMask = + RequirePinCategoryMatch | + RequirePinCategoryMemberReferenceMatch | + AllowSubCategoryObjectSubclasses | + 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(DisplayName = "SubCategory Object PinType Match Rules (mask)"), +}; + +USTRUCT() +struct FFlowPinTypeMatchPolicy +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, Category = PinConnection, meta = (Bitmask, BitmaskEnum = "/Script/Flow.EFlowPinTypeMatchRules")) + EFlowPinTypeMatchRules PinTypeMatchRules = EFlowPinTypeMatchRules::StandardPinTypeMatchRulesMask; + + /* Pin categories to allow beyond an exact match. */ + 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 000000000..e50080af1 --- /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 000000000..a5f7552da --- /dev/null +++ b/Source/Flow/Public/Policies/FlowPreloadHelper.h @@ -0,0 +1,110 @@ +// 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() + +public: + /* Exec output pin fired when all preloads for this node are complete. */ + static const FFlowPin OUTPIN_AllPreloadsComplete; + +public: + virtual ~FFlowPreloadHelper() = default; + + // IFlowCoreExecutableInterface + 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. + * Possible results: + * - 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;); + +#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() + +protected: + /* 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; + + /* 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; + +public: + // IFlowCoreExecutableInterface + 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: +#if WITH_EDITOR + virtual void GetContextInputs(TArray& OutInputPins) const override; +#endif +}; diff --git a/Source/Flow/Public/Policies/FlowPreloadPolicy.h b/Source/Flow/Public/Policies/FlowPreloadPolicy.h new file mode 100644 index 000000000..2d4f45c53 --- /dev/null +++ b/Source/Flow/Public/Policies/FlowPreloadPolicy.h @@ -0,0 +1,30 @@ +// 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 000000000..963ae2e97 --- /dev/null +++ b/Source/Flow/Public/Policies/FlowPreloadPolicyEnums.h @@ -0,0 +1,85 @@ +// 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 000000000..31efd09f0 --- /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 000000000..83e1f699c --- /dev/null +++ b/Source/Flow/Public/Policies/FlowStandardPreloadPolicies.h @@ -0,0 +1,42 @@ +// 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; +}; diff --git a/Source/Flow/Public/Types/FlowActorOwnerComponentRef.h b/Source/Flow/Public/Types/FlowActorOwnerComponentRef.h new file mode 100644 index 000000000..a44a17d02 --- /dev/null +++ b/Source/Flow/Public/Types/FlowActorOwnerComponentRef.h @@ -0,0 +1,42 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/ObjectPtr.h" +#include "FlowActorOwnerComponentRef.generated.h" + +class UActorComponent; + +/** + * Similar to FAnimNodeFunctionRef, providing a FName-based Component binding that is resolved at runtime. + */ +USTRUCT(BlueprintType) +struct FFlowActorOwnerComponentRef +{ + GENERATED_BODY() + +public: + /* Tries to find the component by name on the given actor. */ + UActorComponent* TryResolveComponent(const AActor& InActor, bool bWarnIfFailed = true); + + /* In some cases, the component can be resolved directly. */ + void SetResolvedComponentDirect(UActorComponent& Component); + + /* Returns a resolved component. + * Assumes TryResolveComponent() was called previously. */ + UActorComponent* GetResolvedComponent() const { return ResolvedComponent; } + + bool IsConfigured() const { return !ComponentName.IsNone(); } + bool IsResolved() const; + + static UActorComponent* TryResolveComponentByName(const AActor& InActor, const FName& InComponentName); + +public: + UPROPERTY(VisibleAnywhere, Category = "Flow Actor Owner Component") + FName ComponentName = NAME_None; + +protected: + /* Cached resolved component. + * Resolved at runtime by calling TryResolveComponent. */ + UPROPERTY(Transient) + TObjectPtr ResolvedComponent = nullptr; +}; diff --git a/Source/Flow/Public/Types/FlowArray.h b/Source/Flow/Public/Types/FlowArray.h new file mode 100644 index 000000000..f536495c0 --- /dev/null +++ b/Source/Flow/Public/Types/FlowArray.h @@ -0,0 +1,90 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Algo/Unique.h" +#include "Containers/Array.h" +#include "Math/RandomStream.h" +#include "Templates/Greater.h" + +namespace FlowArray +{ + // Alias for inline-allocated TArray + // (NOTE, UE's TArray will reallocate to heap ("secondary allocation") + // if the fixed capacity is ever exceeded) + + template + using TInlineArray = TArray>; + + template + void ReverseArray(TArray& InOutArray) + { + for (int32 FrontIndex = 0, BackIndex = InOutArray.Num() - 1; FrontIndex < BackIndex; ++FrontIndex, --BackIndex) + { + InOutArray.Swap(FrontIndex, BackIndex); + } + } + + template + void ShuffleArray(TArray& Array, FRandomStream& RandomStream) + { + // Trivial cases + if (Array.Num() <= 2) + { + if (Array.Num() == 2) + { + const bool bShouldSwap = RandomStream.RandRange(0, 1) == 0; + if (bShouldSwap) + { + Array.Swap(0, 1); + } + } + + return; + } + + // Simple shuffle, attempt swaps for each index in the array, once each + for (int32 FromIndex = 0; FromIndex < Array.Num(); ++FromIndex) + { + const int32 IndexOffset = RandomStream.RandRange(1, Array.Num() - 1); + const int32 OtherIndex = (FromIndex + IndexOffset) % Array.Num(); + check(FromIndex != OtherIndex); + + Array.Swap(FromIndex, OtherIndex); + } + } + + template + bool TrySortAndRemoveDuplicatesFromArrayInPlace(TArray& InOutArray) + { + InOutArray.Sort(TGreater{}); + + const int32 SizeBefore = InOutArray.Num(); + const int32 SizeAfter = Algo::Unique(InOutArray); + + if (SizeBefore > SizeAfter) + { + InOutArray.SetNum(SizeAfter); + + return true; + } + + return false; + } + + template + FString FormatArrayString(const TArray& Values, TFunctionRef Formatter, const FString& Separator = TEXT(",")) + { + FString ValueString; + for (const T& Value : Values) + { + if (!ValueString.IsEmpty()) + { + ValueString += Separator; + } + + ValueString += Formatter(Value); + } + + return ValueString; + } +} diff --git a/Source/Flow/Public/Types/FlowAutoDataPinsWorkingData.h b/Source/Flow/Public/Types/FlowAutoDataPinsWorkingData.h new file mode 100644 index 000000000..6b7e5bf14 --- /dev/null +++ b/Source/Flow/Public/Types/FlowAutoDataPinsWorkingData.h @@ -0,0 +1,210 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Nodes/FlowPin.h" + +#include "FlowAutoDataPinsWorkingData.generated.h" + +class IFlowDataPinValueOwnerInterface; +class UFlowNode; +class UObject; +struct FFlowDataPinValue; + +/** + * Entry in MapDataPinNameToPropertySource for how to source a non-trivial pin mapping in TryGatherPropertyOwnersAndPopulateResult. + */ +USTRUCT() +struct FFlowPinPropertySource +{ + GENERATED_BODY() + + FFlowPinPropertySource() = default; + FFlowPinPropertySource(const FName& InPropertyName, int32 InValueOwnerIndex) + : PropertyName(InPropertyName) + , ValueOwnerIndex(InValueOwnerIndex) + { } + + UPROPERTY() + FName PropertyName; + + UPROPERTY() + int32 ValueOwnerIndex = INDEX_NONE; +}; + +/** + * This is a record of a data pin 'value owner' (which must implement the IFlowDataPinValueOwnerInterface) + * It includes the owner pointer itself and the index of the value owner in the FFlowDataPinValueOwnerCollection + */ +struct FFlowDataPinValueOwner +{ + explicit FFlowDataPinValueOwner( + IFlowDataPinValueOwnerInterface& InOwnerInterface, + int32 InValueOwnerIndex) + : OwnerInterface(&InOwnerInterface) + , ValueOwnerIndex(InValueOwnerIndex) + { + check(IsValid()); + } + + FName GetValueOwnerName() const + { + if (const UObject* ValueOwnerAsObject = GetValueOwnerAsObject()) + { + return ValueOwnerAsObject->GetFName(); + } + + return NAME_None; + } + + UObject* GetValueOwnerAsObject(); + const UObject* GetValueOwnerAsObject() const; + + bool IsValid() const + { + return OwnerInterface != nullptr && ValueOwnerIndex != INDEX_NONE; + } + + // The 0th ValueOwnerIndex is the default value owner + bool IsDefaultValueOwner() const { return ValueOwnerIndex == 0; } + + IFlowDataPinValueOwnerInterface* OwnerInterface = nullptr; + + int32 ValueOwnerIndex = INDEX_NONE; +}; + +/** + * A collection of ValueOwner structs that is gathered for use in auto-pin generation and also + * in runtime pin lookup. + */ +struct FFlowDataPinValueOwnerCollection +{ +public: + FLOW_API void AddValueOwner(IFlowDataPinValueOwnerInterface& ValueOwnerInterface); + + TArray& GetValueOwners() { return ValueOwners; } + + bool IsEmpty() const { return ValueOwners.IsEmpty(); } + +protected: + + TArray ValueOwners; +}; + +#if WITH_EDITOR + +/** + * Container for pin data collected during automatic pin generation. + */ +struct FFlowPinSourceData +{ + FFlowPinSourceData(const FFlowPin& InFlowPin, const FFlowDataPinValueOwner& InValueOwner, const FFlowDataPinValue* InDataPinValue = nullptr) + : FlowPin(InFlowPin) + , ValueOwner(InValueOwner) + , DataPinValue(InDataPinValue) + { + } + + FFlowPin FlowPin; + FFlowDataPinValueOwner ValueOwner; + const FFlowDataPinValue* DataPinValue = nullptr; +}; + +/** + * Transient working data used during auto-generation of data pins. + */ +struct FFlowAutoDataPinsWorkingData +{ +public: + struct FDeferredValuePinNamePatch + { + const FFlowDataPinValue* DataPinValue = nullptr; + FName NewPinName = NAME_None; + }; + + struct FBuildResult + { + TArray AutoInputPins; + TArray AutoOutputPins; + TMap MapDataPinNameToPropertySource; + TArray DeferredValuePatches; + + void Reset() + { + AutoInputPins.Reset(); + AutoOutputPins.Reset(); + MapDataPinNameToPropertySource.Reset(); + DeferredValuePatches.Reset(); + } + }; + +public: + FFlowAutoDataPinsWorkingData(const TArray& InputPinsPrev, const TArray& OutputPinsPrev) + : AutoInputDataPinsPrev(InputPinsPrev) + , AutoOutputDataPinsPrev(OutputPinsPrev) + { + } + + /* Builds the proposed next pins, property source map, and staged wrapper patches. */ + FLOW_API void Build(UFlowNode& FlowNode, FBuildResult& OutBuildResult) const; + + FLOW_API void AddFlowDataPinsForClassProperties(FFlowDataPinValueOwner& ValueOwner); + + static void BuildNextFlowPinArray(const TArray& PinSourceDatas, TArray& OutFlowPins); + + static bool CheckIfProposedPinsMatchPreviousPins(const TArray& PrevPins, const TArray& ProposedPins); + + static bool CheckIfProposedMapMatchesPreviousMap( + const TMap& PrevMap, + const TMap& ProposedMap); + +protected: + void AddFlowDataPinForProperty(FProperty* Property, FFlowDataPinValueOwner& ValueOwner); + + static bool ArePropertySourcesEqual(const FFlowPinPropertySource& A, const FFlowPinPropertySource& B); + + static void AddPinMappingToMap( + TMap& InOutMap, + const FName& FinalPinName, + const FName& OriginalPinName, + const FFlowDataPinValueOwner& ValueOwner); + + static void AppendDeferredPatchIfNeeded( + TArray& InOutPatches, + const FFlowPinSourceData& PinSourceData); + + /* Input pins: map + disambiguate only when same-name pins have mismatched type signatures. */ + static void AddInputDataPinsToMapAndDisambiguate( + TArray& InOutAutoInputPinsNext, + TMap& InOutMap, + TArray& InOutDeferredPatches); + + static bool AreFlowPinTypeSignaturesEquivalent(const FFlowPin& A, const FFlowPin& B); + + /* Output pins: existing behavior (always disambiguate duplicates). */ + static void AddOutputDataPinsToMapAndDisambiguate( + TArray& InOutAutoOutputPinsNext, + TMap& InOutMap, + TArray& InOutDeferredPatches); + + static void DisambiguateDuplicatePin( + FFlowPinSourceData& PinSourceData, + TSet& InOutUsedNames, + uint32 LogicalDuplicateIndex, + TArray& InOutDeferredPatches); + + static void ApplyDuplicatePresentation( + FFlowPinSourceData& PinSourceData, + uint32 LogicalDuplicateIndex); + + static void AppendPinSourceToTooltip(FFlowPinSourceData& PinSourceData); + +public: + const TArray& AutoInputDataPinsPrev; + const TArray& AutoOutputDataPinsPrev; + + /* Collected proposals (in provider-defined order) */ + TArray AutoInputDataPinsNext; + TArray AutoOutputDataPinsNext; +}; + +#endif \ No newline at end of file diff --git a/Source/Flow/Public/Types/FlowBranchEnums.h b/Source/Flow/Public/Types/FlowBranchEnums.h new file mode 100644 index 000000000..ecbc9cca1 --- /dev/null +++ b/Source/Flow/Public/Types/FlowBranchEnums.h @@ -0,0 +1,75 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Types/FlowEnumUtils.h" +#include "FlowBranchEnums.generated.h" + +UENUM(BlueprintType) +enum class EFlowPredicateCombinationRule : uint8 +{ + AND UMETA(ToolTip = "Passes if ALL child predicates pass"), + OR UMETA(ToolTip = "Passes if ANY (at least one) child predicates pass"), + + Max UMETA(Hidden), + Invalid UMETA(Hidden), + Min = 0 UMETA(Hidden), +}; +FLOW_ENUM_RANGE_VALUES(EFlowPredicateCombinationRule); + +/* Operator for compare operation. */ +UENUM(BlueprintType) +enum class EFlowPredicateCompareOperatorType : uint8 +{ + // Supported by all DataPin types (& UBlackboardKeyItem subclasses in the AIFlowGraph plugin) + + Equal UMETA(DisplayName = "Is Equal To"), + NotEqual UMETA(DisplayName = "Is Not Equal To"), + + // Supported by numeric (eg, _Int, _Float and _Enum) subclasses only + + Less UMETA(DisplayName = "Is Less Than"), + LessOrEqual UMETA(DisplayName = "Is Less Than Or Equal To"), + Greater UMETA(DisplayName = "Is Greater Than"), + GreaterOrEqual UMETA(DisplayName = "Is Greater Than Or Equal To"), + + Max UMETA(Hidden), + Invalid UMETA(Hidden), + Min = 0 UMETA(Hidden), + + // Subrange for equality operations + EqualityFirst = Equal UMETA(Hidden), + EqualityLast = NotEqual UMETA(Hidden), + + // Subrange for Arithmetic-only operations + ArithmeticFirst = Less UMETA(Hidden), + ArithmeticLast = GreaterOrEqual UMETA(Hidden), +}; +FLOW_ENUM_RANGE_VALUES(EFlowPredicateCompareOperatorType) + +namespace EFlowPredicateCompareOperatorType_Classifiers +{ + FORCEINLINE bool IsEqualityOperation(EFlowPredicateCompareOperatorType Operation) { return FLOW_IS_ENUM_IN_SUBRANGE(Operation, EFlowPredicateCompareOperatorType::Equality); } + FORCEINLINE bool IsArithmeticOperation(EFlowPredicateCompareOperatorType Operation) { return FLOW_IS_ENUM_IN_SUBRANGE(Operation, EFlowPredicateCompareOperatorType::Arithmetic); } + + FORCEINLINE_DEBUGGABLE FString GetOperatorSymbolString(const EFlowPredicateCompareOperatorType OperatorType) + { + static_assert(static_cast(EFlowPredicateCompareOperatorType::Max) == 6, TEXT("This should be kept up to date with the enum")); + switch(OperatorType) + { + case EFlowPredicateCompareOperatorType::Equal: + return TEXT("=="); + case EFlowPredicateCompareOperatorType::NotEqual: + return TEXT("!="); + case EFlowPredicateCompareOperatorType::Less: + return TEXT("<"); + case EFlowPredicateCompareOperatorType::LessOrEqual: + return TEXT("<="); + case EFlowPredicateCompareOperatorType::Greater: + return TEXT(">"); + case EFlowPredicateCompareOperatorType::GreaterOrEqual: + return TEXT(">="); + default: + return TEXT("[Invalid Operator]"); + } + } +} \ No newline at end of file diff --git a/Source/Flow/Public/Types/FlowClassUtils.h b/Source/Flow/Public/Types/FlowClassUtils.h new file mode 100644 index 000000000..3be9d04da --- /dev/null +++ b/Source/Flow/Public/Types/FlowClassUtils.h @@ -0,0 +1,14 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Containers/Array.h" + +class FString; +class UClass; + +#if WITH_EDITOR +namespace FlowClassUtils +{ + TArray GetClassesFromMetadataString(const FString& MetadataString); +} +#endif \ No newline at end of file diff --git a/Source/Flow/Public/Types/FlowDataPinBlueprintLibrary.h b/Source/Flow/Public/Types/FlowDataPinBlueprintLibrary.h new file mode 100644 index 000000000..a95921452 --- /dev/null +++ b/Source/Flow/Public/Types/FlowDataPinBlueprintLibrary.h @@ -0,0 +1,891 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Kismet/BlueprintFunctionLibrary.h" + +#include "FlowDataPinValuesStandard.h" +#include "FlowDataPinResults.h" +#include "FlowDataPinBlueprintLibrary.generated.h" + +struct FFlowDataPinValue; + +/** + * Auto‑cast operators for blueprint to their inner types + */ +UCLASS() +class UFlowDataPinBlueprintLibrary : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +private: + static void ResolveAndExtract_Impl( + UFlowNodeBase* Target, + FName PinName, + EFlowDataPinResolveSimpleResult& SimpleResult, + EFlowDataPinResolveResult& ResultEnum, + auto&& ExtractLambda); + +public: + + /** + * ---------- Pin construction helpers ---------- + */ + + UFUNCTION(BlueprintPure, Category = FlowPin, Meta = (BlueprintThreadSafe, DisplayName = "Make Flow Pin")) + static UPARAM(DisplayName = "Flow Pin") FFlowPin MakeStruct(FName PinName, FText PinFriendlyName, FString PinToolTip) + { + return FFlowPin(PinName, PinFriendlyName, PinToolTip); + } + + UFUNCTION(BlueprintPure, Category = FlowPin, Meta = (BlueprintThreadSafe, DisplayName = "Break Flow Pin")) + static void BreakStruct(UPARAM(DisplayName = "Flow Pin") FFlowPin Ref, FName& OutPinName, FText& OutPinFriendlyName, FString& OutPinToolTip) + { + OutPinName = Ref.PinName; + OutPinFriendlyName = Ref.PinFriendlyName; + OutPinToolTip = Ref.PinToolTip; + } + + /** + * ---------- Resolve As ... functions ---------- + * Full-featured resolve nodes with execution pins for detailed error handling. + * Use these when you need to branch on Success/Failure/Coercion or inspect the exact resolve result. + */ + + /* Resolve a Bool DataPin Value to a single bool. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Bool", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsBool(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Bool& BoolValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, bool& Value, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Resolve a Bool DataPin Value to a bool array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Bool Array", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsBoolArray(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Bool& BoolValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values); + + /* Resolve an Int DataPin Value to a single int32. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Int", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsInt(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Int& IntValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, int32& Value, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Resolve an Int DataPin Value to an int32 array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Int Array", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsIntArray(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Int& IntValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values); + + /* Resolve an Int64 DataPin Value to a single int64. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Int64", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsInt64(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Int64& Int64Value, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, int64& Value, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Resolve an Int64 DataPin Value to an int64 array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Int64 Array", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsInt64Array(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Int64& Int64Value, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values); + + /* Resolve a Float DataPin Value to a single float. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Float", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsFloat(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Float& FloatValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, float& Value, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Resolve a Float DataPin Value to a float array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Float Array", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsFloatArray(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Float& FloatValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values); + + /* Resolve a Double DataPin Value to a single double. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Double", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsDouble(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Double& DoubleValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, double& Value, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Resolve a Double DataPin Value to a double array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Double Array", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsDoubleArray(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Double& DoubleValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values); + + /* Resolve a Name DataPin Value to a single FName. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Name", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsName(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Name& NameValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, FName& Value, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Resolve a Name DataPin Value to an FName array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Name Array", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsNameArray(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Name& NameValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values); + + /* Resolve a String DataPin Value to a single FString. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As String", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsString(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_String& StringValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, FString& Value, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Resolve a String DataPin Value to an FString array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As String Array", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsStringArray(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_String& StringValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values); + + /* Resolve a Text DataPin Value to a single FText. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Text", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsText(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Text& TextValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, FText& Value, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Resolve a Text DataPin Value to an FText array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Text Array", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsTextArray(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Text& TextValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values); + + /* Resolve an Enum DataPin Value to a single uint8. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Enum", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsEnum(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Enum& EnumValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, uint8& Value, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Resolve an Enum DataPin Value to a uint8 array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Enum Array", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsEnumArray(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Enum& EnumValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values); + + /* Resolve a Vector DataPin Value to a single FVector. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Vector", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsVector(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Vector& VectorValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, FVector& Value, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Resolve a Vector DataPin Value to an FVector array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Vector Array", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsVectorArray(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Vector& VectorValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values); + + /* Resolve a Rotator DataPin Value to a single FRotator. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Rotator", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsRotator(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Rotator& RotatorValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, FRotator& Value, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Resolve a Rotator DataPin Value to an FRotator array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Rotator Array", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsRotatorArray(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Rotator& RotatorValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values); + + /* Resolve a Transform DataPin Value to a single FTransform. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Transform", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsTransform(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Transform& TransformValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, FTransform& Value, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Resolve a Transform DataPin Value to an FTransform array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Transform Array", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsTransformArray(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Transform& TransformValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values); + + /* Resolve a GameplayTag DataPin Value to a single FGameplayTag. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As GameplayTag", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsGameplayTag(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_GameplayTag& GameplayTagValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, FGameplayTag& Value, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Resolve a GameplayTag DataPin Value to an FGameplayTag array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As GameplayTag Array", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsGameplayTagArray(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_GameplayTag& GameplayTagValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values); + + /* Resolve a GameplayTagContainer DataPin Value (scalar only). */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As GameplayTagContainer", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsGameplayTagContainer(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_GameplayTagContainer& GameplayTagContainerValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, FGameplayTagContainer& Value); + + /* Resolve an InstancedStruct DataPin Value to a single FInstancedStruct. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As InstancedStruct", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsInstancedStruct(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_InstancedStruct& InstancedStructValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, FInstancedStruct& Value, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Resolve an InstancedStruct DataPin Value to an FInstancedStruct array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As InstancedStruct Array", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsInstancedStructArray(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_InstancedStruct& InstancedStructValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values); + + /* Resolve an Object DataPin Value to a single UObject*. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Object", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsObject(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Object& ObjectValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, UObject*& Value, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Resolve an Object DataPin Value to a UObject* array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Object Array", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsObjectArray(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Object& ObjectValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values); + + /* Resolve a Class DataPin Value to a single UClass*. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Class", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsClass(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Class& ClassValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, UClass*& Value, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Resolve a Class DataPin Value to a UClass* array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, meta = (DisplayName = "Resolve As Class Array", DefaultToSelf = "Target", ExpandEnumAsExecs = "Result")) + static void ResolveAsClassArray(UFlowNodeBase* Target, UPARAM(Ref) const FFlowDataPinValue_Class& ClassValue, EFlowDataPinResolveSimpleResult& Result, EFlowDataPinResolveResult& ResultEnum, TArray& Values); + + // ---------- Auto-Resolve As ... functions ---------- + // Easy-resolve convenience nodes. On failure, logs an error and returns a safe default (false/empty/null). + // Use these for fast, fire-and-forget resolving when you don't expect failures. + + /* Easy Resolve a Bool DataPin Value to a single bool (last value if array). Logs error on failure. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Bool", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static bool AutoConvert_TryResolveAsBool(UPARAM(Ref) const FFlowDataPinValue_Bool& BoolValue, const UFlowNodeBase* Target); + + /* Easy Resolve a Bool DataPin Value to a bool array. Logs error on failure and returns empty array. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Bool Array", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static TArray AutoConvert_TryResolveAsBoolArray(UPARAM(Ref) const FFlowDataPinValue_Bool& BoolValue, const UFlowNodeBase* Target); + + /* Easy Resolve an Int DataPin Value to a single int32. Logs error on failure. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Int", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static int32 AutoConvert_TryResolveAsInt(UPARAM(Ref) const FFlowDataPinValue_Int& IntValue, const UFlowNodeBase* Target); + + /* Easy Resolve an Int DataPin Value to an int32 array. Logs error on failure and returns empty array. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Int Array", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static TArray AutoConvert_TryResolveAsIntArray(UPARAM(Ref) const FFlowDataPinValue_Int& IntValue, const UFlowNodeBase* Target); + + /* Easy Resolve an Int64 DataPin Value to a single int64. Logs error on failure. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Int64", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static int64 AutoConvert_TryResolveAsInt64(UPARAM(Ref) const FFlowDataPinValue_Int64& Int64Value, const UFlowNodeBase* Target); + + /* Easy Resolve an Int64 DataPin Value to an int64 array. Logs error on failure and returns empty array. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Int64 Array", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static TArray AutoConvert_TryResolveAsInt64Array(UPARAM(Ref) const FFlowDataPinValue_Int64& Int64Value, const UFlowNodeBase* Target); + + /* Easy Resolve a Float DataPin Value to a single float. Logs error on failure. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Float", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static float AutoConvert_TryResolveAsFloat(UPARAM(Ref) const FFlowDataPinValue_Float& FloatValue, const UFlowNodeBase* Target); + + /* Easy Resolve a Float DataPin Value to a float array. Logs error on failure and returns empty array. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Float Array", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static TArray AutoConvert_TryResolveAsFloatArray(UPARAM(Ref) const FFlowDataPinValue_Float& FloatValue, const UFlowNodeBase* Target); + + /* Easy Resolve a Double DataPin Value to a single double. Logs error on failure. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Double", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static double AutoConvert_TryResolveAsDouble(UPARAM(Ref) const FFlowDataPinValue_Double& DoubleValue, const UFlowNodeBase* Target); + + /* Easy Resolve a Double DataPin Value to a double array. Logs error on failure and returns empty array. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Double Array", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static TArray AutoConvert_TryResolveAsDoubleArray(UPARAM(Ref) const FFlowDataPinValue_Double& DoubleValue, const UFlowNodeBase* Target); + + /* Easy Resolve a Name DataPin Value to a single FName. Logs error on failure. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Name", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static FName AutoConvert_TryResolveAsName(UPARAM(Ref) const FFlowDataPinValue_Name& NameValue, const UFlowNodeBase* Target); + + /* Easy Resolve a Name DataPin Value to an FName array. Logs error on failure and returns empty array. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Name Array", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static TArray AutoConvert_TryResolveAsNameArray(UPARAM(Ref) const FFlowDataPinValue_Name& NameValue, const UFlowNodeBase* Target); + + /* Easy Resolve a String DataPin Value to a single FString. Logs error on failure. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to String", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static FString AutoConvert_TryResolveAsString(UPARAM(Ref) const FFlowDataPinValue_String& StringValue, const UFlowNodeBase* Target); + + /* Easy Resolve a String DataPin Value to an FString array. Logs error on failure and returns empty array. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to String Array", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static TArray AutoConvert_TryResolveAsStringArray(UPARAM(Ref) const FFlowDataPinValue_String& StringValue, const UFlowNodeBase* Target); + + /* Easy Resolve a Text DataPin Value to a single FText. Logs error on failure. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Text", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static FText AutoConvert_TryResolveAsText(UPARAM(Ref) const FFlowDataPinValue_Text& TextValue, const UFlowNodeBase* Target); + + /* Easy Resolve a Text DataPin Value to an FText array. Logs error on failure and returns empty array. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Text Array", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static TArray AutoConvert_TryResolveAsTextArray(UPARAM(Ref) const FFlowDataPinValue_Text& TextValue, const UFlowNodeBase* Target); + + /* Easy Resolve an Enum DataPin Value to a single uint8. Logs error on failure. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Enum", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static uint8 AutoConvert_TryResolveAsEnum(UPARAM(Ref) const FFlowDataPinValue_Enum& EnumValue, const UFlowNodeBase* Target); + + /* Easy Resolve an Enum DataPin Value to a uint8 array. Logs error on failure and returns empty array. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Enum Array", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static TArray AutoConvert_TryResolveAsEnumArray(UPARAM(Ref) const FFlowDataPinValue_Enum& EnumValue, const UFlowNodeBase* Target); + + /* Easy Resolve a Vector DataPin Value to a single FVector. Logs error on failure. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Vector", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static FVector AutoConvert_TryResolveAsVector(UPARAM(Ref) const FFlowDataPinValue_Vector& VectorValue, const UFlowNodeBase* Target); + + /* Easy Resolve a Vector DataPin Value to an FVector array. Logs error on failure and returns empty array. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Vector Array", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static TArray AutoConvert_TryResolveAsVectorArray(UPARAM(Ref) const FFlowDataPinValue_Vector& VectorValue, const UFlowNodeBase* Target); + + /* Easy Resolve a Rotator DataPin Value to a single FRotator. Logs error on failure. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Rotator", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static FRotator AutoConvert_TryResolveAsRotator(UPARAM(Ref) const FFlowDataPinValue_Rotator& RotatorValue, const UFlowNodeBase* Target); + + /* Easy Resolve a Rotator DataPin Value to an FRotator array. Logs error on failure and returns empty array. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Rotator Array", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static TArray AutoConvert_TryResolveAsRotatorArray(UPARAM(Ref) const FFlowDataPinValue_Rotator& RotatorValue, const UFlowNodeBase* Target); + + /* Easy Resolve a Transform DataPin Value to a single FTransform. Logs error on failure. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Transform", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static FTransform AutoConvert_TryResolveAsTransform(UPARAM(Ref) const FFlowDataPinValue_Transform& TransformValue, const UFlowNodeBase* Target); + + /* Easy Resolve a Transform DataPin Value to an FTransform array. Logs error on failure and returns empty array. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Transform Array", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static TArray AutoConvert_TryResolveAsTransformArray(UPARAM(Ref) const FFlowDataPinValue_Transform& TransformValue, const UFlowNodeBase* Target); + + /* Easy Resolve a GameplayTag DataPin Value to a single FGameplayTag. Logs error on failure. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to GameplayTag", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static FGameplayTag AutoConvert_TryResolveAsGameplayTag(UPARAM(Ref) const FFlowDataPinValue_GameplayTag& GameplayTagValue, const UFlowNodeBase* Target); + + /* Easy Resolve a GameplayTag DataPin Value to an FGameplayTag array. Logs error on failure and returns empty array. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to GameplayTag Array", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static TArray AutoConvert_TryResolveAsGameplayTagArray(UPARAM(Ref) const FFlowDataPinValue_GameplayTag& GameplayTagValue, const UFlowNodeBase* Target); + + /* Easy Resolve a GameplayTagContainer DataPin Value (scalar only). Logs error on failure. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to GameplayTagContainer", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static FGameplayTagContainer AutoConvert_TryResolveAsGameplayTagContainer(UPARAM(Ref) const FFlowDataPinValue_GameplayTagContainer& GameplayTagContainerValue, const UFlowNodeBase* Target); + + /* Easy Resolve an InstancedStruct DataPin Value to a single FInstancedStruct. Logs error on failure. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to InstancedStruct", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static FInstancedStruct AutoConvert_TryResolveAsInstancedStruct(UPARAM(Ref) const FFlowDataPinValue_InstancedStruct& InstancedStructValue, const UFlowNodeBase* Target); + + /* Easy Resolve an InstancedStruct DataPin Value to an FInstancedStruct array. Logs error on failure and returns empty array. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to InstancedStruct Array", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static TArray AutoConvert_TryResolveAsInstancedStructArray(UPARAM(Ref) const FFlowDataPinValue_InstancedStruct& InstancedStructValue, const UFlowNodeBase* Target); + + /* Easy Resolve an Object DataPin Value to a single UObject*. Logs error on failure. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Object", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static UObject* AutoConvert_TryResolveAsObject(UPARAM(Ref) const FFlowDataPinValue_Object& ObjectValue, const UFlowNodeBase* Target); + + /* Easy Resolve an Object DataPin Value to a UObject* array. Logs error on failure and returns empty array. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Object Array", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static TArray AutoConvert_TryResolveAsObjectArray(UPARAM(Ref) const FFlowDataPinValue_Object& ObjectValue, const UFlowNodeBase* Target); + + /* Easy Resolve a Class DataPin Value to a single UClass*. Logs error on failure. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Class", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static UClass* AutoConvert_TryResolveAsClass(UPARAM(Ref) const FFlowDataPinValue_Class& ClassValue, const UFlowNodeBase* Target); + + /* Easy Resolve a Class DataPin Value to a UClass* array. Logs error on failure and returns empty array. */ + UFUNCTION(BlueprintPure, meta = (DisplayName = "Auto-Resolve to Class Array", CompactNodeTitle = "->", BlueprintAutocast, DefaultToSelf = "Target"), Category = DataPins) + static TArray AutoConvert_TryResolveAsClassArray(UPARAM(Ref) const FFlowDataPinValue_Class& ClassValue, const UFlowNodeBase* Target); + + // ---------- Result → result enum converter ---------- + + UFUNCTION(BlueprintPure, meta = (DisplayName = "Convert to Result", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static EFlowDataPinResolveResult AutoConvert_TryExtractResultEnum(const FFlowDataPinResult& DataPinResult) + { + return DataPinResult.Result; + } + + // ---------- Result → native value extractors ---------- + + // Bool + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Bool", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static bool AutoConvert_TryExtractBool(const FFlowDataPinResult& DataPinResult); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Bool Array", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static TArray AutoConvert_TryExtractBoolArray(const FFlowDataPinResult& DataPinResult); + + // Int (int32) + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Int", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static int32 AutoConvert_TryExtractInt(const FFlowDataPinResult& DataPinResult); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Int Array", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static TArray AutoConvert_TryExtractIntArray(const FFlowDataPinResult& DataPinResult); + + // Int64 + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Int64", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static int64 AutoConvert_TryExtractInt64(const FFlowDataPinResult& DataPinResult); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Int64 Array", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static TArray AutoConvert_TryExtractInt64Array(const FFlowDataPinResult& DataPinResult); + + // Float + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Float", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static float AutoConvert_TryExtractFloat(const FFlowDataPinResult& DataPinResult); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Float Array", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static TArray AutoConvert_TryExtractFloatArray(const FFlowDataPinResult& DataPinResult); + + // Double + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Double", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static double AutoConvert_TryExtractDouble(const FFlowDataPinResult& DataPinResult); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Double Array", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static TArray AutoConvert_TryExtractDoubleArray(const FFlowDataPinResult& DataPinResult); + + // Name + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Name", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static FName AutoConvert_TryExtractName(const FFlowDataPinResult& DataPinResult); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Name Array", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static TArray AutoConvert_TryExtractNameArray(const FFlowDataPinResult& DataPinResult); + + // String + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to String", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static FString AutoConvert_TryExtractString(const FFlowDataPinResult& DataPinResult); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to String Array", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static TArray AutoConvert_TryExtractStringArray(const FFlowDataPinResult& DataPinResult); + + // Text + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Text", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static FText AutoConvert_TryExtractText(const FFlowDataPinResult& DataPinResult); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Text Array", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static TArray AutoConvert_TryExtractTextArray(const FFlowDataPinResult& DataPinResult); + + // Enum + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Enum", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static uint8 AutoConvert_TryExtractEnum(const FFlowDataPinResult& DataPinResult); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Enum Array", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static TArray AutoConvert_TryExtractEnumArray(const FFlowDataPinResult& DataPinResult); + + // Vector + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Vector", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static FVector AutoConvert_TryExtractVector(const FFlowDataPinResult& DataPinResult); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Vector Array", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static TArray AutoConvert_TryExtractVectorArray(const FFlowDataPinResult& DataPinResult); + + // Rotator + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Rotator", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static FRotator AutoConvert_TryExtractRotator(const FFlowDataPinResult& DataPinResult); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Rotator Array", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static TArray AutoConvert_TryExtractRotatorArray(const FFlowDataPinResult& DataPinResult); + + // Transform + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Transform", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static FTransform AutoConvert_TryExtractTransform(const FFlowDataPinResult& DataPinResult); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Transform Array", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static TArray AutoConvert_TryExtractTransformArray(const FFlowDataPinResult& DataPinResult); + + // GameplayTag + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to GameplayTag", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static FGameplayTag AutoConvert_TryExtractGameplayTag(const FFlowDataPinResult& DataPinResult); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to GameplayTag Array", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static TArray AutoConvert_TryExtractGameplayTagArray(const FFlowDataPinResult& DataPinResult); + + // GameplayTagContainer + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to GameplayTagContainer", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static FGameplayTagContainer AutoConvert_TryExtractGameplayTagContainer(const FFlowDataPinResult& DataPinResult); + + // InstancedStruct + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to InstancedStruct", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static FInstancedStruct AutoConvert_TryExtractInstancedStruct(const FFlowDataPinResult& DataPinResult); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to InstancedStruct Array", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static TArray AutoConvert_TryExtractInstancedStructArray(const FFlowDataPinResult& DataPinResult); + + // Object + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Object", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static UObject* AutoConvert_TryExtractObject(const FFlowDataPinResult& DataPinResult); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Object Array", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static TArray AutoConvert_TryExtractObjectArray(const FFlowDataPinResult& DataPinResult); + + // Class + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Class", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static UClass* AutoConvert_TryExtractClass(const FFlowDataPinResult& DataPinResult); + + UFUNCTION(BlueprintPure, meta = (DisplayName = "Extract to Class Array", CompactNodeTitle = "->", BlueprintAutocast), Category = DataPins) + static TArray AutoConvert_TryExtractClassArray(const FFlowDataPinResult& DataPinResult); + + // ---------- Get & Set Value functions ---------- + // Direct access to the stored payload on a DataPin Value struct. + // Set functions: Safe for both input and output pins. + // Get functions: ONLY safe on output pins. Using on an input pin triggers a runtime error in editor builds. + + /* Set a single bool on a Bool DataPin Value (input or output pin). + * Replaces any existing values. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Bool Value")) + static void SetBoolValue(bool bInValue, UPARAM(Ref) FFlowDataPinValue_Bool& BoolValue) { BoolValue.Values = { bInValue }; } + + /* Set a bool array on a Bool DataPin Value (input or output pin). + * Replaces the entire array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Bool Values")) + static void SetBoolValues(const TArray& InValues, UPARAM(Ref) FFlowDataPinValue_Bool& BoolValue) { BoolValue.Values = InValues; } + + /* Get a single bool from an output Bool DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Bool Value")) + static bool GetBoolValue(UPARAM(Ref) const FFlowDataPinValue_Bool& BoolValue, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Get the full bool array from an output Bool DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Bool Values")) + static TArray GetBoolValues(UPARAM(Ref) FFlowDataPinValue_Bool& BoolValue); + + /* Set a single int32 on an Int DataPin Value (input or output pin). + * Replaces any existing values. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Int Value")) + static void SetIntValue(int32 InValue, UPARAM(Ref) FFlowDataPinValue_Int& IntValue) { IntValue.Values = { InValue }; } + + /* Set an int32 array on an Int DataPin Value (input or output pin). + * Replaces the entire array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Int Values")) + static void SetIntValues(const TArray& InValues, UPARAM(Ref) FFlowDataPinValue_Int& IntValue) { IntValue.Values = InValues; } + + /* Get a single int32 from an output Int DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Int Value")) + static int32 GetIntValue(UPARAM(Ref) const FFlowDataPinValue_Int& IntValue, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Get the full int32 array from an output Int DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Int Values")) + static TArray GetIntValues(UPARAM(Ref) FFlowDataPinValue_Int& IntValue); + + /* Set a single int64 on an Int64 DataPin Value (input or output pin). + * Replaces any existing values. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Int64 Value")) + static void SetInt64Value(int64 InValue, UPARAM(Ref) FFlowDataPinValue_Int64& Int64Value) { Int64Value.Values = { InValue }; } + + /* Set an int64 array on an Int64 DataPin Value (input or output pin). + * Replaces the entire array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Int64 Values")) + static void SetInt64Values(const TArray& InValues, UPARAM(Ref) FFlowDataPinValue_Int64& Int64Value) { Int64Value.Values = InValues; } + + /* Get a single int64 from an output Int64 DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Int64 Value")) + static int64 GetInt64Value(UPARAM(Ref) const FFlowDataPinValue_Int64& Int64Value, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Get the full int64 array from an output Int64 DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Int64 Values")) + static TArray GetInt64Values(UPARAM(Ref) FFlowDataPinValue_Int64& Int64Value); + + /* Set a single float on a Float DataPin Value (input or output pin). + * Replaces any existing values. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Float Value")) + static void SetFloatValue(float InValue, UPARAM(Ref) FFlowDataPinValue_Float& FloatValue) { FloatValue.Values = { InValue }; } + + /* Set a float array on a Float DataPin Value (input or output pin). + * Replaces the entire array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Float Values")) + static void SetFloatValues(const TArray& InValues, UPARAM(Ref) FFlowDataPinValue_Float& FloatValue) { FloatValue.Values = InValues; } + + /* Get a single float from an output Float DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Float Value")) + static float GetFloatValue(UPARAM(Ref) const FFlowDataPinValue_Float& FloatValue, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Get the full float array from an output Float DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Float Values")) + static TArray GetFloatValues(UPARAM(Ref) FFlowDataPinValue_Float& FloatValue); + + /* Set a single double on a Double DataPin Value (input or output pin). + * Replaces any existing values. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Double Value")) + static void SetDoubleValue(double InValue, UPARAM(Ref) FFlowDataPinValue_Double& DoubleValue) { DoubleValue.Values = { InValue }; } + + /* Set a double array on a Double DataPin Value (input or output pin). + * Replaces the entire array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Double Values")) + static void SetDoubleValues(const TArray& InValues, UPARAM(Ref) FFlowDataPinValue_Double& DoubleValue) { DoubleValue.Values = InValues; } + + /* Get a single double from an output Double DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Double Value")) + static double GetDoubleValue(UPARAM(Ref) const FFlowDataPinValue_Double& DoubleValue, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Get the full double array from an output Double DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Double Values")) + static TArray GetDoubleValues(UPARAM(Ref) FFlowDataPinValue_Double& DoubleValue); + + /* Set a single FName on a Name DataPin Value (input or output pin). + * Replaces any existing values. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Name Value")) + static void SetNameValue(FName InValue, UPARAM(Ref) FFlowDataPinValue_Name& NameValue) { NameValue.Values = { InValue }; } + + /* Set an FName array on a Name DataPin Value (input or output pin). + * Replaces the entire array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Name Values")) + static void SetNameValues(const TArray& InValues, UPARAM(Ref) FFlowDataPinValue_Name& NameValue) { NameValue.Values = InValues; } + + /* Get a single FName from an output Name DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Name Value")) + static FName GetNameValue(UPARAM(Ref) const FFlowDataPinValue_Name& NameValue, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Get the full FName array from an output Name DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Name Values")) + static TArray GetNameValues(UPARAM(Ref) FFlowDataPinValue_Name& NameValue); + + /* Set a single FString on a String DataPin Value (input or output pin). + * Replaces any existing values. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set String Value")) + static void SetStringValue(const FString& InValue, UPARAM(Ref) FFlowDataPinValue_String& StringValue) { StringValue.Values = { InValue }; } + + /* Set an FString array on a String DataPin Value (input or output pin). + * Replaces the entire array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set String Values")) + static void SetStringValues(const TArray& InValues, UPARAM(Ref) FFlowDataPinValue_String& StringValue) { StringValue.Values = InValues; } + + /* Get a single FString from an output String DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get String Value")) + static FString GetStringValue(UPARAM(Ref) const FFlowDataPinValue_String& StringValue, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Get the full FString array from an output String DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get String Values")) + static TArray GetStringValues(UPARAM(Ref) FFlowDataPinValue_String& StringValue); + + /* Set a single FText on a Text DataPin Value (input or output pin). + * Replaces any existing values. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Text Value")) + static void SetTextValue(const FText& InValue, UPARAM(Ref) FFlowDataPinValue_Text& TextValue) { TextValue.Values = { InValue }; } + + /* Set an FText array on a Text DataPin Value (input or output pin). + * Replaces the entire array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Text Values")) + static void SetTextValues(const TArray& InValues, UPARAM(Ref) FFlowDataPinValue_Text& TextValue) { TextValue.Values = InValues; } + + /* Get a single FText from an output Text DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Text Value")) + static FText GetTextValue(UPARAM(Ref) const FFlowDataPinValue_Text& TextValue, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Get the full FText array from an output Text DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Text Values")) + static TArray GetTextValues(UPARAM(Ref) FFlowDataPinValue_Text& TextValue); + + /* Set a single enum value (as uint8) on an Enum DataPin Value (input or output pin). + * Requires EnumClass to be set on the struct. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Enum Value")) + static void SetEnumValue(uint8 InValue, UPARAM(Ref) FFlowDataPinValue_Enum& EnumValue); + + /* Set an enum value array (as uint8) on an Enum DataPin Value (input or output pin). + * Requires EnumClass to be set on the struct. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Enum Values")) + static void SetEnumValues(const TArray& InValues, UPARAM(Ref) FFlowDataPinValue_Enum& EnumValue); + + /* Get a single enum value (as uint8) from an output Enum DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Enum Value")) + static uint8 GetEnumValue(UPARAM(Ref) const FFlowDataPinValue_Enum& EnumValue, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Get the full enum value array (as uint8) from an output Enum DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Enum Values")) + static TArray GetEnumValues(UPARAM(Ref) const FFlowDataPinValue_Enum& EnumValue); + + /* Set a single FVector on a Vector DataPin Value (input or output pin). + * Replaces any existing values. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Vector Value")) + static void SetVectorValue(const FVector& InValue, UPARAM(Ref) FFlowDataPinValue_Vector& VectorValue) { VectorValue.Values = { InValue }; } + + /* Set an FVector array on a Vector DataPin Value (input or output pin). + * Replaces the entire array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Vector Values")) + static void SetVectorValues(const TArray& InValues, UPARAM(Ref) FFlowDataPinValue_Vector& VectorValue) { VectorValue.Values = InValues; } + + /* Get a single FVector from an output Vector DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Vector Value")) + static FVector GetVectorValue(UPARAM(Ref) const FFlowDataPinValue_Vector& VectorValue, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Get the full FVector array from an output Vector DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Vector Values")) + static TArray GetVectorValues(UPARAM(Ref) FFlowDataPinValue_Vector& VectorValue); + + /* Set a single FRotator on a Rotator DataPin Value (input or output pin). + * Replaces any existing values. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Rotator Value")) + static void SetRotatorValue(const FRotator& InValue, UPARAM(Ref) FFlowDataPinValue_Rotator& RotatorValue) { RotatorValue.Values = { InValue }; } + + /* Set an FRotator array on a Rotator DataPin Value (input or output pin). + * Replaces the entire array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Rotator Values")) + static void SetRotatorValues(const TArray& InValues, UPARAM(Ref) FFlowDataPinValue_Rotator& RotatorValue) { RotatorValue.Values = InValues; } + + /* Get a single FRotator from an output Rotator DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Rotator Value")) + static FRotator GetRotatorValue(UPARAM(Ref) const FFlowDataPinValue_Rotator& RotatorValue, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Get the full FRotator array from an output Rotator DataPin Value + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Rotator Values")) + static TArray GetRotatorValues(UPARAM(Ref) FFlowDataPinValue_Rotator& RotatorValue); + + /* Set a single FTransform on a Transform DataPin Value (input or output pin). + * Replaces any existing values. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Transform Value")) + static void SetTransformValue(const FTransform& InValue, UPARAM(Ref) FFlowDataPinValue_Transform& TransformValue) { TransformValue.Values = { InValue }; } + + /* Set an FTransform array on a Transform DataPin Value (input or output pin). + * Replaces the entire array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Transform Values")) + static void SetTransformValues(const TArray& InValues, UPARAM(Ref) FFlowDataPinValue_Transform& TransformValue) { TransformValue.Values = InValues; } + + /* Get a single FTransform from an output Transform DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Transform Value")) + static FTransform GetTransformValue(UPARAM(Ref) const FFlowDataPinValue_Transform& TransformValue, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Get the full FTransform array from an output Transform DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Transform Values")) + static TArray GetTransformValues(UPARAM(Ref) FFlowDataPinValue_Transform& TransformValue); + + /* Set a single FGameplayTag on a GameplayTag DataPin Value (input or output pin). + * Replaces any existing values. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set GameplayTag Value")) + static void SetGameplayTagValue(FGameplayTag InValue, UPARAM(Ref) FFlowDataPinValue_GameplayTag& GameplayTagValue) { GameplayTagValue.Values = { InValue }; } + + /* Set an FGameplayTag array on a GameplayTag DataPin Value (input or output pin). + * Replaces the entire array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set GameplayTag Values")) + static void SetGameplayTagValues(const TArray& InValues, UPARAM(Ref) FFlowDataPinValue_GameplayTag& GameplayTagValue) { GameplayTagValue.Values = InValues; } + + /* Get a single FGameplayTag from an output GameplayTag DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get GameplayTag Value")) + static FGameplayTag GetGameplayTagValue(UPARAM(Ref) const FFlowDataPinValue_GameplayTag& GameplayTagValue, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Get the full FGameplayTag array from an output GameplayTag DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get GameplayTag Values")) + static TArray GetGameplayTagValues(UPARAM(Ref) FFlowDataPinValue_GameplayTag& GameplayTagValue); + + /* Set a GameplayTagContainer on a GameplayTagContainer DataPin Value (input or output pin, scalar only). */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set GameplayTagContainer Value")) + static void SetGameplayTagContainerValue(const FGameplayTagContainer& InValue, UPARAM(Ref) FFlowDataPinValue_GameplayTagContainer& GameplayTagContainerValue) { GameplayTagContainerValue.Values = InValue; } + + /* Get the GameplayTagContainer from an output GameplayTagContainer DataPin Value (scalar only). + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get GameplayTagContainer Value")) + static FGameplayTagContainer GetGameplayTagContainerValue(UPARAM(Ref) const FFlowDataPinValue_GameplayTagContainer& GameplayTagContainerValue); + + /* Set a single FInstancedStruct on an InstancedStruct DataPin Value (input or output pin). + * Replaces any existing values. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set InstancedStruct Value")) + static void SetInstancedStructValue(const FInstancedStruct& InValue, UPARAM(Ref) FFlowDataPinValue_InstancedStruct& InstancedStructValue) { InstancedStructValue.Values = { InValue }; } + + /* Set an FInstancedStruct array on an InstancedStruct DataPin Value (input or output pin). + * Replaces the entire array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set InstancedStruct Values")) + static void SetInstancedStructValues(const TArray& InValues, UPARAM(Ref) FFlowDataPinValue_InstancedStruct& InstancedStructValue) { InstancedStructValue.Values = InValues; } + + /* Get a single FInstancedStruct from an output InstancedStruct DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get InstancedStruct Value")) + static FInstancedStruct GetInstancedStructValue(UPARAM(Ref) const FFlowDataPinValue_InstancedStruct& InstancedStructValue, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Get the full FInstancedStruct array from an output InstancedStruct DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get InstancedStruct Values")) + static TArray GetInstancedStructValues(UPARAM(Ref) FFlowDataPinValue_InstancedStruct& InstancedStructValue); + + /* Set a single UObject* on an Object DataPin Value (input or output pin). + * Replaces any existing values. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Object Value")) + static void SetObjectValue(UObject* InValue, UPARAM(Ref) FFlowDataPinValue_Object& ObjectValue) { ObjectValue.Values = { InValue }; } + + /* Set a UObject* array on an Object DataPin Value (input or output pin). + * Replaces the entire array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Object Values")) + static void SetObjectValues(const TArray& InValues, UPARAM(Ref) FFlowDataPinValue_Object& ObjectValue) { ObjectValue.Values = InValues; } + + /* Get a single UObject* from an output Object DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Object Value")) + static UObject* GetObjectValue(UPARAM(Ref) const FFlowDataPinValue_Object& ObjectValue, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Get the full UObject* array from an output Object DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Object Values")) + static TArray GetObjectValues(UPARAM(Ref) FFlowDataPinValue_Object& ObjectValue); + + /* Set a single FSoftClassPath on a Class DataPin Value (input or output pin). + * Replaces any existing values. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Class Value")) + static void SetClassValue(const FSoftClassPath& InValue, UPARAM(Ref) FFlowDataPinValue_Class& ClassValue) { ClassValue.Values = { InValue }; } + + /* Set an FSoftClassPath array on a Class DataPin Value (input or output pin). + * Replaces the entire array. */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Set Class Values")) + static void SetClassValues(const TArray& InValues, UPARAM(Ref) FFlowDataPinValue_Class& ClassValue) { ClassValue.Values = InValues; } + + /* Get a single FSoftClassPath from an output Class DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Class Value")) + static FSoftClassPath GetClassValue(UPARAM(Ref) const FFlowDataPinValue_Class& ClassValue, EFlowSingleFromArray SingleFromArray = EFlowSingleFromArray::LastValue); + + /* Get the full FSoftClassPath array from an output Class DataPin Value. + * DO NOT use on input pins — will error in editor, use Resolve As... functions instead! */ + UFUNCTION(BlueprintCallable, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Get Class Values")) + static TArray GetClassValues(UPARAM(Ref) FFlowDataPinValue_Class& ClassValue); + + // --------- Make Flow DataPin Result - for use in blueprint TrySupplyDataPin implementations ---------- + + UFUNCTION(BlueprintPure, Category = DataPins, meta = (DisplayName = "Make Flow DataPin Result")) + static FFlowDataPinResult MakeFlowDataPinResult_Empty(EFlowDataPinResolveResult Result) { return FFlowDataPinResult(Result); } + + UFUNCTION(BlueprintPure, Category = DataPins, meta = (DisplayName = "Make Bool Flow DataPin Result")) + static FFlowDataPinResult MakeFlowDataPinResult_Bool(const TArray& BoolValues); + + UFUNCTION(BlueprintPure, Category = DataPins, meta = (DisplayName = "Make Int Flow DataPin Result")) + static FFlowDataPinResult MakeFlowDataPinResult_Int(const TArray& IntValues); + + UFUNCTION(BlueprintPure, Category = DataPins, meta = (DisplayName = "Make Int64 Flow DataPin Result")) + static FFlowDataPinResult MakeFlowDataPinResult_Int64(const TArray& Int64Values); + + UFUNCTION(BlueprintPure, Category = DataPins, meta = (DisplayName = "Make Float Flow DataPin Result")) + static FFlowDataPinResult MakeFlowDataPinResult_Float(const TArray& FloatValues); + + UFUNCTION(BlueprintPure, Category = DataPins, meta = (DisplayName = "Make Double Flow DataPin Result")) + static FFlowDataPinResult MakeFlowDataPinResult_Double(const TArray& DoubleValues); + + UFUNCTION(BlueprintPure, Category = DataPins, meta = (DisplayName = "Make Name Flow DataPin Result")) + static FFlowDataPinResult MakeFlowDataPinResult_Name(const TArray& NameValues); + + UFUNCTION(BlueprintPure, Category = DataPins, meta = (DisplayName = "Make String Flow DataPin Result")) + static FFlowDataPinResult MakeFlowDataPinResult_String(const TArray& StringValues); + + UFUNCTION(BlueprintPure, Category = DataPins, meta = (DisplayName = "Make Text Flow DataPin Result")) + static FFlowDataPinResult MakeFlowDataPinResult_Text(const TArray& TextValues); + + UFUNCTION(BlueprintPure, Category = DataPins, meta = (DisplayName = "Make Enum Flow DataPin Result")) + static FFlowDataPinResult MakeFlowDataPinResult_Enum(const TArray& EnumValues, UEnum* EnumClass); + + UFUNCTION(BlueprintPure, Category = DataPins, meta = (DisplayName = "Make Vector Flow DataPin Result")) + static FFlowDataPinResult MakeFlowDataPinResult_Vector(const TArray& VectorValues); + + UFUNCTION(BlueprintPure, Category = DataPins, meta = (DisplayName = "Make Rotator Flow DataPin Result")) + static FFlowDataPinResult MakeFlowDataPinResult_Rotator(const TArray& RotatorValues); + + UFUNCTION(BlueprintPure, Category = DataPins, meta = (DisplayName = "Make Transform Flow DataPin Result")) + static FFlowDataPinResult MakeFlowDataPinResult_Transform(const TArray& TransformValues); + + UFUNCTION(BlueprintPure, Category = DataPins, meta = (DisplayName = "Make GameplayTag Flow DataPin Result")) + static FFlowDataPinResult MakeFlowDataPinResult_GameplayTag(const TArray& GameplayTagValues); + + UFUNCTION(BlueprintPure, Category = DataPins, meta = (DisplayName = "Make GameplayTagContainer Flow DataPin Result")) + static FFlowDataPinResult MakeFlowDataPinResult_GameplayTagContainer(FGameplayTagContainer GameplayTagContainerValue); + + UFUNCTION(BlueprintPure, Category = DataPins, meta = (DisplayName = "Make InstancedStruct Flow DataPin Result")) + static FFlowDataPinResult MakeFlowDataPinResult_InstancedStruct(const TArray& InstancedStructValues); + + UFUNCTION(BlueprintPure, Category = DataPins, meta = (DisplayName = "Make Object Flow DataPin Result")) + static FFlowDataPinResult MakeFlowDataPinResult_Object(const TArray& ObjectValues); + + UFUNCTION(BlueprintPure, Category = DataPins, meta = (DisplayName = "Make Class Flow DataPin Result")) + static FFlowDataPinResult MakeFlowDataPinResult_Class(const TArray& ClassValues); + + // ---------- Override the Make functions to discourage use ---------- + // Ideally, we would forbid Make altogether, but this is the best work-around I have found. + + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Make Bool Flow DataPin Value", DeprecatedFunction, DeprecationMessage = "use SetBoolValue(s) instead")) + static FFlowDataPinValue_Bool MakeStructBool(const FFlowDataPinValue_Bool& OtherValueStruct) { return OtherValueStruct; } + + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Make Int Flow DataPin Value", DeprecatedFunction, DeprecationMessage = "use SetIntValue(s) instead")) + static FFlowDataPinValue_Int MakeStructInt(const FFlowDataPinValue_Int& OtherValueStruct) { return OtherValueStruct; } + + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Make Int64 Flow DataPin Value", DeprecatedFunction, DeprecationMessage = "use SetInt64Value(s) instead")) + static FFlowDataPinValue_Int64 MakeStructInt64(const FFlowDataPinValue_Int64& OtherValueStruct) { return OtherValueStruct; } + + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Make Float Flow DataPin Value", DeprecatedFunction, DeprecationMessage = "use SetFloatValue(s) instead")) + static FFlowDataPinValue_Float MakeStructFloat(const FFlowDataPinValue_Float& OtherValueStruct) { return OtherValueStruct; } + + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Make Double Flow DataPin Value", DeprecatedFunction, DeprecationMessage = "use SetDoubleValue(s) instead")) + static FFlowDataPinValue_Double MakeStructDouble(const FFlowDataPinValue_Double& OtherValueStruct) { return OtherValueStruct; } + + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Make Name Flow DataPin Value", DeprecatedFunction, DeprecationMessage = "use SetNameValue(s) instead")) + static FFlowDataPinValue_Name MakeStructName(const FFlowDataPinValue_Name& OtherValueStruct) { return OtherValueStruct; } + + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Make String Flow DataPin Value", DeprecatedFunction, DeprecationMessage = "use SetStringValue(s) instead")) + static FFlowDataPinValue_String MakeStructString(const FFlowDataPinValue_String& OtherValueStruct) { return OtherValueStruct; } + + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Make Text Flow DataPin Value", DeprecatedFunction, DeprecationMessage = "use SetTextValue(s) instead")) + static FFlowDataPinValue_Text MakeStructText(const FFlowDataPinValue_Text& OtherValueStruct) { return OtherValueStruct; } + + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Make Enum Flow DataPin Value", DeprecatedFunction, DeprecationMessage = "use SetEnumValue(s) instead")) + static FFlowDataPinValue_Enum MakeStructEnum(const FFlowDataPinValue_Enum& OtherValueStruct) { return OtherValueStruct; } + + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Make Vector Flow DataPin Value", DeprecatedFunction, DeprecationMessage = "use SetVectorValue(s) instead")) + static FFlowDataPinValue_Vector MakeStructVector(const FFlowDataPinValue_Vector& OtherValueStruct) { return OtherValueStruct; } + + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Make Rotator Flow DataPin Value", DeprecatedFunction, DeprecationMessage = "use SetRotatorValue(s) instead")) + static FFlowDataPinValue_Rotator MakeStructRotator(const FFlowDataPinValue_Rotator& OtherValueStruct) { return OtherValueStruct; } + + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Make Transform Flow DataPin Value", DeprecatedFunction, DeprecationMessage = "use SetTransformValue(s) instead")) + static FFlowDataPinValue_Transform MakeStructTransform(const FFlowDataPinValue_Transform& OtherValueStruct) { return OtherValueStruct; } + + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Make GameplayTag Flow DataPin Value", DeprecatedFunction, DeprecationMessage = "use SetGameplayTagValue(s) instead")) + static FFlowDataPinValue_GameplayTag MakeStructGameplayTag(const FFlowDataPinValue_GameplayTag& OtherValueStruct) { return OtherValueStruct; } + + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Make GameplayTagContainer Flow DataPin Value", DeprecatedFunction, DeprecationMessage = "use SetGameplayTagContainerValue(s) instead")) + static FFlowDataPinValue_GameplayTagContainer MakeStructGameplayTagContainer(const FFlowDataPinValue_GameplayTagContainer& OtherValueStruct) { return OtherValueStruct; } + + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Make InstancedStruct Flow DataPin Value", DeprecatedFunction, DeprecationMessage = "use SetInstancedStructValue(s) instead")) + static FFlowDataPinValue_InstancedStruct MakeStructInstancedStruct(const FFlowDataPinValue_InstancedStruct& OtherValueStruct) { return OtherValueStruct; } + + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Make Object Flow DataPin Value", DeprecatedFunction, DeprecationMessage = "use SetObjectValue(s) instead")) + static FFlowDataPinValue_Object MakeStructObject(const FFlowDataPinValue_Object& OtherValueStruct) { return OtherValueStruct; } + + UFUNCTION(BlueprintPure, Category = DataPins, Meta = (BlueprintThreadSafe, DisplayName = "Make Class Flow DataPin Value", DeprecatedFunction, DeprecationMessage = "use SetClassValue(s) instead")) + static FFlowDataPinValue_Class MakeStructClass(const FFlowDataPinValue_Class& OtherValueStruct) { return OtherValueStruct; } +}; diff --git a/Source/Flow/Public/Types/FlowDataPinProperties.h b/Source/Flow/Public/Types/FlowDataPinProperties.h new file mode 100644 index 000000000..f97dabc1c --- /dev/null +++ b/Source/Flow/Public/Types/FlowDataPinProperties.h @@ -0,0 +1,522 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "GameplayTagContainer.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "StructUtils/InstancedStruct.h" +#include "UObject/Class.h" + +#include "FlowDataPinProperties.generated.h" + +/** + * #FlowDataPinLegacy + */ + +USTRUCT(DisplayName = "Base - Flow DataPin Property", meta = (Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinProperty +{ + GENERATED_BODY() + + FFlowDataPinProperty() = default; + + virtual ~FFlowDataPinProperty() { } +}; + +/** + * Wrapper struct for a bool that will generate and link to a Data Pin with its same name. + */ +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] Bool - Output Flow Data Pin Property", meta = (FlowPinType = "Bool", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinOutputProperty_Bool : public FFlowDataPinProperty +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + bool Value = false; + +public: + + FFlowDataPinOutputProperty_Bool() { } + explicit FFlowDataPinOutputProperty_Bool(const bool InValue) : Value(InValue) { } +}; + +/** + * Wrapper struct for an int64 that will generate and link to a Data Pin with its same name. + */ +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] Int64 - Output Flow Data Pin Property", meta = (FlowPinType = "Int64", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinOutputProperty_Int64 : public FFlowDataPinProperty +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + int64 Value = 0; + +public: + + FFlowDataPinOutputProperty_Int64() { } + explicit FFlowDataPinOutputProperty_Int64(const int64 InValue) : Value(InValue) { } +}; + +/** + * Wrapper struct for an int32 that will generate and link to a Data Pin with its same name. + */ +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] Int - Output Flow Data Pin Property", meta = (FlowPinType = "Int", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinOutputProperty_Int32 : public FFlowDataPinProperty +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + int32 Value = 0; + +public: + FFlowDataPinOutputProperty_Int32() { } + explicit FFlowDataPinOutputProperty_Int32(const int32 InValue) : Value(InValue) { } +}; + +/** + * Wrapper struct for a Double (64bit float) that will generate and link to a Data Pin with its same name. + */ +USTRUCT(BlueprintType, DisplayName = "Double (float64) - Output Flow Data Pin Property", meta = (FlowPinType = "Double", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinOutputProperty_Double : public FFlowDataPinProperty +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + double Value = 0; + +public: + FFlowDataPinOutputProperty_Double() { } + explicit FFlowDataPinOutputProperty_Double(const double InValue) : Value(InValue) { } +}; + +/** + * Wrapper struct for a Float (32bit) that will generate and link to a Data Pin with its same name. + */ +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] Float - Output Flow Data Pin Property", meta = (FlowPinType = "Float", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinOutputProperty_Float : public FFlowDataPinProperty +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + float Value = 0.0f; + +public: + FFlowDataPinOutputProperty_Float() { } + explicit FFlowDataPinOutputProperty_Float(const float InValue) : Value(InValue) { } +}; + +/** + * Wrapper struct for a FName that will generate and link to a Data Pin with its same name. + */ +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] Name - Output Flow Data Pin Property", meta = (FlowPinType = "Name", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinOutputProperty_Name : public FFlowDataPinProperty +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + FName Value = NAME_None; + +public: + FFlowDataPinOutputProperty_Name() { } + explicit FFlowDataPinOutputProperty_Name(const FName& InValue) : Value(InValue) { } +}; + +/** + * Wrapper struct for a FString that will generate and link to a Data Pin with its same name. + */ +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] String - Output Flow Data Pin Property", meta = (FlowPinType = "String", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinOutputProperty_String : public FFlowDataPinProperty +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + FString Value; + +public: + FFlowDataPinOutputProperty_String() { } + explicit FFlowDataPinOutputProperty_String(const FString& InValue) : Value(InValue) { } +}; + +/** + * Wrapper struct for a FText that will generate and link to a Data Pin with its same name. + */ +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] Text - Output Flow Data Pin Property", meta = (FlowPinType = "Text", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinOutputProperty_Text : public FFlowDataPinProperty +{ + GENERATED_BODY() + +public: + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + FText Value; + +public: + + FFlowDataPinOutputProperty_Text() { } + explicit FFlowDataPinOutputProperty_Text(const FText& InValue) : Value(InValue) { } +}; + +/** + * Wrapper struct for an enum that will generate and link to a Data Pin with its same name. + */ +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] Enum - Output Flow Data Pin Property", meta = (FlowPinType = "Enum", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinOutputProperty_Enum : public FFlowDataPinProperty +{ + GENERATED_BODY() + +public: + /* The selected enum Value. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + FName Value = NAME_None; + + /* Class for this enum. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + TObjectPtr EnumClass = nullptr; + +#if WITH_EDITORONLY_DATA + /* Name of enum defined in c++ code, will take priority over asset from EnumType property. + * This is a work-around because EnumClass cannot find C++ Enums, so you need to type the name of the enum in here, manually. + * See also: UBlackboardKeyType_Enum::PostEditChangeProperty(). */ + UPROPERTY(EditAnywhere, Category = Blackboard) + FString EnumName; +#endif + +public: + FFlowDataPinOutputProperty_Enum() { } + FFlowDataPinOutputProperty_Enum(const FName& InValue, UEnum* InEnumClass) + : Value(InValue) + , EnumClass(InEnumClass) + { + } +}; + +/** + * Wrapper struct for a FVector that will generate and link to a Data Pin with its same name. + */ +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] Vector - Output Flow Data Pin Property", meta = (FlowPinType = "Vector", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinOutputProperty_Vector : public FFlowDataPinProperty +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + FVector Value = FVector::ZeroVector; + +public: + FFlowDataPinOutputProperty_Vector() {} + explicit FFlowDataPinOutputProperty_Vector(const FVector& InValue) : Value(InValue) { } +}; + +/** + * Wrapper struct for a FRotator that will generate and link to a Data Pin with its same name. + */ +USTRUCT(BlueprintType, DisplayName = "Rotator - Output Flow Data Pin Property", meta = (FlowPinType = "Rotator", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinOutputProperty_Rotator : public FFlowDataPinProperty +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + FRotator Value = FRotator::ZeroRotator; + +public: + FFlowDataPinOutputProperty_Rotator() {} + explicit FFlowDataPinOutputProperty_Rotator(const FRotator& InValue) : Value(InValue) { } +}; + +/** + * Wrapper struct for a FTransform that will generate and link to a Data Pin with its same name. + */ +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] Transform - Output Flow Data Pin Property", meta = (FlowPinType = "Transform", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinOutputProperty_Transform : public FFlowDataPinProperty +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + FTransform Value; + +public: + FFlowDataPinOutputProperty_Transform() {} + explicit FFlowDataPinOutputProperty_Transform(const FTransform& InValue) : Value(InValue) { } +}; + +/** + * Wrapper struct for a FGameplayTag that will generate and link to a Data Pin with its same name. + */ +USTRUCT(BlueprintType, DisplayName = "GameplayTag - Output Flow Data Pin Property", meta = (FlowPinType = "GameplayTag", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinOutputProperty_GameplayTag : public FFlowDataPinProperty +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + FGameplayTag Value; + +public: + FFlowDataPinOutputProperty_GameplayTag() {} + explicit FFlowDataPinOutputProperty_GameplayTag(const FGameplayTag& InValue) : Value(InValue) { } +}; + +/** + * Wrapper struct for a FGameplayTagContainer that will generate and link to a Data Pin with its same name. + */ +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] GameplayTagContainer - Output Flow DataPin Property", meta = (FlowPinType = "GameplayTagContainer", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinOutputProperty_GameplayTagContainer : public FFlowDataPinProperty +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + FGameplayTagContainer Value; + +public: + FFlowDataPinOutputProperty_GameplayTagContainer() {} + explicit FFlowDataPinOutputProperty_GameplayTagContainer(const FGameplayTagContainer& InValue) : Value(InValue) { } +}; + +/** + * Wrapper struct for a FInstancedStruct that will generate and link to a Data Pin with its same name. + */ +USTRUCT(BlueprintType, DisplayName = "InstancedStruct - Output Flow DataPin Property", meta = (FlowPinType = "InstancedStruct", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinOutputProperty_InstancedStruct : public FFlowDataPinProperty +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + FInstancedStruct Value; + +public: + FFlowDataPinOutputProperty_InstancedStruct() {} + explicit FFlowDataPinOutputProperty_InstancedStruct(const FInstancedStruct& InValue) : Value(InValue) { } +}; + +/** + * Wrapper struct for a UObject that will generate and link to a Data Pin with its same name. + */ +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] Object - Output Flow DataPin Property", meta = (FlowPinType = "Object", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinOutputProperty_Object : public FFlowDataPinProperty +{ + GENERATED_BODY() + + friend class FFlowDataPinProperty_ObjectCustomizationBase; + +public: + /** + * These pointers are separate so that the default value for the object can be configured + * in the editor according to the type of object that it is (instanced or not). + */ + + /* Object reference if the object is a non-instanced UObject type (ie, not EditInlineNew). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins, DisplayName = "Value (reference)", meta = (EditCondition = "InlineValue == nullptr")) + TObjectPtr ReferenceValue = nullptr; + + /* Object reference if the object is an instanced UObject type (ie, EditInlineNew). */ + UPROPERTY(EditAnywhere, Instanced, BlueprintReadWrite, Category = DataPins, DisplayName = "Value (inline)", meta = (EditCondition = "ReferenceValue == nullptr")) + TObjectPtr InlineValue = nullptr; + +#if WITH_EDITORONLY_DATA + UPROPERTY(EditAnywhere, Category = DataPins, meta = (AllowAbstract)) + TObjectPtr ClassFilter = UObject::StaticClass(); +#endif + +public: + FFlowDataPinOutputProperty_Object() {} + FLOW_API explicit FFlowDataPinOutputProperty_Object(UObject* InValue, UClass* InClassFilter = nullptr); + + UObject* GetObjectValue() const { return ReferenceValue ? ReferenceValue : InlineValue; } +}; + +/** + * Wrapper struct for a UClass that will generate and link to a Data Pin with its same name. + */ +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] Class - Output Flow DataPin Property", meta = (FlowPinType = "Class", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinOutputProperty_Class : public FFlowDataPinProperty +{ + GENERATED_BODY() + + friend class FFlowDataPinProperty_ClassCustomizationBase; + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + FSoftClassPath Value; + +#if WITH_EDITORONLY_DATA + UPROPERTY(EditAnywhere, Category = DataPins, meta = (AllowAbstract)) + TObjectPtr ClassFilter = UObject::StaticClass(); +#endif + +public: + FFlowDataPinOutputProperty_Class() {} + + explicit FFlowDataPinOutputProperty_Class(const FSoftClassPath& InValue, UClass* InClassFilter = nullptr) + : Value(InValue) +#if WITH_EDITOR + , ClassFilter(InClassFilter) +#endif + { + } + + UClass* GetObjectValue() const { return Value.ResolveClass(); } +}; + +/** + * Wrapper-structs for a blueprint defaulted input pin types. + * "Hidden" to keep them out of the TInstancedStruct selection list (but they can still be authored as properties in blueprint) + * "DefaultForInputFlowPin" to change them to a Defaulted-Input property (rather than an output property) + */ + +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] Bool - Input Flow Data Pin Property", meta = (DefaultForInputFlowPin, FlowPinType = "Bool", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinInputProperty_Bool : public FFlowDataPinOutputProperty_Bool +{ + GENERATED_BODY() + + explicit FFlowDataPinInputProperty_Bool(const bool InValue = false) : Super(InValue) { } +}; + +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] Int64 - Input Flow Data Pin Property", meta = (DefaultForInputFlowPin, FlowPinType = "Int64", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinInputProperty_Int64 : public FFlowDataPinOutputProperty_Int64 +{ + GENERATED_BODY() + + explicit FFlowDataPinInputProperty_Int64(const int64 InValue = 0) : Super(InValue) { } +}; + +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] Int - Input Flow Data Pin Property", meta = (DefaultForInputFlowPin, FlowPinType = "Int", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinInputProperty_Int32 : public FFlowDataPinOutputProperty_Int32 +{ + GENERATED_BODY() + + explicit FFlowDataPinInputProperty_Int32(const int32 InValue = 0) : Super(InValue) { } +}; + +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] Double (float64) - Input Flow Data Pin Property", meta = (DefaultForInputFlowPin, FlowPinType = "Double", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinInputProperty_Double : public FFlowDataPinOutputProperty_Double +{ + GENERATED_BODY() + + explicit FFlowDataPinInputProperty_Double(const double InValue = 0.0) : Super(InValue) { } +}; + +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] Float - Input Flow Data Pin Property", meta = (DefaultForInputFlowPin, FlowPinType = "Float", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinInputProperty_Float : public FFlowDataPinOutputProperty_Float +{ + GENERATED_BODY() + + explicit FFlowDataPinInputProperty_Float(const float InValue = 0.0f) : Super(InValue) { } +}; + +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] Name - Input Flow Data Pin Property", meta = (DefaultForInputFlowPin, FlowPinType = "Name", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinInputProperty_Name : public FFlowDataPinOutputProperty_Name +{ + GENERATED_BODY() + + FFlowDataPinInputProperty_Name() : Super() { } + explicit FFlowDataPinInputProperty_Name(const FName& InValue) : Super(InValue) { } +}; + +USTRUCT(BlueprintType, DisplayName = "String - Input Flow Data Pin Property", meta = (DefaultForInputFlowPin, FlowPinType = "String", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinInputProperty_String : public FFlowDataPinOutputProperty_String +{ + GENERATED_BODY() + + FFlowDataPinInputProperty_String() : Super() { } + explicit FFlowDataPinInputProperty_String(const FString& InValue) : Super(InValue) { } +}; + +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] Text - Input Flow Data Pin Property", meta = (DefaultForInputFlowPin, FlowPinType = "Text", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinInputProperty_Text : public FFlowDataPinOutputProperty_Text +{ + GENERATED_BODY() + + FFlowDataPinInputProperty_Text() : Super() { } + explicit FFlowDataPinInputProperty_Text(const FText& InValue) : Super(InValue) { } +}; + +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] Enum - Input Flow Data Pin Property", meta = (DefaultForInputFlowPin, FlowPinType = "Enum", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinInputProperty_Enum : public FFlowDataPinOutputProperty_Enum +{ + GENERATED_BODY() + + FFlowDataPinInputProperty_Enum() : Super() { } + FFlowDataPinInputProperty_Enum(const FName& InValue, UEnum* InEnumClass) : Super(InValue, InEnumClass) { } +}; + +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] Vector - Input Flow Data Pin Property", meta = (DefaultForInputFlowPin, FlowPinType = "Vector", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinInputProperty_Vector : public FFlowDataPinOutputProperty_Vector +{ + GENERATED_BODY() + + FFlowDataPinInputProperty_Vector() : Super() { } + explicit FFlowDataPinInputProperty_Vector(const FVector& InValue) : Super(InValue) { } +}; + +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] Rotator - Input Flow Data Pin Property", meta = (DefaultForInputFlowPin, FlowPinType = "Rotator", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinInputProperty_Rotator : public FFlowDataPinOutputProperty_Rotator +{ + GENERATED_BODY() + + FFlowDataPinInputProperty_Rotator() : Super() { } + explicit FFlowDataPinInputProperty_Rotator(const FRotator& InValue) : Super(InValue) { } +}; + +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] Transform - Input Flow Data Pin Property", meta = (DefaultForInputFlowPin, FlowPinType = "Transform", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinInputProperty_Transform : public FFlowDataPinOutputProperty_Transform +{ + GENERATED_BODY() + + FFlowDataPinInputProperty_Transform() : Super() { } + explicit FFlowDataPinInputProperty_Transform(const FTransform& InValue) : Super(InValue) { } +}; + +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] GameplayTag - Input Flow Data Pin Property", meta = (DefaultForInputFlowPin, FlowPinType = "GameplayTag", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinInputProperty_GameplayTag : public FFlowDataPinOutputProperty_GameplayTag +{ + GENERATED_BODY() + + FFlowDataPinInputProperty_GameplayTag() : Super() { } + explicit FFlowDataPinInputProperty_GameplayTag(const FGameplayTag& InValue) : Super(InValue) { } +}; + +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] GameplayTagContainer - Input Flow DataPin Property", meta = (DefaultForInputFlowPin, FlowPinType = "GameplayTagContainer", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinInputProperty_GameplayTagContainer : public FFlowDataPinOutputProperty_GameplayTagContainer +{ + GENERATED_BODY() + + FFlowDataPinInputProperty_GameplayTagContainer() : Super() { } + explicit FFlowDataPinInputProperty_GameplayTagContainer(const FGameplayTagContainer& InValue) : Super(InValue) { } +}; + +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] InstancedStruct - Input Flow DataPin Property", meta = (DefaultForInputFlowPin, FlowPinType = "InstancedStruct", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinInputProperty_InstancedStruct : public FFlowDataPinOutputProperty_InstancedStruct +{ + GENERATED_BODY() + + FFlowDataPinInputProperty_InstancedStruct() : Super() { } + explicit FFlowDataPinInputProperty_InstancedStruct(const FInstancedStruct& InValue) : Super(InValue) { } +}; + +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] Object - Input Flow DataPin Property", meta = (DefaultForInputFlowPin, FlowPinType = "Object", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinInputProperty_Object : public FFlowDataPinOutputProperty_Object +{ + GENERATED_BODY() + + FFlowDataPinInputProperty_Object() : Super() { } + FFlowDataPinInputProperty_Object(UObject* InValue, UClass* InClassFilter) : Super(InValue, InClassFilter) { } +}; + +USTRUCT(BlueprintType, DisplayName = "[DEPRECATED] Class - Input Flow DataPin Property", meta = (DefaultForInputFlowPin, FlowPinType = "Class", Deprecated, DeprecationMessage = "Use FFlowDataPinValue* instead")) +struct FFlowDataPinInputProperty_Class : public FFlowDataPinOutputProperty_Class +{ + GENERATED_BODY() + + FFlowDataPinInputProperty_Class() : Super() { } + FFlowDataPinInputProperty_Class(const FSoftClassPath& InValue, UClass* InClassFilter) : Super(InValue, InClassFilter) { } +}; diff --git a/Source/Flow/Public/Types/FlowDataPinPropertyToValueMigration.h b/Source/Flow/Public/Types/FlowDataPinPropertyToValueMigration.h new file mode 100644 index 000000000..636c3235c --- /dev/null +++ b/Source/Flow/Public/Types/FlowDataPinPropertyToValueMigration.h @@ -0,0 +1,401 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +// #FlowDataPinLegacy + +#include "Types/FlowDataPinValuesStandard.h" +#include "Types/FlowDataPinProperties.h" + +/* Templated helper to migrate simple types (scalar Value to TArray Values). */ +template +static bool MigrateSimpleType(const TInstancedStruct& Source, TInstancedStruct& Target) +{ + if (!Source.IsValid() || !Source.GetPtr()) + { + return false; + } + + const SourceType* SourceData = Source.GetPtr(); + Target.InitializeAsScriptStruct(TargetType::StaticStruct()); + TargetType* TargetData = Target.GetMutablePtr(); + if (TargetData) + { + TargetData->Values.Add(SourceData->Value); + return true; + } + return false; +} + +/* Specialization for FGameplayTagContainer. */ +template <> +inline bool MigrateSimpleType(const TInstancedStruct& Source, TInstancedStruct& Target) +{ + if (!Source.IsValid() || !Source.GetPtr()) + { + return false; + } + + const FFlowDataPinOutputProperty_GameplayTagContainer* SourceData = Source.GetPtr(); + Target.InitializeAsScriptStruct(FFlowDataPinValue_GameplayTagContainer::StaticStruct()); + FFlowDataPinValue_GameplayTagContainer* TargetData = Target.GetMutablePtr(); + if (TargetData) + { + TargetData->Values.AppendTags(SourceData->Value); + return true; + } + return false; +} + +/* Specialization for FGameplayTagContainer (Input). */ +template <> +inline bool MigrateSimpleType(const TInstancedStruct& Source, TInstancedStruct& Target) +{ + if (!Source.IsValid() || !Source.GetPtr()) + { + return false; + } + + const FFlowDataPinInputProperty_GameplayTagContainer* SourceData = + Source.GetPtr(); + + Target.InitializeAsScriptStruct(FFlowDataPinValue_GameplayTagContainer::StaticStruct()); + FFlowDataPinValue_GameplayTagContainer* TargetData = + Target.GetMutablePtr(); + + if (TargetData) + { + TargetData->Values.AppendTags(SourceData->Value); + return true; + } + return false; +} + +/* Specialization for Enum (handles Value and EnumClass). */ +template <> +inline bool MigrateSimpleType(const TInstancedStruct& Source, TInstancedStruct& Target) +{ + if (!Source.IsValid() || !Source.GetPtr()) + { + return false; + } + + const FFlowDataPinOutputProperty_Enum* SourceData = Source.GetPtr(); + Target.InitializeAsScriptStruct(FFlowDataPinValue_Enum::StaticStruct()); + FFlowDataPinValue_Enum* TargetData = Target.GetMutablePtr(); + if (TargetData) + { + TargetData->Values.Add(SourceData->Value); + TargetData->EnumClass = SourceData->EnumClass; +#if WITH_EDITORONLY_DATA + TargetData->EnumName = SourceData->EnumName; +#endif + return true; + } + return false; +} + +/* Specialization for Enum (Input). */ +template <> +inline bool MigrateSimpleType(const TInstancedStruct& Source, TInstancedStruct& Target) +{ + if (!Source.IsValid() || !Source.GetPtr()) + { + return false; + } + + const FFlowDataPinInputProperty_Enum* SourceData = Source.GetPtr(); + Target.InitializeAsScriptStruct(FFlowDataPinValue_Enum::StaticStruct()); + FFlowDataPinValue_Enum* TargetData = Target.GetMutablePtr(); + if (TargetData) + { + TargetData->Values.Add(SourceData->Value); + TargetData->EnumClass = SourceData->EnumClass; +#if WITH_EDITORONLY_DATA + TargetData->EnumName = SourceData->EnumName; +#endif + return true; + } + return false; +} + +/* Specialization for Object (handles ReferenceValue/InlineValue and ClassFilter). */ +template <> +inline bool MigrateSimpleType(const TInstancedStruct& Source, TInstancedStruct& Target) +{ + if (!Source.IsValid() || !Source.GetPtr()) + { + return false; + } + + const FFlowDataPinOutputProperty_Object* SourceData = Source.GetPtr(); + UScriptStruct* TargetStruct = FFlowDataPinValue_Object::StaticStruct(); + Target.InitializeAsScriptStruct(TargetStruct); + + { + FFlowDataPinValue_Object* TargetData = Target.GetMutablePtr(); + if (TargetData && SourceData->ReferenceValue) + { + TargetData->Values.Add(SourceData->ReferenceValue); +#if WITH_EDITORONLY_DATA + TargetData->ClassFilter = SourceData->ClassFilter; +#endif + return true; + } + } + return false; +} + +/* Specialization for Object (Input). */ +template <> +inline bool MigrateSimpleType(const TInstancedStruct& Source, TInstancedStruct& Target) +{ + if (!Source.IsValid() || !Source.GetPtr()) + { + return false; + } + + const FFlowDataPinInputProperty_Object* SourceData = Source.GetPtr(); + UScriptStruct* TargetStruct = FFlowDataPinValue_Object::StaticStruct(); + Target.InitializeAsScriptStruct(TargetStruct); + + { + FFlowDataPinValue_Object* TargetData = Target.GetMutablePtr(); + if (TargetData && SourceData->ReferenceValue) + { + TargetData->Values.Add(SourceData->ReferenceValue); +#if WITH_EDITORONLY_DATA + TargetData->ClassFilter = SourceData->ClassFilter; +#endif + return true; + } + } + return false; +} + +/* Specialization for Class (handles Value and ClassFilter). */ +template <> +inline bool MigrateSimpleType(const TInstancedStruct& Source, TInstancedStruct& Target) +{ + if (!Source.IsValid() || !Source.GetPtr()) + { + return false; + } + + const FFlowDataPinOutputProperty_Class* SourceData = Source.GetPtr(); + Target.InitializeAsScriptStruct(FFlowDataPinValue_Class::StaticStruct()); + FFlowDataPinValue_Class* TargetData = Target.GetMutablePtr(); + if (TargetData) + { + TargetData->Values.Add(SourceData->Value); +#if WITH_EDITORONLY_DATA + TargetData->ClassFilter = SourceData->ClassFilter; +#endif + return true; + } + return false; +} + +/* Specialization for Class (Input). */ +template <> +inline bool MigrateSimpleType(const TInstancedStruct& Source, TInstancedStruct& Target) +{ + if (!Source.IsValid() || !Source.GetPtr()) + { + return false; + } + + const FFlowDataPinInputProperty_Class* SourceData = Source.GetPtr(); + Target.InitializeAsScriptStruct(FFlowDataPinValue_Class::StaticStruct()); + FFlowDataPinValue_Class* TargetData = Target.GetMutablePtr(); + if (TargetData) + { + TargetData->Values.Add(SourceData->Value); +#if WITH_EDITORONLY_DATA + TargetData->ClassFilter = SourceData->ClassFilter; +#endif + return true; + } + return false; +} + +bool FFlowNamedDataPinProperty::FixupDataPinProperty() +{ + // Skip if no data to migrate or target already has data + if (!DataPinProperty.IsValid() || DataPinValue.IsValid()) + { + DataPinProperty.Reset(); + return false; + } + + // Get source struct type + const UScriptStruct* SourceStruct = DataPinProperty.GetScriptStruct(); + if (!SourceStruct || !SourceStruct->IsChildOf(FFlowDataPinProperty::StaticStruct())) + { + DataPinProperty.Reset(); + return false; + } + + // Map source struct to target struct and migrate data + bool bSuccess = false; + + // Bool (Output and Input) + if (SourceStruct == FFlowDataPinOutputProperty_Bool::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + else if (SourceStruct == FFlowDataPinInputProperty_Bool::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + // Int32 (Output and Input) + else if (SourceStruct == FFlowDataPinOutputProperty_Int32::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + else if (SourceStruct == FFlowDataPinInputProperty_Int32::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + // Int64 (Output and Input) + else if (SourceStruct == FFlowDataPinOutputProperty_Int64::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + else if (SourceStruct == FFlowDataPinInputProperty_Int64::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + // Float (Output and Input) + else if (SourceStruct == FFlowDataPinOutputProperty_Float::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + else if (SourceStruct == FFlowDataPinInputProperty_Float::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + // Double (Output and Input) + else if (SourceStruct == FFlowDataPinOutputProperty_Double::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + else if (SourceStruct == FFlowDataPinInputProperty_Double::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + // Name (Output and Input) + else if (SourceStruct == FFlowDataPinOutputProperty_Name::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + else if (SourceStruct == FFlowDataPinInputProperty_Name::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + // String (Output and Input) + else if (SourceStruct == FFlowDataPinOutputProperty_String::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + else if (SourceStruct == FFlowDataPinInputProperty_String::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + // Text (Output and Input) + else if (SourceStruct == FFlowDataPinOutputProperty_Text::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + else if (SourceStruct == FFlowDataPinInputProperty_Text::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + // Enum (Output and Input) + else if (SourceStruct == FFlowDataPinOutputProperty_Enum::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + else if (SourceStruct == FFlowDataPinInputProperty_Enum::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + // Vector (Output and Input) + else if (SourceStruct == FFlowDataPinOutputProperty_Vector::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + else if (SourceStruct == FFlowDataPinInputProperty_Vector::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + // Rotator (Output and Input) + else if (SourceStruct == FFlowDataPinOutputProperty_Rotator::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + else if (SourceStruct == FFlowDataPinInputProperty_Rotator::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + // Transform (Output and Input) + else if (SourceStruct == FFlowDataPinOutputProperty_Transform::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + else if (SourceStruct == FFlowDataPinInputProperty_Transform::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + // GameplayTag (Output and Input) + else if (SourceStruct == FFlowDataPinOutputProperty_GameplayTag::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + else if (SourceStruct == FFlowDataPinInputProperty_GameplayTag::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + // GameplayTagContainer (Output and Input) + else if (SourceStruct == FFlowDataPinOutputProperty_GameplayTagContainer::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + else if (SourceStruct == FFlowDataPinInputProperty_GameplayTagContainer::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + // InstancedStruct (Output and Input) + else if (SourceStruct == FFlowDataPinOutputProperty_InstancedStruct::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + else if (SourceStruct == FFlowDataPinInputProperty_InstancedStruct::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + // Object (Output and Input) + else if (SourceStruct == FFlowDataPinOutputProperty_Object::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + else if (SourceStruct == FFlowDataPinInputProperty_Object::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + // Class (Output and Input) + else if (SourceStruct == FFlowDataPinOutputProperty_Class::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + else if (SourceStruct == FFlowDataPinInputProperty_Class::StaticStruct()) + { + bSuccess = MigrateSimpleType(DataPinProperty, DataPinValue); + } + + // Clear the deprecated property + DataPinProperty.Reset(); + + return bSuccess; +} + +// -- diff --git a/Source/Flow/Public/Types/FlowDataPinResults.h b/Source/Flow/Public/Types/FlowDataPinResults.h new file mode 100644 index 000000000..0ec8364bf --- /dev/null +++ b/Source/Flow/Public/Types/FlowDataPinResults.h @@ -0,0 +1,396 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "GameplayTagContainer.h" +#include "StructUtils/InstancedStruct.h" + +#include "Types/FlowPinEnums.h" +#include "FlowDataPinResults.generated.h" + +struct FInstancedStruct; +struct FFlowDataPinValue; + +// #FlowDataPinLegacy +struct FFlowDataPinOutputProperty_Object; +struct FFlowDataPinOutputProperty_Class; +// -- + +USTRUCT(BlueprintType, DisplayName = "Flow DataPin Result") +struct FFlowDataPinResult +{ + GENERATED_BODY() + +public: + /* Result for the DataPin resolve attempt. */ + UPROPERTY(BlueprintReadWrite, Category = DataPins) + EFlowDataPinResolveResult Result = EFlowDataPinResolveResult::FailedUnimplemented; + +public: + FLOW_API explicit FFlowDataPinResult() = default; + FLOW_API explicit FFlowDataPinResult(EFlowDataPinResolveResult InResult) : Result(InResult) { } + + template + explicit FFlowDataPinResult(const TFlowDataPinValueSubclass& InValue) : Result(EFlowDataPinResolveResult::Success), ResultValue(TInstancedStruct::Make(InValue)) {} + +public: + UPROPERTY() + TInstancedStruct ResultValue; +}; + +// #FlowDataPinLegacy + +USTRUCT(BlueprintType, DisplayName = "Flow DataPin Result (Bool)", meta = (DeprecatedClass)) +struct FFlowDataPinResult_Bool : public FFlowDataPinResult +{ + GENERATED_BODY() + +public: + UPROPERTY(BlueprintReadWrite, Category = DataPins) + bool Value = false; + +public: + FLOW_API FFlowDataPinResult_Bool() { } + FLOW_API explicit FFlowDataPinResult_Bool(const EFlowDataPinResolveResult InResult) : Super(InResult) { } + FLOW_API explicit FFlowDataPinResult_Bool(const bool InValue) + : Super(EFlowDataPinResolveResult::Success) + , Value(InValue) + { } +}; + +USTRUCT(BlueprintType, DisplayName = "Flow DataPin Result (Int)", meta = (DeprecatedClass)) +struct FFlowDataPinResult_Int : public FFlowDataPinResult +{ + GENERATED_BODY() + +public: + + UPROPERTY(BlueprintReadWrite, Category = DataPins) + int64 Value = 0; + +public: + + FLOW_API FFlowDataPinResult_Int() { } + FLOW_API explicit FFlowDataPinResult_Int(const EFlowDataPinResolveResult InResult) : Super(InResult) { } + FLOW_API explicit FFlowDataPinResult_Int(const int64 InValue) + : Super(EFlowDataPinResolveResult::Success) + , Value(InValue) + { } +}; + +USTRUCT(BlueprintType, DisplayName = "Flow DataPin Result (Float)", meta = (DeprecatedClass)) +struct FFlowDataPinResult_Float : public FFlowDataPinResult +{ + GENERATED_BODY() + +public: + + UPROPERTY(BlueprintReadWrite, Category = DataPins) + double Value = 0; + +public: + + FLOW_API FFlowDataPinResult_Float() { } + FLOW_API explicit FFlowDataPinResult_Float(const EFlowDataPinResolveResult InResult) : Super(InResult) { } + FLOW_API explicit FFlowDataPinResult_Float(const double InValue) + : Super(EFlowDataPinResolveResult::Success) + , Value(InValue) + { } +}; + +USTRUCT(BlueprintType, DisplayName = "Flow DataPin Result (Name)", meta = (DeprecatedClass)) +struct FFlowDataPinResult_Name : public FFlowDataPinResult +{ + GENERATED_BODY() + +public: + UPROPERTY(BlueprintReadWrite, Category = DataPins) + FName Value = NAME_None; + +public: + FLOW_API FFlowDataPinResult_Name() { } + FLOW_API explicit FFlowDataPinResult_Name(const EFlowDataPinResolveResult InResult) : Super(InResult) { } + FLOW_API explicit FFlowDataPinResult_Name(const FName& InValue) + : Super(EFlowDataPinResolveResult::Success) + , Value(InValue) + { } + + FLOW_API void SetValue(const FName& FromName) { Value = FromName; } + FLOW_API void SetValue(const FString& FromString) { Value = FName(FromString); } + FLOW_API void SetValue(const FText& FromText) { Value = FName(FromText.ToString()); } +}; + +USTRUCT(BlueprintType, DisplayName = "Flow DataPin Result (String)", meta = (DeprecatedClass)) +struct FFlowDataPinResult_String : public FFlowDataPinResult +{ + GENERATED_BODY() + +public: + UPROPERTY(BlueprintReadWrite, Category = DataPins) + FString Value; + +public: + FLOW_API FFlowDataPinResult_String() { } + FLOW_API explicit FFlowDataPinResult_String(const EFlowDataPinResolveResult InResult) : Super(InResult) { } + FLOW_API explicit FFlowDataPinResult_String(const FString& InValue) + : Super(EFlowDataPinResolveResult::Success) + , Value(InValue) + { } + + FLOW_API void SetValue(const FName& FromName) { Value = FromName.ToString(); } + FLOW_API void SetValue(const FString& FromString) { Value = FromString; } + FLOW_API void SetValue(const FText& FromText) { Value = FromText.ToString(); } +}; + +USTRUCT(BlueprintType, DisplayName = "Flow DataPin Result (Text)", meta = (DeprecatedClass)) +struct FFlowDataPinResult_Text : public FFlowDataPinResult +{ + GENERATED_BODY() + +public: + UPROPERTY(BlueprintReadWrite, Category = DataPins) + FText Value; + +public: + FLOW_API FFlowDataPinResult_Text() { } + FLOW_API explicit FFlowDataPinResult_Text(const EFlowDataPinResolveResult InResult) : Super(InResult) { } + FLOW_API explicit FFlowDataPinResult_Text(const FText& InValue) + : Super(EFlowDataPinResolveResult::Success) + , Value(InValue) + { } + + FLOW_API void SetValue(const FName& FromName) { Value = FText::FromName(FromName); } + FLOW_API void SetValue(const FString& FromString) { Value = FText::FromString(FromString); } + FLOW_API void SetValue(const FText& FromText) { Value = FromText; } +}; + +USTRUCT(BlueprintType, DisplayName = "Flow DataPin Result (Enum)", meta = (DeprecatedClass)) +struct FFlowDataPinResult_Enum : public FFlowDataPinResult +{ + GENERATED_BODY() + +public: + /* The selected enum Value. */ + UPROPERTY(BlueprintReadWrite, Category = DataPins) + FName Value = NAME_None; + + /* Class for this enum. */ + UPROPERTY(BlueprintReadWrite, Category = DataPins) + TObjectPtr EnumClass = nullptr; + +public: + FLOW_API FFlowDataPinResult_Enum() { } + FLOW_API explicit FFlowDataPinResult_Enum(const EFlowDataPinResolveResult InResult) : Super(InResult) { } + FLOW_API FFlowDataPinResult_Enum(const FName& InValue, UEnum* InEnumClass) + : Super(EFlowDataPinResolveResult::Success) + , Value(InValue) + , EnumClass(InEnumClass) + { } + FLOW_API explicit FFlowDataPinResult_Enum(const uint8 InEnumAsIntValue, UEnum& InEnumClass) + : Super(EFlowDataPinResolveResult::Success) + , Value() + , EnumClass(&InEnumClass) + { + const int32 EnumValueAsIndex = EnumClass->GetIndexByValue(InEnumAsIntValue); + const FText DisplayValueText = EnumClass->GetDisplayNameTextByIndex(EnumValueAsIndex); + const FName EnumValue = FName(DisplayValueText.ToString()); + + Value = EnumValue; + Result = EFlowDataPinResolveResult::Success; + } + + template + static FFlowDataPinResult_Enum BuildResultFromNativeEnumValue(TUnrealNativeEnumType EnumValue) + { + FFlowDataPinResult_Enum Result; + Result.SetFromNativeEnumValue(EnumValue); + + return Result; + } + + template + void SetFromNativeEnumValue(TUnrealNativeEnumType InEnumValue) + { + EnumClass = StaticEnum(); + const FText DisplayValueText = EnumClass->GetDisplayValueAsText(InEnumValue); + const FName EnumValue = FName(DisplayValueText.ToString()); + + Value = EnumValue; + Result = EFlowDataPinResolveResult::Success; + } + + template + TUnrealNativeEnumType GetNativeEnumValue(const EGetByNameFlags GetByNameFlags = EGetByNameFlags::None) const + { + if (!IsValid(EnumClass)) + { + return InvalidValue; + } + + int64 ValueAsInt = EnumClass->GetValueByName(Value, GetByNameFlags); + if (ValueAsInt == INDEX_NONE) + { + return InvalidValue; + } + + return static_cast(ValueAsInt); + } +}; + +USTRUCT(BlueprintType, DisplayName = "Flow DataPin Result (Vector)", meta = (DeprecatedClass)) +struct FFlowDataPinResult_Vector : public FFlowDataPinResult +{ + GENERATED_BODY() + +public: + + UPROPERTY(BlueprintReadWrite, Category = DataPins) + FVector Value = FVector::ZeroVector; + +public: + + FLOW_API FFlowDataPinResult_Vector() { } + FLOW_API explicit FFlowDataPinResult_Vector(const EFlowDataPinResolveResult InResult) : Super(InResult) { } + FLOW_API explicit FFlowDataPinResult_Vector(const FVector& InValue) + : Super(EFlowDataPinResolveResult::Success) + , Value(InValue) + { } +}; + +USTRUCT(BlueprintType, DisplayName = "Flow DataPin Result (Rotator)", meta = (DeprecatedClass)) +struct FFlowDataPinResult_Rotator : public FFlowDataPinResult +{ + GENERATED_BODY() + +public: + UPROPERTY(BlueprintReadWrite, Category = DataPins) + FRotator Value = FRotator::ZeroRotator; + +public: + FLOW_API FFlowDataPinResult_Rotator() { } + FLOW_API explicit FFlowDataPinResult_Rotator(const EFlowDataPinResolveResult InResult) : Super(InResult) { } + FLOW_API explicit FFlowDataPinResult_Rotator(const FRotator& InValue) + : Super(EFlowDataPinResolveResult::Success) + , Value(InValue) + { } +}; + +USTRUCT(BlueprintType, DisplayName = "Flow DataPin Result (Transform)", meta = (DeprecatedClass)) +struct FFlowDataPinResult_Transform : public FFlowDataPinResult +{ + GENERATED_BODY() + +public: + UPROPERTY(BlueprintReadWrite, Category = DataPins) + FTransform Value; + +public: + FLOW_API FFlowDataPinResult_Transform() { } + FLOW_API explicit FFlowDataPinResult_Transform(const EFlowDataPinResolveResult InResult) : Super(InResult) { } + FLOW_API explicit FFlowDataPinResult_Transform(const FTransform& InValue) + : Super(EFlowDataPinResolveResult::Success) + , Value(InValue) + { } +}; + +USTRUCT(BlueprintType, DisplayName = "Flow DataPin Result (GameplayTag)", meta = (DeprecatedClass)) +struct FFlowDataPinResult_GameplayTag : public FFlowDataPinResult +{ + GENERATED_BODY() + +public: + UPROPERTY(BlueprintReadWrite, Category = DataPins) + FGameplayTag Value; + +public: + FLOW_API FFlowDataPinResult_GameplayTag() { } + FLOW_API explicit FFlowDataPinResult_GameplayTag(const EFlowDataPinResolveResult InResult) : Super(InResult) { } + FLOW_API explicit FFlowDataPinResult_GameplayTag(const FGameplayTag& InValue) + : Super(EFlowDataPinResolveResult::Success) + , Value(InValue) + { } +}; + +USTRUCT(BlueprintType, DisplayName = "Flow DataPin Result (GameplayTagContainer)", meta = (DeprecatedClass)) +struct FFlowDataPinResult_GameplayTagContainer : public FFlowDataPinResult +{ + GENERATED_BODY() + +public: + UPROPERTY(BlueprintReadWrite, Category = DataPins) + FGameplayTagContainer Value; + +public: + FLOW_API FFlowDataPinResult_GameplayTagContainer() { } + FLOW_API explicit FFlowDataPinResult_GameplayTagContainer(const EFlowDataPinResolveResult InResult) : Super(InResult) { } + FLOW_API explicit FFlowDataPinResult_GameplayTagContainer(const FGameplayTagContainer& InValue) + : Super(EFlowDataPinResolveResult::Success) + , Value(InValue) + { } +}; + +USTRUCT(BlueprintType, DisplayName = "Flow DataPin Result (InstancedStruct)", meta = (DeprecatedClass)) +struct FFlowDataPinResult_InstancedStruct : public FFlowDataPinResult +{ + GENERATED_BODY() + +public: + UPROPERTY(BlueprintReadWrite, Category = DataPins) + FInstancedStruct Value; + +public: + FLOW_API FFlowDataPinResult_InstancedStruct() { } + FLOW_API explicit FFlowDataPinResult_InstancedStruct(const EFlowDataPinResolveResult InResult) : Super(InResult) { } + FLOW_API explicit FFlowDataPinResult_InstancedStruct(const FInstancedStruct& InValue) + : Super(EFlowDataPinResolveResult::Success) + , Value(InValue) + { } +}; + +USTRUCT(BlueprintType, DisplayName = "Flow DataPin Result (Object)", meta = (DeprecatedClass)) +struct FFlowDataPinResult_Object : public FFlowDataPinResult +{ + GENERATED_BODY() + +public: + UPROPERTY(BlueprintReadWrite, Category = DataPins) + TObjectPtr Value; + +public: + FLOW_API FFlowDataPinResult_Object() { } + FLOW_API explicit FFlowDataPinResult_Object(const EFlowDataPinResolveResult InResult) : Super(InResult) { } + FLOW_API explicit FFlowDataPinResult_Object(UObject* InValue); + + FLOW_API FORCEINLINE void SetValueFromSoftPath(const FSoftObjectPath& SoftPath) { Value = SoftPath.ResolveObject(); } + FLOW_API FORCEINLINE void SetValueFromObjectPtr(UObject* ObjectPtr) { Value = ObjectPtr; } +}; + +USTRUCT(BlueprintType, DisplayName = "Flow DataPin Result (Class)", meta = (DeprecatedClass)) +struct FFlowDataPinResult_Class : public FFlowDataPinResult +{ + GENERATED_BODY() + +protected: + /* SoftClassPath version of the result. + * Both the SoftClassPath and the UClass (if available) will be set for the result. */ + UPROPERTY(BlueprintReadWrite, Category = DataPins) + FSoftClassPath ValuePath; + + /* UClass version of the result. + * Both the SoftClassPath and the UClass (if available) will be set for the result. */ + UPROPERTY(BlueprintReadWrite, Category = DataPins) + TObjectPtr ValueClass = nullptr; + +public: + FLOW_API FFlowDataPinResult_Class() { } + FLOW_API explicit FFlowDataPinResult_Class(const EFlowDataPinResolveResult InResult) : Super(InResult) { } + FLOW_API explicit FFlowDataPinResult_Class(const FSoftClassPath& InValuePath); + FLOW_API explicit FFlowDataPinResult_Class(UClass* InValueClass); + + FLOW_API void SetValueSoftClassAndClassPtr(const FSoftClassPath& SoftPath, UClass* ObjectPtr); + FLOW_API void SetValueFromSoftPath(const FSoftObjectPath& SoftObjectPath); + FLOW_API FORCEINLINE void SetValueFromObjectPtr(UClass* ClassPtr) { SetValueSoftClassAndClassPtr(FSoftClassPath(ClassPtr), ClassPtr); } + + FLOW_API UClass* GetOrResolveClass() const { return IsValid(ValueClass) ? ValueClass.Get() : ValuePath.ResolveClass(); } + FLOW_API FSoftClassPath GetAsSoftClass() const; +}; +// -- \ No newline at end of file diff --git a/Source/Flow/Public/Types/FlowDataPinValue.h b/Source/Flow/Public/Types/FlowDataPinValue.h new file mode 100644 index 000000000..0453f72b4 --- /dev/null +++ b/Source/Flow/Public/Types/FlowDataPinValue.h @@ -0,0 +1,61 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/NameTypes.h" + +#include "FlowPinEnums.h" +#include "FlowPinType.h" +#include "FlowDataPinValue.generated.h" + +struct FFlowDataPinResult; +class FProperty; +class UObject; +class IPropertyHandle; +class UScriptStruct; + +USTRUCT() +struct FFlowDataPinValue +{ + GENERATED_BODY() + + friend class FFlowDataPinValueCustomization; + +public: + /* If a pin was created from this property, this is the cached pin name that was used. + * Which can be used in UFlowDataPinBlueprintLibrary::ResolveAs... functions to lookup the correct pin by name. */ + UPROPERTY(VisibleAnywhere, Category = DataPins) + mutable FName PropertyPinName; + +#if WITH_EDITORONLY_DATA + UPROPERTY(EditAnywhere, Category = DataPins) + bool bIsInputPin = false; + + UPROPERTY(EditAnywhere, Category = DataPins) + EFlowDataMultiType MultiType = EFlowDataMultiType::Single; +#endif + + FFlowDataPinValue() {} + virtual ~FFlowDataPinValue() {} + +#if WITH_EDITOR + FLOW_API bool IsInputPin() const { return bIsInputPin; } + FLOW_API bool IsArray() const { FLOW_ASSERT_ENUM_MAX(EFlowDataMultiType, 2); return MultiType == EFlowDataMultiType::Array; } + + /* Helper to get the Values property handle (implemented by subclasses or via type system). */ + FLOW_API virtual TSharedPtr GetValuesPropertyHandle() const PURE_VIRTUAL(GetValuesPropertyHandle, return nullptr;); +#endif + + /* Pin Type Name (identity). */ + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const PURE_VIRTUAL(GetPinTypeName, return FFlowPinType::PinTypeNameUnknown;); + + /* (optional) Get the field type if one exists (only used for UEnum For Now). */ + FLOW_API virtual UField* GetFieldType() const { return nullptr; } + + /* (optional) */ + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const { return false; } + + /* Resolve the registered data pin type. */ + FLOW_API const FFlowPinType* LookupPinType() const; + + FLOW_API static const FString StringArraySeparator; +}; \ No newline at end of file diff --git a/Source/Flow/Public/Types/FlowDataPinValuesStandard.h b/Source/Flow/Public/Types/FlowDataPinValuesStandard.h new file mode 100644 index 000000000..34f523203 --- /dev/null +++ b/Source/Flow/Public/Types/FlowDataPinValuesStandard.h @@ -0,0 +1,498 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Types/FlowDataPinValue.h" +#include "Types/FlowPinTypesStandard.h" + +#include "StructUtils/InstancedStruct.h" +#include "GameplayTagContainer.h" +#include "UObject/SoftObjectPtr.h" +#include "UObject/SoftObjectPath.h" +#include "UObject/Class.h" +#include "Math/Vector.h" +#include "Math/Rotator.h" +#include "Math/Transform.h" + +#include "FlowDataPinValuesStandard.generated.h" + +//====================================================================== +// Bool +//====================================================================== +USTRUCT(BlueprintType, DisplayName = "Bool - Flow DataPin Value", meta = (FlowPinType = "Bool", HasNativeMake = "/Script/Flow.FlowDataPinBlueprintLibrary.MakeStructBool")) +struct FFlowDataPinValue_Bool : public FFlowDataPinValue +{ + GENERATED_BODY() + +public: + using PinType = FFlowPinType_Bool; + using ValueType = PinType::ValueType; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + TArray Values{ false }; + + FLOW_API FFlowDataPinValue_Bool() = default; + FLOW_API explicit FFlowDataPinValue_Bool(ValueType InValue); + FLOW_API explicit FFlowDataPinValue_Bool(const TArray& InValues); + + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; +}; + +//====================================================================== +// Int (int32) +//====================================================================== +USTRUCT(BlueprintType, DisplayName = "Int - Flow DataPin Value", meta = (FlowPinType = "Int", HasNativeMake = "/Script/Flow.FlowDataPinBlueprintLibrary.MakeStructInt")) +struct FFlowDataPinValue_Int : public FFlowDataPinValue +{ + GENERATED_BODY() + +public: + using PinType = FFlowPinType_Int; + using ValueType = PinType::ValueType; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + TArray Values{ 0 }; + + FLOW_API FFlowDataPinValue_Int() = default; + FLOW_API explicit FFlowDataPinValue_Int(ValueType InValue); + FLOW_API explicit FFlowDataPinValue_Int(const TArray& InValues); + + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; +}; + +//====================================================================== +// Int64 +//====================================================================== +USTRUCT(BlueprintType, DisplayName = "Int64 - Flow DataPin Value", meta = (FlowPinType = "Int64", HasNativeMake = "/Script/Flow.FlowDataPinBlueprintLibrary.MakeStructInt64")) +struct FFlowDataPinValue_Int64 : public FFlowDataPinValue +{ + GENERATED_BODY() + +public: + using PinType = FFlowPinType_Int64; + using ValueType = PinType::ValueType; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + TArray Values{ 0 }; + + FLOW_API FFlowDataPinValue_Int64() = default; + FLOW_API explicit FFlowDataPinValue_Int64(ValueType InValue); + FLOW_API explicit FFlowDataPinValue_Int64(const TArray& InValues); + + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; +}; + +//====================================================================== +// Float +//====================================================================== +USTRUCT(BlueprintType, DisplayName = "Float - Flow DataPin Value", meta = (FlowPinType = "Float", HasNativeMake = "/Script/Flow.FlowDataPinBlueprintLibrary.MakeStructFloat")) +struct FFlowDataPinValue_Float : public FFlowDataPinValue +{ + GENERATED_BODY() + +public: + using PinType = FFlowPinType_Float; + using ValueType = PinType::ValueType; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + TArray Values{ 0.0f }; + + FLOW_API FFlowDataPinValue_Float() = default; + FLOW_API explicit FFlowDataPinValue_Float(ValueType InValue); + FLOW_API explicit FFlowDataPinValue_Float(const TArray& InValues); + + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; +}; + +//====================================================================== +// Double +//====================================================================== +USTRUCT(BlueprintType, DisplayName = "Double - Flow DataPin Value", meta = (FlowPinType = "Double", HasNativeMake = "/Script/Flow.FlowDataPinBlueprintLibrary.MakeStructDouble")) +struct FFlowDataPinValue_Double : public FFlowDataPinValue +{ + GENERATED_BODY() + +public: + using PinType = FFlowPinType_Double; + using ValueType = PinType::ValueType; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + TArray Values{ 0.0 }; + + FLOW_API FFlowDataPinValue_Double() = default; + FLOW_API explicit FFlowDataPinValue_Double(ValueType InValue); + FLOW_API explicit FFlowDataPinValue_Double(const TArray& InValues); + + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; +}; + +//====================================================================== +// Name +//====================================================================== +USTRUCT(BlueprintType, DisplayName = "Name - Flow DataPin Value", meta = (FlowPinType = "Name", HasNativeMake = "/Script/Flow.FlowDataPinBlueprintLibrary.MakeStructName")) +struct FFlowDataPinValue_Name : public FFlowDataPinValue +{ + GENERATED_BODY() + +public: + using PinType = FFlowPinType_Name; + using ValueType = PinType::ValueType; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + TArray Values{ NAME_None }; + + FLOW_API FFlowDataPinValue_Name() = default; + FLOW_API explicit FFlowDataPinValue_Name(const ValueType& InValue); + FLOW_API explicit FFlowDataPinValue_Name(const TArray& InValues); + + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; +}; + +//====================================================================== +// String +//====================================================================== +USTRUCT(BlueprintType, DisplayName = "String - Flow DataPin Value", meta = (FlowPinType = "String", HasNativeMake = "/Script/Flow.FlowDataPinBlueprintLibrary.MakeStructString")) +struct FFlowDataPinValue_String : public FFlowDataPinValue +{ + GENERATED_BODY() + +public: + using PinType = FFlowPinType_String; + using ValueType = PinType::ValueType; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + TArray Values; + + FLOW_API FFlowDataPinValue_String() = default; + FLOW_API explicit FFlowDataPinValue_String(const ValueType& InValue); + FLOW_API explicit FFlowDataPinValue_String(const TArray& InValues); + + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; +}; + +//====================================================================== +// Text +//====================================================================== +USTRUCT(BlueprintType, DisplayName = "Text - Flow DataPin Value", meta = (FlowPinType = "Text", HasNativeMake = "/Script/Flow.FlowDataPinBlueprintLibrary.MakeStructText")) +struct FFlowDataPinValue_Text : public FFlowDataPinValue +{ + GENERATED_BODY() + +public: + using PinType = FFlowPinType_Text; + using ValueType = PinType::ValueType; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + TArray Values; + + FLOW_API FFlowDataPinValue_Text() = default; + FLOW_API explicit FFlowDataPinValue_Text(const ValueType& InValue); + FLOW_API explicit FFlowDataPinValue_Text(const TArray& InValues); + + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; +}; + +//====================================================================== +// Enum +//====================================================================== +USTRUCT(BlueprintType, DisplayName = "Enum - Flow DataPin Value", meta = (FlowPinType = "Enum", HasNativeMake = "/Script/Flow.FlowDataPinBlueprintLibrary.MakeStructEnum")) +struct FFlowDataPinValue_Enum : public FFlowDataPinValue +{ + GENERATED_BODY() + +public: + using PinType = FFlowPinType_Enum; + using ValueType = PinType::ValueType; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + TArray Values; + + // Enum asset reference (advanced) + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins, meta = (NoClear, AdvancedDisplay)) + TSoftObjectPtr EnumClass; + +#if WITH_EDITORONLY_DATA + // Native C++ enum name (advanced) + UPROPERTY(EditAnywhere, Category = DataPins, meta = (AdvancedDisplay)) + FString EnumName; +#endif + + FLOW_API FFlowDataPinValue_Enum() = default; + FLOW_API FFlowDataPinValue_Enum(const TSoftObjectPtr& InEnumClass, const ValueType& InValue); + FLOW_API FFlowDataPinValue_Enum(const TSoftObjectPtr& InEnumClass, const TArray& InValues); + FLOW_API FFlowDataPinValue_Enum(UEnum& InEnumClass, const TArray& InValues); + +#if WITH_EDITOR + FLOW_API void OnEnumNameChanged(); +#endif + + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + virtual UField* GetFieldType() const override; + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; + + // Helper templates + template + static bool TryGetEnumValueByName(const UEnum* EnumClass, const FName& EnumValueName, TUnrealNativeEnumType& OutValue, const EGetByNameFlags GetByNameFlags = EGetByNameFlags::ErrorIfNotFound) + { + if (!IsValid(EnumClass)) + { + return false; + } + + const int32 EnumIndex = EnumClass->GetIndexByName(EnumValueName, GetByNameFlags); + if (EnumIndex != INDEX_NONE) + { + OutValue = static_cast(EnumClass->GetValueByIndex(EnumIndex)); + return true; + } + return false; + } + + template + EFlowDataPinResolveResult TryGetSingleEnumValue(TUnrealNativeEnumType& OutEnumValue, const EFlowSingleFromArray SingleFromArray, EGetByNameFlags GetByNameFlags = EGetByNameFlags::ErrorIfNotFound) const + { + const int32 Index = EFlowSingleFromArray_Classifiers::ConvertToIndex(SingleFromArray, Values.Num()); + if (!Values.IsValidIndex(Index)) + { + return EFlowDataPinResolveResult::FailedInsufficientValues; + } + + UEnum* EnumClassPtr = EnumClass.LoadSynchronous(); + if (!TryGetEnumValueByName(EnumClassPtr, Values[Index], OutEnumValue, GetByNameFlags)) + { + return EFlowDataPinResolveResult::FailedUnknownEnumValue; + } + return EFlowDataPinResolveResult::Success; + } + + template + EFlowDataPinResolveResult TryGetAllNativeEnumValues(TArray& OutEnumValues, EGetByNameFlags GetByNameFlags = EGetByNameFlags::ErrorIfNotFound) const + { + if (Values.IsEmpty()) + { + return EFlowDataPinResolveResult::FailedInsufficientValues; + } + + UEnum* EnumClassPtr = EnumClass.LoadSynchronous(); + OutEnumValues.Reserve(Values.Num()); + + for (const ValueType& ValueName : Values) + { + TUnrealNativeEnumType EnumValue; + if (!TryGetEnumValueByName(EnumClassPtr, ValueName, EnumValue, GetByNameFlags)) + { + return EFlowDataPinResolveResult::FailedUnknownEnumValue; + } + OutEnumValues.Add(EnumValue); + } + return EFlowDataPinResolveResult::Success; + } +}; + +//====================================================================== +// Vector +//====================================================================== +USTRUCT(BlueprintType, DisplayName = "Vector - Flow DataPin Value", meta = (FlowPinType = "Vector", HasNativeMake = "/Script/Flow.FlowDataPinBlueprintLibrary.MakeStructVector")) +struct FFlowDataPinValue_Vector : public FFlowDataPinValue +{ + GENERATED_BODY() + +public: + using PinType = FFlowPinType_Vector; + using ValueType = PinType::ValueType; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + TArray Values{ FVector::ZeroVector }; + + FLOW_API FFlowDataPinValue_Vector() = default; + FLOW_API explicit FFlowDataPinValue_Vector(const ValueType& InValue); + FLOW_API explicit FFlowDataPinValue_Vector(const TArray& InValues); + + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; +}; + +//====================================================================== +// Rotator +//====================================================================== +USTRUCT(BlueprintType, DisplayName = "Rotator - Flow DataPin Value", meta = (FlowPinType = "Rotator", HasNativeMake = "/Script/Flow.FlowDataPinBlueprintLibrary.MakeStructRotator")) +struct FFlowDataPinValue_Rotator : public FFlowDataPinValue +{ + GENERATED_BODY() + +public: + using PinType = FFlowPinType_Rotator; + using ValueType = PinType::ValueType; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + TArray Values{ FRotator::ZeroRotator }; + + FLOW_API FFlowDataPinValue_Rotator() = default; + FLOW_API explicit FFlowDataPinValue_Rotator(const ValueType& InValue); + FLOW_API explicit FFlowDataPinValue_Rotator(const TArray& InValues); + + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; +}; + +//====================================================================== +// Transform +//====================================================================== +USTRUCT(BlueprintType, DisplayName = "Transform - Flow DataPin Value", meta = (FlowPinType = "Transform", HasNativeMake = "/Script/Flow.FlowDataPinBlueprintLibrary.MakeStructTransform")) +struct FFlowDataPinValue_Transform : public FFlowDataPinValue +{ + GENERATED_BODY() + +public: + using PinType = FFlowPinType_Transform; + using ValueType = PinType::ValueType; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + TArray Values{ FTransform::Identity }; + + FLOW_API FFlowDataPinValue_Transform() = default; + FLOW_API explicit FFlowDataPinValue_Transform(const ValueType& InValue); + FLOW_API explicit FFlowDataPinValue_Transform(const TArray& InValues); + + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; +}; + +//====================================================================== +// GameplayTag +//====================================================================== +USTRUCT(BlueprintType, DisplayName = "GameplayTag - Flow DataPin Value", meta = (FlowPinType = "GameplayTag", HasNativeMake = "/Script/Flow.FlowDataPinBlueprintLibrary.MakeStructGameplayTag")) +struct FFlowDataPinValue_GameplayTag : public FFlowDataPinValue +{ + GENERATED_BODY() + +public: + using PinType = FFlowPinType_GameplayTag; + using ValueType = PinType::ValueType; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + TArray Values; + + FLOW_API FFlowDataPinValue_GameplayTag() = default; + FLOW_API explicit FFlowDataPinValue_GameplayTag(const ValueType& InValue); + FLOW_API explicit FFlowDataPinValue_GameplayTag(const TArray& InValues); + + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; +}; + +//====================================================================== +// GameplayTagContainer +//====================================================================== +USTRUCT(BlueprintType, DisplayName = "GameplayTagContainer - Flow DataPin Value", meta = (FlowPinType = "GameplayTagContainer", HasNativeMake = "/Script/Flow.FlowDataPinBlueprintLibrary.MakeStructGameplayTagContainer")) +struct FFlowDataPinValue_GameplayTagContainer : public FFlowDataPinValue +{ + GENERATED_BODY() + +public: + using PinType = FFlowPinType_GameplayTagContainer; + using ValueType = PinType::ValueType; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + FGameplayTagContainer Values; + + FLOW_API FFlowDataPinValue_GameplayTagContainer() = default; + FLOW_API explicit FFlowDataPinValue_GameplayTagContainer(const FGameplayTag& InValue); + FLOW_API explicit FFlowDataPinValue_GameplayTagContainer(const FGameplayTagContainer& InValues); + FLOW_API explicit FFlowDataPinValue_GameplayTagContainer(const TArray& InValues); + FLOW_API explicit FFlowDataPinValue_GameplayTagContainer(const TArray& InValues); + + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; +}; + +//====================================================================== +// InstancedStruct +//====================================================================== +USTRUCT(BlueprintType, DisplayName = "InstancedStruct - Flow DataPin Value", meta = (FlowPinType = "InstancedStruct", HasNativeMake = "/Script/Flow.FlowDataPinBlueprintLibrary.MakeStructInstancedStruct")) +struct FFlowDataPinValue_InstancedStruct : public FFlowDataPinValue +{ + GENERATED_BODY() + +public: + using PinType = FFlowPinType_InstancedStruct; + using ValueType = PinType::ValueType; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + TArray Values; + + FLOW_API FFlowDataPinValue_InstancedStruct() = default; + FLOW_API explicit FFlowDataPinValue_InstancedStruct(const ValueType& InValue); + FLOW_API explicit FFlowDataPinValue_InstancedStruct(const TArray& InValues); + + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; +}; + +//====================================================================== +// Object +//====================================================================== +USTRUCT(BlueprintType, DisplayName = "Object - Flow DataPin Value", meta = (FlowPinType = "Object", HasNativeMake = "/Script/Flow.FlowDataPinBlueprintLibrary.MakeStructObject")) +struct FFlowDataPinValue_Object : public FFlowDataPinValue +{ + GENERATED_BODY() + +public: + using PinType = FFlowPinType_Object; + using ValueType = PinType::ValueType; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + TArray> Values; + +#if WITH_EDITORONLY_DATA + UPROPERTY(EditAnywhere, Category = DataPins, meta = (AllowAbstract, AdvancedDisplay)) + TObjectPtr ClassFilter = UObject::StaticClass(); +#endif + + FLOW_API FFlowDataPinValue_Object() = default; + FLOW_API explicit FFlowDataPinValue_Object(TObjectPtr InObject, UClass* InClassFilter = UObject::StaticClass()); + FLOW_API explicit FFlowDataPinValue_Object(const TArray>& InObjects, UClass* InClassFilter = UObject::StaticClass()); + FLOW_API explicit FFlowDataPinValue_Object(const TArray& InObjects, UClass* InClassFilter = UObject::StaticClass()); + FLOW_API explicit FFlowDataPinValue_Object(AActor* InActor, UClass* InClassFilter = nullptr /* nullptr here defaults to AActor::StaticClass() */ ); + FLOW_API explicit FFlowDataPinValue_Object(const TArray& InActors, UClass* InClassFilter = nullptr /* nullptr here defaults to AActor::StaticClass() */); + + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; +}; + +//====================================================================== +// Class +//====================================================================== +USTRUCT(BlueprintType, DisplayName = "Class - Flow DataPin Value", meta = (FlowPinType = "Class", HasNativeMake = "/Script/Flow.FlowDataPinBlueprintLibrary.MakeStructClass")) +struct FFlowDataPinValue_Class : public FFlowDataPinValue +{ + GENERATED_BODY() + +public: + using PinType = FFlowPinType_Class; + using ValueType = PinType::ValueType; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins) + TArray Values; + +#if WITH_EDITORONLY_DATA + UPROPERTY(EditAnywhere, Category = DataPins, meta = (AllowAbstract, AdvancedDisplay)) + TObjectPtr ClassFilter = UObject::StaticClass(); +#endif + + FLOW_API FFlowDataPinValue_Class() = default; + FLOW_API explicit FFlowDataPinValue_Class(const FSoftClassPath& InPath, UClass* InClassFilter = UObject::StaticClass()); + FLOW_API explicit FFlowDataPinValue_Class(const TArray& InPaths, UClass* InClassFilter = UObject::StaticClass()); + FLOW_API explicit FFlowDataPinValue_Class(const UClass* InClass, UClass* InClassFilter = UObject::StaticClass()); + FLOW_API explicit FFlowDataPinValue_Class(const TArray& InClasses, UClass* InClassFilter = UObject::StaticClass()); + + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinType::GetPinTypeNameStatic(); } + FLOW_API virtual bool TryConvertValuesToString(FString& OutString) const override; +}; \ No newline at end of file diff --git a/Source/Flow/Public/Types/FlowEnumUtils.h b/Source/Flow/Public/Types/FlowEnumUtils.h new file mode 100644 index 000000000..87eeb209a --- /dev/null +++ b/Source/Flow/Public/Types/FlowEnumUtils.h @@ -0,0 +1,127 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Misc/EnumRange.h" +#include + +// Extensions to EnumRange.h + +namespace FlowEnum +{ + template constexpr auto MinOf() { return 0; } + template constexpr auto MaxOf() { return 0; } + + /* NOTE (gtaylor) In this context, a "Valid" enum value is one that is within ::Min to ::Max - 1. + * Invalid values (like ::Invalid) should fall outside of this range. */ + template constexpr bool IsValidEnumValue(const TEnum EnumValue) { return false; } + + /* NOTE (gtaylor) In this context, a subrange is First..Last (where last is an inclusive bound). */ + template constexpr bool IsEnumValueInSubrange(const TEnum EnumValue, const TEnum SubrangeFirst, const TEnum SubrangeLast) { return false; } + + /* Utility templates for Enums. */ + template ::type> + struct safe_underlying_type { + using type = void; + }; + + template + struct safe_underlying_type { + using type = std::underlying_type_t; + }; + + template + struct safe_underlying_type { + using type = T; + }; + + template + constexpr std::underlying_type_t ToUnderlyingType(const EnumType Value) + { + return static_cast>(Value); + } +} +#define FLOW_ENUM_STATIC_CAST_TO_INT(EnumType) \ + namespace FlowEnum { constexpr auto ToInt(EnumType EnumValue) { return static_cast::type>(EnumValue); } } + +#define FLOW_ENUM_STATIC_CAST_MIN_AND_MAX(EnumType, MinValue, MaxValue) \ + namespace FlowEnum { \ + template<> constexpr auto MinOf() { return static_cast::type>(MinValue); } \ + template<> constexpr auto MaxOf() { return static_cast::type>(MaxValue); } \ + } +#define FLOW_ENUM_RANGE_UTILITY_FUNCTIONS(EnumType) \ + namespace FlowEnum { \ + template<> constexpr bool IsEnumValueInSubrange(const EnumType EnumValue, const EnumType SubrangeFirst, const EnumType SubrangeLast) { return ToInt(EnumValue) >= ToInt(SubrangeFirst) && ToInt(EnumValue) <= ToInt(SubrangeLast); } \ + template<> constexpr bool IsValidEnumValue(const EnumType EnumValue) { return ToInt(EnumValue) >= MinOf() && ToInt(EnumValue) < MaxOf(); } \ + } +#define FLOW_IS_ENUM_IN_SUBRANGE(EnumValue, SubrangeTag) FlowEnum::IsEnumValueInSubrange(EnumValue, SubrangeTag##First, SubrangeTag##Last) + +// Macros to static-assert the max or a particular value of an enum is the expected integral value +#define FLOW_ASSERT_ENUM_MAX(EnumType, IntMaxValue) static_assert(FlowEnum::MaxOf() == IntMaxValue, "Ensure this code is correct after making changes to this enum.") +#define FLOW_ASSERT_ENUM_VALUE(EnumValue, IntValue) static_assert(FlowEnum::ToInt(EnumValue) == IntValue, "Ensure this code is correct after making changes to this enum.") + +/** +* Version of ENUM_RANGE_VALUES for 'C-style' enums +* +* Defines a contiguous enum range from MyEnum_Min to (MyEnum_Max - 1) +* +* Example: +* +* enum EMyEnum +* { +* MyEnum_First, +* MyEnum_Second, +* MyEnum_Third, +* +* MyEnum_Max, +* MyEnum_Invalid = -1, +* MyEnum_Min = 0, +* }; +* +* // Defines iteration over EMyEnum to be: First, Second, Third +* ENUM_RANGE_VALUES_WITH_MIN_AND_MAX(EMyEnum, MyEnum_Min, MyEnum_Max) +*/ +#define FLOW_ENUM_RANGE_VALUES_WITH_MIN_AND_MAX(EnumType, EnumMin, EnumMax) \ + ENUM_RANGE_BY_FIRST_AND_LAST(EnumType, static_cast(EnumMin), static_cast(EnumMax) - 1) \ + FLOW_ENUM_STATIC_CAST_MIN_AND_MAX(EnumType, EnumMin, EnumMax) \ + FLOW_ENUM_STATIC_CAST_TO_INT(EnumType) \ + FLOW_ENUM_RANGE_UTILITY_FUNCTIONS(EnumType) + +/** +* Defines a contiguous enum range from Min to (Max - 1) +* +* Examples: +* +* for unsigned int: +* +* enum class EMyEnum : uint32 +* { +* First, +* Second, +* Third, +* +* Max, +* Invalid, +* Min = 0, +* }; +* +* or with signed int: +* +* enum class EMyEnum : int32 +* { +* First, +* Second, +* Third, +* +* Max, +* Invalid = -1, +* Min = 0, +* }; +* +* // Defines iteration over EMyEnum to be: First, Second, Third +* ENUM_RANGE_VALUES(EMyEnum) +*/ +#define FLOW_ENUM_RANGE_VALUES(EnumType) \ + ENUM_RANGE_BY_FIRST_AND_LAST(EnumType, static_cast(EnumType::Min), static_cast(EnumType::Max) - 1) \ + FLOW_ENUM_STATIC_CAST_MIN_AND_MAX(EnumType, EnumType::Min, EnumType::Max) \ + FLOW_ENUM_STATIC_CAST_TO_INT(EnumType) \ + FLOW_ENUM_RANGE_UTILITY_FUNCTIONS(EnumType) diff --git a/Source/Flow/Public/Types/FlowGameplayTagMapUtils.h b/Source/Flow/Public/Types/FlowGameplayTagMapUtils.h new file mode 100644 index 000000000..d2578eb3c --- /dev/null +++ b/Source/Flow/Public/Types/FlowGameplayTagMapUtils.h @@ -0,0 +1,193 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "GameplayTagContainer.h" +#include "GameplayTagsManager.h" + +#include "Types/FlowEnumUtils.h" +#include "Types/FlowArray.h" + +/** + * NOTE (gtaylor) The choice of which EFlowGameplayTagMapExpandPolicy to use will be informed by the map's tolerance + * for memory vs. lookup performance. If speed is not a concern, then fully expanding with AllSubtags can + * make for a single-tech lookup. If memory is more of a concern, then NoExpand will store the minimal information + * in the map keys (potentially requiring multiple parent searches in TryLookupGameplayTagKey). If only the leaf tags + * will be used for lookup, then LeafSubtags expansion policy is a good option. + */ + +UENUM() +enum class EFlowGameplayTagMapExpandPolicy : int8 +{ + AllSubtags, // Apply the payload to all of the tag's child tags + LeafSubtags, // Apply the payload to the tag's leaf child tags + RemoveSubtags, // Remove all of the keys in the result map of the tag's child tags + NoExpand, // Only apply the payload patch to the tag, make no changes to it's child tags + + Max UMETA(Hidden), + Invalid = -1 UMETA(Hidden), + Min = 0 UMETA(Hidden), +}; +FLOW_ENUM_RANGE_VALUES(EFlowGameplayTagMapExpandPolicy); + +namespace FlowMap +{ + /** + * Utility functions for utilizing FGameplayTags as a key in a TMap. + * Expected to be wrapped by the client code to hide some of the details in these function signatures. + */ + + template + void PatchGameplayTagMap( + const TMap& PatchSourceMap, + TMap& InOutPatchedMap) + { + checkf( + &PatchSourceMap != &InOutPatchedMap, + TEXT("We could make this case work, but it would require a temp caching anyway, so letting the caller make the copy if they want to expand in-line")); + + FlowArray::TInlineArray PatchMapKeys; + PatchSourceMap.GenerateKeyArray(PatchMapKeys); + + FLOW_ASSERT_ENUM_MAX(EFlowGameplayTagMapExpandPolicy, 4); + + constexpr bool bProcessSubtags = (ExpandPolicy != EFlowGameplayTagMapExpandPolicy::NoExpand); + + // Only sort the keys if we will be processing the subtags + + if constexpr (bProcessSubtags) + { + PatchMapKeys.StableSort([](const FGameplayTag& Tag0, const FGameplayTag& Tag1) + { + // Sort the keys to apply in order from least specific to most specific + + return Tag0.GetGameplayTagParents().Num() < Tag1.GetGameplayTagParents().Num(); + }); + } + + for (const FGameplayTag& PatchKeyTag : PatchMapKeys) + { + const TPayload& PatchPayload = PatchSourceMap.FindChecked(PatchKeyTag); + + // First, patch the payload in the target map + + InOutPatchedMap.Add(PatchKeyTag, PatchPayload); + + // Now apply the payload to child tag keys in the map + + if constexpr (bProcessSubtags) + { + const UGameplayTagsManager& TagsManager = UGameplayTagsManager::Get(); + const FGameplayTagContainer TagAndChildrenContainer = TagsManager.RequestGameplayTagChildren(PatchKeyTag); + + for (auto ChildTagIt = TagAndChildrenContainer.CreateConstIterator(); ChildTagIt; ++ChildTagIt) + { + const FGameplayTag& ChildTag = *ChildTagIt; + + if constexpr (ExpandPolicy == EFlowGameplayTagMapExpandPolicy::AllSubtags) + { + // Replace all child tag entries (if any) with the patch source tag's payload + InOutPatchedMap.Add(ChildTag, PatchPayload); + } + + if constexpr (ExpandPolicy == EFlowGameplayTagMapExpandPolicy::LeafSubtags) + { + // BB (gtaylor) Is there a lighter-weight way to ask if a tag is a leaf tag? + + const FGameplayTagContainer ChildChildTags = TagsManager.RequestGameplayTagChildren(PatchKeyTag); + const bool bIsChildALeafTag = (ChildChildTags.Num() == 0); + + if (bIsChildALeafTag) + { + // Replace only leaf child tag entries (if any) with the patch source tag's payload + InOutPatchedMap.Add(ChildTag, PatchPayload); + } + } + + if constexpr (ExpandPolicy == EFlowGameplayTagMapExpandPolicy::RemoveSubtags) + { + // Remove all subtag mappings in the map + InOutPatchedMap.Remove(ChildTag); + } + } + } + } + } + + /* (const) Lookup function, which works on a GameplayTag-keyed map. + * It can crawl up the tag ancestry chain to allow general keys to apply to sub-tags. */ + template + const TPayload* TryLookupGameplayTagKey( + const FGameplayTag& KeyTag, + const TMap& GameplayTagToPayloadMap, + const FGameplayTag& KeyTagBase = FGameplayTag::EmptyTag, + int32 ParentTagSearchDepthMax = 0) + { + check(ParentTagSearchDepthMax >= 0); + check( + KeyTagBase == FGameplayTag::EmptyTag || + KeyTag == KeyTagBase || + KeyTag.MatchesTag(KeyTagBase)); + + const TPayload* FoundPayload = GameplayTagToPayloadMap.Find(KeyTag); + + if (!FoundPayload && + ParentTagSearchDepthMax > 0 && + KeyTag != KeyTagBase) + { + // Recurse to direct parent tag, decrementing the allowed search depth + + const FGameplayTag DirectParentTag = KeyTag.RequestDirectParent(); + const int32 NewParentTagSearchDepthMax = ParentTagSearchDepthMax - 1; + + return TryLookupGameplayTagKey(DirectParentTag, GameplayTagToPayloadMap, KeyTagBase, NewParentTagSearchDepthMax); + } + + return FoundPayload; + } + + /* (mutable) Lookup function, which works on a GameplayTag-keyed map. + * It can crawl up the tag ancestry chain to allow general keys to apply to sub-tags. */ + template + TPayload* TryLookupGameplayTagKey( + const FGameplayTag& KeyTag, + TMap& GameplayTagToPayloadMap, + const FGameplayTag& KeyTagBase = FGameplayTag::EmptyTag, + int32 ParentTagSearchDepthMax = 0) + { + // Non-const map signature uses the same lookup code as the const version + + return + const_cast( + TryLookupGameplayTagKey( + KeyTag, + *const_cast*>(&GameplayTagToPayloadMap), + KeyTagBase, + ParentTagSearchDepthMax)); + } + + /* Extracts the key/value pairs from a GameplayTag-keyed map into a sorted array. */ + template + TArray> BuildSortedGameplayTagMapPairs(const TMap& GameplayTagToPayloadMap) + { + FlowArray::TInlineArray MapKeys; + GameplayTagToPayloadMap.GenerateKeyArray(MapKeys); + + MapKeys.StableSort([](const FGameplayTag& Tag0, const FGameplayTag& Tag1) + { + return Tag0.GetGameplayTagParents().Num() < Tag1.GetGameplayTagParents().Num(); + }); + + TArray> Pairs; + Pairs.Reserve(MapKeys.Num()); + + for (const FGameplayTag& KeyTag : MapKeys) + { + const TPayload& Payload = GameplayTagToPayloadMap.FindChecked(KeyTag); + + Pairs.Emplace(KeyTag, Payload); + } + + return Pairs; + } + +} diff --git a/Source/Flow/Public/Types/FlowGameplayTagUtils.h b/Source/Flow/Public/Types/FlowGameplayTagUtils.h new file mode 100644 index 000000000..f16f8f5e3 --- /dev/null +++ b/Source/Flow/Public/Types/FlowGameplayTagUtils.h @@ -0,0 +1,41 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "GameplayTagContainer.h" + +#include "FlowGameplayTagUtils.generated.h" + +/** Encapsulate require and ignore tags + * Adapted from FGameplayTagRequirements, but without the GameplayAbilities module dependency */ +USTRUCT(BlueprintType) +struct FFlowGameplayTagRequirements +{ + GENERATED_BODY() + + /** All of these tags must be present */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = GameplayTags, meta = (DisplayName = "Must Have Tags")) + FGameplayTagContainer RequireTags; + + /** None of these tags may be present */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = GameplayTags, meta = (DisplayName = "Must Not Have Tags")) + FGameplayTagContainer IgnoreTags; + + /** Build up a more complex query that can't be expressed with RequireTags/IgnoreTags alone */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = GameplayTags, meta = (DisplayName = "Query Must Match")) + FGameplayTagQuery TagQuery; + + /** True if all required tags and no ignore tags found */ + FLOW_API bool RequirementsMet(const FGameplayTagContainer& Container) const; + + /** True if neither RequireTags or IgnoreTags has any tags */ + FLOW_API bool IsEmpty() const; + + /** Return debug string */ + FLOW_API FString ToString() const; + + FLOW_API bool operator==(const FFlowGameplayTagRequirements& Other) const; + FLOW_API bool operator!=(const FFlowGameplayTagRequirements& Other) const; + + /** Converts the RequireTags and IgnoreTags fields into an equivalent FGameplayTagQuery */ + [[nodiscard]] FLOW_API FGameplayTagQuery ConvertTagFieldsToTagQuery() const; +}; diff --git a/Source/Flow/Public/Types/FlowInjectComponentsHelper.h b/Source/Flow/Public/Types/FlowInjectComponentsHelper.h new file mode 100644 index 000000000..f26b42761 --- /dev/null +++ b/Source/Flow/Public/Types/FlowInjectComponentsHelper.h @@ -0,0 +1,38 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "FlowInjectComponentsHelper.generated.h" + +class AActor; +class UActorComponent; + +/** + * Configuration helper struct for injecting components onto actors. + */ +USTRUCT() +struct FFlowInjectComponentsHelper +{ + GENERATED_BODY() + +public: + FLOW_API TArray CreateComponentInstancesForActor(AActor& Actor); + + /* Static functions to create a component for injection. */ + static FLOW_API UActorComponent* TryCreateComponentInstanceForActorFromTemplate(AActor& Actor, UActorComponent& ComponentTemplate); + static FLOW_API UActorComponent* TryCreateComponentInstanceForActorFromClass(AActor& Actor, TSubclassOf ComponentClass, const FName& InstanceBaseName); + + /* After creating using one of the above two functions, inject into the actor. */ + static FLOW_API void InjectCreatedComponent(AActor& Actor, UActorComponent& ComponentInstance); + + /* Remove & Destroy the injected component. */ + static FLOW_API void DestroyInjectedComponent(AActor& Actor, UActorComponent& ComponentInstance); + +public: + /* Component (template) to inject on the spawned actor. */ + UPROPERTY(EditAnywhere, Instanced, Category = Configuration) + TArray> ComponentTemplates; + + /* Component (template) to inject on the spawned actor. */ + UPROPERTY(EditAnywhere, Category = Configuration) + TArray> ComponentClasses; +}; diff --git a/Source/Flow/Public/Types/FlowInjectComponentsManager.h b/Source/Flow/Public/Types/FlowInjectComponentsManager.h new file mode 100644 index 000000000..50422c695 --- /dev/null +++ b/Source/Flow/Public/Types/FlowInjectComponentsManager.h @@ -0,0 +1,65 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/Object.h" +#include "FlowInjectComponentsManager.generated.h" + +class UActorComponent; +class UFlowNodeBase; + +/** + * Container for injected component instances + */ +USTRUCT() +struct FLOW_API FFlowComponentInstances +{ + GENERATED_BODY() + +public: + UPROPERTY(Transient) + TArray> Components; +}; + +/** + * Inject components onto actors and will remove them when they are destroyed (or this is shutdown). + */ +UCLASS(MinimalAPI) +class UFlowInjectComponentsManager : public UObject +{ + GENERATED_BODY() + +public: + FLOW_API void InitializeRuntime(); + FLOW_API void ShutdownRuntime(); + + FLOW_API FORCEINLINE void InjectComponentOnActor(AActor& Actor, UActorComponent& ComponentInstance) { AddAndRegisterComponent(Actor, ComponentInstance); } + FLOW_API void InjectComponentsOnActor(AActor& Actor, const TArray& ComponentInstances); + + FLOW_API void RemoveAllInjectedComponentsAndStopMonitoringActor(AActor& Actor); + +protected: + FLOW_API void AddAndRegisterComponent(AActor& Actor, UActorComponent& ComponentInstance); + FLOW_API void RemoveAndUnregisterComponent(AActor& Actor, UActorComponent& ComponentInstance); + + FLOW_API void RegisterOnDestroyedDelegate(AActor& Actor); + FLOW_API void UnregisterOnDestroyedDelegate(AActor& Actor); + + FLOW_API void RemoveInjectedComponents(); + + UFUNCTION() + FLOW_API void OnActorDestroyed(AActor* DestroyedActor); + +public: + DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FFlowBeforeOnActorRemoved, AActor*, SpawnedActor); + + UPROPERTY(BlueprintAssignable) + FFlowBeforeOnActorRemoved BeforeActorRemovedDelegate; + + /* Remove the Injected Components from the Actors when Deinitialized. */ + UPROPERTY() + bool bRemoveInjectedComponentsWhenDeinitializing = true; + + /* Map of spawned components (if we are cleaning up). */ + UPROPERTY(Transient) + TMap, FFlowComponentInstances> ActorToComponentsMap; +}; diff --git a/Source/Flow/Public/Types/FlowNamedDataPinProperty.h b/Source/Flow/Public/Types/FlowNamedDataPinProperty.h new file mode 100644 index 000000000..172979513 --- /dev/null +++ b/Source/Flow/Public/Types/FlowNamedDataPinProperty.h @@ -0,0 +1,102 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "StructUtils/InstancedStruct.h" +#include "UObject/Class.h" +#include "Nodes/FlowPin.h" +#include "Types/FlowDataPinValue.h" + +#include "FlowNamedDataPinProperty.generated.h" + +struct FFlowAutoDataPinsWorkingData; +struct FFlowDataPinProperty; +struct FFlowDataPinValue; +struct FFlowDataPinValueOwner; + +/** + * Wrapper for FFlowDataPinProperty that is used for flow nodes that add dynamic properties, + * with associated data pins, on the flow node instance. + * (as opposed to C++ or blueprint compile-time). + */ +USTRUCT(BlueprintType, DisplayName = "Flow Named DataPin Property") +struct FFlowNamedDataPinProperty +{ + GENERATED_BODY() + +public: + /* Name of this instanced property. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = DataPins, meta = (EditCondition = "bMayChangeNameAndType", HideEditConditionToggle)) + FName Name = NAME_None; + +private: + /* DataPinProperty payload. */ + UPROPERTY(VisibleAnywhere, Category = DataPins, meta = (DeprecatedProperty)) + TInstancedStruct DataPinProperty; + +public: + /* DataPinProperty payload. */ + UPROPERTY(EditAnywhere, Category = DataPins, meta = (ExcludeBaseStruct, NoClear)) + TInstancedStruct DataPinValue; + +#if WITH_EDITORONLY_DATA + /* Unique identifier for property tracking. */ + UPROPERTY(meta=(IgnoreForMemberInitializationTest)) + FGuid Guid = FGuid::NewGuid(); + + /* Tracks if this property overrides its super (auto-clears if matches super). */ + UPROPERTY() + bool bIsOverride = false; + + /* TODO (gtaylor) Does not currently police the type, + * because that prevents the instanced struct contents being edited as well, + * which is not what we want from this feature. + * Will try to fix next pass on the details customization. */ + UPROPERTY() + bool bMayChangeNameAndType = true; +#endif + +public: + FFlowNamedDataPinProperty() = default; + + bool IsValid() const { return Name != NAME_None && DataPinValue.GetPtr() != nullptr; } + + // #FlowDataPinLegacy + bool FixupDataPinProperty(); + // -- + +#if WITH_EDITOR + FLOW_API FFlowPin CreateFlowPin() const; + + FLOW_API FText BuildHeaderText() const; + + void ConfigureForFlowAssetParams() + { + bIsOverride = false; + bMayChangeNameAndType = false; + } + + void ConfigureForFlowAssetStartNode() + { + bIsOverride = false; + bMayChangeNameAndType = true; + } + + static void ConfigurePropertiesForFlowAssetParams(TArray& MutableProperties) + { + for (FFlowNamedDataPinProperty& Property : MutableProperties) + { + Property.ConfigureForFlowAssetParams(); + } + } + + static void ConfigurePropertiesForFlowAssetStartNode(TArray& MutableProperties) + { + for (FFlowNamedDataPinProperty& Property : MutableProperties) + { + Property.ConfigureForFlowAssetStartNode(); + } + } + + FLOW_API void AutoGenerateDataPinForProperty(const FFlowDataPinValueOwner& ValueOwner, FFlowAutoDataPinsWorkingData& InOutWorkingData); +#endif +}; diff --git a/Source/Flow/Public/Types/FlowPinConnectionChange.h b/Source/Flow/Public/Types/FlowPinConnectionChange.h new file mode 100644 index 000000000..8dd0c745c --- /dev/null +++ b/Source/Flow/Public/Types/FlowPinConnectionChange.h @@ -0,0 +1,48 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/Object.h" +#include "FlowPinConnectionChange.generated.h" + +class UFlowNode; + +/** + * Editor-only representation of a change to a node pin's connection. + * PinName is the *final* pin name visible on the node (after any disambiguation / mangling). + */ +USTRUCT(BlueprintType) +struct FLOW_API FFlowPinConnectionChange +{ + GENERATED_BODY() + +public: + + FFlowPinConnectionChange() = default; + explicit FFlowPinConnectionChange( + const FName& ChangedPinName, + UFlowNode* InOldConnectedNode, + const FName& InOldConnectedPinName, + UFlowNode* InNewConnectedNode, + const FName& InNewConnectedPinName) + : PinName(ChangedPinName) + , OldConnectedNode(InOldConnectedNode) + , OldConnectedPinName(InOldConnectedPinName) + , NewConnectedNode(InNewConnectedNode) + , NewConnectedPinName(InNewConnectedPinName) + {} + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Flow") + FName PinName = NAME_None; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Flow") + TObjectPtr OldConnectedNode = nullptr; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Flow") + FName OldConnectedPinName = NAME_None; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Flow") + TObjectPtr NewConnectedNode = nullptr; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Flow") + FName NewConnectedPinName = NAME_None; +}; diff --git a/Source/Flow/Public/Types/FlowPinEnums.h b/Source/Flow/Public/Types/FlowPinEnums.h new file mode 100644 index 000000000..d0a31bb43 --- /dev/null +++ b/Source/Flow/Public/Types/FlowPinEnums.h @@ -0,0 +1,207 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Types/FlowEnumUtils.h" + +#include "FlowPinEnums.generated.h" + +UENUM(BlueprintType, meta = (ScriptName = "LegacyFlowPinNameEnum")) +enum class EFlowPinType : uint8 +{ + // Execution pin + Exec, + + // FBoolProperty + Bool, + + // FByteProperty FInt16Property FIntProperty FInt64Property FUInt16Property FUInt32Property FUInt64Property + Int, + + // FFloatProperty, FDoubleProperty + Float, + + // FNameProperty + Name, + + // FStringProperty + String, + + // FTextProperty + Text, + + // FEnumProperty, FByteProperty + Enum, + + // FVector (FStructProperty) + Vector, + + // FRotator (FStructProperty) + Rotator, + + // FTransform (FStructProperty) + Transform, + + // FGameplayTag (FStructProperty) + GameplayTag, + + // FGameplayTagContainer (FStructProperty) + GameplayTagContainer, + + // FInstancedStruct (FStructProperty) + InstancedStruct, + + // FObjectProperty, FObjectPtrProperty, FWeakObjectProperty, FLazyObjectProperty, FSoftObjectProperty + Object, + + // FClassProperty, FClassPtrProperty, FSoftClassProperty + Class, + + Max UMETA(Hidden), + Invalid UMETA(Hidden), + Min = 0 UMETA(Hidden), +}; +FLOW_ENUM_RANGE_VALUES(EFlowPinType) + +/** + * Result enum for TryResolveDataPin() + */ +UENUM(BlueprintType) +enum class EFlowDataPinResolveResult : uint8 +{ + // Pin resolved successfully + Success, + + // The pin name is unknown + FailedUnknownPin, + + // The pin was requested as an unsupported type + FailedMismatchedType, + + // The Flow Node or AddOn did not implement the necessary function to provide this value + FailedUnimplemented, + + // Failed due to insufficient values (eg. resolving a single value with an empty array) + FailedInsufficientValues, + + // Could not resolve an enum value + FailedUnknownEnumValue, + + // Tried to extract with a null FlowNodeBase + FailedNullFlowNodeBase, + + // The pin is not connected to a node (used in reroutes) + FailedNotConnected, + + // Failed with an error message (see the error log) + FailedWithError, + + Max UMETA(Hidden), + Invalid UMETA(Hidden), + Min = 0 UMETA(Hidden), +}; +FLOW_ENUM_RANGE_VALUES(EFlowDataPinResolveResult) + +UENUM(BlueprintType) +enum class EFlowDataPinResolveSimpleResult : uint8 +{ + Succeeded = 1, + Failed = 0, + + Max UMETA(Hidden), + Invalid UMETA(Hidden), + Min = 0 UMETA(Hidden), +}; +FLOW_ENUM_RANGE_VALUES(EFlowDataPinResolveSimpleResult) + +namespace EFlowDataPinResolveResult_Classifiers +{ + FORCEINLINE bool IsSuccess(const EFlowDataPinResolveResult Result) { return Result == EFlowDataPinResolveResult::Success; } + FORCEINLINE EFlowDataPinResolveSimpleResult ConvertToSimpleResult(EFlowDataPinResolveResult ResultEnum) + { return IsSuccess(ResultEnum) ? EFlowDataPinResolveSimpleResult::Succeeded : EFlowDataPinResolveSimpleResult::Failed; } +}; + +UENUM(BlueprintType) +enum class EFlowDataMultiType : uint8 +{ + Single, + Array, + + // TODO (gtaylor) Consider future types like Set, Map + + Max UMETA(Hidden), + Invalid UMETA(Hidden), + Min = 0 UMETA(Hidden), +}; +FLOW_ENUM_RANGE_VALUES(EFlowDataMultiType) + +UENUM(BlueprintType) +enum class EFlowSingleFromArray : uint8 +{ + // For the Single value, use the [0]th value (First) + FirstValue, + + // For the Single value, use the [N-1]th value (Last) + LastValue, + + // Expect a single value only, log an error if not (and return [0]th) + ExpectSingleValueOnly, + + // Used in the FlowPinType templates for entire array extraction + EntireArray UMETA(Hidden), + + Max UMETA(Hidden), + Invalid UMETA(Hidden), + Min = 0 UMETA(Hidden), +}; +FLOW_ENUM_RANGE_VALUES(EFlowSingleFromArray) + +namespace EFlowSingleFromArray_Classifiers +{ + FORCEINLINE int32 ConvertToIndex(EFlowSingleFromArray SingleFromArray, int32 ArrayMax) + { + FLOW_ASSERT_ENUM_MAX(EFlowSingleFromArray, 4); + switch (SingleFromArray) + { + case EFlowSingleFromArray::FirstValue: + { + if (ArrayMax > 0) + { + return 0; + } + else + { + return INDEX_NONE; + } + } + + case EFlowSingleFromArray::LastValue: + { + if (ArrayMax > 0) + { + return ArrayMax - 1; + } + else + { + return INDEX_NONE; + } + } + + case EFlowSingleFromArray::EntireArray: + check(SingleFromArray != EFlowSingleFromArray::EntireArray); + return INDEX_NONE; + + default: + case EFlowSingleFromArray::ExpectSingleValueOnly: + { + if (ArrayMax == 1) + { + return 0; + } + else + { + return INDEX_NONE; + } + } + } + } +}; diff --git a/Source/Flow/Public/Types/FlowPinType.h b/Source/Flow/Public/Types/FlowPinType.h new file mode 100644 index 000000000..8ab9a5045 --- /dev/null +++ b/Source/Flow/Public/Types/FlowPinType.h @@ -0,0 +1,56 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Math/Color.h" +#include "UObject/NameTypes.h" + +#include "FlowPinEnums.h" + +#if WITH_EDITOR +#include "GraphEditorSettings.h" +#endif + +#include "FlowPinType.generated.h" + +class FFormatArgumentValue; +class IPropertyHandle; + +class UFlowNodeBase; +class UFlowNode; +struct FFlowDataPinResult; +struct FFlowDataPinValue; +struct FFlowPin; +struct FFlowPinTypeName; + +USTRUCT(BlueprintType) +struct FFlowPinType +{ + GENERATED_BODY() + +public: + virtual ~FFlowPinType() {} + + /* Lookup a registered type by name. */ + FLOW_API static const FFlowPinType* LookupPinType(const FFlowPinTypeName& FlowPinTypeName); + + /* Identity. */ + FLOW_API virtual const FFlowPinTypeName& GetPinTypeName() const PURE_VIRTUAL(GetPinTypeName, return PinTypeNameUnknown;); + + /* Value resolution. */ + FLOW_API virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const; + FLOW_API virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const; + +#if WITH_EDITOR + /* Editor visualization. */ + FLOW_API virtual FLinearColor GetPinColor() const { return GetDefault()->DefaultPinTypeColor; } + FLOW_API virtual TSharedPtr GetValuesHandle(const TSharedRef& FlowDataPinValuePropertyHandle) const; + FLOW_API virtual bool SupportsMultiType(EFlowDataMultiType Mode) const { return true; } + FLOW_API virtual UObject* GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const { return nullptr; } + + /* Pin creation. */ + FLOW_API FFlowPin CreateFlowPinFromProperty(const FProperty& Property, void const* InContainer) const; + FLOW_API FFlowPin CreateFlowPinFromValueWrapper(const FName& PinName, const FFlowDataPinValue& Wrapper) const; +#endif + + static const FFlowPinTypeName PinTypeNameUnknown; +}; \ No newline at end of file diff --git a/Source/Flow/Public/Types/FlowPinTypeName.h b/Source/Flow/Public/Types/FlowPinTypeName.h new file mode 100644 index 000000000..b71e7a287 --- /dev/null +++ b/Source/Flow/Public/Types/FlowPinTypeName.h @@ -0,0 +1,43 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/NameTypes.h" +#include "FlowPinTypeName.generated.h" + +USTRUCT(BlueprintType) +struct FFlowPinTypeName +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, Category = FlowPin) + FName Name = NAME_None; + + FFlowPinTypeName() = default; + + explicit FFlowPinTypeName(const TCHAR* InPinName) + : Name(FName(InPinName)) + { + } + + explicit FFlowPinTypeName(const FName& InName) + : Name(InName) + { + } + + explicit FFlowPinTypeName(const FString& InString) + : Name(FName(InString)) + { + } + + friend inline uint32 GetTypeHash(const FFlowPinTypeName& PinTypeName) + { + return GetTypeHash(PinTypeName.Name); + } + + FORCEINLINE bool operator==(const FFlowPinTypeName& Other) const { return Name == Other.Name; } + FORCEINLINE bool operator==(const FName& OtherName) const { return Name == OtherName; } + + FString ToString() const { return Name.ToString(); } + bool IsNone() const { return Name.IsNone(); } +}; diff --git a/Source/Flow/Public/Types/FlowPinTypeNamesStandard.h b/Source/Flow/Public/Types/FlowPinTypeNamesStandard.h new file mode 100644 index 000000000..fd163736e --- /dev/null +++ b/Source/Flow/Public/Types/FlowPinTypeNamesStandard.h @@ -0,0 +1,39 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Containers/Set.h" +#include "UObject/NameTypes.h" + +struct FFlowPinTypeNamesStandard +{ + /* Other Standard Pin Types. */ + FLOW_API static constexpr const TCHAR* PinTypeNameUnknown = TEXT("Unknown"); + FLOW_API static constexpr const TCHAR* PinTypeNameExec = TEXT("Exec"); + + /* "Standard" Data Pin Types. */ + FLOW_API static constexpr const TCHAR* PinTypeNameBool = TEXT("Bool"); + FLOW_API static constexpr const TCHAR* PinTypeNameInt = TEXT("Int"); + FLOW_API static constexpr const TCHAR* PinTypeNameInt64 = TEXT("Int64"); + FLOW_API static constexpr const TCHAR* PinTypeNameFloat = TEXT("Float"); + FLOW_API static constexpr const TCHAR* PinTypeNameDouble = TEXT("Double"); + FLOW_API static constexpr const TCHAR* PinTypeNameEnum = TEXT("Enum"); + FLOW_API static constexpr const TCHAR* PinTypeNameName = TEXT("Name"); + FLOW_API static constexpr const TCHAR* PinTypeNameString = TEXT("String"); + FLOW_API static constexpr const TCHAR* PinTypeNameText = TEXT("Text"); + FLOW_API static constexpr const TCHAR* PinTypeNameVector = TEXT("Vector"); + FLOW_API static constexpr const TCHAR* PinTypeNameRotator = TEXT("Rotator"); + FLOW_API static constexpr const TCHAR* PinTypeNameTransform = TEXT("Transform"); + FLOW_API static constexpr const TCHAR* PinTypeNameGameplayTag = TEXT("GameplayTag"); + FLOW_API static constexpr const TCHAR* PinTypeNameGameplayTagContainer = TEXT("GameplayTagContainer"); + FLOW_API static constexpr const TCHAR* PinTypeNameInstancedStruct = TEXT("InstancedStruct"); + FLOW_API static constexpr const TCHAR* PinTypeNameObject = TEXT("Object"); + FLOW_API static constexpr const TCHAR* PinTypeNameClass = TEXT("Class"); + + // 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/Flow/Public/Types/FlowPinTypeNodeTemplates.h b/Source/Flow/Public/Types/FlowPinTypeNodeTemplates.h new file mode 100644 index 000000000..d4c182b5d --- /dev/null +++ b/Source/Flow/Public/Types/FlowPinTypeNodeTemplates.h @@ -0,0 +1,70 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Types/FlowDataPinResults.h" +#include "Types/FlowDataPinValuesStandard.h" +#include "Types/FlowPinTypeTemplates.h" +#include "Types/FlowArray.h" +#include "Nodes/FlowNode.h" + +/** + * Additional FlowPinType templates that require FlowNode.h include + */ +namespace FlowPinType +{ + template + static bool PopulateResultTemplate(const UObject& PropertyOwnerObject, const UFlowNode& FlowNode, const FName& PropertyName, FFlowDataPinResult& OutResult) + { + using TValue = typename TPinType::ValueType; + using TWrapper = typename TPinType::WrapperType; + using Traits = FlowPinType::FFlowDataPinValueTraits; + + TInstancedStruct ValueStruct; + const FProperty* FoundProperty = nullptr; + const IFlowDataPinValueOwnerInterface* PropertyOwnerInterface = CastChecked(&PropertyOwnerObject); + if (!PropertyOwnerInterface->TryFindPropertyByPinName(PropertyName, FoundProperty, ValueStruct)) + { + OutResult.Result = EFlowDataPinResolveResult::FailedUnknownPin; + return false; + } + + if (ValueStruct.IsValid() && ValueStruct.Get().GetPinTypeName() == TPinType::GetPinTypeNameStatic()) + { + OutResult.ResultValue = ValueStruct; + OutResult.Result = EFlowDataPinResolveResult::Success; + return true; + } + + TArray Values; + if (FlowPinType::IsSuccess(Traits::ExtractFromProperty(FoundProperty, &PropertyOwnerObject, Values))) + { + OutResult.ResultValue = TInstancedStruct::Make(Values); + OutResult.Result = EFlowDataPinResolveResult::Success; + return true; + } + + OutResult.Result = EFlowDataPinResolveResult::FailedMismatchedType; + return false; + } + + template + bool ResolveAndFormatArray( + const UFlowNodeBase& Node, + const FName& PinName, + FFormatArgumentValue& OutValue, + TFunctionRef Formatter) + { + using TValue = typename TPinType::ValueType; + + TArray Values; + const EFlowDataPinResolveResult ResolveResult = Node.TryResolveDataPinValues(PinName, Values); + if (FlowPinType::IsSuccess(ResolveResult)) + { + const FString ValueString = FlowArray::FormatArrayString(Values, Formatter); + OutValue = FFormatArgumentValue(FText::FromString(ValueString)); + return true; + } + + return false; + } +}; \ No newline at end of file diff --git a/Source/Flow/Public/Types/FlowPinTypeTemplates.h b/Source/Flow/Public/Types/FlowPinTypeTemplates.h new file mode 100644 index 000000000..9e1bed958 --- /dev/null +++ b/Source/Flow/Public/Types/FlowPinTypeTemplates.h @@ -0,0 +1,1025 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/NameTypes.h" +#include "UObject/TextProperty.h" +#include "Math/Vector.h" +#include "Math/Rotator.h" +#include "Math/Transform.h" +#include "GameplayTagContainer.h" +#include "StructUtils/InstancedStruct.h" +#include "UObject/Class.h" +#include "UObject/UnrealType.h" +#include "Types/FlowDataPinValuesStandard.h" +#include "Types/FlowDataPinResults.h" +#include "FlowLogChannels.h" +#include +#include + +// #FlowDataPinLegacy +#include "Types/FlowDataPinProperties.h" +// -- + +namespace FlowPinType +{ + // Success check helper + FORCEINLINE bool IsSuccess(EFlowDataPinResolveResult ResultEnum) + { + return EFlowDataPinResolveResult_Classifiers::IsSuccess(ResultEnum); + } + + FORCEINLINE EFlowDataPinResolveSimpleResult ConvertToSimpleResult(EFlowDataPinResolveResult ResultEnum) + { + return EFlowDataPinResolveResult_Classifiers::ConvertToSimpleResult(ResultEnum); + } + + // ----------------------------------------------------------------------- + // Value Conversion System + // ----------------------------------------------------------------------- + + // Numeric conversion dispatcher + template + struct TValueConverter; + + // int32 + template <> struct TValueConverter { static int32 Convert(int64 Val); }; + template <> struct TValueConverter { static int32 Convert(float Val); }; + template <> struct TValueConverter { static int32 Convert(double Val); }; + + // int64 + template <> struct TValueConverter { static int64 Convert(int32 Val); }; + template <> struct TValueConverter { static int64 Convert(float Val); }; + template <> struct TValueConverter { static int64 Convert(double Val); }; + + // float + template <> struct TValueConverter { static float Convert(int32 Val); }; + template <> struct TValueConverter { static float Convert(int64 Val); }; + template <> struct TValueConverter { static float Convert(double Val); }; + + // double + template <> struct TValueConverter { static double Convert(int32 Val); }; + template <> struct TValueConverter { static double Convert(int64 Val); }; + template <> struct TValueConverter { static double Convert(float Val); }; + + // String types + template <> struct TValueConverter { static FName Convert(const FString& Val); }; + template <> struct TValueConverter { static FName Convert(const FText& Val); }; + template <> struct TValueConverter { static FString Convert(const FName& Val); }; + template <> struct TValueConverter { static FString Convert(const FText& Val); }; + template <> struct TValueConverter { static FText Convert(const FName& Val); }; + template <> struct TValueConverter { static FText Convert(const FString& Val); }; + + // To string for logging + template <> struct TValueConverter { static FString Convert(int32 Val); }; + template <> struct TValueConverter { static FString Convert(int64 Val); }; + template <> struct TValueConverter { static FString Convert(float Val); }; + template <> struct TValueConverter { static FString Convert(double Val); }; + template <> struct TValueConverter { static FString Convert(bool Val); }; + + // GameplayTag + template <> struct TValueConverter + { + static FGameplayTag Convert(const FGameplayTagContainer& Container); + }; + + template <> struct TValueConverter + { + static FGameplayTagContainer Convert(const FGameplayTag& Tag); + }; + + // ----------------------------------------------------------------------- + // Array Conversion Helper + // ----------------------------------------------------------------------- + + /** Converts array with logging and clamping. */ + template + void ConvertArray(const TArray& Source, TArray& OutValues, TConverter Converter) + { + OutValues.Reserve(Source.Num()); + for (const TSource& Val : Source) + { +#if !UE_BUILD_SHIPPING + // Lossy conversion warnings + if constexpr (std::is_integral_v && std::is_floating_point_v) + { + int64 iv = FMath::FloorToInt64(Val); + if (iv < std::numeric_limits::min() || iv > std::numeric_limits::max()) + { + UE_LOG(LogFlow, Warning, TEXT("Converting %s to %s (out of range, clamping)"), + *TValueConverter::Convert(Val), TEXT("int")); + } + } + else if constexpr (std::is_same_v && std::is_same_v) + { + if (Val < std::numeric_limits::lowest() || Val > std::numeric_limits::max()) + { + UE_LOG(LogFlow, Warning, TEXT("Converting %s to float (out of range, clamping)"), + *TValueConverter::Convert(Val)); + } + } + else if constexpr (std::is_same_v && (std::is_same_v || std::is_same_v)) + { + FString SourceStr; + if constexpr (std::is_same_v) + { + SourceStr = Val; + } + else + { + SourceStr = TValueConverter::Convert(Val); + } + if (SourceStr.Len() > NAME_SIZE) + { + UE_LOG(LogFlow, Warning, TEXT("Converting '%s' to FName (possible truncation)"), *SourceStr); + } + } +#endif + OutValues.Add(Converter(Val)); + } + } + + // ----------------------------------------------------------------------- + // Internal helper – applies the single-from-array policy after extraction + // ----------------------------------------------------------------------- + + template + FORCEINLINE EFlowDataPinResolveResult ApplySinglePolicy( + const TArray& Source, + TArray& OutValues, + EFlowSingleFromArray Policy) + { + if (Policy == EFlowSingleFromArray::EntireArray) + { + OutValues = Source; + return EFlowDataPinResolveResult::Success; + } + + const int32 Num = Source.Num(); + const int32 Index = EFlowSingleFromArray_Classifiers::ConvertToIndex(Policy, Num); + + if (!Source.IsValidIndex(Index)) + { + return EFlowDataPinResolveResult::FailedInsufficientValues; + } + + OutValues.Add(Source[Index]); + return EFlowDataPinResolveResult::Success; + } + + template + FORCEINLINE EFlowDataPinResolveResult ConvertWithPolicy( + const TArray& Source, + TArray& OutValues, + TConverter Converter, + EFlowSingleFromArray Policy) + { + if (Policy == EFlowSingleFromArray::EntireArray) + { + ConvertArray(Source, OutValues, Converter); + return EFlowDataPinResolveResult::Success; + } + + const int32 Index = EFlowSingleFromArray_Classifiers::ConvertToIndex(Policy, Source.Num()); + if (!Source.IsValidIndex(Index)) + { + return EFlowDataPinResolveResult::FailedInsufficientValues; + } + + OutValues.Add(Converter(Source[Index])); + return EFlowDataPinResolveResult::Success; + } + + // ----------------------------------------------------------------------- + // Numeric Validation & Clamping + // ----------------------------------------------------------------------- + + template + TValue ValidateAndClampNumericValue(TInput Val, TValue MinValue, TValue MaxValue) + { + if constexpr (std::is_floating_point::value) + { +#if !UE_BUILD_SHIPPING + if (!FMath::IsFinite(Val)) + { + UE_LOG(LogFlow, Warning, TEXT("Non-finite value %s encountered during conversion to %s"), + *TValueConverter::Convert(Val), TEXT("numeric")); + return TValue(0); + } +#endif + } + + if constexpr (std::is_floating_point::value) + { + if constexpr (std::is_same::value && std::is_same::value) + { +#if !UE_BUILD_SHIPPING + if (Val < MinValue || Val > MaxValue) + { + UE_LOG(LogFlow, Warning, TEXT("Double value %s out of range for float, clamping"), + *TValueConverter::Convert(Val)); + } +#endif + } + return FMath::Clamp(static_cast(Val), MinValue, MaxValue); + } + else + { + int64 iv = std::is_floating_point::value ? FMath::FloorToInt64(Val) : static_cast(Val); +#if !UE_BUILD_SHIPPING + if (iv < MinValue || iv > MaxValue) + { + UE_LOG(LogFlow, Warning, TEXT("Value %lld out of range for %s, clamping"), iv, TEXT("int")); + } +#endif + return static_cast(FMath::Clamp(iv, static_cast(MinValue), static_cast(MaxValue))); + } + } + + // ----------------------------------------------------------------------- + // ValueConverter Implementations + // ----------------------------------------------------------------------- + + // int32 + inline int32 TValueConverter::Convert(int64 Val) { return ValidateAndClampNumericValue(Val, std::numeric_limits::min(), std::numeric_limits::max()); } + inline int32 TValueConverter::Convert(float Val) { return ValidateAndClampNumericValue(Val, std::numeric_limits::min(), std::numeric_limits::max()); } + inline int32 TValueConverter::Convert(double Val) { return ValidateAndClampNumericValue(Val, std::numeric_limits::min(), std::numeric_limits::max()); } + + // int64 + inline int64 TValueConverter::Convert(int32 Val) { return static_cast(Val); } + inline int64 TValueConverter::Convert(float Val) { return ValidateAndClampNumericValue(Val, MIN_int64, MAX_int64); } + inline int64 TValueConverter::Convert(double Val) { return ValidateAndClampNumericValue(Val, MIN_int64, MAX_int64); } + + // float + inline float TValueConverter::Convert(int32 Val) { return static_cast(Val); } + inline float TValueConverter::Convert(int64 Val) { return ValidateAndClampNumericValue(Val, std::numeric_limits::lowest(), std::numeric_limits::max()); } + inline float TValueConverter::Convert(double Val) { return ValidateAndClampNumericValue(Val, std::numeric_limits::lowest(), std::numeric_limits::max()); } + + // double + inline double TValueConverter::Convert(int32 Val) { return static_cast(Val); } + inline double TValueConverter::Convert(int64 Val) { return static_cast(Val); } + inline double TValueConverter::Convert(float Val) { return static_cast(Val); } + + // String types + inline FName TValueConverter::Convert(const FString& Val) { return FName(*Val); } + inline FName TValueConverter::Convert(const FText& Val) { return FName(*Val.ToString()); } + inline FString TValueConverter::Convert(const FName& Val) { return Val.ToString(); } + inline FString TValueConverter::Convert(const FText& Val) { return Val.ToString(); } + inline FText TValueConverter::Convert(const FName& Val) { return FText::FromName(Val); } + inline FText TValueConverter::Convert(const FString& Val) { return FText::FromString(Val); } + + // String converters for other types + inline FString TValueConverter::Convert(int32 Val) { return FString::Printf(TEXT("%d"), Val); } + inline FString TValueConverter::Convert(int64 Val) { return FString::Printf(TEXT("%lld"), Val); } + inline FString TValueConverter::Convert(float Val) { return FString::Printf(TEXT("%f"), Val); } + inline FString TValueConverter::Convert(double Val) { return FString::Printf(TEXT("%f"), Val); } + inline FString TValueConverter::Convert(bool Val) { return Val ? TEXT("true") : TEXT("false"); } + + // GameplayTag + inline FGameplayTag TValueConverter::Convert(const FGameplayTagContainer& Container) + { +#if !UE_BUILD_SHIPPING + if (Container.Num() > 1) + { + UE_LOG(LogFlow, Warning, TEXT("Multiple tags in container; using first: %s"), *Container.ToStringSimple()); + } +#endif + return Container.Num() > 0 ? Container.GetByIndex(0) : FGameplayTag(); + } + + inline FGameplayTagContainer TValueConverter::Convert(const FGameplayTag& Tag) + { + return FGameplayTagContainer(Tag); + } + + // ----------------------------------------------------------------------- + // Property Traits + // ----------------------------------------------------------------------- + + /* Base for simple scalar types. */ + template + struct FFlowSimplePropertyTraitsBase + { + using ValueType = TPinType::ValueType; + using WrapperType = TPinType::WrapperType; + using PropertyType = TPinType::MainPropertyType; + using LegacyWrapperType = TPinType::LegacyWrapperType; + + static EFlowDataPinResolveResult ExtractFromProperty(const FProperty* Property, const void* Container, TArray& OutValues) + { + // 1. Wrapper struct + if (const FStructProperty* StructProp = CastField(Property)) + { + if (StructProp->Struct == WrapperType::StaticStruct()) + { + const WrapperType* Wrapper = StructProp->ContainerPtrToValuePtr(Container); + OutValues = Wrapper->Values; + return EFlowDataPinResolveResult::Success; + } + + // #FlowDataPinLegacy - support sourcing from old property wrappers For Now(tm) + static const UScriptStruct* OldPropStruct = LegacyWrapperType::StaticStruct(); + if (StructProp->Struct->IsChildOf(OldPropStruct)) + { + const LegacyWrapperType* Wrapper = StructProp->ContainerPtrToValuePtr(Container); + OutValues = { Wrapper->Value }; + return EFlowDataPinResolveResult::Success; + } + // -- + } + + // 2. Direct property + if (const PropertyType* Prop = CastField(Property)) + { + OutValues = { *Prop->template ContainerPtrToValuePtr(Container) }; + return EFlowDataPinResolveResult::Success; + } + + // 3. Array of property + if (const FArrayProperty* ArrProp = CastField(Property)) + { + if (const PropertyType* Inner = CastField(ArrProp->Inner)) + { + FScriptArrayHelper ArrHelper(ArrProp, ArrProp->ContainerPtrToValuePtr(Container)); + OutValues.Reserve(ArrHelper.Num()); + for (int32 i = 0; i < ArrHelper.Num(); ++i) + { + OutValues.Add(*Inner->template ContainerPtrToValuePtr(ArrHelper.GetRawPtr(i))); + } + return EFlowDataPinResolveResult::Success; + } + } + + return EFlowDataPinResolveResult::FailedMismatchedType; + } + + static EFlowDataPinResolveResult ExtractValues(const FFlowDataPinResult& DataPinResult, TArray& OutValues, EFlowSingleFromArray SingleFromArray) + { + if (!IsSuccess(DataPinResult.Result)) + { + return DataPinResult.Result; + } + + if (DataPinResult.ResultValue.GetScriptStruct() == WrapperType::StaticStruct()) + { + const WrapperType& Wrapper = DataPinResult.ResultValue.Get(); + return ApplySinglePolicy(Wrapper.Values, OutValues, SingleFromArray); + } + + return EFlowDataPinResolveResult::FailedMismatchedType; + } + }; + + /* Numeric cross-conversion. */ + template + struct FFlowNumericTraitsBase : public FFlowSimplePropertyTraitsBase + { + using Super = FFlowSimplePropertyTraitsBase; + using ValueType = TPinType::ValueType; + using WrapperType = TPinType::WrapperType; + + static EFlowDataPinResolveResult ExtractValues(const FFlowDataPinResult& DataPinResult, TArray& OutValues, EFlowSingleFromArray SingleFromArray) + { + if (!IsSuccess(DataPinResult.Result)) + { + return DataPinResult.Result; + } + + const UScriptStruct* ScriptStruct = DataPinResult.ResultValue.GetScriptStruct(); + + if (ScriptStruct == WrapperType::StaticStruct()) + { + const WrapperType& Wrapper = DataPinResult.ResultValue.Get(); + return ApplySinglePolicy(Wrapper.Values, OutValues, SingleFromArray); + } + + // Cross-convert from other numeric types + if constexpr (!std::is_same_v) + { + if (ScriptStruct == FFlowDataPinValue_Int::StaticStruct()) + { + const FFlowDataPinValue_Int& Wrapper = DataPinResult.ResultValue.Get(); + return ConvertWithPolicy(Wrapper.Values, OutValues, TValueConverter::Convert, SingleFromArray); + } + } + + if constexpr (!std::is_same_v) + { + if (ScriptStruct == FFlowDataPinValue_Int64::StaticStruct()) + { + const FFlowDataPinValue_Int64& Wrapper = DataPinResult.ResultValue.Get(); + return ConvertWithPolicy(Wrapper.Values, OutValues, TValueConverter::Convert, SingleFromArray); + } + } + + if constexpr (!std::is_same_v) + { + if (ScriptStruct == FFlowDataPinValue_Float::StaticStruct()) + { + const FFlowDataPinValue_Float& Wrapper = DataPinResult.ResultValue.Get(); + return ConvertWithPolicy(Wrapper.Values, OutValues, TValueConverter::Convert, SingleFromArray); + } + } + + if constexpr (!std::is_same_v) + { + if (ScriptStruct == FFlowDataPinValue_Double::StaticStruct()) + { + const FFlowDataPinValue_Double& Wrapper = DataPinResult.ResultValue.Get(); + return ConvertWithPolicy(Wrapper.Values, OutValues, TValueConverter::Convert, SingleFromArray); + } + } + + return EFlowDataPinResolveResult::FailedMismatchedType; + } + }; + + /* String cross-conversion. */ + template + struct FFlowStringTraitsBase : public FFlowSimplePropertyTraitsBase + { + using Super = FFlowSimplePropertyTraitsBase; + using ValueType = TPinType::ValueType; + using WrapperType = TPinType::WrapperType; + + static EFlowDataPinResolveResult ExtractValues(const FFlowDataPinResult& DataPinResult, TArray& OutValues, EFlowSingleFromArray SingleFromArray) + { + if (!IsSuccess(DataPinResult.Result)) + { + return DataPinResult.Result; + } + + const UScriptStruct* ScriptStruct = DataPinResult.ResultValue.GetScriptStruct(); + + if (ScriptStruct == WrapperType::StaticStruct()) + { + const WrapperType& Wrapper = DataPinResult.ResultValue.Get(); + return ApplySinglePolicy(Wrapper.Values, OutValues, SingleFromArray); + } + + // Cross-convert from other string types + if constexpr (!std::is_same_v) + { + if (ScriptStruct == FFlowDataPinValue_Name::StaticStruct()) + { + const FFlowDataPinValue_Name& Wrapper = DataPinResult.ResultValue.Get(); + return ConvertWithPolicy(Wrapper.Values, OutValues, TValueConverter::Convert, SingleFromArray); + } + } + + if constexpr (!std::is_same_v) + { + if (ScriptStruct == FFlowDataPinValue_String::StaticStruct()) + { + const FFlowDataPinValue_String& Wrapper = DataPinResult.ResultValue.Get(); + return ConvertWithPolicy(Wrapper.Values, OutValues, TValueConverter::Convert, SingleFromArray); + } + } + + if constexpr (!std::is_same_v) + { + if (ScriptStruct == FFlowDataPinValue_Text::StaticStruct()) + { + const FFlowDataPinValue_Text& Wrapper = DataPinResult.ResultValue.Get(); + return ConvertWithPolicy(Wrapper.Values, OutValues, TValueConverter::Convert, SingleFromArray); + } + } + + // Fallback to string conversion from any pin value + if (ScriptStruct->IsChildOf(FFlowDataPinValue::StaticStruct())) + { + const FFlowDataPinValue& BaseWrapper = DataPinResult.ResultValue.Get(); + FString StrValue; + if (BaseWrapper.TryConvertValuesToString(StrValue)) + { + if constexpr (std::is_same_v) + { + OutValues = { StrValue }; + } + else + { + OutValues = { TValueConverter::Convert(StrValue) }; + } + return EFlowDataPinResolveResult::Success; + } + } + + return EFlowDataPinResolveResult::FailedMismatchedType; + } + }; + + /* Struct types: Vector, Rotator, etc. */ + template + struct FFlowStructTraitsBase : public FFlowSimplePropertyTraitsBase + { + using ValueType = TPinType::ValueType; + using WrapperType = TPinType::WrapperType; + using LegacyWrapperType = TPinType::LegacyWrapperType; + + static EFlowDataPinResolveResult ExtractFromProperty(const FProperty* Property, const void* Container, TArray& OutValues) + { + static const UScriptStruct* ValueStruct = TBaseStructure::Get(); + + if (const FStructProperty* StructProp = CastField(Property)) + { + static const UScriptStruct* WrapperStruct = TBaseStructure::Get(); + if (StructProp->Struct == WrapperStruct) + { + const WrapperType* Wrapper = StructProp->ContainerPtrToValuePtr(Container); + OutValues = Wrapper->Values; + return EFlowDataPinResolveResult::Success; + } + + if (StructProp->Struct == ValueStruct) + { + OutValues = { *StructProp->ContainerPtrToValuePtr(Container) }; + return EFlowDataPinResolveResult::Success; + } + + // #FlowDataPinLegacy - support sourcing from old property wrappers For Now(tm) + static const UScriptStruct* OldPropStruct = LegacyWrapperType::StaticStruct(); + if (StructProp->Struct->IsChildOf(OldPropStruct)) + { + const LegacyWrapperType* Wrapper = StructProp->ContainerPtrToValuePtr(Container); + OutValues = { Wrapper->Value }; + return EFlowDataPinResolveResult::Success; + } + // -- + } + else if (const FArrayProperty* ArrayProp = CastField(Property)) + { + const FStructProperty* InnerStruct = CastField(ArrayProp->Inner); + if (InnerStruct && InnerStruct->Struct == ValueStruct) + { + FScriptArrayHelper Helper(ArrayProp, ArrayProp->ContainerPtrToValuePtr(Container)); + OutValues.Reserve(Helper.Num()); + for (int32 i = 0; i < Helper.Num(); ++i) + { + OutValues.Add(*reinterpret_cast(Helper.GetRawPtr(i))); + } + return EFlowDataPinResolveResult::Success; + } + } + + return EFlowDataPinResolveResult::FailedMismatchedType; + } + }; + + // ----------------------------------------------------------------------- + // Pin Type Traits + // ----------------------------------------------------------------------- + + template struct FFlowDataPinValueTraits; + + // Scalars + template <> struct FFlowDataPinValueTraits : public FFlowSimplePropertyTraitsBase {}; + template <> struct FFlowDataPinValueTraits : public FFlowNumericTraitsBase {}; + template <> struct FFlowDataPinValueTraits : public FFlowNumericTraitsBase {}; + template <> struct FFlowDataPinValueTraits : public FFlowNumericTraitsBase {}; + template <> struct FFlowDataPinValueTraits : public FFlowNumericTraitsBase {}; + template <> struct FFlowDataPinValueTraits : public FFlowStringTraitsBase {}; + template <> struct FFlowDataPinValueTraits : public FFlowStringTraitsBase {}; + template <> struct FFlowDataPinValueTraits : public FFlowStringTraitsBase {}; + + // Structs + template <> struct FFlowDataPinValueTraits : public FFlowStructTraitsBase {}; + template <> struct FFlowDataPinValueTraits : public FFlowStructTraitsBase {}; + template <> struct FFlowDataPinValueTraits : public FFlowStructTraitsBase {}; + template <> struct FFlowDataPinValueTraits : public FFlowStructTraitsBase {}; + + // Enum + template <> + struct FFlowDataPinValueTraits : public FFlowSimplePropertyTraitsBase + { + using TPinType = FFlowPinType_Enum; + using WrapperType = TPinType::WrapperType; + using ValueType = TPinType::ValueType; + using LegacyWrapperType = TPinType::LegacyWrapperType; + + static EFlowDataPinResolveResult ExtractFromProperty(const FProperty* Property, const void* Container, TArray& OutValues, TSoftObjectPtr& OutEnumClass) + { + const FStructProperty* StructProp = CastField(Property); + if (StructProp && StructProp->Struct == WrapperType::StaticStruct()) + { + const WrapperType* Wrapper = StructProp->ContainerPtrToValuePtr(Container); + OutValues = Wrapper->Values; + OutEnumClass = Wrapper->EnumClass; + return EFlowDataPinResolveResult::Success; + } + + // #FlowDataPinLegacy - support sourcing from old property wrappers For Now(tm) + static const UScriptStruct* OldPropStruct = LegacyWrapperType::StaticStruct(); + if (StructProp && StructProp->Struct->IsChildOf(OldPropStruct)) + { + const LegacyWrapperType* Wrapper = StructProp->ContainerPtrToValuePtr(Container); + OutValues = { Wrapper->Value }; + OutEnumClass = Wrapper->EnumClass; + return EFlowDataPinResolveResult::Success; + } + // -- + + if (const FEnumProperty* EnumProp = CastField(Property)) + { + const void* ContainerPtr = EnumProp->ContainerPtrToValuePtr(Container); + UEnum* EnumClass = EnumProp->GetEnum(); + const FNumericProperty* Underlying = EnumProp->GetUnderlyingProperty(); + int64 RawValue = Underlying->GetSignedIntPropertyValue_InContainer(ContainerPtr); + FString AuthoredName = EnumClass->GetAuthoredNameStringByValue(RawValue); + + OutValues = { FName(AuthoredName) }; + OutEnumClass = EnumClass; + return EFlowDataPinResolveResult::Success; + } + + if (const FArrayProperty* ArrayProp = CastField(Property)) + { + if (const FEnumProperty* Inner = CastField(ArrayProp->Inner)) + { + FScriptArrayHelper Helper(ArrayProp, ArrayProp->ContainerPtrToValuePtr(Container)); + UEnum* EnumClass = Inner->GetEnum(); + const FNumericProperty* Underlying = Inner->GetUnderlyingProperty(); + OutValues.Reserve(Helper.Num()); + for (int32 i = 0; i < Helper.Num(); ++i) + { + int64 RawValue = Underlying->GetSignedIntPropertyValue(Helper.GetRawPtr(i)); + FString Name = EnumClass->GetAuthoredNameStringByValue(RawValue); + OutValues.Add(FName(Name)); + } + OutEnumClass = EnumClass; + return EFlowDataPinResolveResult::Success; + } + } + + return EFlowDataPinResolveResult::FailedMismatchedType; + } + }; + + /* GameplayTag. */ + template <> + struct FFlowDataPinValueTraits : public FFlowStructTraitsBase + { + using PinType = FFlowPinType_GameplayTag; + using ValueType = PinType::ValueType; + using WrapperType = FFlowDataPinValue_GameplayTag; + using ContainerWrapper = FFlowDataPinValue_GameplayTagContainer; + + static EFlowDataPinResolveResult ExtractValues(const FFlowDataPinResult& DataPinResult, TArray& OutValues, EFlowSingleFromArray SingleFromArray) + { + if (!IsSuccess(DataPinResult.Result)) + { + return DataPinResult.Result; + } + + const UScriptStruct* ScriptStruct = DataPinResult.ResultValue.GetScriptStruct(); + + if (ScriptStruct == WrapperType::StaticStruct()) + { + const WrapperType& Wrapper = DataPinResult.ResultValue.Get(); + return ApplySinglePolicy(Wrapper.Values, OutValues, SingleFromArray); + } + + if (ScriptStruct == ContainerWrapper::StaticStruct()) + { + const ContainerWrapper& Wrapper = DataPinResult.ResultValue.Get(); + TArray Temp = Wrapper.Values.GetGameplayTagArray(); + return ApplySinglePolicy(Temp, OutValues, SingleFromArray); + } + + return EFlowDataPinResolveResult::FailedMismatchedType; + } + }; + + /* GameplayTagContainer. */ + template <> + struct FFlowDataPinValueTraits : public FFlowStructTraitsBase + { + using PinType = FFlowPinType_GameplayTagContainer; + using ValueType = PinType::ValueType; + using WrapperType = FFlowDataPinValue_GameplayTagContainer; + + static EFlowDataPinResolveResult ExtractFromProperty(const FProperty* Property, const void* Container, TArray& OutValues) + { + static const UScriptStruct* ValueStruct = TBaseStructure::Get(); + + if (const FStructProperty* StructProp = CastField(Property)) + { + static const UScriptStruct* WrapperStruct = TBaseStructure::Get(); + if (StructProp->Struct == WrapperStruct) + { + const WrapperType* Wrapper = StructProp->ContainerPtrToValuePtr(Container); + OutValues = { Wrapper->Values }; + return EFlowDataPinResolveResult::Success; + } + + if (StructProp->Struct == ValueStruct) + { + OutValues = { *StructProp->ContainerPtrToValuePtr(Container) }; + return EFlowDataPinResolveResult::Success; + } + + // #FlowDataPinLegacy - support sourcing from old property wrappers For Now(tm) + static const UScriptStruct* OldPropStruct = LegacyWrapperType::StaticStruct(); + if (StructProp->Struct->IsChildOf(OldPropStruct)) + { + const LegacyWrapperType* Wrapper = StructProp->ContainerPtrToValuePtr(Container); + OutValues = { Wrapper->Value }; + return EFlowDataPinResolveResult::Success; + } + // -- + } + else if (const FArrayProperty* ArrayProp = CastField(Property)) + { + const FStructProperty* Inner = CastField(ArrayProp->Inner); + if (Inner && Inner->Struct == ValueStruct) + { + FScriptArrayHelper Helper(ArrayProp, ArrayProp->ContainerPtrToValuePtr(Container)); + ValueType Consolidated; + for (int32 i = 0; i < Helper.Num(); ++i) + { + Consolidated.AppendTags(*reinterpret_cast(Helper.GetRawPtr(i))); + } + OutValues = { Consolidated }; + return EFlowDataPinResolveResult::Success; + } + } + + return EFlowDataPinResolveResult::FailedMismatchedType; + } + + static EFlowDataPinResolveResult ExtractValues(const FFlowDataPinResult& DataPinResult, TArray& OutValues, EFlowSingleFromArray SingleFromArray) + { + if (!IsSuccess(DataPinResult.Result)) + { + return DataPinResult.Result; + } + + const UScriptStruct* ScriptStruct = DataPinResult.ResultValue.GetScriptStruct(); + + if (ScriptStruct == WrapperType::StaticStruct()) + { + const WrapperType& Wrapper = DataPinResult.ResultValue.Get(); + OutValues = { Wrapper.Values }; + return EFlowDataPinResolveResult::Success; + } + + if (ScriptStruct == FFlowDataPinValue_GameplayTag::StaticStruct()) + { + const FFlowDataPinValue_GameplayTag& Wrapper = DataPinResult.ResultValue.Get(); + OutValues = { FGameplayTagContainer::CreateFromArray(Wrapper.Values) }; + return EFlowDataPinResolveResult::Success; + } + + return EFlowDataPinResolveResult::FailedMismatchedType; + } + }; + + /* Base for Object, Class. */ + template + struct FFlowObjectTraitsBase + { + using ValueType = TPinType::ValueType; + using WrapperType = TPinType::WrapperType; + using LegacyWrapperType = TPinType::LegacyWrapperType; + + static EFlowDataPinResolveResult ExtractFromProperty(const FProperty* Property, const void* Container, TArray& OutValues) + { + if (const FStructProperty* StructProp = CastField(Property)) + { + if (StructProp->Struct == WrapperType::StaticStruct()) + { + const WrapperType* Wrapper = StructProp->ContainerPtrToValuePtr(Container); + for (const auto& Path : Wrapper->Values) + { + if constexpr (std::is_same_v, FSoftObjectPath> || + std::is_same_v, FSoftClassPath>) + { + OutValues.Add(Cast(Path.ResolveObject())); + } + else + { + OutValues.Add(Cast(Path)); + } + } + return EFlowDataPinResolveResult::Success; + } + + // #FlowDataPinLegacy - support sourcing from old property wrappers For Now(tm) + static const UScriptStruct* OldPropStruct = LegacyWrapperType::StaticStruct(); + if (StructProp->Struct->IsChildOf(OldPropStruct)) + { + const LegacyWrapperType* Wrapper = StructProp->ContainerPtrToValuePtr(Container); + OutValues = { Cast(Wrapper->GetObjectValue()) }; + return EFlowDataPinResolveResult::Success; + } + // -- + } + + if (const FArrayProperty* ArrProp = CastField(Property)) + { + if (const TProperty* InnerObjProp = CastField(ArrProp->Inner)) + { + FScriptArrayHelper ArrHelper(ArrProp, ArrProp->ContainerPtrToValuePtr(Container)); + const int32 Num = ArrHelper.Num(); + OutValues.Reserve(Num); + for (int32 i = 0; i < Num; ++i) + { + OutValues.Add(Cast(InnerObjProp->GetObjectPropertyValue(ArrHelper.GetRawPtr(i)))); + } + return EFlowDataPinResolveResult::Success; + } + else if (const TSoftProperty* InnerSoftProp = CastField(ArrProp->Inner)) + { + FScriptArrayHelper ArrHelper(ArrProp, ArrProp->ContainerPtrToValuePtr(Container)); + const int32 Num = ArrHelper.Num(); + OutValues.Reserve(Num); + for (int32 i = 0; i < Num; ++i) + { + const FSoftObjectPath Path = InnerSoftProp->GetPropertyValue(ArrHelper.GetRawPtr(i)).ToSoftObjectPath(); + OutValues.Add(Cast(Path.TryLoad())); + } + return EFlowDataPinResolveResult::Success; + } + else if (const FWeakObjectProperty* InnerWeakProp = CastField(ArrProp->Inner)) + { + FScriptArrayHelper ArrHelper(ArrProp, ArrProp->ContainerPtrToValuePtr(Container)); + const int32 Num = ArrHelper.Num(); + OutValues.Reserve(Num); + for (int32 i = 0; i < Num; ++i) + { + OutValues.Add(Cast(InnerWeakProp->GetPropertyValue_InContainer(Container).Get())); + } + return EFlowDataPinResolveResult::Success; + } + } + + if (const TProperty* ObjProp = CastField(Property)) + { + OutValues = { Cast(ObjProp->GetObjectPropertyValue_InContainer(Container)) }; + return EFlowDataPinResolveResult::Success; + } + else if (const TSoftProperty* SoftObjProp = CastField(Property)) + { + const FSoftObjectPath Path = SoftObjProp->GetPropertyValue_InContainer(Container).ToSoftObjectPath(); + OutValues = { Cast(Path.TryLoad()) }; + return EFlowDataPinResolveResult::Success; + } + else if (const FWeakObjectProperty* WeakProp = CastField(Property)) + { + OutValues = { Cast(WeakProp->GetPropertyValue_InContainer(Container).Get()) }; + return EFlowDataPinResolveResult::Success; + } + + return EFlowDataPinResolveResult::FailedMismatchedType; + } + + static EFlowDataPinResolveResult ExtractValues(const FFlowDataPinResult& DataPinResult, TArray& OutValues, EFlowSingleFromArray SingleFromArray) + { + if (!IsSuccess(DataPinResult.Result)) + { + return DataPinResult.Result; + } + + if (DataPinResult.ResultValue.GetScriptStruct() == WrapperType::StaticStruct()) + { + const WrapperType& Wrapper = DataPinResult.ResultValue.Get(); + const auto& Source = Wrapper.Values; // this is TArray or TArray + + if (SingleFromArray == EFlowSingleFromArray::EntireArray) + { + OutValues.Reserve(Source.Num()); + for (const auto& Path : Source) + { + if constexpr (std::is_same_v, FSoftObjectPath> || + std::is_same_v, FSoftClassPath>) + { + OutValues.Add(Cast(Path.ResolveObject())); + } + else + { + OutValues.Add(Cast(Path)); + } + } + } + else + { + const int32 Index = EFlowSingleFromArray_Classifiers::ConvertToIndex(SingleFromArray, Source.Num()); + if (!Source.IsValidIndex(Index)) + { + return EFlowDataPinResolveResult::FailedInsufficientValues; + } + + const auto& Path = Source[Index]; + if constexpr (std::is_same_v, FSoftObjectPath> || + std::is_same_v, FSoftClassPath>) + { + OutValues.Add(Cast(Path.ResolveObject())); + } + else + { + OutValues.Add(Cast(Path)); + } + } + + return EFlowDataPinResolveResult::Success; + } + + return EFlowDataPinResolveResult::FailedMismatchedType; + } + }; + + template <> struct FFlowDataPinValueTraits : public FFlowObjectTraitsBase {}; + template <> struct FFlowDataPinValueTraits : public FFlowObjectTraitsBase {}; + + // ----------------------------------------------------------------------- + // Value Extractors + // ----------------------------------------------------------------------- + + template + static EFlowDataPinResolveResult TryExtractValue(const FFlowDataPinResult& DataPinResult, typename TPinType::ValueType& OutValue, EFlowSingleFromArray SingleFromArray) + { + if (!IsSuccess(DataPinResult.Result)) + { + return DataPinResult.Result; + } + + TArray Values; + const EFlowDataPinResolveResult Result = FFlowDataPinValueTraits::ExtractValues(DataPinResult, Values, SingleFromArray); + + if (!IsSuccess(Result)) + { + return Result; + } + + if (Values.IsEmpty()) + { + return EFlowDataPinResolveResult::FailedInsufficientValues; + } + + OutValue = Values[0]; + return EFlowDataPinResolveResult::Success; + } + + template + static EFlowDataPinResolveResult TryExtractValues(const FFlowDataPinResult& DataPinResult, TArray& OutValues) + { + if (!IsSuccess(DataPinResult.Result)) + { + return DataPinResult.Result; + } + + return FFlowDataPinValueTraits::ExtractValues(DataPinResult, OutValues, EFlowSingleFromArray::EntireArray); + } + + /* Special-case single-value extractor for enums (FName + EnumClass). */ + template + static EFlowDataPinResolveResult TryExtractValue(const FFlowDataPinResult& DataPinResult, typename TPinType::ValueType& OutValue, typename TPinType::FieldType*& OutField, EFlowSingleFromArray SingleFromArray) + { + if (!IsSuccess(DataPinResult.Result)) + { + return DataPinResult.Result; + } + + const typename TPinType::WrapperType& Wrapper = DataPinResult.ResultValue.Get(); + OutField = Cast(Wrapper.GetFieldType()); + return TryExtractValue(DataPinResult, OutValue, SingleFromArray); + } + + /* Special-case array-value extractor for enums (TArray + EnumClass). */ + template + static EFlowDataPinResolveResult TryExtractValues(const FFlowDataPinResult& DataPinResult, TArray& OutValues, typename TPinType::FieldType*& OutField) + { + if (!IsSuccess(DataPinResult.Result)) + { + return DataPinResult.Result; + } + + const typename TPinType::WrapperType& Wrapper = DataPinResult.ResultValue.Get(); + OutField = Cast(Wrapper.GetFieldType()); + return TryExtractValues(DataPinResult, OutValues); + } + + /* Special-case single-value extractor for enums (Native enum value). */ + template requires std::is_enum_v + static EFlowDataPinResolveResult TryExtractValue(const FFlowDataPinResult& DataPinResult, TEnumType& OutValue, EFlowSingleFromArray SingleFromArray) + { + if (!IsSuccess(DataPinResult.Result)) + { + return DataPinResult.Result; + } + + const FFlowDataPinValue_Enum& Wrapper = DataPinResult.ResultValue.Get(); + return Wrapper.TryGetSingleEnumValue(OutValue, SingleFromArray); + } + + /* Special-case array-value extractor for enums (Native enum values). */ + template requires std::is_enum_v + static EFlowDataPinResolveResult TryExtractValues(const FFlowDataPinResult& DataPinResult, TArray& OutValues) + { + if (!IsSuccess(DataPinResult.Result)) + { + return DataPinResult.Result; + } + + const FFlowDataPinValue_Enum& Wrapper = DataPinResult.ResultValue.Get(); + return Wrapper.TryGetAllNativeEnumValues(OutValues); + } +} \ No newline at end of file diff --git a/Source/Flow/Public/Types/FlowPinTypesStandard.h b/Source/Flow/Public/Types/FlowPinTypesStandard.h new file mode 100644 index 000000000..ac4b70292 --- /dev/null +++ b/Source/Flow/Public/Types/FlowPinTypesStandard.h @@ -0,0 +1,553 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Types/FlowPinType.h" +#include "Nodes/FlowPin.h" +#include "Math/Vector.h" +#include "Math/Rotator.h" +#include "Math/Transform.h" +#include "GameplayTagContainer.h" +#include "StructUtils/InstancedStruct.h" +#include "UObject/Class.h" + +#if WITH_EDITOR +#include "GraphEditorSettings.h" +#endif + +#include "FlowPinTypesStandard.generated.h" + +struct FFlowDataPinValue_Bool; +struct FFlowDataPinValue_Int; +struct FFlowDataPinValue_Int64; +struct FFlowDataPinValue_Float; +struct FFlowDataPinValue_Double; +struct FFlowDataPinValue_Name; +struct FFlowDataPinValue_String; +struct FFlowDataPinValue_Text; +struct FFlowDataPinValue_Enum; +struct FFlowDataPinValue_Vector; +struct FFlowDataPinValue_Rotator; +struct FFlowDataPinValue_Transform; +struct FFlowDataPinValue_GameplayTag; +struct FFlowDataPinValue_GameplayTagContainer; +struct FFlowDataPinValue_InstancedStruct; +struct FFlowDataPinValue_Object; +struct FFlowDataPinValue_Class; + +// #FlowDataPinLegacy +struct FFlowDataPinOutputProperty_Bool; +struct FFlowDataPinOutputProperty_Int32; +struct FFlowDataPinOutputProperty_Int64; +struct FFlowDataPinOutputProperty_Float; +struct FFlowDataPinOutputProperty_Double; +struct FFlowDataPinOutputProperty_Name; +struct FFlowDataPinOutputProperty_String; +struct FFlowDataPinOutputProperty_Text; +struct FFlowDataPinOutputProperty_Enum; +struct FFlowDataPinOutputProperty_Vector; +struct FFlowDataPinOutputProperty_Rotator; +struct FFlowDataPinOutputProperty_Transform; +struct FFlowDataPinOutputProperty_GameplayTag; +struct FFlowDataPinOutputProperty_GameplayTagContainer; +struct FFlowDataPinOutputProperty_InstancedStruct; +struct FFlowDataPinOutputProperty_Object; +struct FFlowDataPinOutputProperty_Class; +// -- + +/** + * Exec + */ +USTRUCT(BlueprintType) +struct FLOW_API FFlowPinType_Exec : public FFlowPinType +{ + GENERATED_BODY() + + using ValueType = void; + using WrapperType = void; + using MainPropertyType = void; + using LegacyWrapperType = void; + +private: + static const FFlowPinTypeName PinTypeNameExec; +public: + static const FFlowPinTypeName& GetPinTypeNameStatic() { return PinTypeNameExec; } + virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinTypeNameExec; } + +#if WITH_EDITOR + virtual FLinearColor GetPinColor() const override { return GetDefault()->ExecutionPinTypeColor; } + virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; +#endif +}; + +/** + * Bool + */ +USTRUCT(BlueprintType) +struct FLOW_API FFlowPinType_Bool : public FFlowPinType +{ + GENERATED_BODY() + + using ValueType = bool; + using WrapperType = FFlowDataPinValue_Bool; + using MainPropertyType = FBoolProperty; + using LegacyWrapperType = FFlowDataPinOutputProperty_Bool; + +private: + static const FFlowPinTypeName PinTypeNameBool; +public: + static const FFlowPinTypeName& GetPinTypeNameStatic() { return PinTypeNameBool; } + virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinTypeNameBool; } + +#if WITH_EDITOR + virtual FLinearColor GetPinColor() const override { return GetDefault()->BooleanPinTypeColor; } + virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; +#endif + + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; +}; + +/** + * Int + */ +USTRUCT(BlueprintType) +struct FLOW_API FFlowPinType_Int : public FFlowPinType +{ + GENERATED_BODY() + + using ValueType = int32; + using WrapperType = FFlowDataPinValue_Int; + using MainPropertyType = FIntProperty; + using LegacyWrapperType = FFlowDataPinOutputProperty_Int32; + +private: + static const FFlowPinTypeName PinTypeNameInt; +public: + static const FFlowPinTypeName& GetPinTypeNameStatic() { return PinTypeNameInt; } + virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinTypeNameInt; } + +#if WITH_EDITOR + virtual FLinearColor GetPinColor() const override { return GetDefault()->IntPinTypeColor; } + virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; +#endif + + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; +}; + +/** + * Int64 + */ +USTRUCT(BlueprintType) +struct FLOW_API FFlowPinType_Int64 : public FFlowPinType +{ + GENERATED_BODY() + + using ValueType = int64; + using WrapperType = FFlowDataPinValue_Int64; + using MainPropertyType = FInt64Property; + using LegacyWrapperType = FFlowDataPinOutputProperty_Int64; + +private: + static const FFlowPinTypeName PinTypeNameInt64; +public: + static const FFlowPinTypeName& GetPinTypeNameStatic() { return PinTypeNameInt64; } + virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinTypeNameInt64; } + +#if WITH_EDITOR + virtual FLinearColor GetPinColor() const override { return GetDefault()->IntPinTypeColor; } + virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; +#endif + + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; +}; + +/** + * Float + */ +USTRUCT(BlueprintType) +struct FLOW_API FFlowPinType_Float : public FFlowPinType +{ + GENERATED_BODY() + + using ValueType = float; + using WrapperType = FFlowDataPinValue_Float; + using MainPropertyType = FFloatProperty; + using LegacyWrapperType = FFlowDataPinOutputProperty_Float; + +private: + static const FFlowPinTypeName PinTypeNameFloat; +public: + static const FFlowPinTypeName& GetPinTypeNameStatic() { return PinTypeNameFloat; } + virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinTypeNameFloat; } + +#if WITH_EDITOR + virtual FLinearColor GetPinColor() const override { return GetDefault()->FloatPinTypeColor; } + virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; +#endif + + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; +}; + +/** + * Double + */ +USTRUCT(BlueprintType) +struct FLOW_API FFlowPinType_Double : public FFlowPinType +{ + GENERATED_BODY() + + using ValueType = double; + using WrapperType = FFlowDataPinValue_Double; + using MainPropertyType = FDoubleProperty; + using LegacyWrapperType = FFlowDataPinOutputProperty_Double; + +private: + static const FFlowPinTypeName PinTypeNameDouble; +public: + static const FFlowPinTypeName& GetPinTypeNameStatic() { return PinTypeNameDouble; } + virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinTypeNameDouble; } + +#if WITH_EDITOR + virtual FLinearColor GetPinColor() const override { return GetDefault()->FloatPinTypeColor; } + virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; +#endif + + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; +}; + +/** + * Name + */ +USTRUCT(BlueprintType) +struct FLOW_API FFlowPinType_Name : public FFlowPinType +{ + GENERATED_BODY() + + using ValueType = FName; + using WrapperType = FFlowDataPinValue_Name; + using MainPropertyType = FNameProperty; + using LegacyWrapperType = FFlowDataPinOutputProperty_Name; + +private: + static const FFlowPinTypeName PinTypeNameName; +public: + static const FFlowPinTypeName& GetPinTypeNameStatic() { return PinTypeNameName; } + virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinTypeNameName; } + +#if WITH_EDITOR + virtual FLinearColor GetPinColor() const override { return GetDefault()->NamePinTypeColor; } + virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; +#endif + + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; +}; + +/** + * String + */ +USTRUCT(BlueprintType) +struct FLOW_API FFlowPinType_String : public FFlowPinType +{ + GENERATED_BODY() + + using ValueType = FString; + using WrapperType = FFlowDataPinValue_String; + using MainPropertyType = FStrProperty; + using LegacyWrapperType = FFlowDataPinOutputProperty_String; + +private: + static const FFlowPinTypeName PinTypeNameString; +public: + static const FFlowPinTypeName& GetPinTypeNameStatic() { return PinTypeNameString; } + virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinTypeNameString; } + +#if WITH_EDITOR + virtual FLinearColor GetPinColor() const override { return GetDefault()->StringPinTypeColor; } + virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; +#endif + + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; +}; + +/** + * Text + */ +USTRUCT(BlueprintType) +struct FLOW_API FFlowPinType_Text : public FFlowPinType +{ + GENERATED_BODY() + + using ValueType = FText; + using WrapperType = FFlowDataPinValue_Text; + using MainPropertyType = FTextProperty; + using LegacyWrapperType = FFlowDataPinOutputProperty_Text; + +private: + static const FFlowPinTypeName PinTypeNameText; +public: + static const FFlowPinTypeName& GetPinTypeNameStatic() { return PinTypeNameText; } + virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinTypeNameText; } + +#if WITH_EDITOR + virtual FLinearColor GetPinColor() const override { return GetDefault()->TextPinTypeColor; } + virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; +#endif + + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; +}; + +/** + * Enum + */ +USTRUCT(BlueprintType) +struct FLOW_API FFlowPinType_Enum : public FFlowPinType +{ + GENERATED_BODY() + + using ValueType = FName; + using WrapperType = FFlowDataPinValue_Enum; + using MainPropertyType = FEnumProperty; + using FieldType = UEnum; + using LegacyWrapperType = FFlowDataPinOutputProperty_Enum; + +private: + static const FFlowPinTypeName PinTypeNameEnum; +public: + static const FFlowPinTypeName& GetPinTypeNameStatic() { return PinTypeNameEnum; } + virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinTypeNameEnum; } + +#if WITH_EDITOR + virtual FLinearColor GetPinColor() const override { return GetDefault()->DefaultPinTypeColor; } + virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; + virtual UObject* GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const override; +#endif + + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; +}; + +/** + * Vector + */ +USTRUCT(BlueprintType) +struct FLOW_API FFlowPinType_Vector : public FFlowPinType +{ + GENERATED_BODY() + + using ValueType = FVector; + using WrapperType = FFlowDataPinValue_Vector; + using MainPropertyType = FStructProperty; + using LegacyWrapperType = FFlowDataPinOutputProperty_Vector; + +private: + static const FFlowPinTypeName PinTypeNameVector; +public: + static const FFlowPinTypeName& GetPinTypeNameStatic() { return PinTypeNameVector; } + virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinTypeNameVector; } + +#if WITH_EDITOR + virtual FLinearColor GetPinColor() const override { return GetDefault()->VectorPinTypeColor; } + virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; + virtual UObject* GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const override; +#endif + + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; +}; + +/** + * Rotator + */ +USTRUCT(BlueprintType) +struct FLOW_API FFlowPinType_Rotator : public FFlowPinType +{ + GENERATED_BODY() + + using ValueType = FRotator; + using WrapperType = FFlowDataPinValue_Rotator; + using MainPropertyType = FStructProperty; + using LegacyWrapperType = FFlowDataPinOutputProperty_Rotator; + +private: + static const FFlowPinTypeName PinTypeNameRotator; +public: + static const FFlowPinTypeName& GetPinTypeNameStatic() { return PinTypeNameRotator; } + virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinTypeNameRotator; } + +#if WITH_EDITOR + virtual FLinearColor GetPinColor() const override { return GetDefault()->RotatorPinTypeColor; } + virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; + virtual UObject* GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const override; +#endif + + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; +}; + +/** + * Transform + */ +USTRUCT(BlueprintType) +struct FLOW_API FFlowPinType_Transform : public FFlowPinType +{ + GENERATED_BODY() + + using ValueType = FTransform; + using WrapperType = FFlowDataPinValue_Transform; + using MainPropertyType = FStructProperty; + using LegacyWrapperType = FFlowDataPinOutputProperty_Transform; + +private: + static const FFlowPinTypeName PinTypeNameTransform; +public: + static const FFlowPinTypeName& GetPinTypeNameStatic() { return PinTypeNameTransform; } + virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinTypeNameTransform; } + +#if WITH_EDITOR + virtual FLinearColor GetPinColor() const override { return GetDefault()->TransformPinTypeColor; } + virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; + virtual UObject* GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const override; +#endif + + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; +}; + +/** + * GameplayTag + */ +USTRUCT(BlueprintType) +struct FLOW_API FFlowPinType_GameplayTag : public FFlowPinType +{ + GENERATED_BODY() + + using ValueType = FGameplayTag; + using WrapperType = FFlowDataPinValue_GameplayTag; + using MainPropertyType = FStructProperty; + using LegacyWrapperType = FFlowDataPinOutputProperty_GameplayTag; + +private: + static const FFlowPinTypeName PinTypeNameGameplayTag; +public: + static const FFlowPinTypeName& GetPinTypeNameStatic() { return PinTypeNameGameplayTag; } + virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinTypeNameGameplayTag; } + +#if WITH_EDITOR + virtual FLinearColor GetPinColor() const override { return GetDefault()->DefaultPinTypeColor; } + virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; + virtual UObject* GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const override; +#endif + + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; +}; + +/** + * GameplayTagContainer + */ +USTRUCT(BlueprintType) +struct FLOW_API FFlowPinType_GameplayTagContainer : public FFlowPinType +{ + GENERATED_BODY() + + using ValueType = FGameplayTagContainer; + using WrapperType = FFlowDataPinValue_GameplayTagContainer; + using MainPropertyType = FStructProperty; + using LegacyWrapperType = FFlowDataPinOutputProperty_GameplayTagContainer; + +private: + static const FFlowPinTypeName PinTypeNameGameplayTagContainer; +public: + static const FFlowPinTypeName& GetPinTypeNameStatic() { return PinTypeNameGameplayTagContainer; } + virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinTypeNameGameplayTagContainer; } + +#if WITH_EDITOR + virtual FLinearColor GetPinColor() const override { return GetDefault()->DefaultPinTypeColor; } + virtual bool SupportsMultiType(EFlowDataMultiType Mode) const override { FLOW_ASSERT_ENUM_MAX(EFlowDataMultiType, 2); return (Mode == EFlowDataMultiType::Single); } + virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; + virtual UObject* GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const override; +#endif + + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; +}; + +/** + * InstancedStruct + */ +USTRUCT(BlueprintType) +struct FLOW_API FFlowPinType_InstancedStruct : public FFlowPinType +{ + GENERATED_BODY() + + using ValueType = FInstancedStruct; + using WrapperType = FFlowDataPinValue_InstancedStruct; + using MainPropertyType = FStructProperty; + using LegacyWrapperType = FFlowDataPinOutputProperty_InstancedStruct; + +private: + static const FFlowPinTypeName PinTypeNameInstancedStruct; +public: + static const FFlowPinTypeName& GetPinTypeNameStatic() { return PinTypeNameInstancedStruct; } + virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinTypeNameInstancedStruct; } + +#if WITH_EDITOR + virtual FLinearColor GetPinColor() const override { return GetDefault()->StructPinTypeColor; } + virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; + virtual UObject* GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const override; +#endif + + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; +}; + +/** + * Object + */ +USTRUCT(BlueprintType) +struct FLOW_API FFlowPinType_Object : public FFlowPinType +{ + GENERATED_BODY() + + using ValueType = TObjectPtr; + using WrapperType = FFlowDataPinValue_Object; + using MainPropertyType = FObjectProperty; + using LegacyWrapperType = FFlowDataPinOutputProperty_Object; + +private: + static const FFlowPinTypeName PinTypeNameObject; +public: + static const FFlowPinTypeName& GetPinTypeNameStatic() { return PinTypeNameObject; } + virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinTypeNameObject; } + +#if WITH_EDITOR + virtual FLinearColor GetPinColor() const override { return GetDefault()->ObjectPinTypeColor; } + virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; + virtual UObject* GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const override; + + static UClass* TryGetObjectClassFromProperty(const FProperty& MetaDataProperty); + static UClass* TryGetMetaClassFromProperty(const FProperty& MetaDataProperty); +#endif + + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; +}; + +/** + * Class + */ +USTRUCT(BlueprintType) +struct FLOW_API FFlowPinType_Class : public FFlowPinType +{ + GENERATED_BODY() + + using ValueType = TObjectPtr; + using WrapperType = FFlowDataPinValue_Class; + using MainPropertyType = FClassProperty; + using LegacyWrapperType = FFlowDataPinOutputProperty_Class; + +private: + static const FFlowPinTypeName PinTypeNameClass; +public: + static const FFlowPinTypeName& GetPinTypeNameStatic() { return PinTypeNameClass; } + virtual const FFlowPinTypeName& GetPinTypeName() const override { return PinTypeNameClass; } + +#if WITH_EDITOR + virtual FLinearColor GetPinColor() const override { return GetDefault()->ClassPinTypeColor; } + virtual bool ResolveAndFormatPinValue(const UFlowNodeBase& Node, const FName& PinName, FFormatArgumentValue& OutValue) const override; + virtual UObject* GetPinSubCategoryObjectFromProperty(const FProperty* Property, void const* InContainer, const FFlowDataPinValue* Wrapper) const override; +#endif + + virtual bool PopulateResult(const UObject& PropertyOwnerObject, const UFlowNode& Node, const FName& PropertyName, FFlowDataPinResult& OutResult) const override; +}; diff --git a/Source/Flow/Public/Types/FlowStructUtils.h b/Source/Flow/Public/Types/FlowStructUtils.h new file mode 100644 index 000000000..846c08490 --- /dev/null +++ b/Source/Flow/Public/Types/FlowStructUtils.h @@ -0,0 +1,101 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/Field.h" + +#if WITH_EDITOR +namespace FlowStructUtils +{ + template + static UScriptStruct* FindScriptStructForProperty(const FProperty& Property) + { + const FStructProperty* StructProperty = CastField(&Property); + if (!StructProperty) + { + return nullptr; + } + + UScriptStruct* ScriptStruct = TPropertyType::StaticStruct(); + + if (StructProperty->Struct == ScriptStruct) + { + static UScriptStruct* UnrealType = TBaseStructure::Get(); + return UnrealType; + } + + return StructProperty->Struct; + } + + template + TStruct* GetTypedStructValue(FProperty& Prop, void* Container) + { + static_assert(TIsDerivedFrom::IsDerived, "Must be a USTRUCT type"); + if (auto* StructProp = CastField(&Prop)) + { + if (StructProp->Struct->IsChildOf(TStruct::StaticStruct())) + { + return reinterpret_cast(StructProp + ->ContainerPtrToValuePtr(Container)); + } + } + return nullptr; + } + + // Internal SFINAE probe: will fail to compile if TStruct has no StaticStruct(). + template + struct THasStaticStruct + { + private: + template + static auto Test(int) -> decltype(U::StaticStruct(), std::true_type{}); + template + static std::false_type Test(...); + public: + static constexpr bool Value = decltype(Test(0))::value; + }; + + template + FORCEINLINE TStruct* CastStructValue(FProperty* Prop, void* Container) + { + static_assert(THasStaticStruct::Value, + "TStruct must be a USTRUCT type providing StaticStruct()."); + + if (!Prop || !Container) + return nullptr; + + FStructProperty* StructProp = CastField(Prop); + if (!StructProp) + return nullptr; + + // Check exact or derived type. + if (!StructProp->Struct->IsChildOf(TStruct::StaticStruct())) + return nullptr; + + // Retrieve the memory for this property within the container and cast. + void* ValueMem = StructProp->ContainerPtrToValuePtr(Container); + return static_cast(ValueMem); + } + + /* Pointer overload (const). */ + template + FORCEINLINE const TStruct* CastStructValue(const FProperty* Prop, const void* Container) + { + return CastStructValue( + const_cast(Prop), + const_cast(Container)); + } + + /* Reference overloads for convenience. */ + template + FORCEINLINE TStruct* CastStructValue(FProperty& Prop, void* Container) + { + return CastStructValue(&Prop, Container); + } + + template + FORCEINLINE const TStruct* CastStructValue(const FProperty& Prop, const void* Container) + { + return CastStructValue(&Prop, Container); + } +} +#endif \ No newline at end of file diff --git a/Source/FlowDebugger/FlowDebugger.Build.cs b/Source/FlowDebugger/FlowDebugger.Build.cs new file mode 100644 index 000000000..06e489725 --- /dev/null +++ b/Source/FlowDebugger/FlowDebugger.Build.cs @@ -0,0 +1,25 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +using UnrealBuildTool; + +public class FlowDebugger : ModuleRules +{ + public FlowDebugger(ReadOnlyTargetRules target) : base(target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange( + [ + "Flow" + ]); + + PrivateDependencyModuleNames.AddRange( + [ + "Core", + "CoreUObject", + "DeveloperSettings", + "Engine", + "Slate", + "SlateCore", + ]); + } +} \ No newline at end of file diff --git a/Source/FlowDebugger/Private/Debugger/FlowDebuggerSettings.cpp b/Source/FlowDebugger/Private/Debugger/FlowDebuggerSettings.cpp new file mode 100644 index 000000000..899566135 --- /dev/null +++ b/Source/FlowDebugger/Private/Debugger/FlowDebuggerSettings.cpp @@ -0,0 +1,9 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Debugger/FlowDebuggerSettings.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowDebuggerSettings) + +UFlowDebuggerSettings::UFlowDebuggerSettings() +{ +} diff --git a/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp b/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp new file mode 100644 index 000000000..4a731fe1d --- /dev/null +++ b/Source/FlowDebugger/Private/Debugger/FlowDebuggerSubsystem.cpp @@ -0,0 +1,579 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Debugger/FlowDebuggerSubsystem.h" +#include "Debugger/FlowDebuggerSettings.h" + +#include "FlowAsset.h" +#include "FlowSubsystem.h" + +#include "EdGraph/EdGraphNode.h" +#include "EdGraph/EdGraphPin.h" +#include "Engine/Engine.h" +#include "Engine/GameInstance.h" +#include "GameFramework/GameModeBase.h" +#include "GameFramework/WorldSettings.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowDebuggerSubsystem) + +UFlowDebuggerSubsystem::UFlowDebuggerSubsystem() +{ + UFlowSubsystem::OnInstancedTemplateAdded.BindUObject(this, &ThisClass::OnInstancedTemplateAdded); + UFlowSubsystem::OnInstancedTemplateRemoved.BindUObject(this, &ThisClass::OnInstancedTemplateRemoved); +} + +void UFlowDebuggerSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + + FFlowExecutionGate::SetGate(this); + + SetFlowDebuggerState(EFlowDebuggerState::InitialRunning, nullptr); +} + +void UFlowDebuggerSubsystem::Deinitialize() +{ + if (FFlowExecutionGate::GetGate() == this) + { + FFlowExecutionGate::SetGate(nullptr); + } + + SetFlowDebuggerState(EFlowDebuggerState::Invalid, nullptr); + + Super::Deinitialize(); +} + +bool UFlowDebuggerSubsystem::ShouldCreateSubsystem(UObject* Outer) const +{ + // Only create an instance if there is no override implementation defined elsewhere + TArray ChildClasses; + GetDerivedClasses(GetClass(), ChildClasses, false); + if (ChildClasses.Num() > 0) + { + return false; + } + + return true; +} + +void UFlowDebuggerSubsystem::OnInstancedTemplateAdded(UFlowAsset* AssetTemplate) +{ + check(IsValid(AssetTemplate)); + + AssetTemplate->OnPinTriggered.BindUObject(this, &ThisClass::OnPinTriggered); +} + +void UFlowDebuggerSubsystem::OnInstancedTemplateRemoved(UFlowAsset* AssetTemplate) +{ + check(IsValid(AssetTemplate)); + + AssetTemplate->OnPinTriggered.Unbind(); + + OnDebuggerFlowAssetTemplateRemoved.Broadcast(*AssetTemplate); +} + +void UFlowDebuggerSubsystem::OnPinTriggered(UFlowNode* FlowNode, const FName& PinName) +{ + if (FindBreakpoint(FlowNode->NodeGuid, PinName)) + { + MarkAsHit(FlowNode, PinName); + } + + // Node breakpoints waits on any pin triggered + MarkAsHit(FlowNode); +} + +void UFlowDebuggerSubsystem::AddBreakpoint(const FGuid& NodeGuid) +{ + UFlowDebuggerSettings* Settings = GetMutableDefault(); + FNodeBreakpoint& NodeBreakpoint = Settings->NodeBreakpoints.FindOrAdd(NodeGuid); + + NodeBreakpoint.Breakpoint.SetActive(true); + SaveSettings(); +} + +void UFlowDebuggerSubsystem::AddBreakpoint(const FGuid& NodeGuid, const FName& PinName) +{ + UFlowDebuggerSettings* Settings = GetMutableDefault(); + FNodeBreakpoint& NodeBreakpoint = Settings->NodeBreakpoints.FindOrAdd(NodeGuid); + FFlowBreakpoint& PinBreakpoint = NodeBreakpoint.PinBreakpoints.FindOrAdd(PinName); + + PinBreakpoint.SetEnabled(true); + SaveSettings(); +} + +void UFlowDebuggerSubsystem::RemoveAllBreakpoints(const TWeakObjectPtr FlowAsset) +{ + UFlowDebuggerSettings* Settings = GetMutableDefault(); + for (auto& [NodeGuid, Node] : FlowAsset->GetNodes()) + { + if (Settings->NodeBreakpoints.Contains(NodeGuid)) + { + Settings->NodeBreakpoints.Remove(NodeGuid); + } + } + + SaveSettings(); +} + +void UFlowDebuggerSubsystem::RemoveAllBreakpoints(const FGuid& NodeGuid) +{ + UFlowDebuggerSettings* Settings = GetMutableDefault(); + + if (Settings->NodeBreakpoints.Contains(NodeGuid)) + { + Settings->NodeBreakpoints.Remove(NodeGuid); + SaveSettings(); + } +} + +void UFlowDebuggerSubsystem::RemoveNodeBreakpoint(const FGuid& NodeGuid) +{ + UFlowDebuggerSettings* Settings = GetMutableDefault(); + if (FNodeBreakpoint* NodeBreakpoint = Settings->NodeBreakpoints.Find(NodeGuid)) + { + if (NodeBreakpoint->PinBreakpoints.IsEmpty()) + { + // no pin breakpoints here, remove the entire entry + Settings->NodeBreakpoints.Remove(NodeGuid); + } + else + { + // there are pin breakpoints here, only deactivate node breakpoint + NodeBreakpoint->Breakpoint.SetActive(false); + } + + SaveSettings(); + } +} + +void UFlowDebuggerSubsystem::RemovePinBreakpoint(const FGuid& NodeGuid, const FName& PinName) +{ + UFlowDebuggerSettings* Settings = GetMutableDefault(); + if (FNodeBreakpoint* NodeBreakpoint = Settings->NodeBreakpoints.Find(NodeGuid)) + { + if (NodeBreakpoint->PinBreakpoints.Contains(PinName)) + { + NodeBreakpoint->PinBreakpoints.Remove(PinName); + } + + if (!NodeBreakpoint->Breakpoint.IsActive() && NodeBreakpoint->PinBreakpoints.IsEmpty()) + { + // no breakpoints remained, remove the entire entry + Settings->NodeBreakpoints.Remove(NodeGuid); + } + + SaveSettings(); + } +} + +#if WITH_EDITOR +void UFlowDebuggerSubsystem::RemoveObsoletePinBreakpoints(const UEdGraphNode* Node) +{ + UFlowDebuggerSettings* Settings = GetMutableDefault(); + if (FNodeBreakpoint* NodeBreakpoint = Settings->NodeBreakpoints.Find(Node->NodeGuid)) + { + bool bAnythingRemoved = false; + + TSet PinNames; + PinNames.Reserve(Node->Pins.Num()); + for (const UEdGraphPin* Pin : Node->Pins) + { + PinNames.Emplace(Pin->PinName); + } + + TArray PinsToRemove; + PinsToRemove.Reserve(NodeBreakpoint->PinBreakpoints.Num()); + + for (const TPair& PinBreakpoint : NodeBreakpoint->PinBreakpoints) + { + if (!PinNames.Contains(PinBreakpoint.Key)) + { + PinsToRemove.Add(PinBreakpoint.Key); + } + } + + for (const FName& PinName : PinsToRemove) + { + NodeBreakpoint->PinBreakpoints.Remove(PinName); + bAnythingRemoved = true; + } + + if (NodeBreakpoint->IsEmpty()) + { + Settings->NodeBreakpoints.Remove(Node->NodeGuid); + bAnythingRemoved = true; + } + + if (bAnythingRemoved) + { + SaveSettings(); + } + } +} +#endif + +void UFlowDebuggerSubsystem::ToggleBreakpoint(const FGuid& NodeGuid) +{ + if (FindBreakpoint(NodeGuid) == nullptr) + { + AddBreakpoint(NodeGuid); + } + else + { + RemoveNodeBreakpoint(NodeGuid); + } +} + +void UFlowDebuggerSubsystem::ToggleBreakpoint(const FGuid& NodeGuid, const FName& PinName) +{ + if (FindBreakpoint(NodeGuid, PinName) == nullptr) + { + AddBreakpoint(NodeGuid, PinName); + } + else + { + RemovePinBreakpoint(NodeGuid, PinName); + } +} + +FFlowBreakpoint* UFlowDebuggerSubsystem::FindBreakpoint(const FGuid& NodeGuid) +{ + UFlowDebuggerSettings* Settings = GetMutableDefault(); + FNodeBreakpoint* NodeBreakpoint = Settings->NodeBreakpoints.Find(NodeGuid); + if (NodeBreakpoint && NodeBreakpoint->Breakpoint.IsActive()) + { + return &NodeBreakpoint->Breakpoint; + } + + return nullptr; +} + +FFlowBreakpoint* UFlowDebuggerSubsystem::FindBreakpoint(const FGuid& NodeGuid, const FName& PinName) +{ + UFlowDebuggerSettings* Settings = GetMutableDefault(); + FNodeBreakpoint* NodeBreakpoint = Settings->NodeBreakpoints.Find(NodeGuid); + return NodeBreakpoint ? NodeBreakpoint->PinBreakpoints.Find(PinName) : nullptr; +} + +bool UFlowDebuggerSubsystem::HasAnyBreakpoints(const TWeakObjectPtr FlowAsset) +{ + UFlowDebuggerSettings* Settings = GetMutableDefault(); + for (const TPair& Node : FlowAsset->GetNodes()) + { + if (Settings->NodeBreakpoints.Find(Node.Key)) + { + return true; + } + } + + return false; +} + +void UFlowDebuggerSubsystem::SetBreakpointEnabled(const FGuid& NodeGuid, const bool bEnabled) +{ + if (FFlowBreakpoint* NodeBreakpoint = FindBreakpoint(NodeGuid)) + { + NodeBreakpoint->SetEnabled(bEnabled); + SaveSettings(); + } +} + +void UFlowDebuggerSubsystem::SetBreakpointEnabled(const FGuid& NodeGuid, const FName& PinName, const bool bEnabled) +{ + if (FFlowBreakpoint* PinBreakpoint = FindBreakpoint(NodeGuid, PinName)) + { + PinBreakpoint->SetEnabled(bEnabled); + SaveSettings(); + } +} + +void UFlowDebuggerSubsystem::SetAllBreakpointsEnabled(const TWeakObjectPtr FlowAsset, const bool bEnabled) +{ + UFlowDebuggerSettings* Settings = GetMutableDefault(); + for (const TPair& Node : FlowAsset->GetNodes()) + { + if (FNodeBreakpoint* NodeBreakpoint = Settings->NodeBreakpoints.Find(Node.Key)) + { + if (NodeBreakpoint->Breakpoint.IsActive()) + { + NodeBreakpoint->Breakpoint.SetEnabled(bEnabled); + } + + for (auto& [Name, PinBreakpoint] : NodeBreakpoint->PinBreakpoints) + { + PinBreakpoint.SetEnabled(bEnabled); + } + } + } + + SaveSettings(); +} + +bool UFlowDebuggerSubsystem::IsBreakpointEnabled(const FGuid& NodeGuid) +{ + if (const FFlowBreakpoint* PinBreakpoint = FindBreakpoint(NodeGuid)) + { + return PinBreakpoint->IsEnabled(); + } + + return false; +} + +bool UFlowDebuggerSubsystem::IsBreakpointEnabled(const FGuid& NodeGuid, const FName& PinName) +{ + if (const FFlowBreakpoint* PinBreakpoint = FindBreakpoint(NodeGuid, PinName)) + { + return PinBreakpoint->IsEnabled(); + } + + return false; +} + +bool UFlowDebuggerSubsystem::HasAnyBreakpointsEnabled(const TWeakObjectPtr& FlowAsset) +{ + return HasAnyBreakpointsMatching(FlowAsset, true); +} + +bool UFlowDebuggerSubsystem::HasAnyBreakpointsDisabled(const TWeakObjectPtr& FlowAsset) +{ + return HasAnyBreakpointsMatching(FlowAsset, false); +} + +bool UFlowDebuggerSubsystem::HasAnyBreakpointsMatching(const TWeakObjectPtr& FlowAsset, bool bDesiresEnabled) +{ + if (!FlowAsset.IsValid()) + { + return false; + } + + const UFlowDebuggerSettings* Settings = GetDefault(); + if (!Settings) + { + return false; + } + + for (const TPair& NodePair : FlowAsset->GetNodes()) + { + if (const FNodeBreakpoint* NodeBreakpoint = Settings->NodeBreakpoints.Find(NodePair.Key)) + { + // Node-level breakpoint must be active to count (matches original behavior) + if (NodeBreakpoint->Breakpoint.IsActive() && + (NodeBreakpoint->Breakpoint.IsEnabled() == bDesiresEnabled)) + { + return true; + } + + // Pin-level breakpoints + for (const auto& PinPair : NodeBreakpoint->PinBreakpoints) + { + if (PinPair.Value.IsEnabled() == bDesiresEnabled) + { + return true; + } + } + } + } + + return false; +} + +void UFlowDebuggerSubsystem::ClearLastHitBreakpoint() +{ + if (!LastHitNodeGuid.IsValid()) + { + return; + } + + // Pin breakpoint "hit" state lives in the PinBreakpoints map, node breakpoint "hit" lives on NodeBreakpoint.Breakpoint. + if (!LastHitPinName.IsNone()) + { + if (FFlowBreakpoint* PinBreakpoint = FindBreakpoint(LastHitNodeGuid, LastHitPinName)) + { + PinBreakpoint->MarkAsHit(false); + } + } + else + { + if (FFlowBreakpoint* NodeBreakpoint = FindBreakpoint(LastHitNodeGuid)) + { + NodeBreakpoint->MarkAsHit(false); + } + } + + LastHitNodeGuid.Invalidate(); + LastHitPinName = NAME_None; +} + +void UFlowDebuggerSubsystem::MarkAsHit(const UFlowNode* FlowNode) +{ + if (FFlowBreakpoint* NodeBreakpoint = FindBreakpoint(FlowNode->NodeGuid)) + { + if (NodeBreakpoint->IsEnabled()) + { + // Ensure only one breakpoint location is "hit" at a time. + ClearLastHitBreakpoint(); + + NodeBreakpoint->MarkAsHit(true); + + LastHitNodeGuid = FlowNode->NodeGuid; + LastHitPinName = NAME_None; + + OnDebuggerBreakpointHit.Broadcast(FlowNode); + + PauseSession(*FlowNode->GetFlowAsset()); + } + } +} + +void UFlowDebuggerSubsystem::MarkAsHit(const UFlowNode* FlowNode, const FName& PinName) +{ + if (FFlowBreakpoint* PinBreakpoint = FindBreakpoint(FlowNode->NodeGuid, PinName)) + { + if (PinBreakpoint->IsEnabled()) + { + // Ensure only one breakpoint location is "hit" at a time. + ClearLastHitBreakpoint(); + + PinBreakpoint->MarkAsHit(true); + + LastHitNodeGuid = FlowNode->NodeGuid; + LastHitPinName = PinName; + + OnDebuggerBreakpointHit.Broadcast(FlowNode); + + PauseSession(*FlowNode->GetFlowAsset()); + } + } +} + +void UFlowDebuggerSubsystem::PauseSession(UFlowAsset& FlowAssetInstance) +{ + SetFlowDebuggerState(EFlowDebuggerState::Paused, &FlowAssetInstance); +} + +void UFlowDebuggerSubsystem::ResumeSession(UFlowAsset& FlowAssetInstance) +{ + SetFlowDebuggerState(EFlowDebuggerState::Resumed, &FlowAssetInstance); +} + +void UFlowDebuggerSubsystem::StopSession() +{ + SetFlowDebuggerState(EFlowDebuggerState::Invalid, nullptr); +} + +void UFlowDebuggerSubsystem::ClearHitBreakpoints() +{ + UFlowDebuggerSettings* Settings = GetMutableDefault(); + + for (TPair& NodeBreakpoint : Settings->NodeBreakpoints) + { + NodeBreakpoint.Value.Breakpoint.MarkAsHit(false); + + for (TPair& PinBreakpoint : NodeBreakpoint.Value.PinBreakpoints) + { + PinBreakpoint.Value.MarkAsHit(false); + } + } + + ClearLastHitBreakpoint(); +} + +bool UFlowDebuggerSubsystem::IsBreakpointHit(const FGuid& NodeGuid) +{ + if (const FFlowBreakpoint* NodeBreakpoint = FindBreakpoint(NodeGuid)) + { + return NodeBreakpoint->IsHit(); + } + + return false; +} + +bool UFlowDebuggerSubsystem::IsBreakpointHit(const FGuid& NodeGuid, const FName& PinName) +{ + if (const FFlowBreakpoint* PinBreakpoint = FindBreakpoint(NodeGuid, PinName)) + { + return PinBreakpoint->IsHit(); + } + + return false; +} + +void UFlowDebuggerSubsystem::SaveSettings() +{ + UFlowDebuggerSettings* Settings = GetMutableDefault(); + Settings->SaveConfig(); +} + +void UFlowDebuggerSubsystem::SetFlowDebuggerState(EFlowDebuggerState NextState, UFlowAsset* FlowAssetInstance) +{ + if (FlowDebuggerState == NextState) + { + return; + } + + const EFlowDebuggerState PrevState = FlowDebuggerState; + FlowDebuggerState = NextState; + + ManageGameModePaused(PrevState, NextState, FlowAssetInstance); + + // OnFlowDebuggerStateChanged MUST be the final operation in SetFlowDebuggerState + // as it could potentially cause a new FlowDebuggerState entered + { + OnFlowDebuggerStateChanged(PrevState, NextState, FlowAssetInstance); + return; + } +} + +void UFlowDebuggerSubsystem::ManageGameModePaused(EFlowDebuggerState PrevState, EFlowDebuggerState NextState, UFlowAsset* FlowAssetInstance) +{ + if (!IsValid(FlowAssetInstance)) + { + return; + } + + const UWorld* World = FlowAssetInstance->GetWorld(); + AGameModeBase* GameMode = World->GetAuthGameMode(); + if (!IsValid(GameMode)) + { + // No game mode on non-server instances + return; + } + + using namespace EFlowDebuggerState_Classifiers; + + const bool bIsPauseGameModeStatePrev = IsPausedGameState(PrevState); + const bool bIsPauseGameModeStateNext = IsPausedGameState(NextState); + + if (bIsPauseGameModeStatePrev == bIsPauseGameModeStateNext) + { + return; + } + + // Gather some pointers + const UGameInstance* GameInstance = World->GetGameInstance(); + APlayerController* FirstLocalPlayerController = nullptr; + if (IsValid(GameInstance)) + { + FirstLocalPlayerController = GameInstance->GetFirstLocalPlayerController(); + } + + // Change the GameMode pause state + if (bIsPauseGameModeStateNext) + { + if (FirstLocalPlayerController) + { + GameMode->SetPause(FirstLocalPlayerController); + + if (AWorldSettings* WorldSettings = World->GetWorldSettings()) + { + WorldSettings->ForceNetUpdate(); + } + } + } + else + { + // Intentionally do NOT clear hit flags here. The editor-specific resume path will clear the last-hit + // breakpoint safely (without racing against immediate breakpoint hits during flush). + (void)GameMode->ClearPause(); + } +} diff --git a/Source/FlowDebugger/Private/FlowDebuggerModule.cpp b/Source/FlowDebugger/Private/FlowDebuggerModule.cpp new file mode 100644 index 000000000..d0ce92e97 --- /dev/null +++ b/Source/FlowDebugger/Private/FlowDebuggerModule.cpp @@ -0,0 +1,19 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "FlowDebuggerModule.h" + +#include "Modules/ModuleManager.h" + +#define LOCTEXT_NAMESPACE "FlowDebuggerModule" + +void FFlowDebuggerModule::StartupModule() +{ +} + +void FFlowDebuggerModule::ShutdownModule() +{ +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FFlowDebuggerModule, FlowDebugger) diff --git a/Source/FlowDebugger/Public/Debugger/FlowDebuggerSettings.h b/Source/FlowDebugger/Public/Debugger/FlowDebuggerSettings.h new file mode 100644 index 000000000..514badd40 --- /dev/null +++ b/Source/FlowDebugger/Public/Debugger/FlowDebuggerSettings.h @@ -0,0 +1,22 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Engine/DeveloperSettings.h" + +#include "FlowDebuggerTypes.h" +#include "FlowDebuggerSettings.generated.h" + +/** + * + */ +UCLASS(Config = EditorPerProjectUserSettings, meta = (DisplayName = "Flow Debugger")) +class FLOWDEBUGGER_API UFlowDebuggerSettings : public UDeveloperSettings +{ + GENERATED_BODY() + +public: + UFlowDebuggerSettings(); + + UPROPERTY(config) + TMap NodeBreakpoints; +}; diff --git a/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h b/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h new file mode 100644 index 000000000..89346303d --- /dev/null +++ b/Source/FlowDebugger/Public/Debugger/FlowDebuggerSubsystem.h @@ -0,0 +1,150 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Subsystems/EngineSubsystem.h" + +#include "Debugger/FlowDebuggerTypes.h" +#include "Interfaces/FlowExecutionGate.h" +#include "Types/FlowEnumUtils.h" + +#include "FlowDebuggerSubsystem.generated.h" + +class UEdGraphNode; + +class UFlowAsset; +class UFlowNode; + +UENUM() +enum class EFlowDebuggerState +{ + // Initialized, running, but never halted + InitialRunning, + + // Running after being pausing + Resumed, + + // Currently paused at a breakpoint + Paused, + + Max UMETA(Hidden), + Invalid = -1 UMETA(Hidden), + Min = 0 UMETA(Hidden), + + // Subranges for classifier checks + PausedGameFirst = Paused UMETA(Hidden), + PausedGameLast = Paused UMETA(Hidden), + + FlushDeferredTriggersFirst = Resumed UMETA(Hidden), + FlushDeferredTriggersLast = Resumed UMETA(Hidden), +}; +FLOW_ENUM_RANGE_VALUES(EFlowDebuggerState); + +namespace EFlowDebuggerState_Classifiers +{ + FORCEINLINE bool IsPausedGameState(EFlowDebuggerState State) { return FLOW_IS_ENUM_IN_SUBRANGE(State, EFlowDebuggerState::PausedGame); } + FORCEINLINE bool IsFlushDeferredTriggersState(EFlowDebuggerState State) { return FLOW_IS_ENUM_IN_SUBRANGE(State, EFlowDebuggerState::FlushDeferredTriggers); } +} + +DECLARE_MULTICAST_DELEGATE_OneParam(FFlowAssetDebuggerEvent, const UFlowAsset& /*FlowAsset*/); +DECLARE_MULTICAST_DELEGATE_OneParam(FFlowAssetDebuggerBreakpointHitEvent, const UFlowNode* /*FlowNode*/); + +/** +* Persistent subsystem supporting Flow Graph debugging. +* It might be utilized to use cook-specific graph debugger. +*/ +UCLASS() +class FLOWDEBUGGER_API UFlowDebuggerSubsystem : public UEngineSubsystem, public IFlowExecutionGate +{ + GENERATED_BODY() + +public: + UFlowDebuggerSubsystem(); + + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + + virtual bool ShouldCreateSubsystem(UObject* Outer) const override; + +protected: + virtual void OnInstancedTemplateAdded(UFlowAsset* AssetTemplate); + virtual void OnInstancedTemplateRemoved(UFlowAsset* AssetTemplate); + + virtual void OnPinTriggered(UFlowNode* FlowNode, const FName& PinName); + +public: + // IFlowExecutionGate + virtual bool IsFlowExecutionHalted() const override { return EFlowDebuggerState_Classifiers::IsPausedGameState(FlowDebuggerState); } + // -- + + virtual void AddBreakpoint(const FGuid& NodeGuid); + virtual void AddBreakpoint(const FGuid& NodeGuid, const FName& PinName); + + virtual void RemoveAllBreakpoints(const TWeakObjectPtr FlowAsset); + virtual void RemoveAllBreakpoints(const FGuid& NodeGuid); + virtual void RemoveNodeBreakpoint(const FGuid& NodeGuid); + virtual void RemovePinBreakpoint(const FGuid& NodeGuid, const FName& PinName); + +#if WITH_EDITOR + /* Removes obsolete pin breakpoints for provided. Pin list can be changed during node reconstruction. */ + virtual void RemoveObsoletePinBreakpoints(const UEdGraphNode* Node); +#endif + + virtual void ToggleBreakpoint(const FGuid& NodeGuid); + virtual void ToggleBreakpoint(const FGuid& NodeGuid, const FName& PinName); + + virtual FFlowBreakpoint* FindBreakpoint(const FGuid& NodeGuid); + virtual FFlowBreakpoint* FindBreakpoint(const FGuid& NodeGuid, const FName& PinName); + static bool HasAnyBreakpoints(const TWeakObjectPtr FlowAsset); + + virtual void SetBreakpointEnabled(const FGuid& NodeGuid, bool bEnabled); + virtual void SetBreakpointEnabled(const FGuid& NodeGuid, const FName& PinName, bool bEnabled); + virtual void SetAllBreakpointsEnabled(const TWeakObjectPtr FlowAsset, bool bEnabled); + + virtual bool IsBreakpointEnabled(const FGuid& NodeGuid); + virtual bool IsBreakpointEnabled(const FGuid& NodeGuid, const FName& PinName); + static bool HasAnyBreakpointsEnabled(const TWeakObjectPtr& FlowAsset); + static bool HasAnyBreakpointsDisabled(const TWeakObjectPtr& FlowAsset); + static bool HasAnyBreakpointsMatching(const TWeakObjectPtr& FlowAsset, bool bDesiresEnabled); + +protected: + virtual void MarkAsHit(const UFlowNode* FlowNode); + virtual void MarkAsHit(const UFlowNode* FlowNode, const FName& PinName); + + virtual void PauseSession(UFlowAsset& FlowAssetInstance); + virtual void ResumeSession(UFlowAsset& FlowAssetInstance); + virtual void StopSession(); + + /* Clears the "currently hit" breakpoint only (node or pin). + * This avoids races where blanket-clearing all hit flags can erase a newly-hit breakpoint during resume/flush. */ + void ClearLastHitBreakpoint(); + + /* Clears hit state for all breakpoints. Prefer ClearLastHitBreakpoint() for resume/step logic. */ + virtual void ClearHitBreakpoints(); + +private: + void SetFlowDebuggerState(EFlowDebuggerState NextState, UFlowAsset* FlowAssetInstance); + void ManageGameModePaused(EFlowDebuggerState PrevState, EFlowDebuggerState NextState, UFlowAsset* FlowAssetInstance); + +protected: + virtual void OnFlowDebuggerStateChanged(EFlowDebuggerState PrevState, EFlowDebuggerState NextState, UFlowAsset* FlowAssetInstance) {} + +public: + virtual bool IsBreakpointHit(const FGuid& NodeGuid); + virtual bool IsBreakpointHit(const FGuid& NodeGuid, const FName& PinName); + + // Delegates for debugger events (broadcast when pausing, resuming, or hitting breakpoints) + FFlowAssetDebuggerEvent OnDebuggerPaused; + FFlowAssetDebuggerEvent OnDebuggerResumed; + FFlowAssetDebuggerBreakpointHitEvent OnDebuggerBreakpointHit; + FFlowAssetDebuggerEvent OnDebuggerFlowAssetTemplateRemoved; + +protected: + EFlowDebuggerState FlowDebuggerState = EFlowDebuggerState::Invalid; + + // Track the single breakpoint location that is currently "hit" (node or pin). + FGuid LastHitNodeGuid; + FName LastHitPinName; + + /* Saves any modifications made to breakpoints. */ + virtual void SaveSettings(); +}; \ No newline at end of file diff --git a/Source/FlowDebugger/Public/Debugger/FlowDebuggerTypes.h b/Source/FlowDebugger/Public/Debugger/FlowDebuggerTypes.h new file mode 100644 index 000000000..0e9ae043e --- /dev/null +++ b/Source/FlowDebugger/Public/Debugger/FlowDebuggerTypes.h @@ -0,0 +1,72 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "FlowDebuggerTypes.generated.h" + +USTRUCT() +struct FLOWDEBUGGER_API FFlowBreakpoint +{ + GENERATED_BODY() + +protected: + /* Applies only to node breakpoint. + * Pin breakpoints are deactivated by removing element from FNodeBreakpoint::PinBreakpoints. */ + UPROPERTY() + bool bActive; + + UPROPERTY() + uint8 bEnabled : 1; + + UPROPERTY(Transient) + uint8 bHit : 1; + +public: + FFlowBreakpoint() + : bActive(false) + , bEnabled(false) + , bHit(false) + { + }; + + void SetActive(const bool bNowActive) + { + bActive = bNowActive; + bEnabled = bNowActive; + } + + void SetEnabled(const bool bNowEnabled) + { + bEnabled = bNowEnabled; + } + + void MarkAsHit(const bool bNowHit) + { + bHit = bNowHit; + } + + bool IsActive() const { return bActive; } + bool IsEnabled() const { return bEnabled; } + bool IsHit() const { return bHit; } +}; + +USTRUCT() +struct FLOWDEBUGGER_API FNodeBreakpoint +{ + GENERATED_BODY() + +public: + UPROPERTY() + FFlowBreakpoint Breakpoint; + + UPROPERTY() + TMap PinBreakpoints; + + FNodeBreakpoint() + { + }; + + bool IsEmpty() const + { + return !Breakpoint.IsActive() && PinBreakpoints.IsEmpty(); + } +}; diff --git a/Source/FlowDebugger/Public/FlowDebuggerModule.h b/Source/FlowDebugger/Public/FlowDebuggerModule.h new file mode 100644 index 000000000..4a489b04a --- /dev/null +++ b/Source/FlowDebugger/Public/FlowDebuggerModule.h @@ -0,0 +1,11 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Modules/ModuleInterface.h" + +class FLOWDEBUGGER_API FFlowDebuggerModule : public IModuleInterface +{ +public: + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/Source/FlowEditor/FlowEditor.Build.cs b/Source/FlowEditor/FlowEditor.Build.cs index 9623bb4ab..bf270ec93 100644 --- a/Source/FlowEditor/FlowEditor.Build.cs +++ b/Source/FlowEditor/FlowEditor.Build.cs @@ -1,50 +1,62 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - using UnrealBuildTool; public class FlowEditor : ModuleRules { - public FlowEditor(ReadOnlyTargetRules Target) : base(Target) - { - PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + public FlowEditor(ReadOnlyTargetRules target) : base(target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; - PublicDependencyModuleNames.AddRange(new[] - { - "Flow" - }); + PublicDependencyModuleNames.AddRange( + [ + "AssetSearch", + "EditorSubsystem", + "Flow", + "FlowDebugger", + "MessageLog" + ]); - PrivateDependencyModuleNames.AddRange(new[] - { - "ApplicationCore", - "AssetSearch", - "AssetTools", - "BlueprintGraph", - "ClassViewer", - "ContentBrowser", - "Core", - "CoreUObject", - "DetailCustomizations", - "DeveloperSettings", - "EditorFramework", - "EditorStyle", - "Engine", - "GraphEditor", - "InputCore", - "Json", - "JsonUtilities", - "KismetWidgets", - "LevelEditor", - "MovieScene", - "MovieSceneTracks", - "MovieSceneTools", - "Projects", - "PropertyEditor", - "RenderCore", - "Sequencer", - "Slate", - "SlateCore", - "ToolMenus", - "UnrealEd" - }); - } -} + PrivateDependencyModuleNames.AddRange( + [ + "AIModule", // For BlueprintNodeHelpers::DescribeProperty (could be copy/pasted out to remove editor-only dependency) + "ApplicationCore", + "AssetDefinition", + "AssetTools", + "BlueprintGraph", + "ClassViewer", + "ContentBrowser", + "Core", + "CoreUObject", + "DetailCustomizations", + "DeveloperSettings", + "EditorFramework", + "EditorScriptingUtilities", + "EditorStyle", + "Engine", + "EngineAssetDefinitions", + "GraphEditor", + "GameplayTags", + "InputCore", + "Json", + "JsonUtilities", + "Kismet", + "KismetWidgets", + "LevelEditor", + "LevelSequence", + "MovieScene", + "MovieSceneTools", + "MovieSceneTracks", + "Projects", + "PropertyEditor", + "PropertyPath", + "RenderCore", + "Sequencer", + "SequencerCore", + "Slate", + "SlateCore", + "SourceControl", + "ToolMenus", + "UnrealEd" + ]); + } +} \ No newline at end of file diff --git a/Source/FlowEditor/Private/Asset/AssetDefinition_FlowAsset.cpp b/Source/FlowEditor/Private/Asset/AssetDefinition_FlowAsset.cpp new file mode 100644 index 000000000..276c61faa --- /dev/null +++ b/Source/FlowEditor/Private/Asset/AssetDefinition_FlowAsset.cpp @@ -0,0 +1,77 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/AssetDefinition_FlowAsset.h" +#include "Asset/SFlowDiff.h" +#include "FlowEditorModule.h" +#include "Graph/FlowGraphSettings.h" + +#include "FlowAsset.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(AssetDefinition_FlowAsset) + +#define LOCTEXT_NAMESPACE "AssetDefinition_FlowAsset" + +FText UAssetDefinition_FlowAsset::GetAssetDisplayName() const +{ + return LOCTEXT("AssetTypeActions_FlowAsset", "Flow Asset"); +} + +FLinearColor UAssetDefinition_FlowAsset::GetAssetColor() const +{ + return FColor(255, 196, 128); +} + +TSoftClassPtr UAssetDefinition_FlowAsset::GetAssetClass() const +{ + return UFlowAsset::StaticClass(); +} + +TConstArrayView UAssetDefinition_FlowAsset::GetAssetCategories() const +{ + if (GetDefault()->bExposeFlowAssetCreation) + { + static const auto Categories = {FFlowAssetCategoryPaths::Flow}; + return Categories; + } + + return {}; +} + +FAssetSupportResponse UAssetDefinition_FlowAsset::CanLocalize(const FAssetData& InAsset) const +{ + return FAssetSupportResponse::Supported(); +} + +EAssetCommandResult UAssetDefinition_FlowAsset::OpenAssets(const FAssetOpenArgs& OpenArgs) const +{ + for (UFlowAsset* FlowAsset : OpenArgs.LoadObjects()) + { + const FFlowEditorModule* FlowModule = &FModuleManager::LoadModuleChecked("FlowEditor"); + FlowModule->CreateFlowAssetEditor(OpenArgs.GetToolkitMode(), OpenArgs.ToolkitHost, FlowAsset); + } + + return EAssetCommandResult::Handled; +} + +EAssetCommandResult UAssetDefinition_FlowAsset::PerformAssetDiff(const FAssetDiffArgs& DiffArgs) const +{ + if (DiffArgs.OldAsset == nullptr && DiffArgs.NewAsset == nullptr) + { + return EAssetCommandResult::Unhandled; + } + + const UFlowAsset* OldFlow = Cast(DiffArgs.OldAsset); + const UFlowAsset* NewFlow = Cast(DiffArgs.NewAsset); + + // sometimes we're comparing different revisions of one single asset (other + // times we're comparing two completely separate assets altogether) + const bool bIsSingleAsset = !IsValid(OldFlow) || !IsValid(NewFlow) || (OldFlow->GetName() == NewFlow->GetName()); + + static const FText BasicWindowTitle = LOCTEXT("FlowAssetDiff", "FlowAsset Diff"); + const FText WindowTitle = !bIsSingleAsset ? BasicWindowTitle : FText::Format(LOCTEXT("FlowAsset Diff", "{0} - FlowAsset Diff"), FText::FromString(IsValid(NewFlow) ? NewFlow->GetName() : OldFlow->GetName())); + + SFlowDiff::CreateDiffWindow(WindowTitle, OldFlow, NewFlow, DiffArgs.OldRevision, DiffArgs.NewRevision); + return EAssetCommandResult::Handled; +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Asset/AssetDefinition_FlowAssetParams.cpp b/Source/FlowEditor/Private/Asset/AssetDefinition_FlowAssetParams.cpp new file mode 100644 index 000000000..0f726f34d --- /dev/null +++ b/Source/FlowEditor/Private/Asset/AssetDefinition_FlowAssetParams.cpp @@ -0,0 +1,99 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/AssetDefinition_FlowAssetParams.h" +#include "Asset/FlowAssetParams.h" +#include "Asset/FlowAssetParamsUtils.h" +#include "FlowEditorLogChannels.h" +#include "FlowEditorModule.h" +#include "ContentBrowserMenuContexts.h" +#include "ToolMenus.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(AssetDefinition_FlowAssetParams) + +#define LOCTEXT_NAMESPACE "AssetDefinition_FlowAssetParams" + +FText UAssetDefinition_FlowAssetParams::GetAssetDisplayName() const +{ + return LOCTEXT("GetAssetDisplayName", "Flow Asset Params"); +} + +FLinearColor UAssetDefinition_FlowAssetParams::GetAssetColor() const +{ + return FLinearColor(255, 196, 128); +} + +TSoftClassPtr UAssetDefinition_FlowAssetParams::GetAssetClass() const +{ + return UFlowAssetParams::StaticClass(); +} + +TConstArrayView UAssetDefinition_FlowAssetParams::GetAssetCategories() const +{ + static const auto Categories = {FFlowAssetCategoryPaths::Flow}; + return Categories; +} + +FAssetSupportResponse UAssetDefinition_FlowAssetParams::CanLocalize(const FAssetData& InAsset) const +{ + return FAssetSupportResponse::Supported(); +} + +namespace MenuExtension_FlowAssetParams +{ + static void ExecuteCreateChildParams(const FToolMenuContext& InContext) + { + const UContentBrowserAssetContextMenuContext* Context = UContentBrowserAssetContextMenuContext::FindContextWithAssets(InContext); + if (!Context) + { + UE_LOG(LogFlowEditor, Warning, TEXT("No valid context for Create Child Params action")); + return; + } + + const TArray& SelectedParams = Context->LoadSelectedObjects(); + if (SelectedParams.Num() != 1) + { + UE_LOG(LogFlowEditor, Warning, TEXT("Create Child Params requires exactly one selected Flow Asset Params")); + return; + } + + UFlowAssetParams* ParentParams = SelectedParams[0]; + if (!IsValid(ParentParams)) + { + UE_LOG(LogFlowEditor, Error, TEXT("Invalid Flow Asset Params selected for Create Child Params")); + return; + } + + constexpr bool bShowDialogs = true; + FFlowAssetParamsUtils::CreateChildParamsAsset(*ParentParams, bShowDialogs); + } + + static void RegisterContextMenu() + { + UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateLambda([]() + { + FToolMenuOwnerScoped OwnerScoped(UE_MODULE_NAME); + UToolMenu* Menu = UE::ContentBrowser::ExtendToolMenu_AssetContextMenu(UFlowAssetParams::StaticClass()); + + FToolMenuSection& Section = Menu->FindOrAddSection("GetAssetActions"); + Section.AddDynamicEntry("Flow Asset Params Commands", FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection) + { + const TAttribute Label = LOCTEXT("FlowAssetParams_CreateChildParams", "Create Child Params"); + const TAttribute ToolTip = LOCTEXT("FlowAssetParams_CreateChildParamsTooltip", "Creates a new Flow Asset Params inheriting from the selected params."); + const FSlateIcon Icon = FSlateIcon(); + + FToolUIAction UIAction; + UIAction.ExecuteAction = FToolMenuExecuteAction::CreateStatic(&ExecuteCreateChildParams); + UIAction.CanExecuteAction = FToolMenuCanExecuteAction::CreateLambda([](const FToolMenuContext& InContext) + { + const UContentBrowserAssetContextMenuContext* Context = UContentBrowserAssetContextMenuContext::FindContextWithAssets(InContext); + return Context && Context->SelectedAssets.Num() == 1; + }); + InSection.AddMenuEntry("FlowAssetParams_CreateChildParams", Label, ToolTip, Icon, UIAction); + })); + })); + } + + static FDelayedAutoRegisterHelper DelayedAutoRegister(EDelayedRegisterRunPhase::EndOfEngineInit, &RegisterContextMenu); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Asset/AssetTypeActions_FlowAsset.cpp b/Source/FlowEditor/Private/Asset/AssetTypeActions_FlowAsset.cpp deleted file mode 100644 index 576cfabef..000000000 --- a/Source/FlowEditor/Private/Asset/AssetTypeActions_FlowAsset.cpp +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - -#include "Asset/AssetTypeActions_FlowAsset.h" -#include "FlowEditorModule.h" -#include "Graph/FlowGraphSettings.h" - -#include "FlowAsset.h" - -#include "Toolkits/IToolkit.h" - -#define LOCTEXT_NAMESPACE "AssetTypeActions_FlowAsset" - -FText FAssetTypeActions_FlowAsset::GetName() const -{ - return LOCTEXT("AssetTypeActions_FlowAsset", "Flow Asset"); -} - -uint32 FAssetTypeActions_FlowAsset::GetCategories() -{ - return UFlowGraphSettings::Get()->bExposeFlowAssetCreation ? FFlowEditorModule::FlowAssetCategory : 0; -} - -UClass* FAssetTypeActions_FlowAsset::GetSupportedClass() const -{ - return UFlowAsset::StaticClass(); -} - -void FAssetTypeActions_FlowAsset::OpenAssetEditor(const TArray& InObjects, TSharedPtr EditWithinLevelEditor) -{ - const EToolkitMode::Type Mode = EditWithinLevelEditor.IsValid() ? EToolkitMode::WorldCentric : EToolkitMode::Standalone; - - for (auto ObjIt = InObjects.CreateConstIterator(); ObjIt; ++ObjIt) - { - if (UFlowAsset* FlowAsset = Cast(*ObjIt)) - { - FFlowEditorModule* FlowModule = &FModuleManager::LoadModuleChecked("FlowEditor"); - FlowModule->CreateFlowAssetEditor(Mode, EditWithinLevelEditor, FlowAsset); - } - } -} - -#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Asset/FlowAssetDetails.cpp b/Source/FlowEditor/Private/Asset/FlowAssetDetails.cpp deleted file mode 100644 index ef202ebaa..000000000 --- a/Source/FlowEditor/Private/Asset/FlowAssetDetails.cpp +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - -#include "FlowAssetDetails.h" -#include "FlowAsset.h" -#include "Nodes/Route/FlowNode_SubGraph.h" - -#include "DetailLayoutBuilder.h" -#include "PropertyCustomizationHelpers.h" -#include "PropertyEditing.h" -#include "Widgets/Input/SEditableTextBox.h" - -#define LOCTEXT_NAMESPACE "FlowAssetDetails" - -void FFlowAssetDetails::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) -{ - IDetailCategoryBuilder& FlowAssetCategory = DetailBuilder.EditCategory("SubGraph", LOCTEXT("SubGraphCategory", "Sub Graph")); - - TArray> ArrayPropertyHandles; - ArrayPropertyHandles.Add(DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UFlowAsset, CustomInputs))); - ArrayPropertyHandles.Add(DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UFlowAsset, CustomOutputs))); - for (const TSharedPtr& PropertyHandle : ArrayPropertyHandles) - { - if (PropertyHandle.IsValid() && PropertyHandle->AsArray().IsValid()) - { - const TSharedRef ArrayBuilder = MakeShareable(new FDetailArrayBuilder(PropertyHandle.ToSharedRef())); - ArrayBuilder->OnGenerateArrayElementWidget(FOnGenerateArrayElementWidget::CreateSP(this, &FFlowAssetDetails::GenerateCustomPinArray)); - - FlowAssetCategory.AddCustomBuilder(ArrayBuilder); - } - } -} - -void FFlowAssetDetails::GenerateCustomPinArray(TSharedRef PropertyHandle, int32 ArrayIndex, IDetailChildrenBuilder& ChildrenBuilder) -{ - IDetailPropertyRow& PropertyRow = ChildrenBuilder.AddProperty(PropertyHandle); - PropertyRow.ShowPropertyButtons(true); - PropertyRow.ShouldAutoExpand(true); - - PropertyRow.CustomWidget(false) - .ValueContent() - [ - SNew(SEditableTextBox) - .Text(this, &FFlowAssetDetails::GetCustomPinText, PropertyHandle) - .OnTextCommitted_Static(&FFlowAssetDetails::OnCustomPinTextCommitted, PropertyHandle) - .OnVerifyTextChanged_Static(&FFlowAssetDetails::VerifyNewCustomPinText) - ]; -} - -FText FFlowAssetDetails::GetCustomPinText(TSharedRef PropertyHandle) const -{ - FText PropertyValue; - const FPropertyAccess::Result GetValueResult = PropertyHandle->GetValueAsDisplayText(PropertyValue); - ensure(GetValueResult == FPropertyAccess::Success); - return PropertyValue; -} - -void FFlowAssetDetails::OnCustomPinTextCommitted(const FText& InText, ETextCommit::Type InCommitType, TSharedRef PropertyHandle) -{ - const FPropertyAccess::Result SetValueResult = PropertyHandle->SetValueFromFormattedString(InText.ToString()); - ensure(SetValueResult == FPropertyAccess::Success); -} - -bool FFlowAssetDetails::VerifyNewCustomPinText(const FText& InNewText, FText& OutErrorMessage) -{ - const FName NewString = *InNewText.ToString(); - - if (NewString == UFlowNode_SubGraph::StartPin.PinName || NewString == UFlowNode_SubGraph::FinishPin.PinName) - { - OutErrorMessage = LOCTEXT("VerifyTextFailed", "This is a standard pin name of Sub Graph node!"); - return false; - } - - return true; -} - -#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Asset/FlowAssetEditor.cpp b/Source/FlowEditor/Private/Asset/FlowAssetEditor.cpp index 7b4e90d91..ed2881f1d 100644 --- a/Source/FlowEditor/Private/Asset/FlowAssetEditor.cpp +++ b/Source/FlowEditor/Private/Asset/FlowAssetEditor.cpp @@ -2,44 +2,46 @@ #include "Asset/FlowAssetEditor.h" -#include "Asset/FlowAssetToolbar.h" -#include "Asset/FlowDebugger.h" #include "FlowEditorCommands.h" -#include "Graph/FlowGraph.h" -#include "Graph/FlowGraphEditorSettings.h" +#include "FlowEditorLogChannels.h" + +#include "Asset/FlowAssetEditorContext.h" +#include "Asset/FlowAssetToolbar.h" +#include "Asset/FlowMessageLogListing.h" +#include "Graph/FlowGraphEditor.h" #include "Graph/FlowGraphSchema.h" -#include "Graph/FlowGraphSchema_Actions.h" -#include "Graph/Nodes/FlowGraphNode.h" #include "Graph/Widgets/SFlowPalette.h" #include "FlowAsset.h" -#include "Nodes/FlowNode.h" -#include "Nodes/Route/FlowNode_SubGraph.h" -#include "EdGraphUtilities.h" #include "EdGraph/EdGraphNode.h" #include "Editor.h" -#include "EditorStyleSet.h" -#include "Framework/Commands/GenericCommands.h" +#include "EditorClassUtils.h" #include "GraphEditor.h" -#include "GraphEditorActions.h" -#include "HAL/PlatformApplicationMisc.h" #include "IDetailsView.h" -#include "Kismet2/BlueprintEditorUtils.h" +#include "IMessageLogListing.h" #include "Kismet2/DebuggerCommands.h" -#include "LevelEditor.h" +#include "MessageLogModule.h" +#include "Misc/UObjectToken.h" #include "Modules/ModuleManager.h" #include "PropertyEditorModule.h" -#include "ScopedTransaction.h" -#include "SNodePanel.h" #include "ToolMenus.h" #include "Widgets/Docking/SDockTab.h" +#if ENABLE_SEARCH_IN_ASSET_EDITOR +#include "Source/Private/Widgets/SSearchBrowser.h" +#else +#include "Find/FindInFlow.h" +#endif + #define LOCTEXT_NAMESPACE "FlowAssetEditor" const FName FFlowAssetEditor::DetailsTab(TEXT("Details")); const FName FFlowAssetEditor::GraphTab(TEXT("Graph")); const FName FFlowAssetEditor::PaletteTab(TEXT("Palette")); +const FName FFlowAssetEditor::RuntimeLogTab(TEXT("RuntimeLog")); +const FName FFlowAssetEditor::SearchTab(TEXT("Search")); +const FName FFlowAssetEditor::ValidationLogTab(TEXT("ValidationLog")); FFlowAssetEditor::FFlowAssetEditor() : FlowAsset(nullptr) @@ -69,16 +71,12 @@ void FFlowAssetEditor::PostRedo(bool bSuccess) void FFlowAssetEditor::HandleUndoTransaction() { SetUISelectionState(NAME_None); - FocusedGraphEditor->NotifyGraphChanged(); + GraphEditor->NotifyGraphChanged(); FSlateApplication::Get().DismissAllMenus(); } void FFlowAssetEditor::NotifyPostChange(const FPropertyChangedEvent& PropertyChangedEvent, FProperty* PropertyThatChanged) { - if (PropertyChangedEvent.ChangeType != EPropertyChangeType::Interactive) - { - FocusedGraphEditor->NotifyGraphChanged(); - } } FName FFlowAssetEditor::GetToolkitFName() const @@ -108,29 +106,115 @@ void FFlowAssetEditor::RegisterTabSpawners(const TSharedRef& FAssetEditorToolkit::RegisterTabSpawners(InTabManager); - InTabManager->RegisterTabSpawner(GraphTab, FOnSpawnTab::CreateSP(this, &FFlowAssetEditor::SpawnTab_GraphCanvas)) - .SetDisplayName(LOCTEXT("GraphTab", "Viewport")) - .SetGroup(WorkspaceMenuCategoryRef) - .SetIcon(FSlateIcon(FEditorStyle::GetStyleSetName(), "GraphEditor.EventGraph_16x")); - InTabManager->RegisterTabSpawner(DetailsTab, FOnSpawnTab::CreateSP(this, &FFlowAssetEditor::SpawnTab_Details)) - .SetDisplayName(LOCTEXT("DetailsTab", "Details")) - .SetGroup(WorkspaceMenuCategoryRef) - .SetIcon(FSlateIcon(FEditorStyle::GetStyleSetName(), "LevelEditor.Tabs.Details")); + .SetDisplayName(LOCTEXT("DetailsTab", "Details")) + .SetGroup(WorkspaceMenuCategoryRef) + .SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.Tabs.Details")); + + InTabManager->RegisterTabSpawner(GraphTab, FOnSpawnTab::CreateSP(this, &FFlowAssetEditor::SpawnTab_Graph)) + .SetDisplayName(LOCTEXT("GraphTab", "Graph")) + .SetGroup(WorkspaceMenuCategoryRef) + .SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "GraphEditor.EventGraph_16x")); InTabManager->RegisterTabSpawner(PaletteTab, FOnSpawnTab::CreateSP(this, &FFlowAssetEditor::SpawnTab_Palette)) - .SetDisplayName(LOCTEXT("PaletteTab", "Palette")) - .SetGroup(WorkspaceMenuCategoryRef) - .SetIcon(FSlateIcon(FEditorStyle::GetStyleSetName(), "Kismet.Tabs.Palette")); + .SetDisplayName(LOCTEXT("PaletteTab", "Palette")) + .SetGroup(WorkspaceMenuCategoryRef) + .SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "Kismet.Tabs.Palette")); + + InTabManager->RegisterTabSpawner(RuntimeLogTab, FOnSpawnTab::CreateSP(this, &FFlowAssetEditor::SpawnTab_RuntimeLog)) + .SetDisplayName(LOCTEXT("RuntimeLog", "Runtime Log")) + .SetGroup(WorkspaceMenuCategoryRef) + .SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "Kismet.Tabs.CompilerResults")); + + InTabManager->RegisterTabSpawner(SearchTab, FOnSpawnTab::CreateSP(this, &FFlowAssetEditor::SpawnTab_Search)) + .SetDisplayName(LOCTEXT("SearchTab", "Search")) + .SetGroup(WorkspaceMenuCategoryRef) + .SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "Kismet.Tabs.FindResults")); + + InTabManager->RegisterTabSpawner(ValidationLogTab, FOnSpawnTab::CreateSP(this, &FFlowAssetEditor::SpawnTab_ValidationLog)) + .SetDisplayName(LOCTEXT("ValidationLog", "Validation Log")) + .SetGroup(WorkspaceMenuCategoryRef) + .SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "Debug")); } void FFlowAssetEditor::UnregisterTabSpawners(const TSharedRef& InTabManager) { FAssetEditorToolkit::UnregisterTabSpawners(InTabManager); - InTabManager->UnregisterTabSpawner(GraphTab); InTabManager->UnregisterTabSpawner(DetailsTab); + InTabManager->UnregisterTabSpawner(GraphTab); + InTabManager->UnregisterTabSpawner(ValidationLogTab); InTabManager->UnregisterTabSpawner(PaletteTab); + InTabManager->UnregisterTabSpawner(SearchTab); +} + +void FFlowAssetEditor::InitToolMenuContext(FToolMenuContext& MenuContext) +{ + FAssetEditorToolkit::InitToolMenuContext(MenuContext); + + UFlowAssetEditorContext* Context = NewObject(); + Context->FlowAssetEditor = SharedThis(this); + MenuContext.AddObject(Context); +} + +void FFlowAssetEditor::PostRegenerateMenusAndToolbars() +{ + // Provide a hyperlink to view our class + const TSharedRef MenuOverlayBox = SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .ColorAndOpacity(FSlateColor::UseSubduedForeground()) + .ShadowOffset(FVector2D::UnitVector) + .Text(LOCTEXT("FlowAssetEditor_AssetType", "Asset Type: ")) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(0.0f, 0.0f, 8.0f, 0.0f) + [ + FEditorClassUtils::GetSourceLink(FlowAsset->GetClass()) + ]; + + SetMenuOverlay(MenuOverlayBox); +} + +void FFlowAssetEditor::SaveAsset_Execute() +{ + DoPresaveAssetUpdate(); + + FAssetEditorToolkit::SaveAsset_Execute(); +} + +void FFlowAssetEditor::SaveAssetAs_Execute() +{ + DoPresaveAssetUpdate(); + + FAssetEditorToolkit::SaveAssetAs_Execute(); +} + +void FFlowAssetEditor::DoPresaveAssetUpdate() +{ + if (IsValid(FlowAsset)) + { + UFlowGraph* FlowGraph = Cast(FlowAsset->GetGraph()); + if (IsValid(FlowGraph)) + { + FlowGraph->OnSave(); + } + } +} + +bool FFlowAssetEditor::IsTabFocused(const FTabId& TabId) const +{ + if (const TSharedPtr CurrentGraphTab = GetToolkitHost()->GetTabManager()->FindExistingLiveTab(TabId)) + { + return CurrentGraphTab->IsActive(); + } + + return false; } TSharedRef FFlowAssetEditor::SpawnTab_Details(const FSpawnTabArgs& Args) const @@ -144,16 +228,16 @@ TSharedRef FFlowAssetEditor::SpawnTab_Details(const FSpawnTabArgs& Arg ]; } -TSharedRef FFlowAssetEditor::SpawnTab_GraphCanvas(const FSpawnTabArgs& Args) const +TSharedRef FFlowAssetEditor::SpawnTab_Graph(const FSpawnTabArgs& Args) const { check(Args.GetTabId() == GraphTab); TSharedRef SpawnedTab = SNew(SDockTab) .Label(LOCTEXT("FlowGraphTitle", "Graph")); - if (FocusedGraphEditor.IsValid()) + if (GraphEditor.IsValid()) { - SpawnedTab->SetContent(FocusedGraphEditor.ToSharedRef()); + SpawnedTab->SetContent(GraphEditor.ToSharedRef()); } return SpawnedTab; @@ -170,45 +254,113 @@ TSharedRef FFlowAssetEditor::SpawnTab_Palette(const FSpawnTabArgs& Arg ]; } +TSharedRef FFlowAssetEditor::SpawnTab_RuntimeLog(const FSpawnTabArgs& Args) const +{ + check(Args.GetTabId() == RuntimeLogTab); + + return SNew(SDockTab) + .Label(LOCTEXT("FlowRuntimeLogTitle", "Runtime Log")) + [ + RuntimeLog.ToSharedRef() + ]; +} + +TSharedRef FFlowAssetEditor::SpawnTab_Search(const FSpawnTabArgs& Args) const +{ + check(Args.GetTabId() == SearchTab); + + return SNew(SDockTab) + .Label(LOCTEXT("FlowSearchTitle", "Search")) + [ + SNew(SBox) + .AddMetaData(FTagMetaData(TEXT("FlowSearch"))) + [ + SearchBrowser.ToSharedRef() + ] + ]; +} + +TSharedRef FFlowAssetEditor::SpawnTab_ValidationLog(const FSpawnTabArgs& Args) const +{ + check(Args.GetTabId() == ValidationLogTab); + + return SNew(SDockTab) + .Label(LOCTEXT("FlowValidationLogTitle", "Validation Log")) + [ + ValidationLog.ToSharedRef() + ]; +} + void FFlowAssetEditor::InitFlowAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr& InitToolkitHost, UObject* ObjectToEdit) { FlowAsset = CastChecked(ObjectToEdit); + UFlowGraph* FlowGraph = Cast(FlowAsset->GetGraph()); + if (IsValid(FlowGraph)) + { + // Call the OnLoaded event for the flowgraph that is being edited + FlowGraph->OnLoaded(); + } + // Support undo/redo FlowAsset->SetFlags(RF_Transactional); GEditor->RegisterForUndo(this); UFlowGraphSchema::SubscribeToAssetChanges(); - FlowDebugger = MakeShareable(new FFlowDebugger); + FlowAsset->OnDetailsRefreshRequested.BindThreadSafeSP(this, &FFlowAssetEditor::RefreshDetails); BindToolbarCommands(); CreateToolbar(); - BindGraphCommands(); CreateWidgets(); - const TSharedRef StandaloneDefaultLayout = FTabManager::NewLayout("FlowAssetEditor_Layout_v3") + const TSharedRef StandaloneDefaultLayout = FTabManager::NewLayout("FlowAssetEditor_Layout_v5.1") ->AddArea ( FTabManager::NewPrimaryArea()->SetOrientation(Orient_Horizontal) - ->Split - ( - FTabManager::NewStack() - ->SetSizeCoefficient(0.225f) - ->AddTab(DetailsTab, ETabState::OpenedTab) - ) - ->Split - ( - FTabManager::NewStack() - ->SetSizeCoefficient(0.65f) - ->AddTab(GraphTab, ETabState::OpenedTab)->SetHideTabWell(true) - ) - ->Split - ( - FTabManager::NewStack() - ->SetSizeCoefficient(0.125f) - ->AddTab(PaletteTab, ETabState::OpenedTab) - ) + ->Split + ( + FTabManager::NewStack() + ->SetSizeCoefficient(0.225f) + ->AddTab(DetailsTab, ETabState::OpenedTab) + ) + ->Split + ( + FTabManager::NewSplitter() + ->SetSizeCoefficient(0.65f) + ->SetOrientation(Orient_Vertical) + ->Split + ( + FTabManager::NewStack() + ->SetSizeCoefficient(0.8f) + ->SetHideTabWell(true) + ->AddTab(GraphTab, ETabState::OpenedTab) + ) + ->Split + ( + FTabManager::NewStack() + ->SetSizeCoefficient(0.15f) + ->AddTab(RuntimeLogTab, ETabState::ClosedTab) + ) + ->Split + ( + FTabManager::NewStack() + ->SetSizeCoefficient(0.15f) + ->AddTab(SearchTab, ETabState::ClosedTab) + ) + ->Split + ( + FTabManager::NewStack() + ->SetSizeCoefficient(0.15f) + ->AddTab(ValidationLogTab, ETabState::ClosedTab) + ) + ) + ->Split + ( + FTabManager::NewStack() + ->SetSizeCoefficient(0.125f) + ->AddTab(PaletteTab, ETabState::OpenedTab) + ) ); constexpr bool bCreateDefaultStandaloneMenu = true; @@ -243,1059 +395,220 @@ void FFlowAssetEditor::BindToolbarCommands() // Editing ToolkitCommands->MapAction(ToolbarCommands.RefreshAsset, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::RefreshAsset), - FCanExecuteAction::CreateStatic(&FFlowAssetEditor::CanEdit)); + FExecuteAction::CreateSP(this, &FFlowAssetEditor::RefreshAsset), + FCanExecuteAction::CreateStatic(&FFlowAssetEditor::CanEdit)); - // Engine's Play commands - ToolkitCommands->Append(FPlayWorldCommands::GlobalPlayWorldActions.ToSharedRef()); + ToolkitCommands->MapAction(ToolbarCommands.ValidateAsset, + FExecuteAction::CreateSP(this, &FFlowAssetEditor::ValidateAsset_Internal), + FCanExecuteAction()); - // Debugging - ToolkitCommands->MapAction(ToolbarCommands.GoToMasterInstance, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::GoToMasterInstance), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanGoToMasterInstance), - FIsActionChecked(), - FIsActionButtonVisible::CreateStatic(&FFlowAssetEditor::IsPIE)); -} + ToolkitCommands->MapAction(ToolbarCommands.SearchInAsset, + FExecuteAction::CreateSP(this, &FFlowAssetEditor::SearchInAsset), + FCanExecuteAction()); -void FFlowAssetEditor::RefreshAsset() -{ - TArray FlowGraphNodes; - FlowAsset->GetGraph()->GetNodesOfClass(FlowGraphNodes); + ToolkitCommands->MapAction(ToolbarCommands.EditAssetDefaults, + FExecuteAction::CreateSP(this, &FFlowAssetEditor::EditAssetDefaults_Clicked), + FCanExecuteAction()); - for (UFlowGraphNode* GraphNode : FlowGraphNodes) - { - GraphNode->RefreshContextPins(true); - } -} - -void FFlowAssetEditor::GoToMasterInstance() -{ - const UFlowAsset* AssetThatInstancedThisAsset = FlowAsset->GetInspectedInstance()->GetMasterInstance(); - - GEditor->GetEditorSubsystem()->OpenEditorForAsset(AssetThatInstancedThisAsset->GetTemplateAsset()); - AssetThatInstancedThisAsset->GetTemplateAsset()->SetInspectedInstance(AssetThatInstancedThisAsset->GetDisplayName()); -} - -bool FFlowAssetEditor::CanGoToMasterInstance() -{ - return FlowAsset->GetInspectedInstance() && FlowAsset->GetInspectedInstance()->GetNodeOwningThisAssetInstance() != nullptr; -} - -void FFlowAssetEditor::CreateWidgets() -{ - FocusedGraphEditor = CreateGraphWidget(); - - FDetailsViewArgs Args; - Args.bHideSelectionTip = true; - Args.bShowPropertyMatrixButton = false; - Args.DefaultsOnlyVisibility = EEditDefaultsOnlyNodeVisibility::Hide; - Args.NotifyHook = this; - - FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked("PropertyEditor"); - DetailsView = PropertyModule.CreateDetailView(Args); - DetailsView->SetIsPropertyEditingEnabledDelegate(FIsPropertyEditingEnabled::CreateStatic(&FFlowAssetEditor::CanEdit)); - DetailsView->SetObject(FlowAsset); - - Palette = SNew(SFlowPalette, SharedThis(this)); + // Engine's Play commands + ToolkitCommands->Append(FPlayWorldCommands::GlobalPlayWorldActions.ToSharedRef()); } -TSharedRef FFlowAssetEditor::CreateGraphWidget() +void FFlowAssetEditor::RefreshAsset() { - SGraphEditor::FGraphEditorEvents InEvents; - InEvents.OnSelectionChanged = SGraphEditor::FOnSelectionChanged::CreateSP(this, &FFlowAssetEditor::OnSelectedNodesChanged); - InEvents.OnNodeDoubleClicked = FSingleNodeEvent::CreateSP(this, &FFlowAssetEditor::OnNodeDoubleClicked); - InEvents.OnTextCommitted = FOnNodeTextCommitted::CreateSP(this, &FFlowAssetEditor::OnNodeTitleCommitted); - InEvents.OnSpawnNodeByShortcut = SGraphEditor::FOnSpawnNodeByShortcut::CreateStatic(&FFlowAssetEditor::OnSpawnGraphNodeByShortcut, static_cast(FlowAsset->GetGraph())); - - return SNew(SGraphEditor) - .AdditionalCommands(ToolkitCommands) - .IsEditable(true) - .Appearance(GetGraphAppearanceInfo()) - .GraphToEdit(FlowAsset->GetGraph()) - .GraphEvents(InEvents) - .AutoExpandActionMenu(true) - .ShowGraphStateOverlay(false); + // attempt to refresh graph, fix common issues automatically + CastChecked(FlowAsset->GetGraph())->RefreshGraph(); } -FGraphAppearanceInfo FFlowAssetEditor::GetGraphAppearanceInfo() const +void FFlowAssetEditor::RefreshDetails() { - FGraphAppearanceInfo AppearanceInfo; - AppearanceInfo.CornerText = GetCornerText(); - - if (FlowDebugger.IsValid() && FFlowDebugger::IsPlaySessionPaused()) + if (DetailsView.IsValid()) { - AppearanceInfo.PIENotifyText = LOCTEXT("PausedLabel", "PAUSED"); + DetailsView->ForceRefresh(); } - - return AppearanceInfo; -} - -FText FFlowAssetEditor::GetCornerText() const -{ - return LOCTEXT("AppearanceCornerText_FlowAsset", "FLOW"); -} - -void FFlowAssetEditor::BindGraphCommands() -{ - FGraphEditorCommands::Register(); - FFlowGraphCommands::Register(); - FFlowSpawnNodeCommands::Register(); - - const FGenericCommands& GenericCommands = FGenericCommands::Get(); - const FGraphEditorCommandsImpl& GraphCommands = FGraphEditorCommands::Get(); - const FFlowGraphCommands& FlowGraphCommands = FFlowGraphCommands::Get(); - - // Graph commands - ToolkitCommands->MapAction(GraphCommands.CreateComment, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnCreateComment), - FCanExecuteAction::CreateStatic(&FFlowAssetEditor::CanEdit)); - - ToolkitCommands->MapAction(GraphCommands.StraightenConnections, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnStraightenConnections)); - - // Generic Node commands - ToolkitCommands->MapAction(GenericCommands.Undo, - FExecuteAction::CreateStatic(&FFlowAssetEditor::UndoGraphAction), - FCanExecuteAction::CreateStatic(&FFlowAssetEditor::CanEdit)); - - ToolkitCommands->MapAction(GenericCommands.Redo, - FExecuteAction::CreateStatic(&FFlowAssetEditor::RedoGraphAction), - FCanExecuteAction::CreateStatic(&FFlowAssetEditor::CanEdit)); - - ToolkitCommands->MapAction(GenericCommands.SelectAll, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::SelectAllNodes), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanSelectAllNodes)); - - ToolkitCommands->MapAction(GenericCommands.Delete, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::DeleteSelectedNodes), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanDeleteNodes)); - - ToolkitCommands->MapAction(GenericCommands.Copy, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::CopySelectedNodes), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanCopyNodes)); - - ToolkitCommands->MapAction(GenericCommands.Cut, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::CutSelectedNodes), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanCutNodes)); - - ToolkitCommands->MapAction(GenericCommands.Paste, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::PasteNodes), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanPasteNodes)); - - ToolkitCommands->MapAction(GenericCommands.Duplicate, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::DuplicateNodes), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanDuplicateNodes)); - - // Pin commands - ToolkitCommands->MapAction(FlowGraphCommands.RefreshContextPins, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::RefreshContextPins), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanRefreshContextPins)); - - ToolkitCommands->MapAction(FlowGraphCommands.AddInput, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::AddInput), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanAddInput)); - - ToolkitCommands->MapAction(FlowGraphCommands.AddOutput, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::AddOutput), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanAddOutput)); - - ToolkitCommands->MapAction(FlowGraphCommands.RemovePin, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::RemovePin), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanRemovePin)); - - // Breakpoint commands - ToolkitCommands->MapAction(GraphCommands.AddBreakpoint, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnAddBreakpoint), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanAddBreakpoint), - FIsActionChecked(), - FIsActionButtonVisible::CreateSP(this, &FFlowAssetEditor::CanAddBreakpoint) - ); - - ToolkitCommands->MapAction(GraphCommands.RemoveBreakpoint, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnRemoveBreakpoint), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanRemoveBreakpoint), - FIsActionChecked(), - FIsActionButtonVisible::CreateSP(this, &FFlowAssetEditor::CanRemoveBreakpoint) - ); - - ToolkitCommands->MapAction(GraphCommands.EnableBreakpoint, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnEnableBreakpoint), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanEnableBreakpoint), - FIsActionChecked(), - FIsActionButtonVisible::CreateSP(this, &FFlowAssetEditor::CanEnableBreakpoint) - ); - - ToolkitCommands->MapAction(GraphCommands.DisableBreakpoint, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnDisableBreakpoint), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanDisableBreakpoint), - FIsActionChecked(), - FIsActionButtonVisible::CreateSP(this, &FFlowAssetEditor::CanDisableBreakpoint) - ); - - ToolkitCommands->MapAction(GraphCommands.ToggleBreakpoint, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnToggleBreakpoint), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanToggleBreakpoint), - FIsActionChecked(), - FIsActionButtonVisible::CreateSP(this, &FFlowAssetEditor::CanToggleBreakpoint) - ); - - // Pin Breakpoint commands - ToolkitCommands->MapAction(FlowGraphCommands.AddPinBreakpoint, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnAddPinBreakpoint), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanAddPinBreakpoint), - FIsActionChecked(), - FIsActionButtonVisible::CreateSP(this, &FFlowAssetEditor::CanAddPinBreakpoint) - ); - - ToolkitCommands->MapAction(FlowGraphCommands.RemovePinBreakpoint, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnRemovePinBreakpoint), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanRemovePinBreakpoint), - FIsActionChecked(), - FIsActionButtonVisible::CreateSP(this, &FFlowAssetEditor::CanRemovePinBreakpoint) - ); - - ToolkitCommands->MapAction(FlowGraphCommands.EnablePinBreakpoint, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnEnablePinBreakpoint), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanEnablePinBreakpoint), - FIsActionChecked(), - FIsActionButtonVisible::CreateSP(this, &FFlowAssetEditor::CanEnablePinBreakpoint) - ); - - ToolkitCommands->MapAction(FlowGraphCommands.DisablePinBreakpoint, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnDisablePinBreakpoint), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanDisablePinBreakpoint), - FIsActionChecked(), - FIsActionButtonVisible::CreateSP(this, &FFlowAssetEditor::CanDisablePinBreakpoint) - ); - - ToolkitCommands->MapAction(FlowGraphCommands.TogglePinBreakpoint, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnTogglePinBreakpoint), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanTogglePinBreakpoint), - FIsActionChecked(), - FIsActionButtonVisible::CreateSP(this, &FFlowAssetEditor::CanTogglePinBreakpoint) - ); - - // Execution Override commands - ToolkitCommands->MapAction(FlowGraphCommands.ForcePinActivation, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::OnForcePinActivation), - FCanExecuteAction::CreateStatic(&FFlowAssetEditor::IsPIE), - FIsActionChecked(), - FIsActionButtonVisible::CreateStatic(&FFlowAssetEditor::IsPIE) - ); - - // Jump commands - ToolkitCommands->MapAction(FlowGraphCommands.FocusViewport, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::FocusViewport), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanFocusViewport)); - - ToolkitCommands->MapAction(FlowGraphCommands.JumpToNodeDefinition, - FExecuteAction::CreateSP(this, &FFlowAssetEditor::JumpToNodeDefinition), - FCanExecuteAction::CreateSP(this, &FFlowAssetEditor::CanJumpToNodeDefinition)); -} - -void FFlowAssetEditor::UndoGraphAction() -{ - GEditor->UndoTransaction(); } -void FFlowAssetEditor::RedoGraphAction() +void FFlowAssetEditor::ValidateAsset_Internal() { - GEditor->RedoTransaction(); -} - -FReply FFlowAssetEditor::OnSpawnGraphNodeByShortcut(FInputChord InChord, const FVector2D& InPosition, UEdGraph* InGraph) -{ - UEdGraph* Graph = InGraph; + FFlowMessageLog LogResults; + ValidateAsset(LogResults); - if (FFlowSpawnNodeCommands::IsRegistered()) + // push messages to its window + ValidationLogListing->ClearMessages(); + if (LogResults.Messages.Num() > 0) { - const TSharedPtr Action = FFlowSpawnNodeCommands::Get().GetActionByChord(InChord); - if (Action.IsValid()) - { - TArray DummyPins; - Action->PerformAction(Graph, DummyPins, InPosition); - return FReply::Handled(); - } + TabManager->TryInvokeTab(ValidationLogTab); + ValidationLogListing->AddMessages(LogResults.Messages); } - return FReply::Unhandled(); -} + ValidationLogListing->OnDataChanged().Broadcast(); -void FFlowAssetEditor::SetUISelectionState(const FName SelectionOwner) -{ - if (SelectionOwner != CurrentUISelection) - { - ClearSelectionStateFor(CurrentUISelection); - CurrentUISelection = SelectionOwner; - } + FlowAsset->GetGraph()->NotifyGraphChanged(); } -void FFlowAssetEditor::ClearSelectionStateFor(const FName SelectionOwner) +void FFlowAssetEditor::ValidateAsset(FFlowMessageLog& MessageLog) { - if (SelectionOwner == GraphTab) - { - FocusedGraphEditor->ClearSelectionSet(); - } - else if (SelectionOwner == PaletteTab) + UFlowGraph* FlowGraph = Cast(FlowAsset->GetGraph()); + if (FlowGraph) { - if (Palette.IsValid()) - { - Palette->ClearGraphActionMenuSelection(); - } + FlowGraph->ValidateAsset(MessageLog); } } -void FFlowAssetEditor::OnCreateComment() const -{ - FFlowGraphSchemaAction_NewComment CommentAction; - CommentAction.PerformAction(FlowAsset->GetGraph(), nullptr, FocusedGraphEditor->GetPasteLocation()); -} - -void FFlowAssetEditor::OnStraightenConnections() const -{ - FocusedGraphEditor->OnStraightenConnections(); -} - -bool FFlowAssetEditor::CanEdit() -{ - return GEditor->PlayWorld == nullptr; -} - -bool FFlowAssetEditor::IsPIE() +void FFlowAssetEditor::SearchInAsset() { - return GEditor->PlayWorld != nullptr; + TabManager->TryInvokeTab(SearchTab); + SearchBrowser->FocusForUse(); } -EVisibility FFlowAssetEditor::GetDebuggerVisibility() +void FFlowAssetEditor::EditAssetDefaults_Clicked() const { - return GEditor->PlayWorld ? EVisibility::Visible : EVisibility::Collapsed; + DetailsView->SetObject(FlowAsset); } -TSet FFlowAssetEditor::GetSelectedFlowNodes() const +void FFlowAssetEditor::CreateWidgets() { - TSet Result; - - const FGraphPanelSelectionSet SelectedNodes = FocusedGraphEditor->GetSelectedNodes(); - for (FGraphPanelSelectionSet::TConstIterator NodeIt(SelectedNodes); NodeIt; ++NodeIt) + // Details View { - if (UFlowGraphNode* SelectedNode = Cast(*NodeIt)) - { - Result.Emplace(SelectedNode); - } - } - - return Result; -} + FDetailsViewArgs Args; + Args.bHideSelectionTip = true; + Args.bShowPropertyMatrixButton = false; + Args.DefaultsOnlyVisibility = EEditDefaultsOnlyNodeVisibility::Hide; + Args.NotifyHook = this; -int32 FFlowAssetEditor::GetNumberOfSelectedNodes() const -{ - return FocusedGraphEditor->GetSelectedNodes().Num(); -} + FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked("PropertyEditor"); + DetailsView = PropertyModule.CreateDetailView(Args); + DetailsView->SetIsPropertyEditingEnabledDelegate(FIsPropertyEditingEnabled::CreateStatic(&FFlowAssetEditor::CanEdit)); + DetailsView->SetObject(FlowAsset); + } -bool FFlowAssetEditor::GetBoundsForSelectedNodes(class FSlateRect& Rect, float Padding) const -{ - return FocusedGraphEditor->GetBoundsForSelectedNodes(Rect, Padding); -} + // Graph + CreateGraphWidget(); + GraphEditor->OnSelectionChangedEvent.BindRaw(this, &FFlowAssetEditor::OnSelectedNodesChanged); -void FFlowAssetEditor::OnSelectedNodesChanged(const TSet& Nodes) -{ - TArray SelectedObjects; + // Palette + Palette = SNew(SFlowPalette, SharedThis(this)); - if (Nodes.Num() > 0) - { - SetUISelectionState(GraphTab); + // Search +#if ENABLE_SEARCH_IN_ASSET_EDITOR + SearchBrowser = SNew(SSearchBrowser, GetFlowAsset()); +#else + SearchBrowser = SNew(SFindInFlow, SharedThis(this)); +#endif - for (TSet::TConstIterator SetIt(Nodes); SetIt; ++SetIt) - { - if (const UFlowGraphNode* GraphNode = Cast(*SetIt)) - { - SelectedObjects.Add(Cast(GraphNode->GetFlowNode())); - } - else - { - SelectedObjects.Add(*SetIt); - } - } - } - else + // Logs + FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked("MessageLog"); { - SetUISelectionState(NAME_None); - SelectedObjects.Add(GetFlowAsset()); + RuntimeLogListing = FFlowMessageLogListing::GetLogListing(FlowAsset, EFlowLogType::Runtime); + RuntimeLogListing->OnMessageTokenClicked().AddSP(this, &FFlowAssetEditor::OnLogTokenClicked); + RuntimeLog = MessageLogModule.CreateLogListingWidget(RuntimeLogListing.ToSharedRef()); } - - if (DetailsView.IsValid()) { - DetailsView->SetObjects(SelectedObjects); + ValidationLogListing = FFlowMessageLogListing::GetLogListing(FlowAsset, EFlowLogType::Validation); + ValidationLogListing->OnMessageTokenClicked().AddSP(this, &FFlowAssetEditor::OnLogTokenClicked); + ValidationLog = MessageLogModule.CreateLogListingWidget(ValidationLogListing.ToSharedRef()); } } -void FFlowAssetEditor::SelectSingleNode(UEdGraphNode* Node) const -{ - FocusedGraphEditor->ClearSelectionSet(); - FocusedGraphEditor->SetNodeSelection(Node, true); -} - -void FFlowAssetEditor::SelectAllNodes() const -{ - FocusedGraphEditor->SelectAllNodes(); -} - -bool FFlowAssetEditor::CanSelectAllNodes() const +void FFlowAssetEditor::CreateGraphWidget() { - return true; + SAssignNew(GraphEditor, SFlowGraphEditor, SharedThis(this)) + .DetailsView(DetailsView); } -void FFlowAssetEditor::DeleteSelectedNodes() -{ - const FScopedTransaction Transaction(LOCTEXT("DeleteSelectedNode", "Delete Selected Node")); - FocusedGraphEditor->GetCurrentGraph()->Modify(); - FlowAsset->Modify(); - - const FGraphPanelSelectionSet SelectedNodes = FocusedGraphEditor->GetSelectedNodes(); - SetUISelectionState(NAME_None); - - for (FGraphPanelSelectionSet::TConstIterator NodeIt(SelectedNodes); NodeIt; ++NodeIt) - { - UEdGraphNode* Node = CastChecked(*NodeIt); - - if (Node->CanUserDeleteNode()) - { - if (const UFlowGraphNode* FlowGraphNode = Cast(Node)) - { - if (FlowGraphNode->GetFlowNode()) - { - const FGuid NodeGuid = FlowGraphNode->GetFlowNode()->GetGuid(); - FBlueprintEditorUtils::RemoveNode(nullptr, Node, true); - FlowAsset->UnregisterNode(NodeGuid); - continue; - } - } - - FBlueprintEditorUtils::RemoveNode(nullptr, Node, true); - } - } -} - -void FFlowAssetEditor::DeleteSelectedDuplicableNodes() +bool FFlowAssetEditor::CanEdit() { - // Cache off the old selection - const FGraphPanelSelectionSet OldSelectedNodes = FocusedGraphEditor->GetSelectedNodes(); - - // Clear the selection and only select the nodes that can be duplicated - FGraphPanelSelectionSet RemainingNodes; - FocusedGraphEditor->ClearSelectionSet(); - - for (FGraphPanelSelectionSet::TConstIterator SelectedIt(OldSelectedNodes); SelectedIt; ++SelectedIt) - { - if (UEdGraphNode* Node = Cast(*SelectedIt)) - { - if (Node->CanDuplicateNode()) - { - FocusedGraphEditor->SetNodeSelection(Node, true); - } - else - { - RemainingNodes.Add(Node); - } - } - } - - // Delete the duplicable nodes - DeleteSelectedNodes(); - - for (FGraphPanelSelectionSet::TConstIterator SelectedIt(RemainingNodes); SelectedIt; ++SelectedIt) - { - if (UEdGraphNode* Node = Cast(*SelectedIt)) - { - FocusedGraphEditor->SetNodeSelection(Node, true); - } - } + return GEditor->PlayWorld == nullptr; } -bool FFlowAssetEditor::CanDeleteNodes() const +void FFlowAssetEditor::SetUISelectionState(const FName SelectionOwner) { - if (CanEdit()) + if (SelectionOwner != CurrentUISelection) { - const FGraphPanelSelectionSet SelectedNodes = FocusedGraphEditor->GetSelectedNodes(); - for (FGraphPanelSelectionSet::TConstIterator NodeIt(SelectedNodes); NodeIt; ++NodeIt) - { - if (const UEdGraphNode* Node = Cast(*NodeIt)) - { - if (!Node->CanUserDeleteNode()) - { - return false; - } - } - } - - return SelectedNodes.Num() > 0; + ClearSelectionStateFor(CurrentUISelection); + CurrentUISelection = SelectionOwner; } - - return false; } -void FFlowAssetEditor::CutSelectedNodes() -{ - CopySelectedNodes(); - - // Cut should only delete nodes that can be duplicated - DeleteSelectedDuplicableNodes(); -} - -bool FFlowAssetEditor::CanCutNodes() const -{ - return CanCopyNodes() && CanDeleteNodes(); -} - -void FFlowAssetEditor::CopySelectedNodes() const +void FFlowAssetEditor::ClearSelectionStateFor(const FName SelectionOwner) { - const FGraphPanelSelectionSet SelectedNodes = FocusedGraphEditor->GetSelectedNodes(); - for (FGraphPanelSelectionSet::TConstIterator SelectedIt(SelectedNodes); SelectedIt; ++SelectedIt) + if (SelectionOwner == GraphTab) { - if (UFlowGraphNode* Node = Cast(*SelectedIt)) - { - Node->PrepareForCopying(); - } + GraphEditor->ClearSelectionSet(); } - - // Export the selected nodes and place the text on the clipboard - FString ExportedText; - FEdGraphUtilities::ExportNodesToText(SelectedNodes, /*out*/ ExportedText); - FPlatformApplicationMisc::ClipboardCopy(*ExportedText); - - for (FGraphPanelSelectionSet::TConstIterator SelectedIt(SelectedNodes); SelectedIt; ++SelectedIt) + else if (SelectionOwner == PaletteTab) { - if (UFlowGraphNode* Node = Cast(*SelectedIt)) + if (Palette.IsValid()) { - Node->PostCopyNode(); + Palette->ClearGraphActionMenuSelection(); } } } -bool FFlowAssetEditor::CanCopyNodes() const +FName FFlowAssetEditor::GetUISelectionState() const { - if (CanEdit()) - { - const FGraphPanelSelectionSet SelectedNodes = FocusedGraphEditor->GetSelectedNodes(); - for (FGraphPanelSelectionSet::TConstIterator SelectedIt(SelectedNodes); SelectedIt; ++SelectedIt) - { - const UEdGraphNode* Node = Cast(*SelectedIt); - if (Node && Node->CanDuplicateNode()) - { - return true; - } - } - } - - return false; + return CurrentUISelection; } -void FFlowAssetEditor::PasteNodes() +void FFlowAssetEditor::OnSelectedNodesChanged(const TSet& Nodes) { - PasteNodesHere(FocusedGraphEditor->GetPasteLocation()); } -void FFlowAssetEditor::PasteNodesHere(const FVector2D& Location) +#if ENABLE_JUMP_TO_INNER_OBJECT +void FFlowAssetEditor::JumpToInnerObject(UObject* InnerObject) { - SetUISelectionState(NAME_None); - - // Undo/Redo support - const FScopedTransaction Transaction(LOCTEXT("PasteNode", "Paste Node")); - FlowAsset->GetGraph()->Modify(); - FlowAsset->Modify(); - - // Clear the selection set (newly pasted stuff will be selected) - FocusedGraphEditor->ClearSelectionSet(); - - // Grab the text to paste from the clipboard. - FString TextToImport; - FPlatformApplicationMisc::ClipboardPaste(TextToImport); - - // Import the nodes - TSet PastedNodes; - FEdGraphUtilities::ImportNodesFromText(FlowAsset->GetGraph(), TextToImport, /*out*/ PastedNodes); - - //Average position of nodes so we can move them while still maintaining relative distances to each other - FVector2D AvgNodePosition(0.0f, 0.0f); - - for (TSet::TIterator It(PastedNodes); It; ++It) + if (const UFlowNodeBase* FlowNodeBase = Cast(InnerObject)) { - const UEdGraphNode* Node = *It; - AvgNodePosition.X += Node->NodePosX; - AvgNodePosition.Y += Node->NodePosY; + GraphEditor->JumpToNode(FlowNodeBase->GetGraphNode(), true); } - - if (PastedNodes.Num() > 0) - { - const float InvNumNodes = 1.0f / static_cast(PastedNodes.Num()); - AvgNodePosition.X *= InvNumNodes; - AvgNodePosition.Y *= InvNumNodes; - } - - for (TSet::TIterator It(PastedNodes); It; ++It) + else if (const UEdGraphNode* GraphNode = Cast(InnerObject)) { - UEdGraphNode* Node = *It; - - // Give new node a different Guid from the old one - Node->CreateNewGuid(); - - if (const UFlowGraphNode* FlowGraphNode = Cast(Node)) - { - FlowAsset->RegisterNode(Node->NodeGuid, FlowGraphNode->GetFlowNode()); - } - - // Select the newly pasted stuff - FocusedGraphEditor->SetNodeSelection(Node, true); - - Node->NodePosX = (Node->NodePosX - AvgNodePosition.X) + Location.X; - Node->NodePosY = (Node->NodePosY - AvgNodePosition.Y) + Location.Y; - - Node->SnapToGrid(SNodePanel::GetSnapGridSize()); + GraphEditor->JumpToNode(GraphNode, true); } - - // Force new pasted FlowNodes to have same connections as graph nodes - FlowAsset->HarvestNodeConnections(); - - // Update UI - FocusedGraphEditor->NotifyGraphChanged(); - - FlowAsset->PostEditChange(); - FlowAsset->MarkPackageDirty(); } +#endif -bool FFlowAssetEditor::CanPasteNodes() const +void FFlowAssetEditor::OnLogTokenClicked(const TSharedRef& Token) const { - if (CanEdit()) + if (Token->GetType() == EMessageToken::Object) { - FString ClipboardContent; - FPlatformApplicationMisc::ClipboardPaste(ClipboardContent); - - return FEdGraphUtilities::CanImportNodesFromText(FlowAsset->GetGraph(), ClipboardContent); - } - - return false; -} - -void FFlowAssetEditor::DuplicateNodes() -{ - CopySelectedNodes(); - PasteNodes(); -} - -bool FFlowAssetEditor::CanDuplicateNodes() const -{ - return CanCopyNodes(); -} - -void FFlowAssetEditor::OnNodeDoubleClicked(class UEdGraphNode* Node) const -{ - UFlowNode* FlowNode = Cast(Node)->GetFlowNode(); - - if (FlowNode) - { - if (UFlowGraphEditorSettings::Get()->NodeDoubleClickTarget == EFlowNodeDoubleClickTarget::NodeDefinition) - { - Node->JumpToDefinition(); - } - else + const TSharedRef ObjectToken = StaticCastSharedRef(Token); + if (const UObject* Object = ObjectToken->GetObject().Get()) { - const FString AssetPath = FlowNode->GetAssetPath(); - if (!AssetPath.IsEmpty()) + if (Object->IsAsset()) { - GEditor->GetEditorSubsystem()->OpenEditorForAsset(AssetPath); + GEditor->GetEditorSubsystem()->OpenEditorForAsset(const_cast(Object)); } - else if (UObject* AssetToEdit = FlowNode->GetAssetToEdit()) + else { - GEditor->GetEditorSubsystem()->OpenEditorForAsset(AssetToEdit); - - if (IsPIE()) - { - if (UFlowNode_SubGraph* SubGraphNode = Cast(FlowNode)) - { - const TWeakObjectPtr SubFlowInstance = SubGraphNode->GetFlowAsset()->GetFlowInstance(SubGraphNode); - if (SubFlowInstance.IsValid()) - { - SubGraphNode->GetFlowAsset()->GetTemplateAsset()->SetInspectedInstance(SubFlowInstance->GetDisplayName()); - } - } - } + UE_LOG(LogFlowEditor, Warning, TEXT("Unknown type of hyperlinked object (%s), cannot focus it"), *GetNameSafe(Object)); } } } -} - -void FFlowAssetEditor::OnNodeTitleCommitted(const FText& NewText, ETextCommit::Type CommitInfo, UEdGraphNode* NodeBeingChanged) -{ - if (NodeBeingChanged) - { - const FScopedTransaction Transaction(LOCTEXT("RenameNode", "Rename Node")); - NodeBeingChanged->Modify(); - NodeBeingChanged->OnRenameNode(NewText.ToString()); - } -} - -void FFlowAssetEditor::RefreshContextPins() const -{ - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + else if (Token->GetType() == EMessageToken::EdGraph && GraphEditor.IsValid()) { - SelectedNode->RefreshContextPins(true); - } -} + const TSharedRef EdGraphToken = StaticCastSharedRef(Token); -bool FFlowAssetEditor::CanRefreshContextPins() const -{ - if (CanEdit() && GetSelectedFlowNodes().Num() == 1) - { - for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + if (const UEdGraphPin* GraphPin = EdGraphToken->GetPin()) { - return SelectedNode->SupportsContextPins(); - } - } - - return false; -} - -void FFlowAssetEditor::AddInput() const -{ - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - SelectedNode->AddUserInput(); - } -} - -bool FFlowAssetEditor::CanAddInput() const -{ - if (CanEdit() && GetSelectedFlowNodes().Num() == 1) - { - for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - return SelectedNode->CanUserAddInput(); - } - } - - return false; -} - -void FFlowAssetEditor::AddOutput() const -{ - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - SelectedNode->AddUserOutput(); - } -} - -bool FFlowAssetEditor::CanAddOutput() const -{ - if (CanEdit() && GetSelectedFlowNodes().Num() == 1) - { - for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - return SelectedNode->CanUserAddOutput(); - } - } - - return false; -} - -void FFlowAssetEditor::RemovePin() const -{ - if (UEdGraphPin* SelectedPin = FocusedGraphEditor->GetGraphPinForMenu()) - { - if (UFlowGraphNode* SelectedNode = Cast(SelectedPin->GetOwningNode())) - { - SelectedNode->RemoveInstancePin(SelectedPin); - } - } -} - -bool FFlowAssetEditor::CanRemovePin() const -{ - if (CanEdit() && GetSelectedFlowNodes().Num() == 1) - { - if (const UEdGraphPin* Pin = FocusedGraphEditor->GetGraphPinForMenu()) - { - if (const UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) + if (!GraphPin->IsPendingKill()) { - if (Pin->Direction == EGPD_Input) - { - return GraphNode->CanUserRemoveInput(Pin); - } - else - { - return GraphNode->CanUserRemoveOutput(Pin); - } + GraphEditor->JumpToPin(GraphPin); } } - } - - return false; -} - -void FFlowAssetEditor::OnAddBreakpoint() const -{ - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - SelectedNode->NodeBreakpoint.AddBreakpoint(); - } -} - -void FFlowAssetEditor::OnAddPinBreakpoint() const -{ - if (UEdGraphPin* Pin = FocusedGraphEditor->GetGraphPinForMenu()) - { - if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) - { - GraphNode->PinBreakpoints.Add(Pin, FFlowBreakpoint()); - GraphNode->PinBreakpoints[Pin].AddBreakpoint(); - } - } -} - -bool FFlowAssetEditor::CanAddBreakpoint() const -{ - for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - return !SelectedNode->NodeBreakpoint.HasBreakpoint(); - } - - return false; -} - -bool FFlowAssetEditor::CanAddPinBreakpoint() const -{ - if (UEdGraphPin* Pin = FocusedGraphEditor->GetGraphPinForMenu()) - { - if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) - { - return !GraphNode->PinBreakpoints.Contains(Pin) || !GraphNode->PinBreakpoints[Pin].HasBreakpoint(); - } - } - - return false; -} - -void FFlowAssetEditor::OnRemoveBreakpoint() const -{ - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - SelectedNode->NodeBreakpoint.RemoveBreakpoint(); - } -} - -void FFlowAssetEditor::OnRemovePinBreakpoint() const -{ - if (UEdGraphPin* Pin = FocusedGraphEditor->GetGraphPinForMenu()) - { - if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) - { - GraphNode->PinBreakpoints.Remove(Pin); - } - } -} - -bool FFlowAssetEditor::CanRemoveBreakpoint() const -{ - for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - return SelectedNode->NodeBreakpoint.HasBreakpoint(); - } - - return false; -} - -bool FFlowAssetEditor::CanRemovePinBreakpoint() const -{ - if (UEdGraphPin* Pin = FocusedGraphEditor->GetGraphPinForMenu()) - { - if (const UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) - { - return GraphNode->PinBreakpoints.Contains(Pin); - } - } - - return false; -} - -void FFlowAssetEditor::OnEnableBreakpoint() const -{ - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - SelectedNode->NodeBreakpoint.EnableBreakpoint(); - } -} - -void FFlowAssetEditor::OnEnablePinBreakpoint() const -{ - if (UEdGraphPin* Pin = FocusedGraphEditor->GetGraphPinForMenu()) - { - if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) - { - GraphNode->PinBreakpoints[Pin].EnableBreakpoint(); - } - } -} - -bool FFlowAssetEditor::CanEnableBreakpoint() const -{ - if (UEdGraphPin* Pin = FocusedGraphEditor->GetGraphPinForMenu()) - { - if (const UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) + else if (const UEdGraphNode* GraphNode = EdGraphToken->GetGraphNode()) { - return GraphNode->PinBreakpoints.Contains(Pin); + GraphEditor->JumpToNode(GraphNode, true); } } - - for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - return SelectedNode->NodeBreakpoint.CanEnableBreakpoint(); - } - - return false; } -bool FFlowAssetEditor::CanEnablePinBreakpoint() const +void FFlowAssetEditor::JumpToNode(const UEdGraphNode* Node) const { - if (UEdGraphPin* Pin = FocusedGraphEditor->GetGraphPinForMenu()) + if (GetFlowGraph().IsValid()) { - if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) - { - return GraphNode->PinBreakpoints.Contains(Pin) && GraphNode->PinBreakpoints[Pin].CanEnableBreakpoint(); - } + GetFlowGraph()->JumpToNode(Node, false); } - - return false; -} - -void FFlowAssetEditor::OnDisableBreakpoint() const -{ - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - SelectedNode->NodeBreakpoint.DisableBreakpoint(); - } -} - -void FFlowAssetEditor::OnDisablePinBreakpoint() const -{ - if (UEdGraphPin* Pin = FocusedGraphEditor->GetGraphPinForMenu()) - { - if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) - { - GraphNode->PinBreakpoints[Pin].DisableBreakpoint(); - } - } -} - -bool FFlowAssetEditor::CanDisableBreakpoint() const -{ - for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - return SelectedNode->NodeBreakpoint.IsBreakpointEnabled(); - } - - return false; -} - -bool FFlowAssetEditor::CanDisablePinBreakpoint() const -{ - if (UEdGraphPin* Pin = FocusedGraphEditor->GetGraphPinForMenu()) - { - if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) - { - return GraphNode->PinBreakpoints.Contains(Pin) && GraphNode->PinBreakpoints[Pin].IsBreakpointEnabled(); - } - } - - return false; -} - -void FFlowAssetEditor::OnToggleBreakpoint() const -{ - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - SelectedNode->NodeBreakpoint.ToggleBreakpoint(); - } -} - -void FFlowAssetEditor::OnTogglePinBreakpoint() const -{ - if (UEdGraphPin* Pin = FocusedGraphEditor->GetGraphPinForMenu()) - { - if (UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) - { - GraphNode->PinBreakpoints.Add(Pin, FFlowBreakpoint()); - GraphNode->PinBreakpoints[Pin].ToggleBreakpoint(); - } - } -} - -bool FFlowAssetEditor::CanToggleBreakpoint() const -{ - return GetSelectedFlowNodes().Num() > 0; -} - -bool FFlowAssetEditor::CanTogglePinBreakpoint() const -{ - return FocusedGraphEditor->GetGraphPinForMenu() != nullptr; -} - -void FFlowAssetEditor::OnForcePinActivation() const -{ - if (UEdGraphPin* Pin = FocusedGraphEditor->GetGraphPinForMenu()) - { - if (const UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) - { - GraphNode->ForcePinActivation(Pin); - } - } -} - -void FFlowAssetEditor::FocusViewport() const -{ - // Iterator used but should only contain one node - for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - const UFlowNode* FlowNode = Cast(SelectedNode)->GetFlowNode(); - if (UFlowNode* NodeInstance = FlowNode->GetInspectedInstance()) - { - if (AActor* ActorToFocus = NodeInstance->GetActorToFocus()) - { - GEditor->SelectNone(false, false, false); - GEditor->SelectActor(ActorToFocus, true, true, true); - GEditor->NoteSelectionChange(); - - GEditor->MoveViewportCamerasToActor(*ActorToFocus, false); - - const FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked("LevelEditor"); - const TSharedPtr LevelEditorTab = LevelEditorModule.GetLevelEditorInstanceTab().Pin(); - if (LevelEditorTab.IsValid()) - { - LevelEditorTab->DrawAttention(); - } - } - } - - return; - } -} - -bool FFlowAssetEditor::CanFocusViewport() const -{ - return GetSelectedFlowNodes().Num() == 1; -} - -void FFlowAssetEditor::JumpToNodeDefinition() const -{ - // Iterator used but should only contain one node - for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) - { - SelectedNode->JumpToDefinition(); - return; - } -} - -bool FFlowAssetEditor::CanJumpToNodeDefinition() const -{ - return GetSelectedFlowNodes().Num() == 1; } #undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Asset/FlowAssetEditorContext.cpp b/Source/FlowEditor/Private/Asset/FlowAssetEditorContext.cpp new file mode 100644 index 000000000..745571e60 --- /dev/null +++ b/Source/FlowEditor/Private/Asset/FlowAssetEditorContext.cpp @@ -0,0 +1,11 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/FlowAssetEditorContext.h" +#include "Asset/FlowAssetEditor.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowAssetEditorContext) + +UFlowAsset* UFlowAssetEditorContext::GetFlowAsset() const +{ + return FlowAssetEditor.IsValid() ? FlowAssetEditor.Pin()->GetFlowAsset() : nullptr; +} diff --git a/Source/FlowEditor/Private/Asset/FlowAssetFactory.cpp b/Source/FlowEditor/Private/Asset/FlowAssetFactory.cpp index 13e6a31a6..4bcea525b 100644 --- a/Source/FlowEditor/Private/Asset/FlowAssetFactory.cpp +++ b/Source/FlowEditor/Private/Asset/FlowAssetFactory.cpp @@ -3,6 +3,59 @@ #include "Asset/FlowAssetFactory.h" #include "FlowAsset.h" #include "Graph/FlowGraph.h" +#include "Graph/FlowGraphSettings.h" + +#include "ClassViewerFilter.h" +#include "ClassViewerModule.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Kismet2/SClassPickerDialog.h" +#include "Modules/ModuleManager.h" + +#define LOCTEXT_NAMESPACE "FlowAssetFactory" + +class FAssetClassParentFilter final : public IClassViewerFilter +{ +public: + FAssetClassParentFilter() + : DisallowedClassFlags(CLASS_None) + , bDisallowBlueprintBase(false) + { + } + + /** All children of these classes will be included unless filtered out by another setting. */ + TSet AllowedChildrenOfClasses; + + /** Disallowed class flags. */ + EClassFlags DisallowedClassFlags; + + /** Disallow blueprint base classes. */ + bool bDisallowBlueprintBase; + + virtual bool IsClassAllowed(const FClassViewerInitializationOptions& InInitOptions, const UClass* InClass, const TSharedRef InFilterFuncs) override + { + const bool bAllowed = !InClass->HasAnyClassFlags(DisallowedClassFlags) && InFilterFuncs->IfInChildOfClassesSet(AllowedChildrenOfClasses, InClass) != EFilterReturn::Failed; + + if (bAllowed && bDisallowBlueprintBase) + { + if (FKismetEditorUtilities::CanCreateBlueprintOfClass(InClass)) + { + return false; + } + } + + return bAllowed; + } + + virtual bool IsUnloadedClassAllowed(const FClassViewerInitializationOptions& InInitOptions, const TSharedRef InUnloadedClassData, TSharedRef InFilterFuncs) override + { + if (bDisallowBlueprintBase) + { + return false; + } + + return !InUnloadedClassData->HasAnyClassFlags(DisallowedClassFlags) && InFilterFuncs->IfInChildOfClassesSet(AllowedChildrenOfClasses, InUnloadedClassData) != EFilterReturn::Failed; + } +}; UFlowAssetFactory::UFlowAssetFactory(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) @@ -14,9 +67,64 @@ UFlowAssetFactory::UFlowAssetFactory(const FObjectInitializer& ObjectInitializer bEditAfterNew = true; } +bool UFlowAssetFactory::ConfigureProperties() +{ + const FText TitleText = LOCTEXT("CreateFlowAssetOptions", "Pick Flow Asset Class"); + + return ConfigurePropertiesInternal(TitleText); +} + +bool UFlowAssetFactory::ConfigurePropertiesInternal(const FText& TitleText) +{ + const TSoftClassPtr DefaultClass = GetDefault()->DefaultFlowAssetClass; + if (!DefaultClass.IsNull()) + { + AssetClass = DefaultClass.LoadSynchronous(); + if (AssetClass) // Class was set in settings + { + return true; + } + } + + // Load the Class Viewer module to display a class picker + FModuleManager::LoadModuleChecked("ClassViewer"); + + // Fill in options + FClassViewerInitializationOptions Options; + Options.Mode = EClassViewerMode::ClassPicker; + + const TSharedPtr Filter = MakeShareable(new FAssetClassParentFilter); + Filter->DisallowedClassFlags = CLASS_Abstract | CLASS_Deprecated | CLASS_NewerVersionExists | CLASS_HideDropDown; + Filter->AllowedChildrenOfClasses.Add(SupportedClass); + + Options.ClassFilters = {Filter.ToSharedRef()}; + + UClass* ChosenClass = nullptr; + const bool bPressedOk = SClassPickerDialog::PickClass(TitleText, Options, ChosenClass, SupportedClass); + + if (bPressedOk) + { + AssetClass = ChosenClass; + } + + return bPressedOk; +} + UObject* UFlowAssetFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) { - UFlowAsset* NewFlow = NewObject(InParent, Class, Name, Flags | RF_Transactional, Context); - UFlowGraph::CreateGraph(NewFlow); - return NewFlow; + UFlowAsset* NewFlowAsset; + if (AssetClass) + { + NewFlowAsset = NewObject(InParent, AssetClass, Name, Flags | RF_Transactional, Context); + } + else + { + // if we have no asset class, use the passed-in class instead + NewFlowAsset = NewObject(InParent, Class, Name, Flags | RF_Transactional, Context); + } + + UFlowGraph::CreateGraph(NewFlowAsset); + return NewFlowAsset; } + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Asset/FlowAssetIndexer.cpp b/Source/FlowEditor/Private/Asset/FlowAssetIndexer.cpp index 55e1e5bdc..2c7ed92d9 100644 --- a/Source/FlowEditor/Private/Asset/FlowAssetIndexer.cpp +++ b/Source/FlowEditor/Private/Asset/FlowAssetIndexer.cpp @@ -3,20 +3,20 @@ #include "Asset/FlowAssetIndexer.h" #include "FlowAsset.h" -#include "Nodes/FlowNode.h" +#include "Nodes/FlowNodeBase.h" #include "Graph/Nodes/FlowGraphNode_Reroute.h" +#include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphPin.h" #include "EdGraphNode_Comment.h" -#include "Engine/SimpleConstructionScript.h" #include "Internationalization/Text.h" #include "SearchSerializer.h" #include "Utility/IndexerUtilities.h" #define LOCTEXT_NAMESPACE "FFlowAssetIndexer" -/*enum class EFlowAssetIndexerVersion +enum class EFlowAssetIndexerVersion { Empty, Initial, @@ -38,15 +38,6 @@ void FFlowAssetIndexer::IndexAsset(const UObject* InAssetObject, FSearchSerializ { Serializer.BeginIndexingObject(FlowAsset, TEXT("$self")); - // for (const FName& CustomInput : FlowAsset->GetCustomInputs()) - // { - // Serializer.IndexProperty(CustomInput.ToString(), CustomInput); - // } - // for (const FName& CustomOutput : FlowAsset->GetCustomOutputs()) - // { - // Serializer.IndexProperty(CustomOutput.ToString(), CustomOutput); - // } - FIndexerUtilities::IterateIndexableProperties(FlowAsset, [&Serializer](const FProperty* Property, const FString& Value) { Serializer.IndexProperty(Property, Value); @@ -121,18 +112,19 @@ void FFlowAssetIndexer::IndexGraph(const UFlowAsset* InFlowAsset, FSearchSeriali // Indexing Flow Node if (const UFlowGraphNode* FlowGraphNode = Cast(Node)) { - if (const UFlowNode* FlowNode = FlowGraphNode->GetFlowNode()) + if (const UFlowNodeBase* FlowNodeBase = FlowGraphNode->GetFlowNodeBase()) { - const FString NodeFriendlyName = FString::Printf(TEXT("%s: %s"), *FlowNode->GetClass()->GetName(), *FlowNode->GetNodeDescription()); - Serializer.BeginIndexingObject(FlowNode, NodeFriendlyName); - FIndexerUtilities::IterateIndexableProperties(FlowNode, [&Serializer](const FProperty* Property, const FString& Value) + const FString NodeFriendlyName = FString::Printf(TEXT("%s: %s"), *FlowNodeBase->GetClass()->GetName(), *FlowNodeBase->GetNodeDescription()); + Serializer.BeginIndexingObject(FlowNodeBase, NodeFriendlyName); + FIndexerUtilities::IterateIndexableProperties(FlowNodeBase, [&Serializer](const FProperty* Property, const FString& Value) { Serializer.IndexProperty(Property, Value); }); + FlowGraphNode->AdditionalNodeIndexing(Serializer); Serializer.EndIndexingObject(); } } } -}*/ +} #undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Asset/FlowAssetParamsFactory.cpp b/Source/FlowEditor/Private/Asset/FlowAssetParamsFactory.cpp new file mode 100644 index 000000000..194e1d3f2 --- /dev/null +++ b/Source/FlowEditor/Private/Asset/FlowAssetParamsFactory.cpp @@ -0,0 +1,176 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/FlowAssetParamsFactory.h" + +#include "Asset/FlowAssetParams.h" +#include "Asset/FlowAssetParamsUtils.h" + +#include "ContentBrowserModule.h" +#include "Framework/Application/SlateApplication.h" +#include "IContentBrowserSingleton.h" +#include "Misc/MessageDialog.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Layout/SBorder.h" +#include "Widgets/SBoxPanel.h" +#include "Widgets/SWindow.h" +#include "Widgets/Text/STextBlock.h" + +#define LOCTEXT_NAMESPACE "FlowAssetParamsFactory" + +UFlowAssetParamsFactory::UFlowAssetParamsFactory() +{ + SupportedClass = UFlowAssetParams::StaticClass(); + + bCreateNew = true; + bEditorImport = false; + bEditAfterNew = true; +} + +bool UFlowAssetParamsFactory::ConfigureProperties() +{ + SelectedParentParams.Reset(); + return ShowParentPickerDialog(); +} + +bool UFlowAssetParamsFactory::ShowParentPickerDialog() +{ + const FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked("ContentBrowser"); + + // Holds current required parent selection (the params asset) + TSharedPtr CurrentSelection = MakeShared(); + + FAssetPickerConfig ParamsPickerConfig; + ParamsPickerConfig.Filter.ClassPaths.Add(UFlowAssetParams::StaticClass()->GetClassPathName()); + ParamsPickerConfig.InitialAssetViewType = EAssetViewType::List; + ParamsPickerConfig.SelectionMode = ESelectionMode::Single; + ParamsPickerConfig.bAllowNullSelection = false; + ParamsPickerConfig.bFocusSearchBoxWhenOpened = true; + + ParamsPickerConfig.OnAssetSelected = FOnAssetSelected::CreateLambda( + [CurrentSelection](const FAssetData& AssetData) + { + *CurrentSelection = AssetData; + }); + + const TSharedRef ParamsPicker = ContentBrowserModule.Get().CreateAssetPicker(ParamsPickerConfig); + + bool bUserAccepted = false; + + TSharedPtr PickerWindow = SNew(SWindow) + .Title(LOCTEXT("CreateChildParamsTitle", "Create Flow Asset Params")) + .SizingRule(ESizingRule::UserSized) + .ClientSize(FVector2D(850, 600)) + .SupportsMinimize(false) + .SupportsMaximize(false); + + PickerWindow->SetContent( + SNew(SBorder) + .Padding(8.f) + [ + SNew(SVerticalBox) + + // Parent picker + + SVerticalBox::Slot() + .FillHeight(1.0f) + .Padding(0.f, 0.f, 0.f, 8.f) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(STextBlock) + .Text(LOCTEXT("CreateChildParamsHelp", "Choose Parent Flow Asset Params:\n")) + ] + + SVerticalBox::Slot() + .FillHeight(1.0f) + [ + ParamsPicker + ] + ] + + // Buttons + + SVerticalBox::Slot() + .AutoHeight() + .HAlign(HAlign_Right) + [ + SNew(SHorizontalBox) + + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(0.f, 0.f, 6.f, 0.f) + [ + SNew(SButton) + .Text(LOCTEXT("OK", "OK")) + .IsEnabled_Lambda([CurrentSelection]() + { + return CurrentSelection->IsValid(); + }) + .OnClicked_Lambda([&bUserAccepted, PickerWindow]() + { + bUserAccepted = true; + PickerWindow->RequestDestroyWindow(); + return FReply::Handled(); + }) + ] + + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SButton) + .Text(LOCTEXT("Cancel", "Cancel")) + .OnClicked_Lambda([PickerWindow]() + { + PickerWindow->RequestDestroyWindow(); + return FReply::Handled(); + }) + ] + ] + ] + ); + + FSlateApplication::Get().AddModalWindow(PickerWindow.ToSharedRef(), nullptr); + + if (!bUserAccepted || !CurrentSelection->IsValid()) + { + return false; + } + + SelectedParentParams = TSoftObjectPtr(CurrentSelection->ToSoftObjectPath()); + if (SelectedParentParams.IsNull()) + { + FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("NoParentSelected", "You must select a parent Flow Asset Params asset.")); + + return false; + } + + return true; +} + +UObject* UFlowAssetParamsFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + if (SelectedParentParams.IsNull()) + { + FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("FactoryMissingParent", "No parent params were selected.")); + + return nullptr; + } + + UFlowAssetParams* Parent = SelectedParentParams.LoadSynchronous(); + if (!IsValid(Parent)) + { + FMessageDialog::Open(EAppMsgType::Ok, + FText::Format(LOCTEXT("ParentLoadFail", "Failed to load selected parent params:\n{0}"), + FText::FromString(SelectedParentParams.ToString()))); + + return nullptr; + } + + FText FailureReason; + constexpr bool bShowDialogs = true; + UFlowAssetParams* NewParams = FFlowAssetParamsUtils::CreateChildParamsAsset(*Parent, bShowDialogs, &FailureReason); + + // FactoryCreateNew expects the created asset (or nullptr). The helper already shows dialogs/logs on failure. + return NewParams; +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp b/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp index 99259929d..af262a4e6 100644 --- a/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp +++ b/Source/FlowEditor/Private/Asset/FlowAssetToolbar.cpp @@ -1,14 +1,20 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #include "Asset/FlowAssetToolbar.h" + +#include "Graph/FlowGraphUtils.h" #include "Asset/FlowAssetEditor.h" +#include "Asset/FlowAssetEditorContext.h" +#include "Asset/SAssetRevisionMenu.h" #include "FlowEditorCommands.h" #include "FlowAsset.h" +#include "Nodes/Graph/FlowNode_SubGraph.h" -#include "EditorStyleSet.h" +#include "Brushes/SlateRoundedBoxBrush.h" #include "Kismet2/DebuggerCommands.h" #include "Misc/Attribute.h" +#include "Misc/MessageDialog.h" #include "Subsystems/AssetEditorSubsystem.h" #include "ToolMenu.h" #include "ToolMenuSection.h" @@ -16,28 +22,45 @@ #include "Widgets/SBoxPanel.h" #include "Widgets/Text/STextBlock.h" +#include "AssetToolsModule.h" +#include "ISourceControlModule.h" +#include "ISourceControlProvider.h" +#include "SourceControlHelpers.h" + #define LOCTEXT_NAMESPACE "FlowDebuggerToolbar" ////////////////////////////////////////////////////////////////////////// // Flow Asset Instance List FText SFlowAssetInstanceList::NoInstanceSelectedText = LOCTEXT("NoInstanceSelected", "No instance selected"); +FText SFlowAssetInstanceList::AllContextsText = LOCTEXT("All", "All"); void SFlowAssetInstanceList::Construct(const FArguments& InArgs, const TWeakObjectPtr InTemplateAsset) { TemplateAsset = InTemplateAsset; + if (TemplateAsset.IsValid()) { TemplateAsset->OnDebuggerRefresh().AddSP(this, &SFlowAssetInstanceList::RefreshInstances); RefreshInstances(); } - // create dropdown - SAssignNew(Dropdown, SComboBox>) - .OptionsSource(&InstanceNames) - .OnGenerateWidget(this, &SFlowAssetInstanceList::OnGenerateWidget) - .OnSelectionChanged(this, &SFlowAssetInstanceList::OnSelectionChanged) - .Visibility_Static(&FFlowAssetEditor::GetDebuggerVisibility) + ContextComboBox = SNew(SComboBox>) + .OptionsSource(&Contexts) + .Visibility(this, &SFlowAssetInstanceList::GetContextVisibility) + .OnGenerateWidget(this, &SFlowAssetInstanceList::OnGenerateContextWidget) + .OnSelectionChanged(this, &SFlowAssetInstanceList::OnContextSelectionChanged) + .ContentPadding(FMargin(0.f, 2.f)) + [ + SNew(STextBlock) + .Text(this, &SFlowAssetInstanceList::GetSelectedContextName) + ]; + + InstanceComboBox = SNew(SComboBox>) + .OptionsSource(&Instances) + .OnGenerateWidget(this, &SFlowAssetInstanceList::OnGenerateInstanceWidget) + .OnSelectionChanged(this, &SFlowAssetInstanceList::OnInstanceSelectionChanged) + .ContentPadding(FMargin(0.f, 2.f)) [ SNew(STextBlock) .Text(this, &SFlowAssetInstanceList::GetSelectedInstanceName) @@ -45,7 +68,20 @@ void SFlowAssetInstanceList::Construct(const FArguments& InArgs, const TWeakObje ChildSlot [ - Dropdown.ToSharedRef() + SNew(SHorizontalBox) + .Visibility_Static(&SFlowAssetInstanceList::GetDebuggerVisibility) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(0.0f, 0.0f, 8.0f, 0.0f) + [ + ContextComboBox.ToSharedRef() + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(0.0f, 0.0f, 4.0f, 0.0f) + [ + InstanceComboBox.ToSharedRef() + ] ]; } @@ -57,54 +93,172 @@ SFlowAssetInstanceList::~SFlowAssetInstanceList() } } +EVisibility SFlowAssetInstanceList::GetDebuggerVisibility() +{ + return GEditor->PlayWorld ? EVisibility::Visible : EVisibility::Collapsed; +} + void SFlowAssetInstanceList::RefreshInstances() { - // collect instance names of this Flow Asset - InstanceNames = {MakeShareable(new FName(*NoInstanceSelectedText.ToString()))}; - TemplateAsset->GetInstanceDisplayNames(InstanceNames); + Contexts.Empty(); + Instances.Empty(); + InstancesPerContext.Empty(); - // select instance - if (const UFlowAsset* InspectedInstance = TemplateAsset->GetInspectedInstance()) + if (GEditor->ShouldEndPlayMap()) + { + // prevent redundant refreshing list for every asset instance being destroyed + return; + } + + // add empty context, users sees this as the default "All" option + NoContext = MakeShareable(new FObjectKey(nullptr)); + Contexts.Add(NoContext); + + // add empty instance as default + Instances.Add(MakeShareable(new FObjectKey(nullptr))); + + // gather all instances of given UFlowAsset + for (const UFlowAsset* ActiveInstance : TemplateAsset->GetActiveInstances()) { - const FName& InspectedInstanceName = InspectedInstance->GetDisplayName(); - for (const TSharedPtr& Instance : InstanceNames) + Instances.Add(MakeShareable(new FObjectKey(ActiveInstance))); + + // support World context in case of online multiplayer { - if (*Instance == InspectedInstanceName) + const UWorld* World = ActiveInstance->GetWorld(); + if (World && !InstancesPerContext.Contains(World)) { - SelectedInstance = Instance; - break; + FText WorldName = FText::FromString(GetDebugStringForWorld(World)); + Contexts.Add(MakeShareable(new FObjectKey(World))); + InstancesPerContext.Add(World, FFlowAssetInstanceContext(WorldName)); + } + + if (FFlowAssetInstanceContext* FoundContext = InstancesPerContext.Find(World)) + { + FoundContext->AssetInstances.Add(Instances.Last()); } } + + // todo: support Local Player context in case of split-screen } - else + + // set empty context by default, user must choose a specific context + if (!SelectedContext.IsValid() || !Contexts.Contains(SelectedContext)) { - // default object is always available - SelectedInstance = InstanceNames[0]; + SelectedContext = NoContext; } + + // pre-select instance if current one does no longer exists + if (!SelectedInstance.IsValid() || !Instances.Contains(SelectedInstance)) + { + if (Instances.Num() > 1) + { + if (SelectedContext->ResolveObjectPtr()) + { + // try to set first Instance for a selected context + const FFlowAssetInstanceContext* InstanceContext = InstancesPerContext.Find(*SelectedContext.Get()); + if (InstanceContext && InstanceContext->AssetInstances.Num() > 0) + { + SelectedInstance = InstanceContext->AssetInstances[0]; + } + } + else + { + // set first active instance for any context + SelectedInstance = Instances[1]; + } + } + else + { + // set empty instance if there's no active instances + SelectedInstance = Instances[0]; + } + } +} + +EVisibility SFlowAssetInstanceList::GetContextVisibility() const +{ + // switching makes sense only if we have at least 1 specific context + return InstancesPerContext.Num() > 1 ? EVisibility::Visible : EVisibility::Collapsed; } -TSharedRef SFlowAssetInstanceList::OnGenerateWidget(const TSharedPtr Item) const +TSharedRef SFlowAssetInstanceList::OnGenerateContextWidget(TSharedPtr Item) { - return SNew(STextBlock).Text(FText::FromName(*Item.Get())); + const FFlowAssetInstanceContext* Context = InstancesPerContext.Find(Item->ResolveObjectPtr()); + return SNew(STextBlock).Text(Context ? Context->DisplayText : AllContextsText); } -void SFlowAssetInstanceList::OnSelectionChanged(const TSharedPtr SelectedItem, const ESelectInfo::Type SelectionType) +void SFlowAssetInstanceList::OnContextSelectionChanged(TSharedPtr SelectedItem, ESelectInfo::Type SelectionType) +{ + if (SelectionType != ESelectInfo::Direct) + { + SelectedContext = SelectedItem; + + if (TemplateAsset.IsValid()) + { + TemplateAsset->SetInspectedInstance(nullptr); + } + } +} + +FText SFlowAssetInstanceList::GetSelectedContextName() const +{ + if (SelectedContext.IsValid()) + { + const UObject* Context = SelectedContext->ResolveObjectPtr(); + if (const FFlowAssetInstanceContext* InstanceContext = InstancesPerContext.Find(Context)) + { + return InstanceContext->DisplayText; + } + } + + return AllContextsText; +} + +TSharedRef SFlowAssetInstanceList::OnGenerateInstanceWidget(const TSharedPtr Item) const +{ + return SNew(STextBlock).Text(JoinInstanceAndContextTexts(*Item.Get())); +} + +void SFlowAssetInstanceList::OnInstanceSelectionChanged(const TSharedPtr SelectedItem, const ESelectInfo::Type SelectionType) { if (SelectionType != ESelectInfo::Direct) { SelectedInstance = SelectedItem; + const UFlowAsset* Instance = Cast(SelectedInstance->ResolveObjectPtr()); if (TemplateAsset.IsValid()) { - const FName NewSelectedInstanceName = (SelectedInstance.IsValid() && *SelectedInstance != *InstanceNames[0]) ? *SelectedInstance : NAME_None; - TemplateAsset->SetInspectedInstance(NewSelectedInstanceName); + TemplateAsset->SetInspectedInstance(Instance); } } } FText SFlowAssetInstanceList::GetSelectedInstanceName() const { - return SelectedInstance.IsValid() ? FText::FromName(*SelectedInstance) : NoInstanceSelectedText; + return SelectedInstance.IsValid() ? JoinInstanceAndContextTexts(*SelectedInstance.Get()) : NoInstanceSelectedText; +} + +FText SFlowAssetInstanceList::JoinInstanceAndContextTexts(const FObjectKey& AssetInstance) const +{ + if (const UFlowAsset* Instance = Cast(AssetInstance.ResolveObjectPtr())) + { + FText Result = FText::FromName(Instance->GetFName()); + + // add context name if there are multiple contexts present + if (InstancesPerContext.Num() > 1) + { + if (const FFlowAssetInstanceContext* Context = InstancesPerContext.Find(Instance->GetWorld())) + { + static const FText OpeningBracket = FText::AsCultureInvariant(TEXT("(")); + static const FText ClosingBracket = FText::AsCultureInvariant(TEXT(")")); + Result = FText::Format(Result, OpeningBracket, Context->DisplayText, ClosingBracket); + } + } + + return Result; + } + + return NoInstanceSelectedText; } ////////////////////////////////////////////////////////////////////////// @@ -116,60 +270,82 @@ void SFlowAssetBreadcrumb::Construct(const FArguments& InArgs, const TWeakObject // create breadcrumb SAssignNew(BreadcrumbTrail, SBreadcrumbTrail) - .OnCrumbClicked(this, &SFlowAssetBreadcrumb::OnCrumbClicked) - .Visibility_Static(&FFlowAssetEditor::GetDebuggerVisibility) - .ButtonStyle(FEditorStyle::Get(), "FlatButton") - .DelimiterImage(FEditorStyle::GetBrush("Sequencer.BreadcrumbIcon")) - .PersistentBreadcrumbs(true) - .TextStyle(FEditorStyle::Get(), "Sequencer.BreadcrumbText"); + .Visibility_Static(&SFlowAssetInstanceList::GetDebuggerVisibility) + .OnCrumbClicked(this, &SFlowAssetBreadcrumb::OnCrumbClicked) + .ButtonStyle(FAppStyle::Get(), "SimpleButton") + .TextStyle(FAppStyle::Get(), "NormalText") + .ButtonContentPadding(FMargin(2.0f, 4.0f)) + .DelimiterImage(FAppStyle::GetBrush("Icons.ChevronRight")) + .ShowLeadingDelimiter(true) + .PersistentBreadcrumbs(true); ChildSlot [ - SNew(SVerticalBox) - + SVerticalBox::Slot() - .HAlign(HAlign_Right) - .VAlign(VAlign_Center) - .AutoHeight() - .Padding(25.0f, 10.0f) + SNew(SBorder) + .Visibility(this, &SFlowAssetBreadcrumb::GetBreadcrumbVisibility) + .BorderImage(new FSlateRoundedBoxBrush(FStyleColors::Transparent, 4, FStyleColors::InputOutline, 1)) [ - BreadcrumbTrail.ToSharedRef() + SNew(SBox) + .MaxDesiredWidth(500.f) + [ + BreadcrumbTrail.ToSharedRef() + ] ] ]; - // fill breadcrumb + TemplateAsset->OnDebuggerRefresh().AddSP(this, &SFlowAssetBreadcrumb::FillBreadcrumb); + FillBreadcrumb(); +} + +EVisibility SFlowAssetBreadcrumb::GetBreadcrumbVisibility() const +{ + return GEditor->PlayWorld && TemplateAsset->GetInspectedInstance() ? EVisibility::Visible : EVisibility::Collapsed; +} + +void SFlowAssetBreadcrumb::FillBreadcrumb() const +{ BreadcrumbTrail->ClearCrumbs(); - if (UFlowAsset* InspectedInstance = TemplateAsset->GetInspectedInstance()) + if (const UFlowAsset* InspectedInstance = TemplateAsset->GetInspectedInstance()) { - TArray InstancesFromRoot = {InspectedInstance}; + TArray> InstancesFromRoot = {InspectedInstance}; const UFlowAsset* CheckedInstance = InspectedInstance; - while (UFlowAsset* MasterInstance = CheckedInstance->GetMasterInstance()) + while (UFlowAsset* ParentInstance = CheckedInstance->GetParentInstance()) { - InstancesFromRoot.Insert(MasterInstance, 0); - CheckedInstance = MasterInstance; + InstancesFromRoot.Insert(ParentInstance, 0); + CheckedInstance = ParentInstance; } - for (UFlowAsset* Instance : InstancesFromRoot) + for (int32 Index = 0; Index < InstancesFromRoot.Num(); Index++) { - TAttribute CrumbNameAttribute = MakeAttributeSP(this, &SFlowAssetBreadcrumb::GetBreadcrumbText, Instance); - BreadcrumbTrail->PushCrumb(CrumbNameAttribute, FFlowBreadcrumb(Instance)); + TWeakObjectPtr Instance = InstancesFromRoot[Index]; + TWeakObjectPtr ChildInstance = Index < InstancesFromRoot.Num() - 1 ? InstancesFromRoot[Index + 1] : nullptr; + + BreadcrumbTrail->PushCrumb(FText::FromName(Instance->GetFName()), FFlowBreadcrumb(Instance, ChildInstance)); } } } void SFlowAssetBreadcrumb::OnCrumbClicked(const FFlowBreadcrumb& Item) const { - ensure(TemplateAsset->GetInspectedInstance()); - - if (Item.InstanceName != TemplateAsset->GetInspectedInstance()->GetDisplayName()) + const UFlowAsset* InspectedInstance = TemplateAsset->GetInspectedInstance(); + if (InspectedInstance == nullptr || Item.CurrentInstance != TemplateAsset) { - GEditor->GetEditorSubsystem()->OpenEditorForAsset(Item.AssetPathName); - } -} + const TWeakObjectPtr ClickedInstance = Item.CurrentInstance; + UFlowAsset* ClickedTemplateAsset = ClickedInstance->GetTemplateAsset(); -FText SFlowAssetBreadcrumb::GetBreadcrumbText(const TWeakObjectPtr FlowInstance) const -{ - return FlowInstance.IsValid() ? FText::FromName(FlowInstance->GetDisplayName()) : FText(); + if (GEditor->GetEditorSubsystem()->OpenEditorForAsset(ClickedTemplateAsset)) + { + ClickedTemplateAsset->SetInspectedInstance(ClickedInstance); + if (const TSharedPtr FlowAssetEditor = FFlowGraphUtils::GetFlowAssetEditor(ClickedTemplateAsset)) + { + if (Item.ChildInstance.IsValid()) + { + FlowAssetEditor->JumpToNode(Item.ChildInstance->GetNodeOwningThisAssetInstance()->GetGraphNode()); + } + } + } + } } ////////////////////////////////////////////////////////////////////////// @@ -184,28 +360,139 @@ FFlowAssetToolbar::FFlowAssetToolbar(const TSharedPtr InAssetE void FFlowAssetToolbar::BuildAssetToolbar(UToolMenu* ToolbarMenu) const { - FToolMenuSection& Section = ToolbarMenu->AddSection("Editing"); - Section.InsertPosition = FToolMenuInsert("Asset", EToolMenuInsertType::After); - - Section.AddEntry(FToolMenuEntry::InitToolBarButton(FFlowToolbarCommands::Get().RefreshAsset)); + { + FToolMenuSection& Section = ToolbarMenu->AddSection("FlowAsset"); + Section.InsertPosition = FToolMenuInsert("Asset", EToolMenuInsertType::After); + + // add buttons + Section.AddEntry(FToolMenuEntry::InitToolBarButton(FFlowToolbarCommands::Get().RefreshAsset)); + Section.AddEntry(FToolMenuEntry::InitToolBarButton(FFlowToolbarCommands::Get().ValidateAsset)); + Section.AddEntry(FToolMenuEntry::InitToolBarButton(FFlowToolbarCommands::Get().EditAssetDefaults)); + } + + { + FToolMenuSection& Section = ToolbarMenu->AddSection("View"); + Section.InsertPosition = FToolMenuInsert("FlowAsset", EToolMenuInsertType::After); + + // Visual Diff: menu to choose asset revision compared with the current one + Section.AddDynamicEntry("SourceControlCommands", FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection) + { + const UFlowAssetEditorContext* Context = InSection.FindContext(); + if (Context && Context->FlowAssetEditor.IsValid()) + { + InSection.InsertPosition = FToolMenuInsert(); + FToolMenuEntry DiffEntry = FToolMenuEntry::InitComboButton( + "Diff", + FUIAction(), + FOnGetContent::CreateStatic(&FFlowAssetToolbar::MakeDiffMenu, Context), + LOCTEXT("Diff", "Diff"), + LOCTEXT("FlowAssetEditorDiffToolTip", "Diff against previous revisions"), + FSlateIcon(FAppStyle::Get().GetStyleSetName(), "BlueprintDiff.ToolbarIcon") + ); + DiffEntry.StyleNameOverride = "CalloutToolbar"; + InSection.AddEntry(DiffEntry); + } + })); + + Section.AddEntry(FToolMenuEntry::InitToolBarButton( + FFlowToolbarCommands::Get().SearchInAsset, + TAttribute(), + TAttribute(), + FSlateIcon(FAppStyle::GetAppStyleSetName(), "Kismet.Tabs.FindResults") + )); + } +} + +/** Delegate called to diff a specific revision with the current */ +// Copy from FBlueprintEditorToolbar::OnDiffRevisionPicked +static void OnDiffRevisionPicked(FRevisionInfo const& RevisionInfo, const FString& Filename, TWeakObjectPtr CurrentAsset) +{ + ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); + + // Get the SCC state + const FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(Filename, EStateCacheUsage::Use); + if (SourceControlState.IsValid()) + { + for (int32 HistoryIndex = 0; HistoryIndex < SourceControlState->GetHistorySize(); HistoryIndex++) + { + TSharedPtr Revision = SourceControlState->GetHistoryItem(HistoryIndex); + check(Revision.IsValid()); + if (Revision->GetRevision() == RevisionInfo.Revision) + { + // Get the revision of this package from source control + FString PreviousTempPkgName; + if (Revision->Get(PreviousTempPkgName)) + { + // Try and load that package + UPackage* PreviousTempPkg = LoadPackage(nullptr, *PreviousTempPkgName, LOAD_ForDiff | LOAD_DisableCompileOnLoad); + if (PreviousTempPkg) + { + const FString PreviousAssetName = FPaths::GetBaseFilename(Filename, true); + UObject* PreviousAsset = FindObject(PreviousTempPkg, *PreviousAssetName); + if (PreviousAsset) + { + const FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked(TEXT("AssetTools")); + const FRevisionInfo OldRevision = {Revision->GetRevision(), Revision->GetCheckInIdentifier(), Revision->GetDate()}; + const FRevisionInfo CurrentRevision = {TEXT(""), Revision->GetCheckInIdentifier(), Revision->GetDate()}; + AssetToolsModule.Get().DiffAssets(PreviousAsset, CurrentAsset.Get(), OldRevision, CurrentRevision); + } + } + else + { + FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("UnableToLoadAssets", "Unable to load assets to diff. Content may no longer be supported?")); + } + } + break; + } + } + } } -void FFlowAssetToolbar::BuildDebuggerToolbar(UToolMenu* ToolbarMenu) +// Variant of FBlueprintEditorToolbar::MakeDiffMenu +TSharedRef FFlowAssetToolbar::MakeDiffMenu(const UFlowAssetEditorContext* Context) { - FToolMenuSection& Section = ToolbarMenu->AddSection("Debugging"); - Section.InsertPosition = FToolMenuInsert("Asset", EToolMenuInsertType::After); - - FPlayWorldCommands::BuildToolbar(Section); + if (ISourceControlModule::Get().IsEnabled() && ISourceControlModule::Get().GetProvider().IsAvailable()) + { + UFlowAsset* FlowAsset = Context ? Context->FlowAssetEditor.Pin()->GetFlowAsset() : nullptr; + if (FlowAsset) + { + FString Filename = SourceControlHelpers::PackageFilename(FlowAsset->GetPathName()); + TWeakObjectPtr AssetPtr = FlowAsset; - TWeakObjectPtr TemplateAsset = FlowAssetEditor.Pin()->GetFlowAsset(); - - AssetInstanceList = SNew(SFlowAssetInstanceList, TemplateAsset); - Section.AddEntry(FToolMenuEntry::InitWidget("AssetInstances", AssetInstanceList.ToSharedRef(), FText(), true)); + // Add our async SCC task widget + return SNew(SAssetRevisionMenu, Filename) + .OnRevisionSelected_Static(&OnDiffRevisionPicked, AssetPtr); + } + else + { + // if asset is null then this means that multiple assets are selected + FMenuBuilder MenuBuilder(true, nullptr); + MenuBuilder.AddMenuEntry(LOCTEXT("NoRevisionsForMultipleFlowAssets", "Multiple Flow Assets selected"), FText(), FSlateIcon(), FUIAction()); + return MenuBuilder.MakeWidget(); + } + } - Section.AddEntry(FToolMenuEntry::InitToolBarButton(FFlowToolbarCommands::Get().GoToMasterInstance)); + FMenuBuilder MenuBuilder(true, nullptr); + MenuBuilder.AddMenuEntry(LOCTEXT("SourceControlDisabled", "Source control is disabled"), FText(), FSlateIcon(), FUIAction()); + return MenuBuilder.MakeWidget(); +} + +void FFlowAssetToolbar::BuildDebuggerToolbar(UToolMenu* ToolbarMenu) const +{ + FToolMenuSection& Section = ToolbarMenu->AddSection("Debug"); + Section.InsertPosition = FToolMenuInsert("View", EToolMenuInsertType::After); - Breadcrumb = SNew(SFlowAssetBreadcrumb, TemplateAsset); - Section.AddEntry(FToolMenuEntry::InitWidget("AssetBreadcrumb", Breadcrumb.ToSharedRef(), FText(), true)); + Section.AddDynamicEntry("DebuggingCommands", FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection) + { + const UFlowAssetEditorContext* Context = InSection.FindContext(); + if (Context && Context->GetFlowAsset()) + { + FPlayWorldCommands::BuildToolbar(InSection); + + InSection.AddEntry(FToolMenuEntry::InitWidget("AssetInstances", SNew(SFlowAssetInstanceList, Context->GetFlowAsset()), FText(), true)); + InSection.AddEntry(FToolMenuEntry::InitWidget("AssetBreadcrumb", SNew(SFlowAssetBreadcrumb, Context->GetFlowAsset()), FText(), true)); + } + })); } #undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp b/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp new file mode 100644 index 000000000..eed0440a5 --- /dev/null +++ b/Source/FlowEditor/Private/Asset/FlowDebugEditorSubsystem.cpp @@ -0,0 +1,257 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/FlowDebugEditorSubsystem.h" +#include "Asset/FlowAssetEditor.h" +#include "Asset/FlowMessageLogListing.h" +#include "Graph/FlowGraph.h" +#include "Graph/FlowGraphEditor.h" +#include "Graph/FlowGraphUtils.h" +#include "Graph/Nodes/FlowGraphNode.h" +#include "Interfaces/FlowExecutionGate.h" +#include "FlowAsset.h" +#include "FlowSubsystem.h" + +#include "Editor/UnrealEdEngine.h" +#include "Engine/Engine.h" +#include "Engine/World.h" +#include "Framework/Notifications/NotificationManager.h" +#include "Subsystems/AssetEditorSubsystem.h" +#include "Templates/Function.h" +#include "UnrealEdGlobals.h" +#include "Widgets/Notifications/SNotificationList.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowDebugEditorSubsystem) + +#define LOCTEXT_NAMESPACE "FlowDebugEditorSubsystem" + +UFlowDebugEditorSubsystem::UFlowDebugEditorSubsystem() +{ + FEditorDelegates::BeginPIE.AddUObject(this, &ThisClass::OnBeginPIE); + FEditorDelegates::ResumePIE.AddUObject(this, &ThisClass::OnResumePIE); + FEditorDelegates::EndPIE.AddUObject(this, &ThisClass::OnEndPIE); + + OnDebuggerBreakpointHit.AddUObject(this, &ThisClass::OnBreakpointHit); +} + +void UFlowDebugEditorSubsystem::OnInstancedTemplateAdded(UFlowAsset* AssetTemplate) +{ + Super::OnInstancedTemplateAdded(AssetTemplate); + + if (!RuntimeLogs.Contains(AssetTemplate)) + { + RuntimeLogs.Add(AssetTemplate, FFlowMessageLogListing::GetLogListing(AssetTemplate, EFlowLogType::Runtime)); + AssetTemplate->OnRuntimeMessageAdded().AddUObject(this, &UFlowDebugEditorSubsystem::OnRuntimeMessageAdded); + } +} + +void UFlowDebugEditorSubsystem::OnInstancedTemplateRemoved(UFlowAsset* AssetTemplate) +{ + AssetTemplate->OnRuntimeMessageAdded().RemoveAll(this); + + Super::OnInstancedTemplateRemoved(AssetTemplate); +} + +void UFlowDebugEditorSubsystem::OnRuntimeMessageAdded(const UFlowAsset* AssetTemplate, const TSharedRef& Message) const +{ + const TSharedPtr Log = RuntimeLogs.FindRef(AssetTemplate); + if (Log.IsValid()) + { + Log->AddMessage(Message); + Log->OnDataChanged().Broadcast(); + } +} + +void UFlowDebugEditorSubsystem::OnBeginPIE(const bool bIsSimulating) +{ + // Clear all logs from a previous session + RuntimeLogs.Empty(); + + // Clear any stale "hit" state from previous run + ClearHitBreakpoints(); +} + +void UFlowDebugEditorSubsystem::OnResumePIE(const bool bIsSimulating) +{ + // Editor-level resume event (also used by Advance Single Frame). + // This does not necessarily flow through AGameModeBase::ClearPause(), so we must unhalt Flow here. + ClearLastHitBreakpoint(); + + if (HaltedOnFlowAssetInstance.IsValid()) + { + ResumeSession(*HaltedOnFlowAssetInstance.Get()); + } +} + +void UFlowDebugEditorSubsystem::OnEndPIE(const bool bIsSimulating) +{ + // Ensure we don't carry over a halted state between PIE sessions. + ClearHitBreakpoints(); + + StopSession(); + + for (const TPair, TSharedPtr>& Log : RuntimeLogs) + { + if (Log.Key.IsValid() && Log.Value->NumMessages(EMessageSeverity::Warning) > 0) + { + FNotificationInfo Info{FText::FromString(TEXT("Flow Graph reported in-game issues"))}; + Info.ExpireDuration = 15.0; + + Info.HyperlinkText = FText::Format(LOCTEXT("OpenFlowAssetHyperlink", "Open {0}"), FText::FromString(Log.Key->GetName())); + Info.Hyperlink = FSimpleDelegate::CreateLambda([this, Log]() + { + UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem(); + if (AssetEditorSubsystem->OpenEditorForAsset(Log.Key.Get())) + { + AssetEditorSubsystem->FindEditorForAsset(Log.Key.Get(), true)->InvokeTab(FFlowAssetEditor::RuntimeLogTab); + } + }); + + const TSharedPtr Notification = FSlateNotificationManager::Get().AddNotification(Info); + if (Notification.IsValid()) + { + Notification->SetCompletionState(SNotificationItem::CS_Fail); + } + } + } +} + +void UFlowDebugEditorSubsystem::PauseSession(UFlowAsset& FlowAssetInstance) +{ + HaltedOnFlowAssetInstance = &FlowAssetInstance; + + Super::PauseSession(FlowAssetInstance); +} + +void UFlowDebugEditorSubsystem::ResumeSession(UFlowAsset& FlowAssetInstance) +{ + HaltedOnFlowAssetInstance = &FlowAssetInstance; + + Super::ResumeSession(FlowAssetInstance); +} + +void UFlowDebugEditorSubsystem::StopSession() +{ + // Drop any pending deferred triggers — we are stopping the session entirely + if (HaltedOnFlowAssetInstance.IsValid()) + { + UFlowSubsystem* FlowSubsystem = HaltedOnFlowAssetInstance->GetFlowSubsystem(); + + if (IsValid(FlowSubsystem)) + { + FlowSubsystem->ClearAllDeferredTriggerScopes(); + } + } + + HaltedOnFlowAssetInstance.Reset(); + + Super::StopSession(); +} + +void UFlowDebugEditorSubsystem::OnFlowDebuggerStateChanged(EFlowDebuggerState PrevState, EFlowDebuggerState NextState, UFlowAsset* FlowAssetInstance) +{ + check(PrevState != NextState); + + using namespace EFlowDebuggerState_Classifiers; + + const bool bIsPausedGameStatePrev = IsPausedGameState(PrevState); + const bool bIsPausedGameStateNext = IsPausedGameState(NextState); + + // Handle Pause/Unpause of the game & pie systems + if (bIsPausedGameStatePrev != bIsPausedGameStateNext) + { + const bool bWasPaused = GUnrealEd->SetPIEWorldsPaused(bIsPausedGameStateNext); + + if (bIsPausedGameStateNext && !bWasPaused) + { + GUnrealEd->PlaySessionPaused(); + } + else if (!bIsPausedGameStateNext && bWasPaused) + { + GUnrealEd->PlaySessionResumed(); + } + } + + // Issue the broadcasts for specific state entry + FLOW_ASSERT_ENUM_MAX(EFlowDebuggerState, 3); + if (NextState == EFlowDebuggerState::Paused) + { + OnDebuggerPaused.Broadcast(*FlowAssetInstance); + } + else if (NextState == EFlowDebuggerState::Resumed) + { + OnDebuggerResumed.Broadcast(*FlowAssetInstance); + } + + UFlowSubsystem* FlowSubsystem = + IsValid(FlowAssetInstance) ? + FlowAssetInstance->GetFlowSubsystem() : + nullptr; + + if (FlowSubsystem && IsFlushDeferredTriggersState(NextState)) + { + // Flush any deferred triggers now that halt is cleared. + FlowSubsystem->TryFlushAllDeferredTriggerScopes(); + + // NOTE (gtaylor) this flush needs to be the last thing we do in this function + // (thus the explicit return to emphasize it), as this flush can be interrupted by another breakpoint + return; + } +} + +void UFlowDebugEditorSubsystem::OnBreakpointHit(const UFlowNode* FlowNode) const +{ + UFlowAsset* TemplateAsset = const_cast(FlowNode->GetFlowAsset()->GetTemplateAsset()); + if (!IsValid(TemplateAsset)) + { + return; + } + + UAssetEditorSubsystem* AssetEditorSubsystem = GEditor ? GEditor->GetEditorSubsystem() : nullptr; + if (!AssetEditorSubsystem) + { + return; + } + + if (!AssetEditorSubsystem->OpenEditorForAsset(TemplateAsset)) + { + return; + } + + TemplateAsset->SetInspectedInstance(FlowNode->GetFlowAsset()); + + UFlowGraph* FlowGraph = Cast(TemplateAsset->GetGraph()); + if (!IsValid(FlowGraph)) + { + return; + } + + // NOTE: This may be redundant call, but it ensures Slate re-queries breakpoint hit state and updates node overlays immediately. + FlowGraph->NotifyGraphChanged(); + + UEdGraphNode* NodeToFocus = nullptr; + for (UEdGraphNode* Node : FlowGraph->Nodes) + { + UFlowGraphNode* FlowGraphNode = Cast(Node); + if (IsValid(FlowGraphNode) && FlowGraphNode->NodeGuid == FlowNode->NodeGuid) + { + NodeToFocus = FlowGraphNode; + break; + } + } + + if (!NodeToFocus) + { + return; + } + + const TSharedPtr GraphEditor = FFlowGraphUtils::GetFlowGraphEditor(FlowGraph); + if (GraphEditor.IsValid()) + { + constexpr bool bRequestRename = false; + constexpr bool bSelectNode = true; + + GraphEditor->JumpToNode(NodeToFocus, bRequestRename, bSelectNode); + } +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/FlowEditor/Private/Asset/FlowDebugger.cpp b/Source/FlowEditor/Private/Asset/FlowDebugger.cpp deleted file mode 100644 index f7e2aea37..000000000 --- a/Source/FlowEditor/Private/Asset/FlowDebugger.cpp +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - -#include "Asset/FlowDebugger.h" - -#include "Engine/Engine.h" -#include "Engine/World.h" -#include "Templates/Function.h" -#include "UnrealEd.h" - -FFlowDebugger::FFlowDebugger() -{ -} - -FFlowDebugger::~FFlowDebugger() -{ -} - -void ForEachGameWorld(const TFunction& Func) -{ - for (const FWorldContext& PieContext : GUnrealEd->GetWorldContexts()) - { - UWorld* PlayWorld = PieContext.World(); - if (PlayWorld && PlayWorld->IsGameWorld()) - { - Func(PlayWorld); - } - } -} - -bool AreAllGameWorldPaused() -{ - bool bPaused = true; - ForEachGameWorld([&](const UWorld* World) - { - bPaused = bPaused && World->bDebugPauseExecution; - }); - return bPaused; -} - -void FFlowDebugger::PausePlaySession() -{ - bool bPaused = false; - ForEachGameWorld([&](UWorld* World) - { - if (!World->bDebugPauseExecution) - { - World->bDebugPauseExecution = true; - bPaused = true; - } - }); - if (bPaused) - { - GUnrealEd->PlaySessionPaused(); - } -} - -bool FFlowDebugger::IsPlaySessionPaused() -{ - return AreAllGameWorldPaused(); -} diff --git a/Source/FlowEditor/Private/Asset/FlowDiffControl.cpp b/Source/FlowEditor/Private/Asset/FlowDiffControl.cpp new file mode 100644 index 000000000..c85a47c41 --- /dev/null +++ b/Source/FlowEditor/Private/Asset/FlowDiffControl.cpp @@ -0,0 +1,459 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/FlowDiffControl.h" +#include "Asset/SFlowDiff.h" + +#include "FlowAsset.h" + +#include "EdGraph/EdGraph.h" +#include "GraphDiffControl.h" +#include "Graph/Nodes/FlowGraphNode.h" +#include "SBlueprintDiff.h" + +#define LOCTEXT_NAMESPACE "SFlowDiffControl" + +namespace +{ + FString GetDiffKeyFromDiffResult(const FDiffResultItem& Difference) + { + if (Difference.Result.Pin1) + { + return Difference.Result.Pin1->PinId.ToString() + Difference.Result.Pin2->PinId.ToString(); + } + + const UFlowGraphNode* FlowGraphNode = Cast(Difference.Result.Node1); + if (!IsValid(FlowGraphNode)) + { + FlowGraphNode = Cast(Difference.Result.Node2); + } + + if (IsValid(FlowGraphNode) && IsValid(FlowGraphNode->GetNodeTemplate())) + { + return FString::FromInt(FlowGraphNode->GetNodeTemplate()->GetUniqueID()); //->GetName(); + } + + if (IsValid(Difference.Result.Node1)) + { + return FString::FromInt(Difference.Result.Node1->GetUniqueID()); + } + + if (IsValid(Difference.Result.Node2)) + { + return FString::FromInt(Difference.Result.Node2->GetUniqueID()); + } + + return TEXT("Invalid Diff!"); + } + + FString GetNodeNameFromDiffResult(const FDiffResultItem& Difference) + { + if (Difference.Result.Pin1) + { + return TEXT("Invalid Diff!"); + } + + const UFlowGraphNode* FlowGraphNode = Cast(Difference.Result.Node1); + if (!IsValid(FlowGraphNode)) + { + FlowGraphNode = Cast(Difference.Result.Node2); + } + + if (IsValid(FlowGraphNode) && IsValid(FlowGraphNode->GetNodeTemplate())) + { + return FlowGraphNode->GetNodeTemplate()->GetName(); + } + + if (IsValid(Difference.Result.Node1)) + { + return Difference.Result.Node1->GetName(); + } + + if (IsValid(Difference.Result.Node2)) + { + return Difference.Result.Node2->GetName(); + } + + return TEXT("Invalid Diff!"); + } + + void ModifyDiffDisplayName(FDiffSingleResult& InOutResult, const FString& DiffKey) + { + //Only modify the DisplayStrings of node changes, because they are not very descriptive. + //Other generated DisplayStrings seem fine, such as pin connection changes. + if ((IsValid(InOutResult.Node1) || IsValid(InOutResult.Node2)) + && InOutResult.Pin1 == nullptr) + { + FString AdditionalDescription; + switch (InOutResult.Category) + { + case EDiffType::Category::SUBTRACTION: + AdditionalDescription = TEXT("Removed "); + break; + case EDiffType::Category::ADDITION: + AdditionalDescription = TEXT("Added "); + break; + default: + break; + } + + InOutResult.DisplayString = FText::FromString(AdditionalDescription + DiffKey); + } + } +} + +///////////////////////////////////////////////////////////////////////////// +/// FFlowAssetDiffControl + +FFlowAssetDiffControl::FFlowAssetDiffControl(const UFlowAsset* InOldFlowAsset, const UFlowAsset* InNewFlowAsset, FOnDiffEntryFocused InSelectionCallback) + : FDetailsDiffControl(InOldFlowAsset, InNewFlowAsset, InSelectionCallback, false) +{ +} + +// TDetailsDiffControl::GenerateTreeEntries + "NoDifferences" entry + category label +void FFlowAssetDiffControl::GenerateTreeEntries(TArray>& OutTreeEntries, TArray>& OutRealDifferences) +{ + FDetailsDiffControl::GenerateTreeEntries(OutTreeEntries, OutRealDifferences); + + const bool bHasDifferences = Children.Num() != 0; + if (!bHasDifferences) + { + // make one child informing the user that there are no differences: + Children.Push(FBlueprintDifferenceTreeEntry::NoDifferencesEntry()); + } + + static const FText AssetPropertiesLabel = LOCTEXT("AssetPropertiesLabel", "Properties"); + static const FText AssetPropertiesTooltip = LOCTEXT("AssetPropertiesTooltip", "The list of changes made to Flow Asset properties."); + OutTreeEntries.Push(FBlueprintDifferenceTreeEntry::CreateCategoryEntry( + AssetPropertiesLabel, + AssetPropertiesTooltip, + SelectionCallback, + Children, + bHasDifferences + )); +} + +///////////////////////////////////////////////////////////////////////////// +/// FFlowGraphToDiff + +FFlowGraphToDiff::FFlowGraphToDiff(SFlowDiff* InDiffWidget, UEdGraph* InGraphOld, UEdGraph* InGraphNew, const FRevisionInfo& InRevisionOld, const FRevisionInfo& InRevisionNew) + : FoundDiffs(MakeShared>()) + , DiffWidget(InDiffWidget) + , GraphOld(InGraphOld) + , GraphNew(InGraphNew) + , RevisionOld(InRevisionOld) + , RevisionNew(InRevisionNew) +{ + check(InGraphOld || InGraphNew); + + if (InGraphNew) + { + OnGraphChangedDelegateHandle = InGraphNew->AddOnGraphChangedHandler(FOnGraphChanged::FDelegate::CreateRaw(this, &FFlowGraphToDiff::OnGraphChanged)); + } + + BuildDiffSourceArray(); +} + +FFlowGraphToDiff::~FFlowGraphToDiff() +{ + if (GraphNew) + { + GraphNew->RemoveOnGraphChangedHandler(OnGraphChangedDelegateHandle); + } +} + +ENodeDiffType FFlowGraphToDiff::GetNodeDiffType(const UEdGraphNode& Node) const +{ + if (IsValid(GraphOld) && Node.GetGraph() == GraphOld) + { + return ENodeDiffType::Old; + } + + if (IsValid(GraphNew) && Node.GetGraph() == GraphNew) + { + return ENodeDiffType::New; + } + + return ENodeDiffType::Invalid; +} + +TSharedPtr FFlowGraphToDiff::GetFlowObjectDiff(const FDiffResultItem& DiffResultItem) +{ + const FString DiffKey = GetDiffKeyFromDiffResult(DiffResultItem); + check(FlowObjectDiffsByNodeName.Contains(DiffKey)); + return *FlowObjectDiffsByNodeName.Find(DiffKey); +} + +void FFlowGraphToDiff::GenerateTreeEntries(TArray>& OutTreeEntries, TArray>& OutRealDifferences) +{ + if (!DiffListSource.IsEmpty()) + { + RealDifferencesStartIndex = OutRealDifferences.Num(); + } + + check(DiffWidget != nullptr); + TArray> Children; + + for (const TSharedPtr& Difference : DiffListSource) + { + FString DiffKey = GetDiffKeyFromDiffResult(*Difference.Get()); + const TSharedPtr* FlowNodeDiff = FlowObjectDiffsByNodeName.Find(DiffKey); + + //generate a FlowObjectDiff if not found + if (FlowNodeDiff == nullptr) + { + TSharedPtr GenerateNodeEntry = GenerateFlowObjectDiff(Difference); + FlowNodeDiff = &FlowObjectDiffsByNodeName.Add(DiffKey, GenerateNodeEntry); + } + + //defer generating certain child tree entries, so they can go at the bottom of the list. + if (Difference->Result.Diff == EDiffType::Type::NODE_MOVED + || Difference->Result.Diff == EDiffType::Type::NODE_COMMENT) + { + (*FlowNodeDiff)->LowPriorityChildDiffResult.Add(Difference); + } + + //Use the highest priority category for the parent diff for the purpose of color. + //Note that a lower category is a higher priority one. + //One issue is that every change (even moves or comments) trigger a "Modification" DiffResult, so we need to + //handle that special case separately. Only allow a "Modification" DiffResult to change colors if there is + //actually a property change, or if a sub node was added/removed. + if (Difference->Result.Category != EDiffType::Category::MODIFICATION + && (*FlowNodeDiff)->DiffResult->Result.Category > Difference->Result.Category) + { + (*FlowNodeDiff)->DiffResult->Result.Diff = Difference->Result.Diff; + (*FlowNodeDiff)->DiffResult->Result.Category = Difference->Result.Category; + } + } + + //loop through the generated FlowObjectDiffs to: + //a) add DiffTreeEntries to their correct place in the tree, + //b) generate property diffs, + //c) set DiffType and Category to change colors where appropriate. + for (const auto& Nodes : FlowObjectDiffsByNodeName) + { + TSharedPtr FlowNodeDiff = Nodes.Value; + OutRealDifferences.Push(FlowNodeDiff->DiffTreeEntry); + + UEdGraphNode* Node1 = FlowNodeDiff->DiffResult->Result.Node1; + //not a node diff + if (!IsValid(Node1)) + { + Children.Push(FlowNodeDiff->DiffTreeEntry); + continue; + } + + FlowNodeDiff->ParentNodeDiff = FindParentNode(Cast(Node1)); + if (FlowNodeDiff->ParentNodeDiff.IsValid()) + { + const TSharedPtr ParentNode = FlowNodeDiff->ParentNodeDiff.Pin(); + ParentNode->DiffTreeEntry->Children.Push(FlowNodeDiff->DiffTreeEntry); + + //if a parent has any sub node changes, make sure the color does not stay as "minor" + if (ParentNode->DiffResult->Result.Category > FlowNodeDiff->DiffResult->Result.Category) + { + ParentNode->DiffResult->Result.Diff = EDiffType::Type::NODE_PROPERTY; + ParentNode->DiffResult->Result.Category = EDiffType::Category::MODIFICATION; + } + } + else + { + Children.Push(FlowNodeDiff->DiffTreeEntry); + } + + //find and generate property diffs. + TArray PropertyDiffsArray; + FlowNodeDiff->DiffProperties(PropertyDiffsArray); + + //generate property diff tree entries. + for (const auto& PropertyDiffEntry : PropertyDiffsArray) + { + check(FlowNodeDiff.IsValid()); + TSharedPtr& FlowObjectPropertyDiff = FlowNodeDiff->PropertyDiffArgList.Add_GetRef(MakeShared(FlowNodeDiff, PropertyDiffEntry)); + + TSharedPtr ChildEntry = MakeShared( + FOnDiffEntryFocused::CreateSP(DiffWidget, &SFlowDiff::OnDiffListSelectionChanged, FlowObjectPropertyDiff), + FGenerateDiffEntryWidget::CreateStatic(&GenerateObjectDiffWidget, FlowObjectPropertyDiff->PropertyDiff, RightRevision)); + + Nodes.Value->DiffTreeEntry->Children.Push(ChildEntry); + OutRealDifferences.Push(ChildEntry); + } + + //if a property changed, allow it to change the color of it's parent tree entry. + if (PropertyDiffsArray.Num() > 0) + { + if (FlowNodeDiff->DiffResult->Result.Category > EDiffType::Category::MODIFICATION) + { + FlowNodeDiff->DiffResult->Result.Diff = EDiffType::Type::NODE_PROPERTY; + FlowNodeDiff->DiffResult->Result.Category = EDiffType::Category::MODIFICATION; + } + } + } + + //generate low priority child tree entries. This for loop should go after generating other tree entries, so that + //these lower priority child trees are at the bottom. + for (const auto& Nodes : FlowObjectDiffsByNodeName) + { + const TSharedPtr FlowNodeDiff = Nodes.Value; + + for (const auto& Difference : Nodes.Value->LowPriorityChildDiffResult) + { + TSharedPtr ChildEntry = MakeShared( + FOnDiffEntryFocused::CreateRaw(DiffWidget, &SFlowDiff::OnDiffListSelectionChanged, FlowNodeDiff->DiffEntryFocusArg), + FGenerateDiffEntryWidget::CreateSP(Difference.ToSharedRef(), &FDiffResultItem::GenerateWidget)); + + FlowNodeDiff->DiffTreeEntry->Children.Push(ChildEntry); + OutRealDifferences.Push(ChildEntry); + } + } + + if (Children.Num() == 0) + { + // make one child informing the user that there are no differences: + Children.Push(FBlueprintDifferenceTreeEntry::NoDifferencesEntry()); + } + + const TSharedPtr Entry = MakeShared( + FOnDiffEntryFocused::CreateRaw(DiffWidget, &SFlowDiff::OnGraphSelectionChanged, TSharedPtr(AsShared()), ESelectInfo::Direct), + FGenerateDiffEntryWidget::CreateSP(AsShared(), &FFlowGraphToDiff::GenerateCategoryWidget), + Children); + + OutTreeEntries.Push(Entry); +} + +TSharedPtr FFlowGraphToDiff::GenerateFlowObjectDiff(const TSharedPtr& Difference) +{ + //copy the first diff found in order to enable jumping to the node in the graph. + TSharedPtr DuplicatedFirstFoundDiffResult = MakeShared(Difference->Result); + + ModifyDiffDisplayName(DuplicatedFirstFoundDiffResult->Result, GetNodeNameFromDiffResult(*Difference.Get())); + + TSharedPtr NewFlowObjectDiff = MakeShared(DuplicatedFirstFoundDiffResult, *this); + + static const FSingleObjectDiffEntry InvalidDiff = FSingleObjectDiffEntry(); //do not specify a property change for the main object diff itself. + NewFlowObjectDiff->DiffEntryFocusArg = MakeShared( + NewFlowObjectDiff, + FSingleObjectDiffEntry(InvalidDiff)); + + NewFlowObjectDiff->DiffTreeEntry = MakeShared( + FOnDiffEntryFocused::CreateRaw(DiffWidget, &SFlowDiff::OnDiffListSelectionChanged, NewFlowObjectDiff->DiffEntryFocusArg), + FGenerateDiffEntryWidget::CreateSP(DuplicatedFirstFoundDiffResult.ToSharedRef(), &FDiffResultItem::GenerateWidget)); + + return NewFlowObjectDiff; +} + +TSharedPtr FFlowGraphToDiff::FindParentNode(UFlowGraphNode* Node) +{ + if (!IsValid(Node)) + { + return nullptr; + } + + const UFlowGraphNode* ParentNode = Node->GetParentNode(); + for (auto& FlowNodeDiff : FlowObjectDiffsByNodeName) + { + //don't allow a pin diff be the parent of anything. + if (FlowNodeDiff.Value->DiffResult->Result.Pin1) + { + continue; + } + //if parent node is set, use that. + if (IsValid(ParentNode)) + { + if (FlowNodeDiff.Value->DiffResult->Result.Node1 == ParentNode + || FlowNodeDiff.Value->DiffResult->Result.Node2 == ParentNode) + { + return FlowNodeDiff.Value; + } + } + //if parent node is not set (not set in node removal changes for some reason), + //try to find the parent in the SubNodes of known node changes. + else + { + const UFlowGraphNode* NodeToCheck = Cast(FlowNodeDiff.Value->DiffResult->Result.Node1); + if (IsValid(NodeToCheck)) + { + const int32 Index = NodeToCheck->SubNodes.Find(Node); + if (Index != INDEX_NONE) + { + return FlowNodeDiff.Value; + } + } + } + } + + return nullptr; +} + +FText FFlowGraphToDiff::GetToolTip() const +{ + if (GraphOld && GraphNew) + { + if (DiffListSource.Num() > 0) + { + return LOCTEXT("ContainsDifferences", "Revisions are different"); + } + else + { + return LOCTEXT("GraphsIdentical", "Revisions appear to be identical"); + } + } + else + { + const UEdGraph* GoodGraph = GraphOld ? GraphOld : GraphNew; + check(GoodGraph); + const FRevisionInfo& Revision = GraphNew ? RevisionOld : RevisionNew; + FText RevisionText = LOCTEXT("CurrentRevision", "Current Revision"); + + if (!Revision.Revision.IsEmpty()) + { + RevisionText = FText::Format(LOCTEXT("Revision Number", "Revision {0}"), FText::FromString(Revision.Revision)); + } + + return FText::Format(LOCTEXT("MissingGraph", "Graph '{0}' missing from {1}"), FText::FromString(GoodGraph->GetName()), RevisionText); + } +} + +TSharedRef FFlowGraphToDiff::GenerateCategoryWidget() const +{ + const UEdGraph* Graph = GraphOld ? GraphOld : GraphNew; + check(Graph); + + FLinearColor Color = (GraphOld && GraphNew) ? DiffViewUtils::Identical() : FLinearColor(0.3f, 0.3f, 1.f); + + if (DiffListSource.Num() > 0) + { + Color = DiffViewUtils::Differs(); + } + + return SNew(SHorizontalBox) + + SHorizontalBox::Slot() + [ + SNew(STextBlock) + .ColorAndOpacity(Color) + .Text(FText::FromString(TEXT("Graph"))) + .ToolTipText(GetToolTip()) + ] + + DiffViewUtils::Box(GraphOld != nullptr, Color) + + DiffViewUtils::Box(GraphNew != nullptr, Color); +} + +void FFlowGraphToDiff::BuildDiffSourceArray() +{ + FoundDiffs->Empty(); + FGraphDiffControl::DiffGraphs(GraphOld, GraphNew, *FoundDiffs); + + Algo::SortBy(*FoundDiffs, &FDiffSingleResult::Diff); + + DiffListSource.Empty(); + for (const FDiffSingleResult& Diff : *FoundDiffs) + { + DiffListSource.Add(MakeShared(Diff)); + } +} + +void FFlowGraphToDiff::OnGraphChanged(const FEdGraphEditAction& Action) const +{ + DiffWidget->OnGraphChanged(this); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Asset/FlowImportUtils.cpp b/Source/FlowEditor/Private/Asset/FlowImportUtils.cpp new file mode 100644 index 000000000..981dcc23e --- /dev/null +++ b/Source/FlowEditor/Private/Asset/FlowImportUtils.cpp @@ -0,0 +1,415 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/FlowImportUtils.h" + +#include "Asset/FlowAssetFactory.h" +#include "FlowEditorDefines.h" +#include "FlowEditorLogChannels.h" +#include "Graph/FlowGraphSchema_Actions.h" +#include "Graph/FlowGraph.h" + +#include "FlowAsset.h" +#include "Nodes/FlowPin.h" +#include "Nodes/Graph/FlowNode_Start.h" + +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetToolsModule.h" +#include "EdGraphSchema_K2.h" +#include "EditorAssetLibrary.h" +#include "Misc/ScopedSlowTask.h" + +#if ENABLE_ASYNC_NODES_IMPORT +#include "K2Node_BaseAsyncTask.h" +#endif +#include "K2Node_CallFunction.h" +#include "K2Node_Event.h" +#include "K2Node_IfThenElse.h" +#include "K2Node_Knot.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowImportUtils) + +#define LOCTEXT_NAMESPACE "FlowImportUtils" + +TMap> UFlowImportUtils::FunctionsToFlowNodes = TMap>(); +TMap, FBlueprintToFlowPinName> UFlowImportUtils::PinMappings = TMap, FBlueprintToFlowPinName>(); + +UFlowAsset* UFlowImportUtils::ImportBlueprintGraph(UObject* BlueprintAsset, const TSubclassOf FlowAssetClass, const FString FlowAssetName, + const TMap> InFunctionsToFlowNodes, const TMap, FBlueprintToFlowPinName> InPinMappings, const FName StartEventName) +{ + if (BlueprintAsset == nullptr || FlowAssetClass == nullptr || FlowAssetName.IsEmpty() || StartEventName.IsNone()) + { + return nullptr; + } + + UBlueprint* Blueprint = Cast(BlueprintAsset); + UFlowAsset* FlowAsset = nullptr; + + // we assume that users want to have a converted asset in the same folder as the legacy blueprint + const FString PackageFolder = FPaths::GetPath(Blueprint->GetOuter()->GetPathName()); + + if (!FPackageName::DoesPackageExist(PackageFolder / FlowAssetName, nullptr)) // create a new asset + { + IAssetTools& AssetTools = FModuleManager::GetModuleChecked("AssetTools").Get(); + UFactory* Factory = Cast(UFlowAssetFactory::StaticClass()->GetDefaultObject()); + + if (UObject* NewAsset = AssetTools.CreateAsset(FlowAssetName, PackageFolder, FlowAssetClass, Factory)) + { + FlowAsset = Cast(NewAsset); + } + } + else // load existing asset + { + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(AssetRegistryConstants::ModuleName); + + const FString PackageName = PackageFolder / (FlowAssetName + TEXT(".") + FlowAssetName); + const FAssetData& FoundAssetData = AssetRegistryModule.GetRegistry().GetAssetByObjectPath(FSoftObjectPath(PackageName)); + + FlowAsset = Cast(FoundAssetData.GetAsset()); + } + + // import graph + if (FlowAsset) + { + FunctionsToFlowNodes = InFunctionsToFlowNodes; + PinMappings = InPinMappings; + + ImportBlueprintGraph(Blueprint, FlowAsset, StartEventName); + FunctionsToFlowNodes.Empty(); + PinMappings.Empty(); + + Cast(FlowAsset->GetGraph())->RefreshGraph(); + UEditorAssetLibrary::SaveLoadedAsset(FlowAsset->GetPackage()); + } + + return FlowAsset; +} + +void UFlowImportUtils::ImportBlueprintGraph(UBlueprint* Blueprint, UFlowAsset* FlowAsset, const FName StartEventName) +{ + ensureAlways(Blueprint && FlowAsset); + + UEdGraph* BlueprintGraph = Blueprint->UbergraphPages.IsValidIndex(0) ? Blueprint->UbergraphPages[0] : nullptr; + if (BlueprintGraph == nullptr) + { + return; + } + + FScopedSlowTask ExecuteAssetTask(BlueprintGraph->Nodes.Num(), FText::Format(LOCTEXT("FFlowGraphUtils::ImportBlueprintGraph", "Reading {0}"), FText::FromString(Blueprint->GetFriendlyName()))); + ExecuteAssetTask.MakeDialog(); + + TMap SourceNodes; + UEdGraphNode* StartNode = nullptr; + + for (UEdGraphNode* ThisNode : BlueprintGraph->Nodes) + { + ExecuteAssetTask.EnterProgressFrame(1, FText::Format(LOCTEXT("FFlowGraphUtils::ImportBlueprintGraph", "Processing blueprint node: {0}"), ThisNode->GetNodeTitle(ENodeTitleType::ListView))); + + // non-pure K2Nodes or UK2Node_Knot + const UK2Node* K2Node = Cast(ThisNode); + if (K2Node && (!K2Node->IsNodePure() || Cast(K2Node))) + { + FImportedGraphNode& NodeImport = SourceNodes.FindOrAdd(ThisNode->NodeGuid); + NodeImport.SourceGraphNode = ThisNode; + + // create map of all non-pure blueprint nodes with theirs pin connections + for (const UEdGraphPin* ThisPin : ThisNode->Pins) + { + for (const UEdGraphPin* LinkedPin : ThisPin->LinkedTo) + { + if (LinkedPin && LinkedPin->GetOwningNode()) + { + const FConnectedPin ConnectedPin(LinkedPin->GetOwningNode()->NodeGuid, LinkedPin->PinName); + + if (ThisPin->Direction == EGPD_Input) + { + NodeImport.Incoming.Add(ThisPin->PinName, ConnectedPin); + } + else + { + NodeImport.Outgoing.Add(ThisPin->PinName, ConnectedPin); + } + } + } + } + + // we need to know the default entry point of blueprint graph + const UK2Node_Event* EventNode = Cast(ThisNode); + if (EventNode && (EventNode->EventReference.GetMemberName() == StartEventName || EventNode->CustomFunctionName == StartEventName)) + { + StartNode = ThisNode; + } + } + } + + // can't start import if provided graph doesn't have required start node + if (StartNode == nullptr) + { + return; + } + + // clear existing graph + UFlowGraph* FlowGraph = Cast(FlowAsset->GetGraph()); + for (const TPair& Node : FlowAsset->GetNodes()) + { + if (UFlowGraphNode* FlowGraphNode = Cast(Node.Value->GetGraphNode())) + { + FlowGraph->GetSchema()->BreakNodeLinks(*FlowGraphNode); + FlowGraphNode->DestroyNode(); + } + + FlowAsset->UnregisterNode(Node.Key); + } + + TMap TargetNodes; + + // recreated UFlowNode_Start, assign it a blueprint node FGuid + UFlowGraphNode* StartGraphNode = FFlowGraphSchemaAction_NewNode::CreateNode(FlowGraph, nullptr, UFlowNode_Start::StaticClass(), FVector2f::ZeroVector); + FlowGraph->GetSchema()->SetNodeMetaData(StartGraphNode, FNodeMetadata::DefaultGraphNode); + StartGraphNode->NodeGuid = StartNode->NodeGuid; + UFlowNode* StartFlowNode = Cast(StartGraphNode->GetFlowNodeBase()); + StartFlowNode->SetGuid(StartNode->NodeGuid); + TargetNodes.Add(StartGraphNode->NodeGuid, StartGraphNode); + + // execute graph import + // iterate all nodes separately, ensures we import all possible nodes and connect them together + for (const TPair& SourceNode : SourceNodes) + { + ImportBlueprintFunction(FlowAsset, SourceNode.Value, SourceNodes, TargetNodes); + } +} + +void UFlowImportUtils::ImportBlueprintFunction(const UFlowAsset* FlowAsset, const FImportedGraphNode& NodeImport, const TMap& SourceNodes, TMap& TargetNodes) +{ + ensureAlways(NodeImport.SourceGraphNode); + TSubclassOf MatchingFlowNodeClass = nullptr; + + // find FlowNode class matching provided UFunction name + FName FunctionName = NAME_None; + if (const UK2Node_CallFunction* FunctionNode = Cast(NodeImport.SourceGraphNode)) + { + FunctionName = FunctionNode->GetFunctionName(); + } +#if ENABLE_ASYNC_NODES_IMPORT + else if (const UK2Node_BaseAsyncTask* AsyncTaskNode = Cast(NodeImport.SourceGraphNode)) + { + FunctionName = AsyncTaskNode->GetProxyFactoryFunctionName(); + } +#endif + else if (Cast(NodeImport.SourceGraphNode)) + { + FunctionName = TEXT("Reroute"); + } + else if (Cast(NodeImport.SourceGraphNode)) + { + FunctionName = TEXT("Branch"); + } + + if (!FunctionName.IsNone()) + { + // find FlowNode class matching provided UFunction name + MatchingFlowNodeClass = FunctionsToFlowNodes.FindRef(FunctionName); + } + + if (MatchingFlowNodeClass == nullptr) + { + UE_LOG(LogFlowEditor, Error, TEXT("Can't find Flow Node class for K2Node, function name %s"), *FunctionName.ToString()); + return; + } + + const FGuid& NodeGuid = NodeImport.SourceGraphNode->NodeGuid; + + // create a new Flow Graph node + const FVector2d Location = FVector2D(NodeImport.SourceGraphNode->NodePosX, NodeImport.SourceGraphNode->NodePosY); + UFlowGraphNode* FlowGraphNode = FFlowGraphSchemaAction_NewNode::ImportNode(FlowAsset->GetGraph(), nullptr, MatchingFlowNodeClass, NodeGuid, Location); + + if (FlowGraphNode == nullptr) + { + return; + } + TargetNodes.Add(NodeGuid, FlowGraphNode); + + // transfer properties from UFunction input parameters to Flow Node properties + { + TMap InputPins; + GetValidInputPins(NodeImport.SourceGraphNode, InputPins); + + UClass* FlowNodeClass = FlowGraphNode->GetFlowNodeBase()->GetClass(); + for (TFieldIterator PropIt(FlowNodeClass, EFieldIteratorFlags::IncludeSuper); PropIt && (PropIt->PropertyFlags & CPF_Edit); ++PropIt) + { + const FProperty* Param = *PropIt; + const bool bIsEditable = !Param->HasAnyPropertyFlags(CPF_Deprecated); + if (bIsEditable) + { + if (const UEdGraphPin* MatchingInputPin = FindPinMatchingToProperty(FlowNodeClass, Param, InputPins)) + { + if (MatchingInputPin->LinkedTo.Num() == 0) // nothing connected to pin, so user can set value directly on this pin + { + FString const PinValue = MatchingInputPin->GetDefaultAsString(); + uint8* Offset = Param->ContainerPtrToValuePtr(FlowGraphNode->GetFlowNodeBase()); + Param->ImportText_Direct(*PinValue, Offset, FlowGraphNode->GetFlowNodeBase(), PPF_Copy, GLog); + } + } + else // try to find matching Pin in connected pure nodes + { + bool bPinFound = false; + for (const TPair InputPin : InputPins) + { + for (const UEdGraphPin* LinkedPin : InputPin.Value->LinkedTo) + { + if (LinkedPin && LinkedPin->GetOwningNode()) // try to read value from the first pure node connected to the pin + { + // in theory, we could put this part in recursive loop, iterating pure nodes until we find one with matching Pin Name + // in practice, iterating blueprint graph isn't that easy as might encounter Make/Break nodes, array builders + // if someone is willing put work to it, you're welcome to make a pull request + + UK2Node* LinkedK2Node = Cast(LinkedPin->GetOwningNode()); + if (LinkedK2Node && LinkedK2Node->IsNodePure()) + { + TMap PureNodePins; + GetValidInputPins(LinkedK2Node, PureNodePins); + + if (const UEdGraphPin* PureInputPin = FindPinMatchingToProperty(FlowNodeClass, Param, PureNodePins)) + { + if (PureInputPin->LinkedTo.Num() == 0) // nothing connected to pin, so user can set value directly on this pin + { + FString const PinValue = PureInputPin->GetDefaultAsString(); + uint8* Offset = Param->ContainerPtrToValuePtr(FlowGraphNode->GetFlowNodeBase()); + Param->ImportText_Direct(*PinValue, Offset, FlowGraphNode->GetFlowNodeBase(), PPF_Copy, GLog); + + bPinFound = true; + } + } + } + + // there can be only single valid connection on input parameter pin + break; + } + } + + if (bPinFound) + { + break; + } + } + } + } + } + } + + // Flow Nodes with Context Pins needs to update related data and call OnReconstructionRequested.ExecuteIfBound() in order to fully construct a graph node + FlowGraphNode->GetFlowNodeBase()->PostImport(); + + // connect new node to all already recreated nodes + for (const TPair& Connection : NodeImport.Incoming) + { + UEdGraphPin* ThisPin = nullptr; + for (UEdGraphPin* FlowInputPin : FlowGraphNode->InputPins) + { + if (FlowGraphNode->InputPins.Num() == 1 || Connection.Key == FlowInputPin->PinName) + { + ThisPin = FlowInputPin; + break; + } + } + if (ThisPin == nullptr) + { + continue; + } + + UEdGraphPin* ConnectedPin = nullptr; + if (UFlowGraphNode* ConnectedNode = TargetNodes.FindRef(Connection.Value.NodeGuid)) + { + for (UEdGraphPin* FlowOutputPin : ConnectedNode->OutputPins) + { + if (ConnectedNode->OutputPins.Num() == 1 || Connection.Value.PinName == FlowOutputPin->PinName + || (Connection.Value.PinName == UEdGraphSchema_K2::PN_Then && FlowOutputPin->PinName == FName("TRUE")) + || (Connection.Value.PinName == UEdGraphSchema_K2::PN_Else && FlowOutputPin->PinName == FName("FALSE"))) + { + ConnectedPin = FlowOutputPin; + break; + } + } + } + + // link the pin to existing node + if (ConnectedPin) + { + FlowAsset->GetGraph()->GetSchema()->TryCreateConnection(ThisPin, ConnectedPin); + } + } + for (const TPair& Connection : NodeImport.Outgoing) + { + UEdGraphPin* ThisPin = nullptr; + for (UEdGraphPin* FlowOutputPin : FlowGraphNode->OutputPins) + { + if (FlowGraphNode->OutputPins.Num() == 1 || Connection.Key == FlowOutputPin->PinName + || (Connection.Key == UEdGraphSchema_K2::PN_Then && FlowOutputPin->PinName == FName("TRUE")) + || (Connection.Key == UEdGraphSchema_K2::PN_Else && FlowOutputPin->PinName == FName("FALSE"))) + { + ThisPin = FlowOutputPin; + break; + } + } + if (ThisPin == nullptr) + { + continue; + } + + UEdGraphPin* ConnectedPin = nullptr; + if (UFlowGraphNode* ConnectedNode = TargetNodes.FindRef(Connection.Value.NodeGuid)) + { + for (UEdGraphPin* FlowInputPin : ConnectedNode->InputPins) + { + if (ConnectedNode->InputPins.Num() == 1 || Connection.Value.PinName == FlowInputPin->PinName) + { + ConnectedPin = FlowInputPin; + break; + } + } + } + + // link the pin to existing node + if (ConnectedPin) + { + FlowAsset->GetGraph()->GetSchema()->TryCreateConnection(ThisPin, ConnectedPin); + } + } +} + +void UFlowImportUtils::GetValidInputPins(const UEdGraphNode* GraphNode, TMap& Result) +{ + for (const UEdGraphPin* Pin : GraphNode->Pins) + { + if (Pin->Direction == EGPD_Input && !Pin->bHidden && !Pin->bOrphanedPin) + { + Result.Add(Pin->PinName, Pin); + } + } +} + +const UEdGraphPin* UFlowImportUtils::FindPinMatchingToProperty(UClass* FlowNodeClass, const FProperty* Property, const TMap Pins) +{ + const FName& PropertyAuthoredName = *Property->GetAuthoredName(); + + // if Pin Name is exactly the same as Flow Node property name + if (const UEdGraphPin* Pin = Pins.FindRef(PropertyAuthoredName)) + { + return Pin; + } + + // if not, check if appropriate Pin Mapping has been provided + if (const FBlueprintToFlowPinName* PinMapping = PinMappings.Find(FlowNodeClass)) + { + if (const FName* MappedPinName = PinMapping->NodePropertiesToFunctionPins.Find(PropertyAuthoredName)) + { + if (const UEdGraphPin* Pin = Pins.FindRef(*MappedPinName)) + { + return Pin; + } + } + } + + return nullptr; +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Asset/FlowMessageLogListing.cpp b/Source/FlowEditor/Private/Asset/FlowMessageLogListing.cpp new file mode 100644 index 000000000..5c89ebe50 --- /dev/null +++ b/Source/FlowEditor/Private/Asset/FlowMessageLogListing.cpp @@ -0,0 +1,74 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/FlowMessageLogListing.h" + +#include "MessageLogModule.h" +#include "Modules/ModuleManager.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowMessageLogListing) + +#define LOCTEXT_NAMESPACE "FlowMessageLogListing" + +FFlowMessageLogListing::FFlowMessageLogListing(const UFlowAsset* InFlowAsset, const EFlowLogType Type) + : Log(RegisterLogListing(InFlowAsset, Type)) +{ +} + +FFlowMessageLogListing::~FFlowMessageLogListing() +{ + // Unregister the log so it will be ref-counted to zero if it has no messages + if (Log->NumMessages(EMessageSeverity::Info) == 0) + { + FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked("MessageLog"); + MessageLogModule.UnregisterLogListing(Log->GetName()); + } +} + +TSharedRef FFlowMessageLogListing::RegisterLogListing(const UFlowAsset* InFlowAsset, const EFlowLogType Type) +{ + FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked("MessageLog"); + + const FName LogName = GetListingName(InFlowAsset, Type); + + // Register the log (this will return an existing log if it has been used before) + FMessageLogInitializationOptions LogInitOptions; + LogInitOptions.bShowInLogWindow = false; + MessageLogModule.RegisterLogListing(LogName, LOCTEXT("FlowGraphLogLabel", "FlowGraph"), LogInitOptions); + return MessageLogModule.GetLogListing(LogName); +} + +TSharedRef FFlowMessageLogListing::GetLogListing(const UFlowAsset* InFlowAsset, const EFlowLogType Type) +{ + FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked("MessageLog"); + const FName LogName = GetListingName(InFlowAsset, Type); + + // Create a new message log + if (!MessageLogModule.IsRegisteredLogListing(LogName)) + { + MessageLogModule.RegisterLogListing(LogName, FText::FromString(GetLogLabel(Type))); + } + + return MessageLogModule.GetLogListing(LogName); +} + +FString FFlowMessageLogListing::GetLogLabel(const EFlowLogType Type) +{ + const FString TypeAsString = StaticEnum()->GetNameStringByIndex(static_cast(Type)); + return FString::Printf(TEXT("Flow%sLog"), *TypeAsString); +} + +FName FFlowMessageLogListing::GetListingName(const UFlowAsset* InFlowAsset, const EFlowLogType Type) +{ + FName LogListingName; + if (InFlowAsset) + { + LogListingName = *FString::Printf(TEXT("%s::%s::%s"), *GetLogLabel(Type), *InFlowAsset->GetName(), *InFlowAsset->AssetGuid.ToString()); + } + else + { + LogListingName = "FlowGraph"; + } + return LogListingName; +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Asset/FlowObjectDiff.cpp b/Source/FlowEditor/Private/Asset/FlowObjectDiff.cpp new file mode 100644 index 000000000..5c95fde06 --- /dev/null +++ b/Source/FlowEditor/Private/Asset/FlowObjectDiff.cpp @@ -0,0 +1,86 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/FlowObjectDiff.h" + +#include "Asset/FlowDiffControl.h" +#include "Nodes/FlowNodeBase.h" +#include "Graph/Nodes/FlowGraphNode.h" + +#include "DiffResults.h" +#include "EdGraph/EdGraph.h" +#include "SBlueprintDiff.h" + +///////////////////////////////////////////////////////////////////////////// +/// FFlowNodePropertyDiff +FFlowObjectDiffArgs::FFlowObjectDiffArgs(TWeakPtr InFlowNodeDiff, const FSingleObjectDiffEntry& InPropertyDiff) + : FlowNodeDiff(InFlowNodeDiff), + PropertyDiff(InPropertyDiff) +{ +} + +///////////////////////////////////////////////////////////////////////////// +/// FFlowObjectDiff +FFlowObjectDiff::FFlowObjectDiff(TSharedPtr InDiffResult, const FFlowGraphToDiff& GraphToDiff) + : DiffResult(InDiffResult) +{ + //ensure we do not generate details panels for pin changes. + if (InDiffResult->Result.Pin1 == nullptr && InDiffResult->Result.Pin2 == nullptr) + { + InitializeDetailsDiffFromNode(InDiffResult->Result.Node1, InDiffResult->Result.Object1, GraphToDiff); + InitializeDetailsDiffFromNode(InDiffResult->Result.Node2, InDiffResult->Result.Object2, GraphToDiff); + } +} + +void FFlowObjectDiff::InitializeDetailsDiffFromNode(UEdGraphNode* Node, const UObject* Object, const FFlowGraphToDiff& GraphToDiff) +{ + if (!IsValid(Node)) + { + return; + } + + if (!IsValid(Object)) + { + const UFlowGraphNode* FlowGraphNode = Cast(Node); + if (IsValid(FlowGraphNode)) + { + Object = FlowGraphNode->GetNodeTemplate(); + } + } + const ENodeDiffType NodeDiffType = GraphToDiff.GetNodeDiffType(*Node); + + if (NodeDiffType == ENodeDiffType::Old && !OldDetailsView.IsValid()) + { + OldDetailsView = MakeShared(Object); + } + else if (NodeDiffType == ENodeDiffType::New && !NewDetailsView.IsValid()) + { + NewDetailsView = MakeShared(Object); + } +} + +void FFlowObjectDiff::DiffProperties(TArray& OutPropertyDiffsArray) const +{ + if (OldDetailsView.IsValid() && NewDetailsView.IsValid()) + { + static constexpr bool bSortByDisplayOrder = true; + OldDetailsView->DiffAgainst(*NewDetailsView.Get(), OutPropertyDiffsArray, bSortByDisplayOrder); + } +} + +void FFlowObjectDiff::OnSelectDiff(const FSingleObjectDiffEntry& Property) const +{ + if (Property.DiffType == EPropertyDiffType::Type::Invalid) + { + return; + } + + if (OldDetailsView.IsValid()) + { + OldDetailsView->HighlightProperty(Property.Identifier); + } + + if (NewDetailsView.IsValid()) + { + NewDetailsView->HighlightProperty(Property.Identifier); + } +} diff --git a/Source/FlowEditor/Private/Asset/SAssetRevisionMenu.cpp b/Source/FlowEditor/Private/Asset/SAssetRevisionMenu.cpp new file mode 100644 index 000000000..b6ac1ef8c --- /dev/null +++ b/Source/FlowEditor/Private/Asset/SAssetRevisionMenu.cpp @@ -0,0 +1,242 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/SAssetRevisionMenu.h" + +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "IAssetTypeActions.h" +#include "ISourceControlModule.h" +#include "ISourceControlRevision.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "SourceControlOperations.h" +#include "Widgets/Images/SThrobber.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Layout/SBorder.h" +#include "Widgets/SBoxPanel.h" +#include "Widgets/Text/STextBlock.h" + +#define LOCTEXT_NAMESPACE "SFlowRevisionMenu" + +/** */ +namespace ESourceControlQueryState +{ + enum Type + { + NotQueried, + QueryInProgress, + Queried, + }; +} + +//------------------------------------------------------------------------------ +SAssetRevisionMenu::~SAssetRevisionMenu() +{ + // cancel any operation if this widget is destroyed while in progress + if (SourceControlQueryState == ESourceControlQueryState::QueryInProgress) + { + ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); + if (SourceControlQueryOp.IsValid() && SourceControlProvider.CanCancelOperation(SourceControlQueryOp.ToSharedRef())) + { + SourceControlProvider.CancelOperation(SourceControlQueryOp.ToSharedRef()); + } + } +} + +//------------------------------------------------------------------------------ +void SAssetRevisionMenu::Construct(const FArguments& InArgs, const FString& InFilename) +{ + bIncludeLocalRevision = InArgs._bIncludeLocalRevision; + OnRevisionSelected = InArgs._OnRevisionSelected; + + SourceControlQueryState = ESourceControlQueryState::NotQueried; + + ChildSlot + [ + SAssignNew(MenuBox, SVerticalBox) + + SVerticalBox::Slot() + [ + SNew(SBorder) + .Visibility(this, &SAssetRevisionMenu::GetInProgressVisibility) + .BorderImage(FAppStyle::GetBrush("Menu.Background")) + .Content() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(SThrobber) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(2.0f, 0.0f, 4.0f, 0.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("DiffMenuOperationInProgress", "Updating history...")) + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + .HAlign(HAlign_Right) + .VAlign(VAlign_Center) + [ + SNew(SButton) + .Visibility(this, &SAssetRevisionMenu::GetCancelButtonVisibility) + .OnClicked(this, &SAssetRevisionMenu::OnCancelButtonClicked) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .Content() + [ + SNew(STextBlock) + .Text(LOCTEXT("DiffMenuCancelButton", "Cancel")) + ] + ] + ] + ] + ]; + + Filename = InFilename; + if (!Filename.IsEmpty()) + { + // make sure the history info is up to date + SourceControlQueryOp = ISourceControlOperation::Create(); + SourceControlQueryOp->SetUpdateHistory(true); + ISourceControlModule::Get().GetProvider().Execute(SourceControlQueryOp.ToSharedRef(), Filename, EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateSP(this, &SAssetRevisionMenu::OnSourceControlQueryComplete)); + + SourceControlQueryState = ESourceControlQueryState::QueryInProgress; + } +} + +//------------------------------------------------------------------------------ +EVisibility SAssetRevisionMenu::GetInProgressVisibility() const +{ + return (SourceControlQueryState == ESourceControlQueryState::QueryInProgress) ? EVisibility::Visible : EVisibility::Collapsed; +} + +//------------------------------------------------------------------------------ +EVisibility SAssetRevisionMenu::GetCancelButtonVisibility() const +{ + const ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); + return SourceControlQueryOp.IsValid() && SourceControlProvider.CanCancelOperation(SourceControlQueryOp.ToSharedRef()) ? EVisibility::Visible : EVisibility::Collapsed; +} + +//------------------------------------------------------------------------------ +FReply SAssetRevisionMenu::OnCancelButtonClicked() const +{ + if (SourceControlQueryOp.IsValid()) + { + ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); + SourceControlProvider.CancelOperation(SourceControlQueryOp.ToSharedRef()); + } + + return FReply::Handled(); +} + +//------------------------------------------------------------------------------ +void SAssetRevisionMenu::OnSourceControlQueryComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult) +{ + check(SourceControlQueryOp == InOperation); + + + // Add pop-out menu for each revision + FMenuBuilder MenuBuilder(/*bInShouldCloseWindowAfterMenuSelection =*/true, /*InCommandList =*/nullptr); + + MenuBuilder.BeginSection("AddDiffRevision", LOCTEXT("Revisions", "Revisions")); + if (bIncludeLocalRevision) + { + FText const ToolTipText = LOCTEXT("LocalRevisionToolTip", "The current copy you have saved to disk (locally)"); + + FOnRevisionSelected OnRevisionSelectedDelegate = OnRevisionSelected; + auto OnMenuItemSelected = [OnRevisionSelectedDelegate, this]() + { + OnRevisionSelectedDelegate.ExecuteIfBound(FRevisionInfo::InvalidRevision(), Filename); + }; + + MenuBuilder.AddMenuEntry(LOCTEXT("LocalRevision", "Local"), ToolTipText, FSlateIcon(), FUIAction(FExecuteAction::CreateLambda(OnMenuItemSelected))); + } + + if (InResult == ECommandResult::Succeeded) + { + // get the cached state + ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); + FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(Filename, EStateCacheUsage::Use); + + if (SourceControlState.IsValid() && SourceControlState->GetHistorySize() > 0) + { + // Figure out the highest revision # (so we can label it "Depot") + int32 LatestRevision = 0; + for (int32 HistoryIndex = 0; HistoryIndex < SourceControlState->GetHistorySize(); HistoryIndex++) + { + TSharedPtr Revision = SourceControlState->GetHistoryItem(HistoryIndex); + if (Revision.IsValid() && Revision->GetRevisionNumber() > LatestRevision) + { + LatestRevision = Revision->GetRevisionNumber(); + } + } + + for (int32 HistoryIndex = 0; HistoryIndex < SourceControlState->GetHistorySize(); HistoryIndex++) + { + TSharedPtr Revision = SourceControlState->GetHistoryItem(HistoryIndex); + if (Revision.IsValid()) + { + FInternationalization& I18N = FInternationalization::Get(); + + FText Label = FText::Format(LOCTEXT("RevisionNumber", "Revision {0}"), FText::AsNumber(Revision->GetRevisionNumber(), nullptr, I18N.GetInvariantCulture())); + + FFormatNamedArguments Args; + Args.Add(TEXT("CheckInNumber"), FText::AsNumber(Revision->GetCheckInIdentifier(), nullptr, I18N.GetInvariantCulture())); + Args.Add(TEXT("Revision"), FText::FromString(Revision->GetRevision())); + Args.Add(TEXT("UserName"), FText::FromString(Revision->GetUserName())); + Args.Add(TEXT("DateTime"), FText::AsDate(Revision->GetDate())); + Args.Add(TEXT("ChanglistDescription"), FText::FromString(Revision->GetDescription())); + FText ToolTipText; + if (ISourceControlModule::Get().GetProvider().UsesChangelists()) + { + ToolTipText = FText::Format(LOCTEXT("ChangelistToolTip", "CL #{CheckInNumber} {UserName} \n{DateTime} \n{ChanglistDescription}"), Args); + } + else + { + ToolTipText = FText::Format(LOCTEXT("RevisionToolTip", "{Revision} {UserName} \n{DateTime} \n{ChanglistDescription}"), Args); + } + + if (LatestRevision == Revision->GetRevisionNumber()) + { + Label = LOCTEXT("Depo", "Depot"); + } + + FRevisionInfo RevisionInfo = { + Revision->GetRevision(), + Revision->GetCheckInIdentifier(), + Revision->GetDate() + }; + FOnRevisionSelected OnRevisionSelectedDelegate = OnRevisionSelected; + auto OnMenuItemSelected = [RevisionInfo, OnRevisionSelectedDelegate, this]() + { + OnRevisionSelectedDelegate.ExecuteIfBound(RevisionInfo, Filename); + }; + MenuBuilder.AddMenuEntry(TAttribute(Label), ToolTipText, FSlateIcon(), FUIAction(FExecuteAction::CreateLambda(OnMenuItemSelected))); + } + } + } + else if (!bIncludeLocalRevision) + { + // Show 'empty' item in toolbar + MenuBuilder.AddMenuEntry(LOCTEXT("NoRevisonHistory", "No revisions found"), FText(), FSlateIcon(), FUIAction()); + } + } + else if (!bIncludeLocalRevision) + { + // Show 'empty' item in toolbar + MenuBuilder.AddMenuEntry(LOCTEXT("NoRevisonHistory", "No revisions found"), FText(), FSlateIcon(), FUIAction()); + } + + MenuBuilder.EndSection(); + MenuBox->AddSlot() + [ + MenuBuilder.MakeWidget(nullptr, 500) + ]; + + SourceControlQueryOp.Reset(); + SourceControlQueryState = ESourceControlQueryState::Queried; +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Asset/SFlowDiff.cpp b/Source/FlowEditor/Private/Asset/SFlowDiff.cpp new file mode 100644 index 000000000..2b1be2976 --- /dev/null +++ b/Source/FlowEditor/Private/Asset/SFlowDiff.cpp @@ -0,0 +1,948 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/SFlowDiff.h" + +#include "Asset/FlowDiffControl.h" +#include "FlowAsset.h" +#include "Graph/Nodes/FlowGraphNode.h" + +#include "EdGraphUtilities.h" +#include "Editor.h" +#include "Framework/Application/SlateApplication.h" +#include "Framework/Commands/GenericCommands.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "Framework/MultiBox/MultiBoxDefs.h" +#include "Graph/Nodes/FlowGraphNode.h" +#include "GraphDiffControl.h" +#include "HAL/PlatformApplicationMisc.h" +#include "Internationalization/Text.h" +#include "PropertyEditorModule.h" +#include "SBlueprintDiff.h" +#include "SDetailsSplitter.h" +#include "SlateOptMacros.h" +#include "Subsystems/AssetEditorSubsystem.h" +#include "Widgets/Layout/SSpacer.h" + +#define LOCTEXT_NAMESPACE "SFlowDiff" + +static const FName DetailsMode = FName(TEXT("DetailsMode")); +static const FName GraphMode = FName(TEXT("GraphMode")); + +FFlowDiffPanel::FFlowDiffPanel() + : FlowAsset(nullptr) + , bShowAssetName(false) +{ +} + +static int32 GetCurrentIndex(SListView> const& ListView, const TArray>& ListViewSource) +{ + const TArray>& Selected = ListView.GetSelectedItems(); + if (Selected.Num() == 1) + { + for (int32 Index = 0; Index < ListViewSource.Num(); ++Index) + { + if (ListViewSource[Index] == Selected[0]) + { + return Index; + } + } + } + return -1; +} + +void FlowDiffUtils::SelectNextRow(SListView>& ListView, const TArray>& ListViewSource) +{ + const int32 CurrentIndex = GetCurrentIndex(ListView, ListViewSource); + const int32 NextIndex = CurrentIndex + 1; + if (ListViewSource.IsValidIndex(NextIndex)) + { + ListView.SetSelection(ListViewSource[NextIndex]); + } +} + +void FlowDiffUtils::SelectPrevRow(SListView>& ListView, const TArray>& ListViewSource) +{ + const int32 CurrentIndex = GetCurrentIndex(ListView, ListViewSource); + const int32 PrevIndex = CurrentIndex - 1; + if (ListViewSource.IsValidIndex(PrevIndex)) + { + ListView.SetSelection(ListViewSource[PrevIndex]); + } +} + +bool FlowDiffUtils::HasNextDifference(const SListView>& ListView, const TArray>& ListViewSource) +{ + const int32 CurrentIndex = GetCurrentIndex(ListView, ListViewSource); + return ListViewSource.IsValidIndex(CurrentIndex + 1); +} + +bool FlowDiffUtils::HasPrevDifference(const SListView>& ListView, const TArray>& ListViewSource) +{ + const int32 CurrentIndex = GetCurrentIndex(ListView, ListViewSource); + return ListViewSource.IsValidIndex(CurrentIndex - 1); +} + +BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION + +void SFlowDiff::Construct(const FArguments& InArgs) +{ + check(InArgs._OldFlow || InArgs._NewFlow); + PanelOld.FlowAsset = InArgs._OldFlow; + PanelNew.FlowAsset = InArgs._NewFlow; + PanelOld.RevisionInfo = InArgs._OldRevision; + PanelNew.RevisionInfo = InArgs._NewRevision; + PanelOld.bIsOldPanel = true; + PanelNew.bIsOldPanel = false; + + // sometimes we want to clearly identify the assets being diffed (when it's + // not the same asset in each panel) + PanelOld.bShowAssetName = InArgs._ShowAssetNames; + PanelNew.bShowAssetName = InArgs._ShowAssetNames; + + bLockViews = true; + + if (InArgs._ParentWindow.IsValid()) + { + WeakParentWindow = InArgs._ParentWindow; + + AssetEditorCloseDelegate = GEditor->GetEditorSubsystem()->OnAssetEditorRequestClose().AddSP(this, &SFlowDiff::OnCloseAssetEditor); + } + + FToolBarBuilder NavToolBarBuilder(TSharedPtr(), FMultiBoxCustomization::None); + NavToolBarBuilder.AddToolBarButton( + FUIAction( + FExecuteAction::CreateSP(this, &SFlowDiff::PrevDiff), + FCanExecuteAction::CreateSP(this, &SFlowDiff::HasPrevDiff) + ) + , NAME_None + , LOCTEXT("PrevDiffLabel", "Prev") + , LOCTEXT("PrevDiffTooltip", "Go to previous difference") + , FSlateIcon(FAppStyle::GetAppStyleSetName(), "BlueprintDif.PrevDiff") + ); + NavToolBarBuilder.AddToolBarButton( + FUIAction( + FExecuteAction::CreateSP(this, &SFlowDiff::NextDiff), + FCanExecuteAction::CreateSP(this, &SFlowDiff::HasNextDiff) + ) + , NAME_None + , LOCTEXT("NextDiffLabel", "Next") + , LOCTEXT("NextDiffTooltip", "Go to next difference") + , FSlateIcon(FAppStyle::GetAppStyleSetName(), "BlueprintDif.NextDiff") + ); + + FToolBarBuilder GraphToolbarBuilder(TSharedPtr(), FMultiBoxCustomization::None); + GraphToolbarBuilder.AddToolBarButton( + FUIAction(FExecuteAction::CreateSP(this, &SFlowDiff::OnToggleLockView)) + , NAME_None + , LOCTEXT("LockGraphsLabel", "Lock/Unlock") + , LOCTEXT("LockGraphsTooltip", "Force all graph views to change together, or allow independent scrolling/zooming") + , TAttribute(this, &SFlowDiff::GetLockViewImage) + ); + GraphToolbarBuilder.AddToolBarButton( + FUIAction(FExecuteAction::CreateSP(this, &SFlowDiff::OnToggleSplitViewMode)) + , NAME_None + , LOCTEXT("SplitGraphsModeLabel", "Vertical/Horizontal") + , LOCTEXT("SplitGraphsModeLabelTooltip", "Toggles the split view of graphs between vertical and horizontal") + , TAttribute(this, &SFlowDiff::GetSplitViewModeImage) + ); + + DifferencesTreeView = DiffTreeView::CreateTreeView(&PrimaryDifferencesList); + + GenerateDifferencesList(); + + const auto TextBlock = [](FText Text) -> TSharedRef + { + return SNew(SBox) + .Padding(FMargin(4.0f, 10.0f)) + .VAlign(VAlign_Center) + .HAlign(HAlign_Left) + [ + SNew(STextBlock) + .Visibility(EVisibility::HitTestInvisible) + .TextStyle(FAppStyle::Get(), "DetailsView.CategoryTextStyle") + .Text(Text) + ]; + }; + + TopRevisionInfoWidget = + SNew(SSplitter) + .Visibility(EVisibility::HitTestInvisible) + + SSplitter::Slot() + .Value(.2f) + [ + SNew(SBox) + ] + + SSplitter::Slot() + .Value(.8f) + [ + SNew(SSplitter) + .PhysicalSplitterHandleSize(10.0f) + + SSplitter::Slot() + .Value(.5f) + [ + TextBlock(DiffViewUtils::GetPanelLabel(PanelOld.FlowAsset, PanelOld.RevisionInfo, FText())) + ] + + SSplitter::Slot() + .Value(.5f) + [ + TextBlock(DiffViewUtils::GetPanelLabel(PanelNew.FlowAsset, PanelNew.RevisionInfo, FText())) + ] + ]; + + GraphToolBarWidget = + SNew(SSplitter) + .Visibility(EVisibility::HitTestInvisible) + + SSplitter::Slot() + .Value(.2f) + [ + SNew(SBox) + ] + + SSplitter::Slot() + .Value(.8f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + [ + GraphToolbarBuilder.MakeWidget() + ] + ]; + + this->ChildSlot + [ + SNew(SBorder) + .BorderImage(FAppStyle::GetBrush("Docking.Tab", ".ContentAreaBrush")) + [ + SNew(SOverlay) + + SOverlay::Slot() + .VAlign(VAlign_Top) + [ + TopRevisionInfoWidget.ToSharedRef() + ] + + SOverlay::Slot() + .VAlign(VAlign_Top) + .Padding(0.0f, 6.0f, 0.0f, 4.0f) + [ + GraphToolBarWidget.ToSharedRef() + ] + + SOverlay::Slot() + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + .Padding(0.0f, 2.0f, 0.0f, 2.0f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .Padding(4.f) + .AutoWidth() + [ + NavToolBarBuilder.MakeWidget() + ] + + SHorizontalBox::Slot() + [ + SNew(SSpacer) + ] + ] + + SVerticalBox::Slot() + [ + SNew(SSplitter) + + SSplitter::Slot() + .Value(.2f) + [ + SNew(SBorder) + .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) + [ + DifferencesTreeView.ToSharedRef() + ] + ] + + SSplitter::Slot() + .Value(.8f) + [ + SAssignNew(ModeContents, SBox) + ] + ] + ] + ] + ]; + + SetCurrentMode(DetailsMode); +} + +END_SLATE_FUNCTION_BUILD_OPTIMIZATION + +SFlowDiff::~SFlowDiff() +{ + if (AssetEditorCloseDelegate.IsValid()) + { + GEditor->GetEditorSubsystem()->OnAssetEditorRequestClose().Remove(AssetEditorCloseDelegate); + } +} + +void SFlowDiff::OnCloseAssetEditor(UObject* Asset, const EAssetEditorCloseReason CloseReason) +{ + if (PanelOld.FlowAsset == Asset || PanelNew.FlowAsset == Asset || CloseReason == EAssetEditorCloseReason::CloseAllAssetEditors) + { + // Tell our window to close and set our selves to collapsed to try and stop it from ticking + SetVisibility(EVisibility::Collapsed); + + if (AssetEditorCloseDelegate.IsValid()) + { + GEditor->GetEditorSubsystem()->OnAssetEditorRequestClose().Remove(AssetEditorCloseDelegate); + } + + if (WeakParentWindow.IsValid()) + { + WeakParentWindow.Pin()->RequestDestroyWindow(); + } + } +} + +void SFlowDiff::OnGraphSelectionChanged(const TSharedPtr Item, ESelectInfo::Type SelectionType) +{ + if (!Item.IsValid()) + { + return; + } + + FocusOnGraphRevisions(Item.Get()); +} + +void SFlowDiff::OnGraphChanged(const FFlowGraphToDiff* Diff) +{ + if (PanelNew.GraphEditor.IsValid() && PanelNew.GraphEditor.Pin()->GetCurrentGraph() == Diff->GetGraphNew()) + { + FocusOnGraphRevisions(Diff); + } +} + +TSharedRef SFlowDiff::DefaultEmptyPanel() +{ + return SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(LOCTEXT("BlueprintDifGraphsToolTip", "Select Graph to Diff")) + ]; +} + +TSharedPtr SFlowDiff::CreateDiffWindow(const FText WindowTitle, const UFlowAsset* OldFlow, const UFlowAsset* NewFlow, const FRevisionInfo& OldRevision, const FRevisionInfo& NewRevision) +{ + // sometimes we're comparing different revisions of one single asset (other + // times we're comparing two completely separate assets altogether) + const bool bIsSingleAsset = !IsValid(OldFlow) || !IsValid(NewFlow) || (OldFlow->GetName() == NewFlow->GetName()); + + TSharedPtr Window = SNew(SWindow) + .Title(WindowTitle) + .ClientSize(FVector2D(1000, 800)); + + Window->SetContent(SNew(SFlowDiff) + .OldFlow(OldFlow) + .NewFlow(NewFlow) + .OldRevision(OldRevision) + .NewRevision(NewRevision) + .ShowAssetNames(!bIsSingleAsset) + .ParentWindow(Window)); + + // Make this window a child of the modal window if we've been spawned while one is active. + const TSharedPtr ActiveModal = FSlateApplication::Get().GetActiveModalWindow(); + if (ActiveModal.IsValid()) + { + FSlateApplication::Get().AddWindowAsNativeChild(Window.ToSharedRef(), ActiveModal.ToSharedRef()); + } + else + { + FSlateApplication::Get().AddWindow(Window.ToSharedRef()); + } + + return Window; +} + +void SFlowDiff::NextDiff() const +{ + DiffTreeView::HighlightNextDifference(DifferencesTreeView.ToSharedRef(), RealDifferences, PrimaryDifferencesList); +} + +void SFlowDiff::PrevDiff() const +{ + DiffTreeView::HighlightPrevDifference(DifferencesTreeView.ToSharedRef(), RealDifferences, PrimaryDifferencesList); +} + +bool SFlowDiff::HasNextDiff() const +{ + return DiffTreeView::HasNextDifference(DifferencesTreeView.ToSharedRef(), RealDifferences); +} + +bool SFlowDiff::HasPrevDiff() const +{ + return DiffTreeView::HasPrevDifference(DifferencesTreeView.ToSharedRef(), RealDifferences); +} + +FFlowGraphToDiff* SFlowDiff::FindGraphToDiffEntry(const FString& GraphPath) const +{ + const FString SearchGraphPath = GraphToDiff->GetGraphOld() ? FGraphDiffControl::GetGraphPath(GraphToDiff->GetGraphOld()) : FGraphDiffControl::GetGraphPath(GraphToDiff->GetGraphNew()); + if (SearchGraphPath.Equals(GraphPath, ESearchCase::CaseSensitive)) + { + return GraphToDiff.Get(); + } + + return nullptr; +} + +void SFlowDiff::FocusOnGraphRevisions(const FFlowGraphToDiff* Diff) +{ + UEdGraph* Graph = Diff->GetGraphOld() ? Diff->GetGraphOld() : Diff->GetGraphNew(); + + const FString GraphPath = FGraphDiffControl::GetGraphPath(Graph); + HandleGraphChanged(GraphPath); + + ResetGraphEditors(); +} + +void SFlowDiff::OnDiffListSelectionChanged(TSharedPtr FlowObjectDiffArgs) +{ + const TSharedPtr FlowObjectDiff = FlowObjectDiffArgs->FlowNodeDiff.Pin(); + if (!ensure(FlowObjectDiff.IsValid())) + { + return; + } + + check(!FlowObjectDiff->DiffResult->Result.OwningObjectPath.IsEmpty()); + FocusOnGraphRevisions(FindGraphToDiffEntry(FlowObjectDiff->DiffResult->Result.OwningObjectPath)); + + const TSharedPtr ParentFlowNodeDiff = FlowObjectDiff->ParentNodeDiff.Pin(); + const FDiffSingleResult& Result = ParentFlowNodeDiff.IsValid() ? ParentFlowNodeDiff->DiffResult->Result : FlowObjectDiff->DiffResult->Result; + + const auto SafeClearSelection = [](TWeakPtr GraphEditor) + { + const TSharedPtr GraphEditorPtr = GraphEditor.Pin(); + if (GraphEditorPtr.IsValid()) + { + GraphEditorPtr->ClearSelectionSet(); + } + }; + + SafeClearSelection(PanelNew.GraphEditor); + SafeClearSelection(PanelOld.GraphEditor); + + // PanelDefaultDetailsView can be used for displaying nodes on click. Clear out it's content before potentially trying to show an empty panel. + PanelOld.PanelDefaultDetailsView->SetObject(nullptr); + PanelNew.PanelDefaultDetailsView->SetObject(nullptr); + + //Select the details panel to display below the graphs. + //Show an empty details panel if there is no generated details panel. + const TSharedPtr OldDetailsPanel = FlowObjectDiff->OldDetailsView.IsValid() ? + FlowObjectDiff->OldDetailsView->DetailsWidget() : PanelOld.PanelDefaultDetailsView.ToSharedRef(); + const TSharedPtr NewDetailsPanel = FlowObjectDiff->NewDetailsView.IsValid() ? + FlowObjectDiff->NewDetailsView->DetailsWidget() : PanelNew.PanelDefaultDetailsView.ToSharedRef(); + + GraphDiffSplitter->SetBottomLeftContent(OldDetailsPanel.ToSharedRef()); + GraphDiffSplitter->SetBottomRightContent(NewDetailsPanel.ToSharedRef()); + + if (Result.Pin1) + { + GetDiffPanelForNode(*Result.Pin1->GetOwningNode()).FocusDiff(*Result.Pin1); + if (Result.Pin2) + { + GetDiffPanelForNode(*Result.Pin2->GetOwningNode()).FocusDiff(*Result.Pin2); + } + } + else if (Result.Node1) + { + FlowObjectDiff->OnSelectDiff(FlowObjectDiffArgs->PropertyDiff); + + GetDiffPanelForNode(*Result.Node1).FocusDiff(*Result.Node1); + if (Result.Node2) + { + GetDiffPanelForNode(*Result.Node2).FocusDiff(*Result.Node2); + } + } +} + +void SFlowDiff::OnToggleLockView() +{ + bLockViews = !bLockViews; + ResetGraphEditors(); +} + +void SFlowDiff::OnToggleSplitViewMode() +{ + bVerticalSplitGraphMode = !bVerticalSplitGraphMode; + + if (SSplitter* DiffGraphSplitterPtr = DiffGraphSplitter.Get()) + { + DiffGraphSplitterPtr->SetOrientation(bVerticalSplitGraphMode ? Orient_Horizontal : Orient_Vertical); + } +} + +FSlateIcon SFlowDiff::GetLockViewImage() const +{ + return FSlateIcon(FAppStyle::GetAppStyleSetName(), bLockViews ? "Icons.Lock" : "Icons.Unlock"); +} + +FSlateIcon SFlowDiff::GetSplitViewModeImage() const +{ + return FSlateIcon(FAppStyle::GetAppStyleSetName(), bVerticalSplitGraphMode ? "BlueprintDif.VerticalDiff.Small" : "BlueprintDif.HorizontalDiff.Small"); +} + +void SFlowDiff::ResetGraphEditors() const +{ + if (PanelOld.GraphEditor.IsValid() && PanelNew.GraphEditor.IsValid()) + { + if (bLockViews) + { + PanelOld.GraphEditor.Pin()->LockToGraphEditor(PanelNew.GraphEditor); + PanelNew.GraphEditor.Pin()->LockToGraphEditor(PanelOld.GraphEditor); + } + else + { + PanelOld.GraphEditor.Pin()->UnlockFromGraphEditor(PanelNew.GraphEditor); + PanelNew.GraphEditor.Pin()->UnlockFromGraphEditor(PanelOld.GraphEditor); + } + } +} + +void FFlowDiffPanel::GeneratePanel(UEdGraph* NewGraph, UEdGraph* OldGraph) +{ + const TSharedPtr> Diff = MakeShared>(); + FGraphDiffControl::DiffGraphs(OldGraph, NewGraph, *Diff); + GeneratePanel(NewGraph, Diff, {}); +} + +void FFlowDiffPanel::GeneratePanel(UEdGraph* Graph, TSharedPtr> DiffResults, TAttribute FocusedDiffResult) +{ + if (GraphEditor.IsValid() && GraphEditor.Pin()->GetCurrentGraph() == Graph) + { + return; + } + + TSharedPtr Widget = SNew(SBorder) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + [ + SNew(STextBlock).Text(LOCTEXT("FlowDiffPanelNoGraphTip", "Graph does not exist in this revision")) + ]; + + if (Graph) + { + SGraphEditor::FGraphEditorEvents InEvents; + { + const auto ContextMenuHandler = [](UEdGraph* CurrentGraph, const UEdGraphNode* InGraphNode, const UEdGraphPin* InGraphPin, FMenuBuilder* MenuBuilder, bool bIsDebugging) + { + MenuBuilder->AddMenuEntry(FGenericCommands::Get().Copy); + return FActionMenuContent(MenuBuilder->MakeWidget()); + }; + + InEvents.OnCreateNodeOrPinMenu = SGraphEditor::FOnCreateNodeOrPinMenu::CreateStatic(ContextMenuHandler); + } + + // Node single-click path (via SNodePanel) + InEvents.OnNodeSingleClicked = SGraphEditor::FOnNodeSingleClicked::CreateRaw(this, &FFlowDiffPanel::OnNodeClicked); + + // Selection-change path (covers sub-node/AddOn clicks) + InEvents.OnSelectionChanged = SGraphEditor::FOnSelectionChanged::CreateLambda([this](const FGraphPanelSelectionSet& NewSelection) + { + if (NewSelection.Num() == 1) + { + UObject* SelectedObj = NewSelection.Array()[0]; + OnNodeClicked(SelectedObj); + } + }); + + if (!GraphEditorCommands.IsValid()) + { + GraphEditorCommands = MakeShared(); + + GraphEditorCommands->MapAction( + FGenericCommands::Get().Copy, + FExecuteAction::CreateRaw(this, &FFlowDiffPanel::CopySelectedNodes), + FCanExecuteAction::CreateRaw(this, &FFlowDiffPanel::CanCopyNodes) + ); + } + + const TSharedRef Editor = SNew(SGraphEditor) + .AdditionalCommands(GraphEditorCommands) + .GraphToEdit(Graph) + .GraphToDiff(nullptr) + .DiffResults(DiffResults) + .FocusedDiffResult(FocusedDiffResult) + .IsEditable(false) + .GraphEvents(InEvents); + + GraphEditor = Editor; + Widget = Editor; + } + + GraphEditorBox->SetContent(Widget.ToSharedRef()); +} + +void FFlowDiffPanel::OnNodeClicked(UObject* ClickedNode) +{ + UFlowGraphNode* ClickedFlowGraphNode = Cast(ClickedNode); + if (IsValid(ClickedFlowGraphNode)) + { + PanelDefaultDetailsView->SetObject(ClickedFlowGraphNode->GetFlowNodeBase()); + } + else + { + PanelDefaultDetailsView->SetObject(nullptr); + } + + if (GraphDiffSplitter.IsValid()) + { + if (bIsOldPanel) + { + GraphDiffSplitter.Pin()->SetBottomLeftContent(PanelDefaultDetailsView.ToSharedRef()); + } + else + { + GraphDiffSplitter.Pin()->SetBottomRightContent(PanelDefaultDetailsView.ToSharedRef()); + } + } +} + +FGraphPanelSelectionSet FFlowDiffPanel::GetSelectedNodes() const +{ + FGraphPanelSelectionSet CurrentSelection; + const TSharedPtr FocusedGraphEd = GraphEditor.Pin(); + if (FocusedGraphEd.IsValid()) + { + CurrentSelection = FocusedGraphEd->GetSelectedNodes(); + } + return CurrentSelection; +} + +void FFlowDiffPanel::CopySelectedNodes() const +{ + // Export the selected nodes and place the text on the clipboard + const FGraphPanelSelectionSet SelectedNodes = GetSelectedNodes(); + + FString ExportedText; + FEdGraphUtilities::ExportNodesToText(SelectedNodes, /*out*/ ExportedText); + FPlatformApplicationMisc::ClipboardCopy(*ExportedText); +} + +bool FFlowDiffPanel::CanCopyNodes() const +{ + // If any of the nodes can be duplicated then we should allow copying + const FGraphPanelSelectionSet SelectedNodes = GetSelectedNodes(); + for (FGraphPanelSelectionSet::TConstIterator SelectedIter(SelectedNodes); SelectedIter; ++SelectedIter) + { + const UEdGraphNode* Node = Cast(*SelectedIter); + if ((Node != nullptr) && Node->CanDuplicateNode()) + { + return true; + } + } + return false; +} + +void FFlowDiffPanel::FocusDiff(const UEdGraphPin& Pin) const +{ + GraphEditor.Pin()->JumpToPin(&Pin); +} + +void FFlowDiffPanel::FocusDiff(const UEdGraphNode& Node) const +{ + if (GraphEditor.IsValid()) + { + GraphEditor.Pin()->JumpToNode(&Node, false); + } +} + +FFlowDiffPanel& SFlowDiff::GetDiffPanelForNode(const UEdGraphNode& Node) +{ + const ENodeDiffType NodeDiffType = GraphToDiff->GetNodeDiffType(Node); + + if (NodeDiffType == ENodeDiffType::Old) + { + return PanelOld; + } + if (NodeDiffType == ENodeDiffType::New) + { + return PanelNew; + } + + ensureMsgf(false, TEXT("Looking for node %s but it cannot be found in provided panels"), *Node.GetName()); + static FFlowDiffPanel Default; + return Default; +} + +void SFlowDiff::HandleGraphChanged(const FString& GraphPath) +{ + SetCurrentMode(GraphMode); + + UEdGraph* GraphOld = nullptr; + UEdGraph* GraphNew = nullptr; + TSharedPtr> DiffResults; + int32 RealDifferencesStartIndex = INDEX_NONE; + { + UEdGraph* NewGraph = GraphToDiff->GetGraphNew(); + UEdGraph* OldGraph = GraphToDiff->GetGraphOld(); + const FString OtherGraphPath = NewGraph ? FGraphDiffControl::GetGraphPath(NewGraph) : FGraphDiffControl::GetGraphPath(OldGraph); + if (GraphPath.Equals(OtherGraphPath)) + { + GraphNew = NewGraph; + GraphOld = OldGraph; + DiffResults = GraphToDiff->FoundDiffs; + RealDifferencesStartIndex = GraphToDiff->RealDifferencesStartIndex; + } + } + + const TAttribute FocusedDiffResult = TAttribute::CreateLambda( + [this, RealDifferencesStartIndex]() + { + int32 FocusedIndex = INDEX_NONE; + if (RealDifferencesStartIndex != INDEX_NONE) + { + FocusedIndex = DiffTreeView::CurrentDifference(DifferencesTreeView.ToSharedRef(), RealDifferences) - RealDifferencesStartIndex; + } + + // find selected index in all the graphs, and subtract the index of the first entry in this graph + return FocusedIndex; + }); + + // only regenerate PanelOld if the old graph has changed + if (PanelOld.FlowAsset && (!PanelOld.GraphEditor.IsValid() || GraphOld != PanelOld.GraphEditor.Pin()->GetCurrentGraph())) + { + PanelOld.GeneratePanel(GraphOld, DiffResults, FocusedDiffResult); + } + + // only regenerate PanelNew if the old graph has changed + if (PanelNew.FlowAsset && (!PanelNew.GraphEditor.IsValid() || GraphNew != PanelNew.GraphEditor.Pin()->GetCurrentGraph())) + { + PanelNew.GeneratePanel(GraphNew, DiffResults, FocusedDiffResult); + } +} + +void SFlowDiff::GenerateDifferencesList() +{ + PrimaryDifferencesList.Empty(); + RealDifferences.Empty(); + ModePanels.Empty(); + + const auto CreateInspector = [](const UObject* Object) + { + FPropertyEditorModule& EditModule = FModuleManager::Get().GetModuleChecked("PropertyEditor"); + + FNotifyHook* NotifyHook = nullptr; + + FDetailsViewArgs DetailsViewArgs; + DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea; + DetailsViewArgs.bHideSelectionTip = true; + DetailsViewArgs.NotifyHook = NotifyHook; + DetailsViewArgs.ViewIdentifier = FName("ObjectInspector"); + TSharedRef DetailsView = EditModule.CreateDetailView(DetailsViewArgs); + DetailsView->SetObject(const_cast(Object)); + + return DetailsView; + }; + + PanelOld.PanelDefaultDetailsView = CreateInspector(nullptr); + PanelNew.PanelDefaultDetailsView = CreateInspector(nullptr); + + // Now that we have done the diffs, create the panel widgets + ModePanels.Add(DetailsMode, GenerateDetailsPanel()); + ModePanels.Add(GraphMode, GenerateGraphPanel()); + + DifferencesTreeView->RebuildList(); +} + +SFlowDiff::FDiffControl SFlowDiff::GenerateDetailsPanel() +{ + const TSharedPtr NewDiffControl = MakeShared(PanelOld.FlowAsset, PanelNew.FlowAsset, FOnDiffEntryFocused::CreateRaw(this, &SFlowDiff::SetCurrentMode, DetailsMode)); + NewDiffControl->GenerateTreeEntries(PrimaryDifferencesList, RealDifferences); + + FDiffControl Ret; + Ret.DiffControl = NewDiffControl; + + const TSharedRef Splitter = SNew(SDetailsSplitter); + if (PanelOld.FlowAsset) + { + if (PanelNew.FlowAsset) + { + Splitter->AddSlot( + SDetailsSplitter::Slot() + .Value(0.5f) + .DetailsView(NewDiffControl->GetDetailsWidget(PanelOld.FlowAsset)) + .DifferencesWithRightPanel(NewDiffControl.ToSharedRef(), &FFlowAssetDiffControl::GetDifferencesWithRight, Cast(PanelOld.FlowAsset)) + ); + } + else + { + Splitter->AddSlot( + SDetailsSplitter::Slot() + .Value(0.5f) + .DetailsView(NewDiffControl->GetDetailsWidget(PanelOld.FlowAsset)) + ); + } + } + else + { + Splitter->AddSlot( + SDetailsSplitter::Slot() + .Value(0.5f) + .DetailsView(PanelOld.PanelDefaultDetailsView) + ); + } + + if ( PanelNew.FlowAsset) + { + if (PanelOld.FlowAsset) + { + Splitter->AddSlot( + SDetailsSplitter::Slot() + .Value(0.5f) + .DetailsView(NewDiffControl->GetDetailsWidget(PanelNew.FlowAsset)) + .DifferencesWithRightPanel(NewDiffControl.ToSharedRef(), &FFlowAssetDiffControl::GetDifferencesWithRight, Cast(PanelOld.FlowAsset)) + ); + } + else + { + Splitter->AddSlot( + SDetailsSplitter::Slot() + .Value(0.5f) + .DetailsView(NewDiffControl->GetDetailsWidget(PanelNew.FlowAsset)) + ); + } + } + else + { + Splitter->AddSlot( + SDetailsSplitter::Slot() + .Value(0.5f) + .DetailsView(PanelNew.PanelDefaultDetailsView) + ); + } + + Ret.Widget = Splitter; + + return Ret; +} + +SFlowDiff::FDiffControl SFlowDiff::GenerateGraphPanel() +{ + // We only have a single permanent graph in Flow Asset + GraphToDiff = MakeShared( + this, + IsValid(PanelOld.FlowAsset) ? PanelOld.FlowAsset->GetGraph() : nullptr, + IsValid(PanelNew.FlowAsset) ? PanelNew.FlowAsset->GetGraph() : nullptr, + PanelOld.RevisionInfo, + PanelNew.RevisionInfo); + GraphToDiff->GenerateTreeEntries(PrimaryDifferencesList, RealDifferences); + + SAssignNew(GraphDiffSplitter,SSplitter2x2) + .TopLeft()[ GenerateGraphWidgetForPanel(PanelOld) ] + .TopRight()[ GenerateGraphWidgetForPanel(PanelNew) ] + + .BottomLeft()[ PanelOld.PanelDefaultDetailsView.ToSharedRef() ] + .BottomRight()[ PanelNew.PanelDefaultDetailsView.ToSharedRef() ]; + + //the panels need a pointer to GraphDiffSplitter to update DetailsViews on click of a node. + PanelOld.GraphDiffSplitter = GraphDiffSplitter; + PanelNew.GraphDiffSplitter = GraphDiffSplitter; + + static const FVector2D GraphPercentage = {.5f, .7f}; + static const FVector2D DetailsViewPercentage = {.5f, .3f}; + static FVector2D Percentages[] = {GraphPercentage, DetailsViewPercentage, GraphPercentage, DetailsViewPercentage}; + GraphDiffSplitter->SetSplitterPercentages(MakeArrayView(Percentages, UE_ARRAY_COUNT(Percentages))); + + FDiffControl Ret; + Ret.Widget = GraphDiffSplitter; + + return Ret; +} + +TSharedRef SFlowDiff::GenerateGraphWidgetForPanel(FFlowDiffPanel& OutDiffPanel) const +{ + if (!IsValid(OutDiffPanel.FlowAsset)) + { + return SNullWidget::NullWidget; + } + + return SNew(SOverlay) + + SOverlay::Slot() // Graph slot + [ + SAssignNew(OutDiffPanel.GraphEditorBox, SBox) + .HAlign(HAlign_Fill) + [ + DefaultEmptyPanel() + ] + ] + + SOverlay::Slot() // Revision info slot + .VAlign(VAlign_Bottom) + .HAlign(HAlign_Right) + .Padding(FMargin(20.0f, 10.0f)) + [ + GenerateRevisionInfoWidgetForPanel(OutDiffPanel.OverlayGraphRevisionInfo, DiffViewUtils::GetPanelLabel(OutDiffPanel.FlowAsset, OutDiffPanel.RevisionInfo, FText())) + ]; +} + +TSharedRef SFlowDiff::GenerateRevisionInfoWidgetForPanel(TSharedPtr& OutGeneratedWidget, const FText& InRevisionText) const +{ + return SAssignNew(OutGeneratedWidget, SBox) + .Padding(FMargin(4.0f, 10.0f)) + .VAlign(VAlign_Center) + .HAlign(HAlign_Left) + [ + SNew(STextBlock) + .TextStyle(FAppStyle::Get(), "DetailsView.CategoryTextStyle") + .Text(InRevisionText) + .ShadowColorAndOpacity(FColor::Black) + .ShadowOffset(FVector2D(1.4, 1.4)) + ]; +} + +void SFlowDiff::SetCurrentMode(FName NewMode) +{ + if (CurrentMode == NewMode) + { + return; + } + + CurrentMode = NewMode; + + const FDiffControl* FoundControl = ModePanels.Find(NewMode); + + if (FoundControl) + { + ModeContents->SetContent(FoundControl->Widget.ToSharedRef()); + } + else + { + ensureMsgf(false, TEXT("Diff panel does not support mode %s"), *NewMode.ToString()); + } + + OnModeChanged(NewMode); +} + +void SFlowDiff::UpdateTopSectionVisibility(const FName& InNewViewMode) const +{ + SSplitter* GraphToolBarPtr = GraphToolBarWidget.Get(); + SSplitter* TopRevisionInfoWidgetPtr = TopRevisionInfoWidget.Get(); + + if (!GraphToolBarPtr || !TopRevisionInfoWidgetPtr) + { + return; + } + + if (InNewViewMode == GraphMode) + { + GraphToolBarPtr->SetVisibility(EVisibility::Visible); + TopRevisionInfoWidgetPtr->SetVisibility(EVisibility::Collapsed); + } + else + { + GraphToolBarPtr->SetVisibility(EVisibility::Collapsed); + TopRevisionInfoWidgetPtr->SetVisibility(EVisibility::HitTestInvisible); + } +} + +void SFlowDiff::OnModeChanged(const FName& InNewViewMode) const +{ + UpdateTopSectionVisibility(InNewViewMode); +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowActorOwnerComponentFilters.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowActorOwnerComponentFilters.cpp new file mode 100644 index 000000000..9ddb3c1e0 --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowActorOwnerComponentFilters.cpp @@ -0,0 +1,186 @@ + // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "FlowActorOwnerComponentFilters.h" + +#include "Components/ActorComponent.h" +#include "GameFramework/Actor.h" +#include "UObject/UObjectIterator.h" + +#if WITH_EDITOR +#include "EditorClassUtils.h" +#endif // WITH_EDITOR + +#if WITH_EDITOR + +const FName FFlowActorOwnerComponentFilters::NAME_AllowedClasses = "AllowedClasses"; +const FName FFlowActorOwnerComponentFilters::NAME_DisallowedClasses = "DisallowedClasses"; +const FName FFlowActorOwnerComponentFilters::NAME_MustImplement = "MustImplement"; + +void FFlowActorOwnerComponentFilters::BuildFiltersFromMetadata(const FProperty& ComponentNameProperty) +{ + if (bHasBuiltFilters) + { + return; + } + + BuildClassFilters(ComponentNameProperty); + BuildInterfaceFilters(ComponentNameProperty); + + bHasBuiltFilters = true; +} + +void FFlowActorOwnerComponentFilters::BuildClassFilters(const FProperty& ComponentNameProperty) +{ + // NOTE (gtaylor) Adapted from FComponentReferenceCustomization::BuildClassFilters() + + auto AddToClassFilters = [this](const UClass* Class, TArray& ComponentList) + { + if (Class->IsChildOf(UActorComponent::StaticClass())) + { + ComponentList.Add(Class); + } + }; + + auto ParseClassFilters = [this, AddToClassFilters](const FString& MetaDataString, TArray& ComponentList) + { + if (!MetaDataString.IsEmpty()) + { + TArray ClassFilterNames; + MetaDataString.ParseIntoArrayWS(ClassFilterNames, TEXT(","), true); + + for (const FString& ClassName : ClassFilterNames) + { + UClass* Class = FindFirstObject(*ClassName, EFindFirstObjectOptions::EnsureIfAmbiguous); + if (!Class) + { + Class = LoadObject(nullptr, *ClassName); + } + + if (Class) + { + // If the class is an interface, expand it to be all classes in memory that implement the class. + if (Class->HasAnyClassFlags(CLASS_Interface)) + { + for (TObjectIterator ClassIt; ClassIt; ++ClassIt) + { + UClass* const ClassWithInterface = (*ClassIt); + if (ClassWithInterface->ImplementsInterface(Class)) + { + AddToClassFilters(ClassWithInterface, ComponentList); + } + } + } + else + { + AddToClassFilters(Class, ComponentList); + } + } + } + } + }; + + // Account for the allowed classes specified in the property metadata + const FString& AllowedClassesFilterString = ComponentNameProperty.GetMetaData(NAME_AllowedClasses); + ParseClassFilters(AllowedClassesFilterString, MutableView(AllowedComponentClassFilters)); + + // Account for disallowed classes specified in the property metadata + const FString& DisallowedClassesFilterString = ComponentNameProperty.GetMetaData(NAME_DisallowedClasses); + ParseClassFilters(DisallowedClassesFilterString, MutableView(DisallowedComponentClassFilters)); +} + +void FFlowActorOwnerComponentFilters::BuildInterfaceFilters(const FProperty& ComponentNameProperty) +{ + auto ParseInterfaceFilters = [this](const FString& MetaDataString, TArray& RequiredInterfaces) + { + if (!MetaDataString.IsEmpty()) + { + TArray InterfaceFilterNames; + MetaDataString.ParseIntoArrayWS(InterfaceFilterNames, TEXT(","), true); + + for (const FString& InterfaceName : InterfaceFilterNames) + { + if (const UClass* RequiredInterface = FEditorClassUtils::GetClassFromString(InterfaceName)) + { + RequiredInterfaces.Add(RequiredInterface); + } + } + } + }; + + // MustImplement interface(s) + const FString& MustImplementInterfacesFilterString = ComponentNameProperty.GetMetaData(NAME_MustImplement); + ParseInterfaceFilters(MustImplementInterfacesFilterString, MutableView(RequiredInterfaceFilters)); +} + +bool FFlowActorOwnerComponentFilters::IsFilteredComponent(const UActorComponent& Component) const +{ + check(bHasBuiltFilters); + + // For Now(tm) at least, hard coding excluding Transient components + // (could make this configurable, but that doesn't make any sense for FRGIPeerComponentReference) + constexpr bool bAllowTransient = false; + if constexpr (!bAllowTransient) + { + const EObjectFlags Flags = Component.GetFlags(); + const bool bIsTransient = (Flags & RF_Transient) != 0; + if (bIsTransient) + { + // The component is allowed to be transient if the owning actor is also marked as transient. + // This happens with level instance actors placed in a level. + if (const AActor* CompOwnerActor = Component.GetOwner()) + { + const EObjectFlags OuterFlags = CompOwnerActor->GetFlags(); + const bool bIsOuterTransient = (OuterFlags & RF_Transient) != 0; + if(!bIsOuterTransient) + { + return false; + } + } + } + } + + // Check for required interface(s) + for (const UClass* RequiredInterface : RequiredInterfaceFilters) + { + if (IsValid(RequiredInterface) && !Component.GetClass()->ImplementsInterface(RequiredInterface)) + { + return false; + } + } + + // NOTE (gtaylor) Adapted from FComponentReferenceCustomization::IsFilteredObject + + bool bAllowedToSetBasedOnFilter = true; + + const UClass* ObjectClass = Component.GetClass(); + if (AllowedComponentClassFilters.Num() > 0) + { + bAllowedToSetBasedOnFilter = false; + for (const UClass* AllowedClass : AllowedComponentClassFilters) + { + const bool bAllowedClassIsInterface = AllowedClass->HasAnyClassFlags(CLASS_Interface); + if (ObjectClass->IsChildOf(AllowedClass) || (bAllowedClassIsInterface && ObjectClass->ImplementsInterface(AllowedClass))) + { + bAllowedToSetBasedOnFilter = true; + break; + } + } + } + + if (DisallowedComponentClassFilters.Num() > 0 && bAllowedToSetBasedOnFilter) + { + for (const UClass* DisallowedClass : DisallowedComponentClassFilters) + { + const bool bDisallowedClassIsInterface = DisallowedClass->HasAnyClassFlags(CLASS_Interface); + if (ObjectClass->IsChildOf(DisallowedClass) || (bDisallowedClassIsInterface && ObjectClass->ImplementsInterface(DisallowedClass))) + { + bAllowedToSetBasedOnFilter = false; + break; + } + } + } + + return bAllowedToSetBasedOnFilter; +} + +#endif // WITH_EDITOR diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowActorOwnerComponentFilters.h b/Source/FlowEditor/Private/DetailCustomizations/FlowActorOwnerComponentFilters.h new file mode 100644 index 000000000..12e0891ed --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowActorOwnerComponentFilters.h @@ -0,0 +1,57 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "UObject/ObjectPtr.h" + +#include "FlowActorOwnerComponentFilters.generated.h" + +// Forward Declarations +class UActorComponent; +class IPropertyHandle; + +// Metadata-derived filters to describe qualifying UActorComponents +// for a given FFlowActorOwnerComponentRef +USTRUCT() +struct FFlowActorOwnerComponentFilters +{ + GENERATED_BODY() + +#if WITH_EDITOR +public: + void BuildFiltersFromMetadata(const FProperty& ComponentNameProperty); + + // Returns true if the Component passes the filters (built in BuildFiltersFromMetadata) + bool IsFilteredComponent(const UActorComponent& Component) const; + +protected: + void BuildClassFilters(const FProperty& ComponentNameProperty); + void BuildInterfaceFilters(const FProperty& ComponentNameProperty); + +#endif // WITH_EDITOR + +protected: + +#if WITH_EDITORONLY_DATA + // Classes that can be used with this property + UPROPERTY(Transient) + TArray> AllowedComponentClassFilters; + + // Classes that can NOT be used with this property + UPROPERTY(Transient) + TArray> DisallowedComponentClassFilters; + + // Must implement (all) interface(s) + UPROPERTY(Transient) + TArray> RequiredInterfaceFilters; + + // Has BuildClassFiltersFromMetadata been called? + UPROPERTY(Transient) + bool bHasBuiltFilters = false; + + // Meta-data keys + static const FName NAME_AllowedClasses; + static const FName NAME_DisallowedClasses; + static const FName NAME_MustImplement; +#endif // WITH_EDITORONLY_DATA +}; diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowActorOwnerComponentRefCustomization.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowActorOwnerComponentRefCustomization.cpp new file mode 100644 index 000000000..2a79b67f8 --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowActorOwnerComponentRefCustomization.cpp @@ -0,0 +1,142 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowActorOwnerComponentRefCustomization.h" + +#include "AddOns/FlowNodeAddOn.h" +#include "FlowAsset.h" +#include "FlowActorOwnerComponentFilters.h" +#include "Nodes/FlowNode.h" + +#include "UObject/UnrealType.h" +#include "GameFramework/Actor.h" + +void FFlowActorOwnerComponentRefCustomization::CustomizeChildren(TSharedRef InStructPropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ + // Do not include children properties (the header is all we need to show for this struct) +} + +TSharedPtr FFlowActorOwnerComponentRefCustomization::GetCuratedNamePropertyHandle() const +{ + check(StructPropertyHandle->IsValidHandle()); + + TSharedPtr FoundHandle = StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FFlowActorOwnerComponentRef, ComponentName)); + check(FoundHandle); + + return FoundHandle; +} + +TArray FFlowActorOwnerComponentRefCustomization::GetCuratedNameOptions() const +{ + TArray Results; + + UClass* ExpectedOwnerClass = TryGetExpectedOwnerClass(); + if (!IsValid(ExpectedOwnerClass) || !ExpectedOwnerClass->IsChildOf()) + { + return Results; + } + + Results = GetFlowActorOwnerComponents(ExpectedOwnerClass); + + return Results; +} + +UClass* FFlowActorOwnerComponentRefCustomization::TryGetExpectedOwnerClass() const +{ + const UFlowNode* NodeOwner = TryGetFlowNodeOuter(); + if (!IsValid(NodeOwner)) + { + return nullptr; + } + + const UFlowAsset* FlowAsset = NodeOwner->GetFlowAsset(); + if (!IsValid(FlowAsset)) + { + return nullptr; + } + + UClass* ExpectedOwnerClass = FlowAsset->GetExpectedOwnerClass(); + return ExpectedOwnerClass; +} + +TArray FFlowActorOwnerComponentRefCustomization::GetFlowActorOwnerComponents(TSubclassOf ExpectedActorOwnerClass) const +{ + TArray AllComponents; + + AActor::GetActorClassDefaultComponents(ExpectedActorOwnerClass, AllComponents); + + // Array for components that pass the metadata filter + TArray PassedComponentNames; + PassedComponentNames.Reserve(AllComponents.Num()); + + const FProperty* MetadataProperty = StructPropertyHandle->GetMetaDataProperty(); + if (ensure(MetadataProperty)) + { + // Pull the metadata from the struct property, setting up the AllowedClass filters, etc. + FFlowActorOwnerComponentFilters Filters; + Filters.BuildFiltersFromMetadata(*MetadataProperty); + + for (const UActorComponent* ActorComponent : AllComponents) + { + if (Filters.IsFilteredComponent(*ActorComponent)) + { + FString ComponentCleanedName = ActorComponent->GetFName().ToString(); + + // Some components end with _GEN_VARIABLE, remove that suffix so we can match component names + ComponentCleanedName.RemoveFromEnd(UActorComponent::ComponentTemplateNameSuffix); + + PassedComponentNames.Add(FName(ComponentCleanedName)); + } + } + } + + return PassedComponentNames; +} + +void FFlowActorOwnerComponentRefCustomization::SetCuratedName(const FName& NewComponentName) +{ + TSharedPtr ComponentNameHandle = StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FFlowActorOwnerComponentRef, ComponentName)); + + check(ComponentNameHandle); + + ComponentNameHandle->SetPerObjectValue(0, NewComponentName.ToString()); +} + +bool FFlowActorOwnerComponentRefCustomization::TryGetCuratedName(FName& OutName) const +{ + const FFlowActorOwnerComponentRef* ComponentRef = GetFlowActorOwnerComponentRef(); + if (ComponentRef) + { + OutName = ComponentRef->ComponentName; + + return true; + } + else + { + return false; + } +} + +UFlowNode* FFlowActorOwnerComponentRefCustomization::TryGetFlowNodeOuter() const +{ + check(StructPropertyHandle->IsValidHandle()); + + TArray OuterObjects; + StructPropertyHandle->GetOuterObjects(OuterObjects); + + for (UObject* OuterObject : OuterObjects) + { + UFlowNode* FlowNodeOuter = Cast(OuterObject); + if (IsValid(FlowNodeOuter)) + { + return FlowNodeOuter; + } + + UFlowNodeAddOn* FlowNodeAddOnOuter = Cast(OuterObject); + if (IsValid(FlowNodeAddOnOuter)) + { + return FlowNodeAddOnOuter->GetFlowNode(); + } + } + + return nullptr; +} diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowAssetDetails.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowAssetDetails.cpp new file mode 100644 index 000000000..415d2d6eb --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowAssetDetails.cpp @@ -0,0 +1,156 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowAssetDetails.h" +#include "FlowAsset.h" +#include "Nodes/Graph/FlowNode_SubGraph.h" + +#include "DetailLayoutBuilder.h" +#include "IDetailChildrenBuilder.h" +#include "PropertyCustomizationHelpers.h" + +#include "Graph/FlowGraphEditor.h" +#include "Graph/FlowGraphUtils.h" + +#include "Nodes/Graph/FlowNode_CustomInput.h" +#include "Nodes/Graph/FlowNode_CustomOutput.h" + +#include "Widgets/Input/SEditableTextBox.h" +#include "Widgets/SBoxPanel.h" + +#define LOCTEXT_NAMESPACE "FlowAssetDetails" + +void FFlowAssetDetails::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) +{ + DetailBuilder.GetObjectsBeingCustomized(ObjectsBeingEdited); + + IDetailCategoryBuilder& FlowAssetCategory = DetailBuilder.EditCategory("SubGraph", LOCTEXT("SubGraphCategory", "Sub Graph")); + + TArray> ArrayPropertyHandles; + CustomInputsHandle = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UFlowAsset, CustomInputs)); + CustomOutputsHandle = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UFlowAsset, CustomOutputs)); + ArrayPropertyHandles.Add(CustomInputsHandle); + ArrayPropertyHandles.Add(CustomOutputsHandle); + for (const TSharedPtr& PropertyHandle : ArrayPropertyHandles) + { + if (PropertyHandle.IsValid() && PropertyHandle->AsArray().IsValid()) + { + const TSharedRef ArrayBuilder = MakeShareable(new FDetailArrayBuilder(PropertyHandle.ToSharedRef())); + ArrayBuilder->OnGenerateArrayElementWidget(FOnGenerateArrayElementWidget::CreateSP(this, &FFlowAssetDetails::GenerateCustomPinArray)); + + FlowAssetCategory.AddCustomBuilder(ArrayBuilder); + } + } +} + +void FFlowAssetDetails::GenerateCustomPinArray(TSharedRef PropertyHandle, int32 ArrayIndex, IDetailChildrenBuilder& ChildrenBuilder) +{ + IDetailPropertyRow& PropertyRow = ChildrenBuilder.AddProperty(PropertyHandle); + PropertyRow.ShowPropertyButtons(true); + PropertyRow.ShouldAutoExpand(true); + + PropertyRow.CustomWidget(false) + .ValueContent() + [ + SNew(SHorizontalBox) + + + SHorizontalBox::Slot() + .FillWidth(1.f) + .Padding(2.f, 0.f) + .VAlign(VAlign_Center) + [ + SNew(SEditableTextBox) + .Text(this, &FFlowAssetDetails::GetCustomPinText, PropertyHandle) + .OnTextCommitted_Static(&FFlowAssetDetails::OnCustomPinTextCommitted, PropertyHandle) + .OnVerifyTextChanged_Static(&FFlowAssetDetails::VerifyNewCustomPinText) + ] + + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + [ + PropertyCustomizationHelpers::MakeBrowseButton( + FSimpleDelegate::CreateRaw(this, &FFlowAssetDetails::OnBrowseClicked, PropertyHandle), + LOCTEXT("SelectEventNode", "Select Event Node in Graph"), + TAttribute::CreateRaw(this, &FFlowAssetDetails::IsBrowseEnabled, PropertyHandle), + true) // intentionally true, to set "correct" icon + ] + ]; +} + +FText FFlowAssetDetails::GetCustomPinText(TSharedRef PropertyHandle) const +{ + FText PropertyValue; + const FPropertyAccess::Result GetValueResult = PropertyHandle->GetValueAsDisplayText(PropertyValue); + ensure(GetValueResult == FPropertyAccess::Success); + return PropertyValue; +} + +void FFlowAssetDetails::OnCustomPinTextCommitted(const FText& InText, ETextCommit::Type InCommitType, TSharedRef PropertyHandle) +{ + const FPropertyAccess::Result SetValueResult = PropertyHandle->SetValueFromFormattedString(InText.ToString()); + ensure(SetValueResult == FPropertyAccess::Success); +} + +bool FFlowAssetDetails::VerifyNewCustomPinText(const FText& InNewText, FText& OutErrorMessage) +{ + const FName NewString = *InNewText.ToString(); + + if (NewString == UFlowNode_SubGraph::StartPin.PinName || NewString == UFlowNode_SubGraph::FinishPin.PinName) + { + OutErrorMessage = LOCTEXT("VerifyTextFailed", "This is a standard pin name of Sub Graph node!"); + return false; + } + + return true; +} + +void FFlowAssetDetails::OnBrowseClicked(TSharedRef PropertyHandle) +{ + ensure(ObjectsBeingEdited[0].IsValid()); + + UFlowAsset* Asset = Cast(ObjectsBeingEdited[0]); + UFlowNode_CustomEventBase* EventNode = GetCustomEventNode(PropertyHandle); + + if (EventNode) + { + TSharedPtr Editor = FFlowGraphUtils::GetFlowGraphEditor(Asset->GetGraph()); + Editor->ClearSelectionSet(); + Editor->SelectSingleNode(EventNode->GetGraphNode()); + Editor->ZoomToFit(true); + } +} + +bool FFlowAssetDetails::IsBrowseEnabled(TSharedRef PropertyHandle) const +{ + return GetCustomEventNode(PropertyHandle) != nullptr; +} + +UFlowNode_CustomEventBase* FFlowAssetDetails::GetCustomEventNode(TSharedRef PropertyHandle) const +{ + ensure(ObjectsBeingEdited[0].IsValid()); + + UFlowAsset* Asset = Cast(ObjectsBeingEdited[0]); + FName Text = FName(GetCustomPinText( PropertyHandle ).ToString()); + TSharedPtr ArrayHandle = PropertyHandle->GetParentHandle(); + + if (ArrayHandle->IsSamePropertyNode(CustomInputsHandle)) + { + UFlowNode_CustomInput* Input = Asset->TryFindCustomInputNodeByEventName(Text); + if (Input) + { + return Input; + } + } + else if (ArrayHandle->IsSamePropertyNode(CustomOutputsHandle)) + { + UFlowNode_CustomOutput* Output = Asset->TryFindCustomOutputNodeByEventName(Text); + if (Output) + { + return Output; + } + } + + return nullptr; +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowAssetParamsPtrCustomization.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowAssetParamsPtrCustomization.cpp new file mode 100644 index 000000000..dc3406b04 --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowAssetParamsPtrCustomization.cpp @@ -0,0 +1,192 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowAssetParamsPtrCustomization.h" +#include "Asset/FlowAssetParams.h" +#include "FlowAsset.h" +#include "FlowComponent.h" +#include "FlowEditorLogChannels.h" +#include "Interfaces/FlowAssetProviderInterface.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "ContentBrowserModule.h" +#include "DetailLayoutBuilder.h" +#include "DetailWidgetRow.h" +#include "IContentBrowserSingleton.h" +#include "PropertyCustomizationHelpers.h" +#include "Widgets/Input/SButton.h" + +#define LOCTEXT_NAMESPACE "FlowAssetParamsPtrCustomization" + +TSharedRef FFlowAssetParamsPtrCustomization::MakeInstance() +{ + return MakeShared(); +} + +void FFlowAssetParamsPtrCustomization::CustomizeHeader( + TSharedRef PropertyHandle, + FDetailWidgetRow& HeaderRow, + IPropertyTypeCustomizationUtils& CustomizationUtils) +{ + StructPropertyHandle = PropertyHandle; + + const TSharedRef ObjectPicker = SNew(SObjectPropertyEntryBox) + .PropertyHandle(PropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FFlowAssetParamsPtr, AssetPtr))) + .AllowedClass(UFlowAssetParams::StaticClass()) + .AllowClear(true) + .DisplayUseSelected(false) + .DisplayBrowse(false) + .DisplayCompactSize(true) + .OnShouldFilterAsset(this, &FFlowAssetParamsPtrCustomization::ShouldFilterAsset); + + // Show create button if ShowCreateNew metadata is specified + const bool bShowCreateButton = PropertyHandle->HasMetaData(TEXT("ShowCreateNew")); + + HeaderRow + .NameContent()[PropertyHandle->CreatePropertyNameWidget()] + .ValueContent() + .MinDesiredWidth(200.f) + .MaxDesiredWidth(800.f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f) + [ + ObjectPicker + ] + + SHorizontalBox::Slot().AutoWidth().Padding(2, 0) + .VAlign(VAlign_Center) + [ + bShowCreateButton ? + PropertyCustomizationHelpers::MakeAddButton( + FSimpleDelegate::CreateSP(this, &FFlowAssetParamsPtrCustomization::HandleCreateNew), + LOCTEXT("CreateNewAsset", "Create New") + ) : + SNullWidget::NullWidget + ] + ]; +} + +void FFlowAssetParamsPtrCustomization::CustomizeChildren( + TSharedRef PropertyHandle, + IDetailChildrenBuilder& ChildBuilder, + IPropertyTypeCustomizationUtils& CustomizationUtils) +{ +} + +void FFlowAssetParamsPtrCustomization::HandleCreateNew() +{ + if (!StructPropertyHandle.IsValid()) + { + UE_LOG(LogFlowEditor, Error, TEXT("Invalid property handle for FFlowAssetParamsPtr customization")); + + return; + } + + TArray OuterObjects; + StructPropertyHandle->GetOuterObjects(OuterObjects); + if (OuterObjects.Num() == 0) + { + UE_LOG(LogFlowEditor, Error, TEXT("No outer objects found for BaseAssetParams")); + + return; + } + + const FName PropertyName = StructPropertyHandle->GetProperty()->GetFName(); + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + + TArray AssetsToSync; + + for (UObject* OuterObject : OuterObjects) + { + UFlowAsset* FlowAsset = CastChecked(OuterObject, ECastCheckedType::NullAllowed); + if (!IsValid(FlowAsset)) + { + UE_LOG(LogFlowEditor, Error, TEXT("Outer object is not a valid UFlowAsset: %s"), *OuterObject->GetPathName()); + + continue; + } + + if (PropertyName != GET_MEMBER_NAME_CHECKED(UFlowAsset, BaseAssetParams)) + { + UE_LOG(LogFlowEditor, Error, TEXT("Property %s is not BaseAssetParams for %s"), *PropertyName.ToString(), *FlowAsset->GetPathName()); + + continue; + } + + UFlowAssetParams* NewParams = FlowAsset->GenerateParamsFromStartNode(); + if (IsValid(NewParams)) + { + StructPropertyHandle->NotifyPreChange(); + StructPropertyHandle->SetValueFromFormattedString(NewParams->GetPathName()); + StructPropertyHandle->NotifyPostChange(EPropertyChangeType::ValueSet); + + AssetRegistryModule.Get().AssetCreated(NewParams); + AssetsToSync.Add(NewParams); + } + } + + if (!AssetsToSync.IsEmpty()) + { + FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked("ContentBrowser"); + ContentBrowserModule.Get().SyncBrowserToAssets(AssetsToSync, true); + } +} + +bool FFlowAssetParamsPtrCustomization::ShouldFilterAsset(const FAssetData& AssetData) const +{ + UFlowAssetParams* Params = Cast(AssetData.GetAsset()); + if (!Params) + { + // Filter out invalid assets + return true; + } + + // Ensure Params->OwnerFlowAsset is valid + if (Params->OwnerFlowAsset.IsNull()) + { + UE_LOG(LogFlowEditor, Warning, TEXT("OwnerFlowAsset is null for %s"), *AssetData.GetFullName()); + + // Filter out if OwnerFlowAsset is invalid + return true; + } + + // Check if child params are allowed + const bool bHideChildParams = StructPropertyHandle->HasMetaData(TEXT("HideChildParams")); + if (bHideChildParams && !Params->ParentParams.AssetPtr.IsNull()) + { + // Filter out params with non-null ParentParams unless allowed + return true; + } + + TArray OuterObjects; + StructPropertyHandle->GetOuterObjects(OuterObjects); + if (OuterObjects.IsEmpty()) + { + UE_LOG(LogFlowEditor, Warning, TEXT("No outer objects found for FFlowAssetParamsPtr customization")); + + // Filter out if no outer objects + return true; + } + + // All OwnerAssets must match Params->OwnerFlowAsset + for (UObject* OuterObject : OuterObjects) + { + UFlowAsset* OwnerAssetCur = Cast(OuterObject); + if (!OwnerAssetCur) + { + if (IFlowAssetProviderInterface* AssetProvider = Cast(OuterObject)) + { + OwnerAssetCur = AssetProvider->ProvideFlowAsset(); + } + } + + if (!IsValid(OwnerAssetCur) || Params->OwnerFlowAsset != OwnerAssetCur) + { + // Filter out if any OwnerAsset doesn't match + return true; + } + } + + // Allow the asset + return false; +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowDataPinValueCustomization.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowDataPinValueCustomization.cpp new file mode 100644 index 000000000..731a7394d --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowDataPinValueCustomization.cpp @@ -0,0 +1,548 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowDataPinValueCustomization.h" +#include "Interfaces/FlowDataPinValueOwnerInterface.h" +#include "Types/FlowDataPinValuesStandard.h" +#include "UnrealExtensions/VisibilityArrayBuilder.h" + +#include "DetailLayoutBuilder.h" +#include "DetailWidgetRow.h" +#include "IDetailChildrenBuilder.h" +#include "IDetailPropertyRow.h" +#include "IPropertyUtilities.h" +#include "ScopedTransaction.h" +#include "Widgets/Input/SCheckBox.h" +#include "Widgets/Input/SComboBox.h" +#include "Widgets/Text/STextBlock.h" +#include "UObject/EnumProperty.h" + +#define LOCTEXT_NAMESPACE "FlowDataPinValueCustomization" + +static const TCHAR HiddenMeta[] = TEXT("Hidden"); + +FText FFlowDataPinValueCustomization::GetMultiTypeTooltip() +{ + return LOCTEXT("MultiTypeTooltip", + "Select whether this Data Pin holds a Single value or an Array of values.\n" + "Changing from Array to Single trims the array to the first element."); +} +FText FFlowDataPinValueCustomization::GetInputPinTooltip() +{ + return LOCTEXT("InputPinTooltip", + "Marks this Data Pin as an Input.\nChecked = Input Pin, Unchecked = Output Pin."); +} + +TSharedRef FFlowDataPinValueCustomization::MakeInstance() +{ + return MakeShareable(new FFlowDataPinValueCustomization()); +} + +void FFlowDataPinValueCustomization::CustomizeHeader(TSharedRef InStructPropertyHandle, + FDetailWidgetRow& HeaderRow, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ + Super::CustomizeHeader(InStructPropertyHandle, HeaderRow, StructCustomizationUtils); + + CacheHandles(InStructPropertyHandle, StructCustomizationUtils); + CacheOwnerInterface(); + CacheArraySupported(); + + // Populate MultiTypeOptions from enum (respect bArraySupported) + MultiTypeOptions.Reset(); + if (const UEnum* MultiTypeEnum = StaticEnum()) + { + const int32 NumEnums = FMath::Min(static_cast(FlowEnum::MaxOf()), MultiTypeEnum->NumEnums()); + for (int32 i = 0; i < NumEnums; ++i) + { + if (MultiTypeEnum->HasMetaData(HiddenMeta, i)) + { + continue; + } + const int64 Value = MultiTypeEnum->GetValueByIndex(i); + EFlowDataMultiType MT = static_cast(Value); + if (!bArraySupported && MT == EFlowDataMultiType::Array) + { + continue; + } + MultiTypeOptions.Add(MakeShareable(new int32(static_cast(Value)))); + } + } + + // If current mode is Array but unsupported, force Single (non-transactable) + if (!bArraySupported && MultiTypeHandle.IsValid()) + { + uint8 CurrentValue = 0; + if (MultiTypeHandle->GetValue(CurrentValue) == FPropertyAccess::Success && + static_cast(CurrentValue) == EFlowDataMultiType::Array) + { + MultiTypeHandle->SetValue(static_cast(EFlowDataMultiType::Single), + EPropertyValueSetFlags::NotTransactable); + } + + if (MultiTypeComboBox.IsValid()) + { + MultiTypeComboBox->SetEnabled(false); + } + } + + // Select current + const EFlowDataMultiType CurrentType = GetCurrentMultiType(); + for (auto& Opt : MultiTypeOptions) + { + if (Opt.IsValid() && static_cast(*Opt) == CurrentType) + { + SelectedMultiType = Opt; + break; + } + } + + TSharedRef HeaderBox = SNew(SHorizontalBox); + + // MultiType control (combo or static label if array unsupported) + if (bArraySupported) + { + HeaderBox->AddSlot() + .FillWidth(1.0f) + .VAlign(VAlign_Center) + [ + SAssignNew(MultiTypeComboBox, SComboBox>) + .OptionsSource(&MultiTypeOptions) + .OnGenerateWidget(this, &FFlowDataPinValueCustomization::GenerateMultiTypeWidget) + .OnSelectionChanged(this, &FFlowDataPinValueCustomization::OnMultiTypeChanged) + .IsEnabled(this, &FFlowDataPinValueCustomization::GetInputPinCheckboxEnabled) + .ToolTipText(GetMultiTypeTooltip()) + .Content() + [ + SNew(STextBlock) + .Text(this, &FFlowDataPinValueCustomization::GetSelectedMultiTypeText) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + ]; + } + else + { + HeaderBox->AddSlot() + .FillWidth(1.0f) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(LOCTEXT("MultiTypeForcedSingle", "Single")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .ToolTipText(LOCTEXT("MultiTypeForcedSingleTooltip", "This pin type does not support Array mode.")) + ]; + } + + // Input Pin checkbox + HeaderBox->AddSlot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(4.f, 0.f) + [ + SNew(SCheckBox) + .IsChecked(this, &FFlowDataPinValueCustomization::GetCurrentIsInputPin) + .OnCheckStateChanged(this, &FFlowDataPinValueCustomization::OnInputPinChanged) + .IsEnabled(this, &FFlowDataPinValueCustomization::GetInputPinCheckboxEnabled) + .Visibility(this, &FFlowDataPinValueCustomization::GetInputPinCheckboxVisibility) + .ToolTipText(GetInputPinTooltip()) + [ + SNew(STextBlock) + .Text(LOCTEXT("InputPin", "Input Pin")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + ]; + + HeaderRow + .NameContent() + [ + SNew(STextBlock) + .Text(StructPropertyHandle->GetPropertyDisplayName()) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(250.f) + [ + HeaderBox + ]; +} + +void FFlowDataPinValueCustomization::CustomizeChildren(TSharedRef InStructPropertyHandle, + IDetailChildrenBuilder& StructBuilder, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ + BuildValueRows(InStructPropertyHandle, StructBuilder, StructCustomizationUtils); +} + +void FFlowDataPinValueCustomization::BuildValueRows(TSharedRef InStructPropertyHandle, + IDetailChildrenBuilder& StructBuilder, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ + CacheHandles(InStructPropertyHandle, StructCustomizationUtils); + CacheArraySupported(); + + if (!ValuesHandle.IsValid()) + { + return; + } + + if (bArraySupported) + { + EnsureSingleElementExists(); + } + + BuildSingleBranch(StructBuilder); + if (bArraySupported) + { + BuildArrayBranch(StructBuilder); + } +} + +void FFlowDataPinValueCustomization::BuildSingleBranch(IDetailChildrenBuilder& StructBuilder) +{ + if (GetSingleModeVisibility() == EVisibility::Collapsed) + { + return; + } + + if (!ValuesHandle.IsValid()) + { + return; + } + + TSharedPtr ValueToShow = bArraySupported + ? ValuesHandle->GetChildHandle(0) + : ValuesHandle; + + if (!ValueToShow.IsValid()) + { + return; + } + + IDetailPropertyRow& Row = StructBuilder.AddProperty(ValueToShow.ToSharedRef()); + Row.ShouldAutoExpand(true); +} + +void FFlowDataPinValueCustomization::BuildArrayBranch(IDetailChildrenBuilder& StructBuilder) +{ + if (GetArrayModeVisibility() == EVisibility::Collapsed) + { + return; + } + + if (bArraySupported && ValuesHandle.IsValid() && ValuesHandle->AsArray()) + { + IDetailPropertyRow& Row = StructBuilder.AddProperty(ValuesHandle.ToSharedRef()); + Row.ShouldAutoExpand(true); + } +} + +void FFlowDataPinValueCustomization::RequestRefresh() +{ + if (PropertyUtilities.IsValid()) + { + PropertyUtilities->RequestRefresh(); + } +} + +void FFlowDataPinValueCustomization::EnsureSingleElementExists() +{ + if (!ValuesHandle.IsValid()) + { + return; + } + + if (bArraySupported) + { + if (auto AsArray = ValuesHandle->AsArray()) + { + uint32 Num = 0; + AsArray->GetNumElements(Num); + if (Num == 0) + { + AsArray->AddItem(); + } + } + } +} + +void FFlowDataPinValueCustomization::CacheHandles(const TSharedRef& PropertyHandle, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ + CustomizationUtils = &StructCustomizationUtils; + MultiTypeHandle = PropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FFlowDataPinValue, MultiType)); + IsInputPinHandle = PropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FFlowDataPinValue, bIsInputPin)); + PropertyUtilities = StructCustomizationUtils.GetPropertyUtilities(); + + if (auto* Value = GetFlowDataPinValueBeingEdited()) + { + DataPinType = Value->LookupPinType(); + if (DataPinType) + { + ValuesHandle = DataPinType->GetValuesHandle(PropertyHandle); + } + } +} + +void FFlowDataPinValueCustomization::CacheOwnerInterface() +{ + OwnerInterface = nullptr; + TArray Outers; + StructPropertyHandle->GetOuterObjects(Outers); + + if (Outers.Num() == 1) + { + OwnerInterface = Cast(Outers[0]); + } +} + +void FFlowDataPinValueCustomization::CacheArraySupported() +{ + bArraySupported = DataPinType ? DataPinType->SupportsMultiType(EFlowDataMultiType::Array) : true; +} + +void FFlowDataPinValueCustomization::OnMultiTypeChanged(TSharedPtr NewSelection, ESelectInfo::Type) +{ + if (!NewSelection.IsValid() || !MultiTypeHandle.IsValid()) + { + return; + } + + if (!bArraySupported) + { + return; + } + + const EFlowDataMultiType NewType = static_cast(*NewSelection); + + bool bNeedsTrim = (NewType == EFlowDataMultiType::Single); + if (bNeedsTrim && ValuesHandle.IsValid()) + { + if (auto AsArray = ValuesHandle->AsArray()) + { + uint32 NumElements = 0; + AsArray->GetNumElements(NumElements); + bNeedsTrim = NumElements > 1; + } + } + + FScopedTransaction Transaction(LOCTEXT("ChangePinMultiType", "Change Pin MultiType")); + MultiTypeHandle->NotifyPreChange(); + MultiTypeHandle->SetValue(static_cast(NewType)); + if (bNeedsTrim) + { + TrimArrayToSingle(); + } + MultiTypeHandle->NotifyPostChange(EPropertyChangeType::ValueSet); + + SelectedMultiType = NewSelection; + + // Preferred: trigger owner rebuild +#if WITH_EDITOR + if (OwnerInterface) + { + OwnerInterface->RequestFlowDataPinValuesDetailsRebuild(); + } + else + { + RequestRefresh(); + } +#endif +} + +void FFlowDataPinValueCustomization::OnInputPinChanged(ECheckBoxState NewState) +{ + if (!IsInputPinHandle.IsValid()) + { + return; + } + + bool Existing = false; + IsInputPinHandle->GetValue(Existing); + const bool bNewValue = NewState == ECheckBoxState::Checked; + + if (Existing == bNewValue) + { + return; + } + + FScopedTransaction Transaction(LOCTEXT("ChangeInputPin", "Change Input Pin")); + IsInputPinHandle->NotifyPreChange(); + IsInputPinHandle->SetValue(bNewValue); + IsInputPinHandle->NotifyPostChange(EPropertyChangeType::ValueSet); + + RequestRefresh(); +} + +void FFlowDataPinValueCustomization::TrimArrayToSingle() +{ + if (!ValuesHandle.IsValid()) + { + return; + } + + if (auto AsArray = ValuesHandle->AsArray()) + { + uint32 NumElements = 0; + AsArray->GetNumElements(NumElements); + + if (NumElements == 0) + { + AsArray->AddItem(); + } + else + { + while (NumElements > 1) + { + AsArray->DeleteItem(NumElements - 1); + AsArray->GetNumElements(NumElements); + } + } + + RequestRefresh(); + } +} + +EFlowDataMultiType FFlowDataPinValueCustomization::GetCurrentMultiType() const +{ + if (MultiTypeHandle.IsValid()) + { + uint8 Value = 0; + if (MultiTypeHandle->GetValue(Value) == FPropertyAccess::Success) + { + return static_cast(Value); + } + } + return EFlowDataMultiType::Single; +} + +ECheckBoxState FFlowDataPinValueCustomization::GetCurrentIsInputPin() const +{ + if (IsInputPinHandle.IsValid()) + { + bool Value = false; + IsInputPinHandle->GetValue(Value); + return Value ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + } + return ECheckBoxState::Unchecked; +} + +EVisibility FFlowDataPinValueCustomization::GetSingleModeVisibility() const +{ + return GetCurrentMultiType() == EFlowDataMultiType::Single ? EVisibility::Visible : EVisibility::Collapsed; +} + +EVisibility FFlowDataPinValueCustomization::GetArrayModeVisibility() const +{ + if (!bArraySupported) + { + return EVisibility::Collapsed; + } + return GetCurrentMultiType() == EFlowDataMultiType::Array ? EVisibility::Visible : EVisibility::Collapsed; +} + +EVisibility FFlowDataPinValueCustomization::GetInputPinCheckboxVisibility() const +{ + return OwnerInterface && OwnerInterface->ShowFlowDataPinValueInputPinCheckbox() + ? EVisibility::Visible + : EVisibility::Collapsed; +} + +bool FFlowDataPinValueCustomization::GetInputPinCheckboxEnabled() const +{ + return OwnerInterface ? OwnerInterface->CanModifyFlowDataPinType() : true; +} + +TSharedRef FFlowDataPinValueCustomization::GenerateMultiTypeWidget(TSharedPtr Item) const +{ + const UEnum* MultiTypeEnum = StaticEnum(); + return SNew(STextBlock) + .Text(Item.IsValid() && MultiTypeEnum + ? MultiTypeEnum->GetDisplayNameTextByValue(*Item) + : FText::GetEmpty()) + .Font(IDetailLayoutBuilder::GetDetailFont()); +} + +FText FFlowDataPinValueCustomization::GetSelectedMultiTypeText() const +{ + const UEnum* MultiTypeEnum = StaticEnum(); + return (SelectedMultiType.IsValid() && MultiTypeEnum) + ? MultiTypeEnum->GetDisplayNameTextByValue(*SelectedMultiType) + : FText::GetEmpty(); +} + +void FFlowDataPinValueCustomization::BuildVisibilityAwareArray( + IDetailChildrenBuilder& StructBuilder, + TSharedPtr ArrayHandle, + TFunction, int32, IDetailChildrenBuilder&, const TAttribute&)> Generator, + TAttribute VisibilityAttribute) +{ + if (!ArrayHandle.IsValid() || !bArraySupported) + { + return; + } + + TSharedRef ArrayBuilder = + MakeShareable(new FVisibilityArrayBuilder(ArrayHandle.ToSharedRef(), true, true, true)); + + ArrayBuilder->SetVisibilityGetter([VisibilityAttribute]() + { + return VisibilityAttribute.Get(); + }); + + ArrayBuilder->OnGenerateArrayElementWidget( + FOnGenerateArrayElementWidgetVisible::CreateLambda( + [Generator](TSharedRef Elem, int32 Index, IDetailChildrenBuilder& Child, const TAttribute& RowVis) + { + Generator(Elem, Index, Child, RowVis); + })); + + StructBuilder.AddCustomBuilder(ArrayBuilder); +} + +void FFlowDataPinValueCustomization::ValidateArrayElements(TSharedPtr ArrayHandle, + TFunction)> IsValidPredicate, + TFunction)> InvalidateAction) +{ + if (!ArrayHandle.IsValid()) + { + return; + } + + auto AsArray = ArrayHandle->AsArray(); + if (!AsArray.IsValid()) + { + return; + } + + uint32 Num = 0; + AsArray->GetNumElements(Num); + + TArray> ToInvalidate; + ToInvalidate.Reserve(Num); + + for (uint32 i = 0; i < Num; ++i) + { + TSharedPtr Elem = ArrayHandle->GetChildHandle(i); + if (!Elem.IsValid()) + { + continue; + } + if (!IsValidPredicate(Elem)) + { + ToInvalidate.Add(Elem); + } + } + + if (ToInvalidate.Num() > 0) + { + const FScopedTransaction Tx(LOCTEXT("InvalidateArrayElements", "Clear Invalid Data Pin Values")); + for (auto& H : ToInvalidate) + { + if (H.IsValid()) + { + InvalidateAction(H); + } + } + } +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowDataPinValueCustomization_Class.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowDataPinValueCustomization_Class.cpp new file mode 100644 index 000000000..0b68de1d2 --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowDataPinValueCustomization_Class.cpp @@ -0,0 +1,386 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowDataPinValueCustomization_Class.h" +#include "Interfaces/FlowDataPinValueOwnerInterface.h" +#include "Types/FlowDataPinValuesStandard.h" +#include "UnrealExtensions/VisibilityArrayBuilder.h" + +#include "DetailLayoutBuilder.h" +#include "DetailWidgetRow.h" +#include "IDetailChildrenBuilder.h" +#include "PropertyHandle.h" +#include "EditorClassUtils.h" +#include "UObject/SoftObjectPath.h" +#include "IPropertyUtilities.h" +#include "ScopedTransaction.h" + +#define LOCTEXT_NAMESPACE "FlowDataPinValueCustomization_Class" + +FFlowDataPinValue_Class* FFlowDataPinValueCustomization_Class::GetValueStruct() const +{ + return IFlowExtendedPropertyTypeCustomization::TryGetTypedStructValue(StructPropertyHandle); +} + +bool FFlowDataPinValueCustomization_Class::ShouldShowSourceRow() const +{ + return OwnerInterface ? OwnerInterface->ShowFlowDataPinValueClassFilter(GetValueStruct()) : true; +} + +bool FFlowDataPinValueCustomization_Class::IsSourceEditable() const +{ + if (bHasMetaClass) + { + return false; // forced meta class: show disabled + } + return OwnerInterface ? OwnerInterface->CanEditFlowDataPinValueClassFilter(GetValueStruct()) : true; +} + +void FFlowDataPinValueCustomization_Class::BuildValueRows( + TSharedRef InStructPropertyHandle, + IDetailChildrenBuilder& StructBuilder, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ + CacheHandles(InStructPropertyHandle, StructCustomizationUtils); + CacheArraySupported(); // from base + if (!ValuesHandle.IsValid()) + { + return; + } + + ClassFilterHandle = StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FFlowDataPinValue_Class, ClassFilter)); + + TrySetClassFilterFromMetaData(); + ExtractMetadata(); + RefreshEffectiveFilter(); + + const bool bShowSource = ShouldShowSourceRow(); + if (bShowSource) + { + BuildClassFilterRow(StructBuilder, IsSourceEditable()); + } + + EnsureSingleElementExists(); + BuildSingleBranch(StructBuilder); + if (bArraySupported) + { + BuildArrayBranch(StructBuilder); + } + + BindDelegates(); + ValidateAllElements(); +} + +void FFlowDataPinValueCustomization_Class::ExtractMetadata() +{ + if (!StructPropertyHandle.IsValid()) + { + return; + } + + const FString& MustImplement = StructPropertyHandle->GetMetaData(TEXT("MustImplement")); + RequiredInterface = FEditorClassUtils::GetClassFromString(MustImplement); + + bAllowAbstract = StructPropertyHandle->HasMetaData(TEXT("AllowAbstract")); + bIsBlueprintBaseOnly = StructPropertyHandle->HasMetaData(TEXT("IsBlueprintBaseOnly")) || + StructPropertyHandle->HasMetaData(TEXT("BlueprintBaseOnly")); + bShowTreeView = StructPropertyHandle->HasMetaData(TEXT("ShowTreeView")); + bHideViewOptions = StructPropertyHandle->HasMetaData(TEXT("HideViewOptions")); + bShowDisplayNames = StructPropertyHandle->HasMetaData(TEXT("ShowDisplayNames")); + + bHasMetaClass = !StructPropertyHandle->GetMetaData(TEXT("MetaClass")).IsEmpty(); + + if (const FProperty* MetaProp = StructPropertyHandle->GetMetaDataProperty()) + { + bAllowNone = !(MetaProp->PropertyFlags & CPF_NoClear); + } + else + { + bAllowNone = true; + } +} + +void FFlowDataPinValueCustomization_Class::BuildClassFilterRow(IDetailChildrenBuilder& StructBuilder, bool bSourceEditable) const +{ + if (!ClassFilterHandle.IsValid()) + { + return; + } + + IDetailPropertyRow& Row = StructBuilder.AddProperty(ClassFilterHandle.ToSharedRef()); + Row.DisplayName(LOCTEXT("ClassFilterLabel", "Class Filter")); + Row.IsEnabled(bSourceEditable); + Row.ToolTip(bHasMetaClass + ? LOCTEXT("ClassFilterMetaTooltip", "Class Filter is fixed by MetaClass metadata and cannot be edited.") + : LOCTEXT("ClassFilterTooltip", "Class Filter constrains which classes can be selected.")); +} + +void FFlowDataPinValueCustomization_Class::BuildSingleBranch(IDetailChildrenBuilder& StructBuilder) +{ + auto First = ValuesHandle->GetChildHandle(0); + if (!First.IsValid()) + { + return; + } + + StructBuilder.AddCustomRow(LOCTEXT("ClassSingleSearch", "Class")) + .Visibility(TAttribute::CreateSP(this, &FFlowDataPinValueCustomization_Class::GetSingleModeVisibility)) + .NameContent() + [ + SNew(STextBlock) + .Text(LOCTEXT("ClassValueLabel", "Class")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(250.f) + [ + SNew(SClassPropertyEntryBox) + .MetaClass(CachedEffectiveFilter.Get() ? CachedEffectiveFilter.Get() : UObject::StaticClass()) + .RequiredInterface(RequiredInterface) + .AllowAbstract(bAllowAbstract) + .IsBlueprintBaseOnly(bIsBlueprintBaseOnly) + .AllowNone(bAllowNone) + .ShowTreeView(bShowTreeView) + .HideViewOptions(bHideViewOptions) + .ShowDisplayNames(bShowDisplayNames) + .IsEnabled(AreValuesEditable()) + .SelectedClass_Lambda([this, First]() -> const UClass* + { + return GetSelectedClassForHandle(First); + }) + .OnSetClass_Lambda([this, First](const UClass* NewClass) + { + OnSetClassForHandle(NewClass, First); + }) + ]; +} + +void FFlowDataPinValueCustomization_Class::BuildArrayBranch(IDetailChildrenBuilder& StructBuilder) +{ + BuildVisibilityAwareArray(StructBuilder, + ValuesHandle, + [this](TSharedRef ElementHandle, int32 Index, IDetailChildrenBuilder& ChildBuilder, const TAttribute& RowVis) + { + IDetailPropertyRow& Row = ChildBuilder.AddProperty(ElementHandle); + Row.Visibility(RowVis); + + Row.CustomWidget() + .NameContent() + [ + SNew(STextBlock) + .Text(FText::Format(LOCTEXT("ClassArrayElemLabelFmt", "Class {0}"), FText::AsNumber(Index))) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(250.f) + [ + SNew(SClassPropertyEntryBox) + .MetaClass(CachedEffectiveFilter.Get() ? CachedEffectiveFilter.Get() : UObject::StaticClass()) + .RequiredInterface(RequiredInterface) + .AllowAbstract(bAllowAbstract) + .IsBlueprintBaseOnly(bIsBlueprintBaseOnly) + .AllowNone(bAllowNone) + .ShowTreeView(bShowTreeView) + .HideViewOptions(bHideViewOptions) + .ShowDisplayNames(bShowDisplayNames) + .IsEnabled(AreValuesEditable()) + .SelectedClass_Lambda([this, ElementHandle]() -> const UClass* + { + return GetSelectedClassForHandle(ElementHandle); + }) + .OnSetClass_Lambda([this, ElementHandle](const UClass* NewClass) + { + OnSetClassForHandle(NewClass, ElementHandle); + }) + ]; + }, + TAttribute::CreateSP(this, &FFlowDataPinValueCustomization_Class::GetArrayModeVisibility)); +} + +void FFlowDataPinValueCustomization_Class::BindDelegates() +{ + if (ClassFilterHandle.IsValid()) + { + ClassFilterHandle->SetOnPropertyValueChanged( + FSimpleDelegate::CreateSP(this, &FFlowDataPinValueCustomization_Class::OnClassFilterChanged)); + } + if (ValuesHandle.IsValid()) + { + ValuesHandle->SetOnPropertyValueChanged( + FSimpleDelegate::CreateSP(this, &FFlowDataPinValueCustomization_Class::OnValuesChanged)); + } +} + +void FFlowDataPinValueCustomization_Class::OnClassFilterChanged() +{ + RefreshEffectiveFilter(); + ValidateAllElements(); + + if (CustomizationUtils) + { + if (auto Utils = CustomizationUtils->GetPropertyUtilities()) + { + Utils->RequestRefresh(); + } + } +} + +void FFlowDataPinValueCustomization_Class::OnValuesChanged() +{ + ValidateAllElements(); +} + +void FFlowDataPinValueCustomization_Class::TrySetClassFilterFromMetaData() const +{ + if (!StructPropertyHandle.IsValid() || !ClassFilterHandle.IsValid()) + { + return; + } + + const FString& MetaClassName = StructPropertyHandle->GetMetaData(TEXT("MetaClass")); + if (MetaClassName.IsEmpty()) + { + return; + } + + if (UClass* MetaClass = FEditorClassUtils::GetClassFromString(MetaClassName)) + { + UObject* Existing = nullptr; + ClassFilterHandle->GetValue(Existing); + if (Existing != MetaClass) + { + ClassFilterHandle->SetValue(MetaClass, EPropertyValueSetFlags::DefaultFlags); + } + } +} + +UClass* FFlowDataPinValueCustomization_Class::DeriveBestClassFilter() const +{ + if (!StructPropertyHandle.IsValid()) + { + return nullptr; + } + + const FString& MetaClassName = StructPropertyHandle->GetMetaData(TEXT("MetaClass")); + if (!MetaClassName.IsEmpty()) + { + if (UClass* MetaClass = FEditorClassUtils::GetClassFromString(MetaClassName)) + { + return MetaClass; + } + } + + if (ClassFilterHandle.IsValid()) + { + UObject* Raw = nullptr; + if (ClassFilterHandle->GetValue(Raw) == FPropertyAccess::Success && Raw) + { + return Cast(Raw); + } + } + + return nullptr; +} + +void FFlowDataPinValueCustomization_Class::RefreshEffectiveFilter() +{ + CachedEffectiveFilter = DeriveBestClassFilter(); +} + +void FFlowDataPinValueCustomization_Class::ValidateAllElements() +{ + ValidateArrayElements(ValuesHandle, + [this](TSharedPtr Elem) + { + return IsElementValid(Elem); + }, + [](TSharedPtr Elem) + { + if (Elem.IsValid()) + { + Elem->SetValueFromFormattedString(TEXT("None")); + } + }); +} + +bool FFlowDataPinValueCustomization_Class::IsElementValid(TSharedPtr ElementHandle) const +{ + if (!ElementHandle.IsValid()) + { + return true; + } + + UClass* FilterClass = CachedEffectiveFilter.Get(); + if (!FilterClass) + { + return true; + } + + FString Path; + if (!GetElementPathString(ElementHandle, Path) || IsNoneString(Path)) + { + return true; + } + + FSoftClassPath SCP(Path); + if (UClass* Loaded = SCP.TryLoadClass()) + { + return Loaded->IsChildOf(FilterClass); + } + return false; +} + +const UClass* FFlowDataPinValueCustomization_Class::GetSelectedClassForHandle(TSharedPtr ElementHandle) +{ + if (!ElementHandle.IsValid()) + { + return nullptr; + } + + FString Path; + if (ElementHandle->GetValueAsFormattedString(Path) != FPropertyAccess::Success || IsNoneString(Path)) + { + return nullptr; + } + return FEditorClassUtils::GetClassFromString(Path); +} + +void FFlowDataPinValueCustomization_Class::OnSetClassForHandle(const UClass* NewClass, TSharedPtr ElementHandle) const +{ + if (!ElementHandle.IsValid()) + { + return; + } + + const UClass* Filter = CachedEffectiveFilter.Get(); + if (Filter && NewClass && !NewClass->IsChildOf(Filter)) + { + NewClass = nullptr; + } + + FString Current; + ElementHandle->GetValueAsFormattedString(Current); + const FString NewValue = NewClass ? NewClass->GetPathName() : TEXT("None"); + if (Current == NewValue) + { + return; + } + + FScopedTransaction Tx(LOCTEXT("SetClassArrayElement", "Set Class Value")); + ElementHandle->SetValueFromFormattedString(NewValue); +} + +bool FFlowDataPinValueCustomization_Class::GetElementPathString(const TSharedPtr& ElementHandle, FString& OutPath) +{ + if (!ElementHandle.IsValid()) + { + return false; + } + return ElementHandle->GetValueAsFormattedString(OutPath) == FPropertyAccess::Success; +} + +bool FFlowDataPinValueCustomization_Class::IsNoneString(const FString& Str) +{ + return Str.IsEmpty() || Str.Equals(TEXT("None"), ESearchCase::IgnoreCase); +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowDataPinValueCustomization_Enum.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowDataPinValueCustomization_Enum.cpp new file mode 100644 index 000000000..b63079521 --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowDataPinValueCustomization_Enum.cpp @@ -0,0 +1,433 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowDataPinValueCustomization_Enum.h" +#include "Interfaces/FlowDataPinValueOwnerInterface.h" +#include "Types/FlowDataPinValuesStandard.h" + +#include "DetailLayoutBuilder.h" +#include "DetailWidgetRow.h" +#include "IDetailChildrenBuilder.h" +#include "IPropertyUtilities.h" +#include "PropertyHandle.h" +#include "ScopedTransaction.h" + +#include "Widgets/Input/SComboBox.h" +#include "Widgets/Text/STextBlock.h" + +#define LOCTEXT_NAMESPACE "FlowDataPinValueCustomization_Enum" + +FFlowDataPinValue_Enum* FFlowDataPinValueCustomization_Enum::GetEnumValueStruct() const +{ + return IFlowExtendedPropertyTypeCustomization::TryGetTypedStructValue(StructPropertyHandle); +} + +bool FFlowDataPinValueCustomization_Enum::ShouldShowSourceRow() const +{ + return OwnerInterface ? OwnerInterface->ShowFlowDataPinValueClassFilter(GetEnumValueStruct()) : true; +} + +bool FFlowDataPinValueCustomization_Enum::IsSourceEditable() const +{ + return OwnerInterface ? OwnerInterface->CanEditFlowDataPinValueClassFilter(GetEnumValueStruct()) : true; +} + +void FFlowDataPinValueCustomization_Enum::BuildValueRows( + TSharedRef InStructPropertyHandle, + IDetailChildrenBuilder& StructBuilder, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ + CacheHandles(InStructPropertyHandle, StructCustomizationUtils); + CacheArraySupported(); // base + CacheEnumHandles(InStructPropertyHandle); + + if (!bMultiTypeDelegateBound && MultiTypeHandle.IsValid()) + { + MultiTypeHandle->SetOnPropertyValueChanged( + FSimpleDelegate::CreateSP(this, &FFlowDataPinValueCustomization_Enum::OnMultiTypeChanged)); + bMultiTypeDelegateBound = true; + } + + const bool bShowSource = ShouldShowSourceRow(); + const bool bSourceEditable = IsSourceEditable(); + + if (bShowSource && EnumClassHandle.IsValid()) + { + IDetailPropertyRow& RowEnumClass = StructBuilder.AddProperty(EnumClassHandle.ToSharedRef()); + RowEnumClass.IsEnabled(bSourceEditable); + RowEnumClass.ToolTip(GetEnumSourceTooltip()); + EnumClassHandle->SetOnPropertyValueChanged( + FSimpleDelegate::CreateSP(this, &FFlowDataPinValueCustomization_Enum::OnEnumSourceChanged)); + } + + if (bShowSource && EnumNameHandle.IsValid()) + { + IDetailPropertyRow& RowEnumClassName = StructBuilder.AddProperty(EnumNameHandle.ToSharedRef()); + RowEnumClassName.IsEnabled(bSourceEditable); + RowEnumClassName.ToolTip(LOCTEXT("EnumNameTooltip", "Name of native C++ enum type to derive EnumClass.")); + EnumNameHandle->SetOnPropertyValueChanged( + FSimpleDelegate::CreateSP(this, &FFlowDataPinValueCustomization_Enum::OnEnumSourceChanged)); + } + + RebuildEnumData(); + EnsureSingleElementExists(); + BuildSingle(StructBuilder); + if (bArraySupported) + { + BuildArray(StructBuilder); + } +} + +void FFlowDataPinValueCustomization_Enum::CacheEnumHandles(const TSharedRef& StructHandle) +{ + EnumClassHandle = StructHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FFlowDataPinValue_Enum, EnumClass)); + EnumNameHandle = StructHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FFlowDataPinValue_Enum, EnumName)); +} + +void FFlowDataPinValueCustomization_Enum::OnEnumSourceChanged() +{ + RebuildEnumData(); + + if (CustomizationUtils) + { + if (auto Utils = CustomizationUtils->GetPropertyUtilities()) + { + Utils->RequestRefresh(); + } + } +} + +void FFlowDataPinValueCustomization_Enum::RebuildEnumData() +{ + EnumeratorOptions.Reset(); + bEnumResolved = false; + + if (FFlowDataPinValue_Enum* EnumStruct = GetEnumValueStruct()) + { + EnumStruct->OnEnumNameChanged(); + } + + if (UEnum* EnumObj = ResolveEnum()) + { + CollectEnumerators(*EnumObj); + bEnumResolved = EnumeratorOptions.Num() > 0; + } + + ValidateStoredValues(); +} + +UEnum* FFlowDataPinValueCustomization_Enum::ResolveEnum() const +{ + const FFlowDataPinValue_Enum* Data = GetEnumValueStruct(); + return Data ? Data->EnumClass.LoadSynchronous() : nullptr; +} + +void FFlowDataPinValueCustomization_Enum::CollectEnumerators(UEnum& EnumObj) +{ + const int32 Max = EnumObj.GetMaxEnumValue(); + static const TCHAR* HiddenKey = TEXT("Hidden"); + + for (int32 Index = 0; Index < Max; ++Index) + { + if (!EnumObj.IsValidEnumValue(Index)) + { + continue; + } + if (EnumObj.HasMetaData(HiddenKey, Index)) + { + continue; + } + + const FText Display = EnumObj.GetDisplayNameTextByIndex(Index); + EnumeratorOptions.Add(MakeShared(*Display.ToString())); + } +} + +void FFlowDataPinValueCustomization_Enum::ValidateStoredValues() +{ + if (!ValuesHandle.IsValid()) + { + return; + } + + TArray ValidNames; + for (auto& Opt : EnumeratorOptions) + { + if (Opt.IsValid()) + { + ValidNames.Add(*Opt); + } + } + + if (auto AsArray = ValuesHandle->AsArray()) + { + uint32 Count = 0; + AsArray->GetNumElements(Count); + + if (GetSingleModeVisibility() == EVisibility::Visible && Count == 0) + { + AsArray->AddItem(); + AsArray->GetNumElements(Count); + } + + for (uint32 i = 0; i < Count; ++i) + { + auto Elem = ValuesHandle->GetChildHandle(i); + if (!Elem.IsValid()) + { + continue; + } + + FName Current; + if (Elem->GetValue(Current) == FPropertyAccess::Success) + { + if (!IsValueValid(Current)) + { + Elem->SetValue(ValidNames.Num() > 0 ? ValidNames[0] : FName(NAME_None)); + } + } + } + } +} + +bool FFlowDataPinValueCustomization_Enum::IsValueValid(const FName& Candidate) const +{ + if (Candidate.IsNone()) + { + return EnumeratorOptions.Num() == 0; + } + for (auto& Opt : EnumeratorOptions) + { + if (Opt.IsValid() && *Opt == Candidate) + { + return true; + } + } + return false; +} + +TSharedPtr FFlowDataPinValueCustomization_Enum::FindEnumeratorMatch(const FName& Current) const +{ + for (auto& Opt : EnumeratorOptions) + { + if (Opt.IsValid() && *Opt == Current) + { + return Opt; + } + } + return nullptr; +} + +void FFlowDataPinValueCustomization_Enum::BuildSingle(IDetailChildrenBuilder& StructBuilder) +{ + if (!ValuesHandle.IsValid()) + { + return; + } + + auto First = ValuesHandle->GetChildHandle(0); + if (!First.IsValid()) + { + if (auto AsArray = ValuesHandle->AsArray()) + { + AsArray->AddItem(); + First = ValuesHandle->GetChildHandle(0); + } + } + + if (!First.IsValid()) + { + return; + } + + StructBuilder.AddCustomRow(LOCTEXT("EnumSingleSearch", "Value")) + .Visibility(TAttribute::CreateSP(this, &FFlowDataPinValueCustomization_Enum::GetSingleModeVisibility)) + .NameContent() + [ + SNew(STextBlock) + .Text(LOCTEXT("EnumValueLabel", "Value")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(200.f) + [ + SNew(SComboBox>) + .OptionsSource(&EnumeratorOptions) + .OnGenerateWidget(this, &FFlowDataPinValueCustomization_Enum::GenerateEnumeratorWidget) + .OnSelectionChanged_Static(&FFlowDataPinValueCustomization_Enum::OnSingleValueChanged, First) + .IsEnabled(this, &FFlowDataPinValueCustomization_Enum::IsValueEditingEnabled) + .InitiallySelectedItem([this, First]() + { + FName Current; + if (First->GetValue(Current) == FPropertyAccess::Success) + { + return FindEnumeratorMatch(Current); + } + return EnumeratorOptions.Num() > 0 ? EnumeratorOptions[0] : nullptr; + }()) + .Content() + [ + SNew(STextBlock) + .Text_Lambda([this, First]() + { + FName Current; + if (First->GetValue(Current) == FPropertyAccess::Success && !Current.IsNone()) + { + return GetEnumeratorDisplayText(Current); + } + return LOCTEXT("EnumNonePlaceholder", ""); + }) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .ToolTipText(GetEnumSourceTooltip()) + ] + ]; +} + +void FFlowDataPinValueCustomization_Enum::BuildArray(IDetailChildrenBuilder& StructBuilder) +{ + BuildVisibilityAwareArray(StructBuilder, + ValuesHandle, + [this](TSharedRef ElementHandle, int32 Index, IDetailChildrenBuilder& ChildBuilder, const TAttribute& RowVis) + { + IDetailPropertyRow& Row = ChildBuilder.AddProperty(ElementHandle); + Row.Visibility(RowVis); + + Row.CustomWidget() + .NameContent() + [ + SNew(STextBlock) + .Text(FText::AsNumber(Index)) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(200.f) + [ + SNew(SComboBox>) + .OptionsSource(&EnumeratorOptions) + .OnGenerateWidget(this, &FFlowDataPinValueCustomization_Enum::GenerateEnumeratorWidget) + .OnSelectionChanged_Static(&FFlowDataPinValueCustomization_Enum::OnArrayElementChanged, TSharedPtr(ElementHandle)) + .IsEnabled(this, &FFlowDataPinValueCustomization_Enum::IsValueEditingEnabled) + .InitiallySelectedItem([this, ElementHandle]() + { + FName Current; + if (ElementHandle->GetValue(Current) == FPropertyAccess::Success) + { + return FindEnumeratorMatch(Current); + } + return EnumeratorOptions.Num() > 0 ? EnumeratorOptions[0] : nullptr; + }()) + .Content() + [ + SNew(STextBlock) + .Text_Lambda([this, ElementHandle]() + { + FName Current; + if (ElementHandle->GetValue(Current) == FPropertyAccess::Success && !Current.IsNone()) + { + return GetEnumeratorDisplayText(Current); + } + return LOCTEXT("EnumNonePlaceholder", ""); + }) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .ToolTipText(GetEnumSourceTooltip()) + ] + ]; + }, + TAttribute::CreateSP(this, &FFlowDataPinValueCustomization_Enum::GetArrayModeVisibility)); +} + +TSharedRef FFlowDataPinValueCustomization_Enum::GenerateEnumeratorWidget(TSharedPtr Item) const +{ + const FName Name = Item.IsValid() ? *Item : NAME_None; + return SNew(STextBlock) + .Text(GetEnumeratorDisplayText(Name)) + .Font(IDetailLayoutBuilder::GetDetailFont()); +} + +FText FFlowDataPinValueCustomization_Enum::GetEnumeratorDisplayText(const FName& Value) +{ + return Value.IsNone() ? LOCTEXT("EnumNoneDisplay", "") : FText::FromName(Value); +} + +FText FFlowDataPinValueCustomization_Enum::GetEnumSourceTooltip() const +{ + const FFlowDataPinValue_Enum* Data = GetEnumValueStruct(); + if (!Data) + { + return LOCTEXT("EnumTooltipMissing", "Enum value struct not available."); + } + + FString Source; + if (!Data->EnumName.IsEmpty()) + { + Source = FString::Printf(TEXT("Native Enum: %s"), *Data->EnumName); + } + if (Source.IsEmpty() && Data->EnumClass.IsValid()) + { + Source = FString::Printf(TEXT("Enum Asset: %s"), *Data->EnumClass.ToString()); + } + if (Source.IsEmpty()) + { + Source = TEXT("No enum source selected"); + } + return FText::FromString(Source); +} + +void FFlowDataPinValueCustomization_Enum::OnSingleValueChanged(TSharedPtr NewSelection, ESelectInfo::Type SelectInfo, TSharedPtr ElementHandle) +{ + if (!ElementHandle.IsValid() || !NewSelection.IsValid()) + { + return; + } + + FName Current; + ElementHandle->GetValue(Current); + if (Current == *NewSelection) + { + return; + } + + FScopedTransaction Tx(LOCTEXT("SetEnumSingleValue", "Set Enum Value")); + ElementHandle->SetValue(*NewSelection); +} + +void FFlowDataPinValueCustomization_Enum::OnArrayElementChanged(TSharedPtr NewSelection, ESelectInfo::Type SelectInfo,TSharedPtr ElementHandle) +{ + if (!ElementHandle.IsValid() || !NewSelection.IsValid()) + { + return; + } + + FName Current; + ElementHandle->GetValue(Current); + if (Current == *NewSelection) + { + return; + } + + FScopedTransaction Tx(LOCTEXT("SetEnumArrayElement", "Set Enum Array Element")); + ElementHandle->SetValue(*NewSelection); +} + +void FFlowDataPinValueCustomization_Enum::OnMultiTypeChanged() +{ + // If array not supported, ignore switching + if (!bArraySupported) + { + return; + } + + if (GetArrayModeVisibility() == EVisibility::Collapsed) + { + EnsureSingleElementExists(); + } + + if (CustomizationUtils) + { + if (auto Utils = CustomizationUtils->GetPropertyUtilities()) + { + Utils->RequestRefresh(); + } + } +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowDataPinValueCustomization_Object.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowDataPinValueCustomization_Object.cpp new file mode 100644 index 000000000..b4db1500f --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowDataPinValueCustomization_Object.cpp @@ -0,0 +1,297 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowDataPinValueCustomization_Object.h" +#include "Interfaces/FlowDataPinValueOwnerInterface.h" +#include "Types/FlowDataPinValuesStandard.h" + +#include "DetailLayoutBuilder.h" +#include "DetailWidgetRow.h" +#include "IDetailChildrenBuilder.h" +#include "PropertyHandle.h" +#include "IPropertyUtilities.h" +#include "PropertyCustomizationHelpers.h" +#include "ScopedTransaction.h" +#include "EditorClassUtils.h" + +#define LOCTEXT_NAMESPACE "FlowDataPinValueCustomization_Object" + +FFlowDataPinValue_Object* FFlowDataPinValueCustomization_Object::GetValueStruct() const +{ + return IFlowExtendedPropertyTypeCustomization::TryGetTypedStructValue(StructPropertyHandle); +} + +bool FFlowDataPinValueCustomization_Object::ShouldShowSourceRow() const +{ + return OwnerInterface ? OwnerInterface->ShowFlowDataPinValueClassFilter(GetValueStruct()) : true; +} + +bool FFlowDataPinValueCustomization_Object::IsSourceEditable() const +{ + if (bMetaClassForced) + { + return false; + } + return OwnerInterface ? OwnerInterface->CanEditFlowDataPinValueClassFilter(GetValueStruct()) : true; +} + +void FFlowDataPinValueCustomization_Object::BuildValueRows( + TSharedRef InStructPropertyHandle, + IDetailChildrenBuilder& StructBuilder, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ + CacheHandles(InStructPropertyHandle, StructCustomizationUtils); + CacheArraySupported(); // base + if (!ValuesHandle.IsValid()) + { + return; + } + + ClassFilterHandle = StructPropertyHandle->GetChildHandle(TEXT("ClassFilter")); + + TryApplyMetaClass(); + ResolveEffectiveFilter(); + + const bool bShowSource = ShouldShowSourceRow(); + if (bShowSource) + { + BuildClassFilterRow(StructBuilder, IsSourceEditable()); + } + + EnsureSingleElementExists(); + BuildSingleBranch(StructBuilder); + if (bArraySupported) + { + BuildArrayBranch(StructBuilder); + } + + BindDelegates(); + ValidateAll(); +} + +void FFlowDataPinValueCustomization_Object::TryApplyMetaClass() +{ + if (!StructPropertyHandle.IsValid() || !ClassFilterHandle.IsValid()) + { + return; + } + + const FString& MetaClassName = StructPropertyHandle->GetMetaData(TEXT("MetaClass")); + if (MetaClassName.IsEmpty()) + { + bMetaClassForced = false; + return; + } + + if (UClass* Meta = FEditorClassUtils::GetClassFromString(MetaClassName)) + { + UObject* Existing = nullptr; + ClassFilterHandle->GetValue(Existing); + if (Existing != Meta) + { + ClassFilterHandle->SetValue(Meta, EPropertyValueSetFlags::DefaultFlags); + } + bMetaClassForced = true; + } + else + { + bMetaClassForced = false; + } +} + +void FFlowDataPinValueCustomization_Object::ResolveEffectiveFilter() +{ + if (bMetaClassForced) + { + const FString& MetaClassName = StructPropertyHandle->GetMetaData(TEXT("MetaClass")); + EffectiveFilterClass = FEditorClassUtils::GetClassFromString(MetaClassName); + return; + } + + if (ClassFilterHandle.IsValid()) + { + UObject* Obj = nullptr; + if (ClassFilterHandle->GetValue(Obj) == FPropertyAccess::Success) + { + EffectiveFilterClass = Cast(Obj); + return; + } + } + + EffectiveFilterClass = nullptr; +} + +void FFlowDataPinValueCustomization_Object::BuildClassFilterRow(IDetailChildrenBuilder& StructBuilder, bool bSourceEditable) const +{ + if (!ClassFilterHandle.IsValid()) + { + return; + } + + IDetailPropertyRow& Row = StructBuilder.AddProperty(ClassFilterHandle.ToSharedRef()); + Row.DisplayName(LOCTEXT("ObjClassFilter", "Class Filter")); + Row.IsEnabled(bSourceEditable); + Row.ToolTip(bMetaClassForced + ? LOCTEXT("ObjClassFilterMetaTooltip", "Class Filter is fixed by MetaClass metadata and cannot be edited.") + : LOCTEXT("ObjClassFilterTooltip", "Class Filter constrains which object classes are selectable.")); +} + +void FFlowDataPinValueCustomization_Object::BuildSingleBranch(IDetailChildrenBuilder& StructBuilder) +{ + auto First = ValuesHandle->GetChildHandle(0); + if (!First.IsValid()) + { + return; + } + + StructBuilder.AddCustomRow(LOCTEXT("ObjectSingleSearch", "Object")) + .Visibility(TAttribute::CreateSP(this, &FFlowDataPinValueCustomization_Object::GetSingleModeVisibility)) + .NameContent() + [ + SNew(STextBlock) + .Text(LOCTEXT("ObjectValueLabel", "Object")) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(250.f) + [ + BuildObjectValueWidgetForElement(First) + ]; +} + +void FFlowDataPinValueCustomization_Object::BuildArrayBranch(IDetailChildrenBuilder& StructBuilder) +{ + BuildVisibilityAwareArray(StructBuilder, + ValuesHandle, + [this](TSharedRef ElementHandle, int32 Index, IDetailChildrenBuilder& ChildBuilder, const TAttribute& RowVis) + { + IDetailPropertyRow& Row = ChildBuilder.AddProperty(ElementHandle); + Row.Visibility(RowVis); + + Row.CustomWidget() + .NameContent() + [ + SNew(STextBlock) + .Text(FText::Format(LOCTEXT("ObjectArrayElemFmt", "Object {0}"), FText::AsNumber(Index))) + .Font(IDetailLayoutBuilder::GetDetailFont()) + ] + .ValueContent() + .MinDesiredWidth(250.f) + [ + BuildObjectValueWidgetForElement(ElementHandle) + ]; + }, + TAttribute::CreateSP(this, &FFlowDataPinValueCustomization_Object::GetArrayModeVisibility)); +} + +TSharedRef FFlowDataPinValueCustomization_Object::BuildObjectValueWidgetForElement(TSharedPtr ElementHandle) const +{ + return SNew(SObjectPropertyEntryBox) + .PropertyHandle(ElementHandle) + .AllowedClass(EffectiveFilterClass.Get() ? EffectiveFilterClass.Get() : UObject::StaticClass()) + .AllowClear(AreValuesEditable()) + .IsEnabled(AreValuesEditable()) + .ToolTipText(AreValuesEditable() + ? LOCTEXT("ObjectPickerTooltip", "Select an object reference.") + : LOCTEXT("ObjectPickerLockedTooltip", "Object references are not editable.")); +} + +void FFlowDataPinValueCustomization_Object::BindDelegates() +{ + if (ClassFilterHandle.IsValid()) + { + ClassFilterHandle->SetOnPropertyValueChanged( + FSimpleDelegate::CreateSP(this, &FFlowDataPinValueCustomization_Object::OnClassFilterChanged)); + } + if (ValuesHandle.IsValid()) + { + ValuesHandle->SetOnPropertyValueChanged( + FSimpleDelegate::CreateSP(this, &FFlowDataPinValueCustomization_Object::OnValuesChanged)); + } +} + +void FFlowDataPinValueCustomization_Object::OnClassFilterChanged() +{ + ResolveEffectiveFilter(); + ValidateAll(); + + if (CustomizationUtils) + { + if (auto Utils = CustomizationUtils->GetPropertyUtilities()) + { + Utils->RequestRefresh(); + } + } +} + +void FFlowDataPinValueCustomization_Object::OnValuesChanged() +{ + ValidateAll(); +} + +void FFlowDataPinValueCustomization_Object::ValidateAll() +{ + ValidateArrayElements(ValuesHandle, + [this](TSharedPtr Elem) + { + return IsElementValid(Elem); + }, + [this](TSharedPtr Elem) + { + InvalidateElement(Elem); + }); +} + +bool FFlowDataPinValueCustomization_Object::IsElementValid(TSharedPtr ElementHandle) const +{ + if (!ElementHandle.IsValid()) + { + return true; + } + + UClass* Filter = EffectiveFilterClass.Get(); + if (!Filter) + { + return true; + } + + UObject* Obj = GetObjectValue(ElementHandle); + return !Obj || Obj->IsA(Filter); +} + +void FFlowDataPinValueCustomization_Object::InvalidateElement(TSharedPtr ElementHandle) +{ + if (ElementHandle.IsValid()) + { + SetObjectValue(ElementHandle, nullptr); + } +} + +UObject* FFlowDataPinValueCustomization_Object::GetObjectValue(TSharedPtr ElementHandle) +{ + UObject* Obj = nullptr; + if (ElementHandle.IsValid()) + { + ElementHandle->GetValue(Obj); + } + return Obj; +} + +void FFlowDataPinValueCustomization_Object::SetObjectValue(TSharedPtr ElementHandle, UObject* NewObj) +{ + if (!ElementHandle.IsValid()) + { + return; + } + + UObject* Current = nullptr; + ElementHandle->GetValue(Current); + if (Current == NewObj) + { + return; + } + + FScopedTransaction Tx(LOCTEXT("SetObjectValue", "Set Object Reference")); + ElementHandle->SetValue(NewObj); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowDetailsAddOnUI.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowDetailsAddOnUI.cpp new file mode 100644 index 000000000..1286546df --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowDetailsAddOnUI.cpp @@ -0,0 +1,92 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowDetailsAddOnUI.h" + +#include "Graph/Nodes/FlowGraphNode.h" +#include "Graph/Widgets/SGraphEditorActionMenuFlow.h" +#include "Nodes/FlowNodeBase.h" + +#include "EdGraph/EdGraph.h" +#include "UObject/UObjectIterator.h" +#include "Widgets/Layout/SBox.h" +#include "Widgets/Text/STextBlock.h" + +#define LOCTEXT_NAMESPACE "FlowDetailsAddOnUI" + +UFlowGraphNode* FFlowDetailsAddOnUI::FindGraphNodeForEditedObject(UObject* EditedObject) +{ + if (!IsValid(EditedObject)) + { + return nullptr; + } + + // Find any UFlowGraphNode whose node instance matches the edited object. + for (TObjectIterator It; It; ++It) + { + UFlowGraphNode* Candidate = *It; + if (!IsValid(Candidate)) + { + continue; + } + + UObject* NodeInstance = Candidate->GetFlowNodeBase(); + if (NodeInstance == EditedObject) + { + return Candidate; + } + } + + return nullptr; +} + +UEdGraph* FFlowDetailsAddOnUI::GetOwningEdGraph(UFlowGraphNode* GraphNode) +{ + return IsValid(GraphNode) ? GraphNode->GetGraph() : nullptr; +} + +bool FFlowDetailsAddOnUI::CanAttachAddOn(UObject* EditedObject) +{ + UFlowGraphNode* GraphNode = FindGraphNodeForEditedObject(EditedObject); + if (!IsValid(GraphNode)) + { + return false; + } + + return IsValid(GetOwningEdGraph(GraphNode)); +} + +TSharedRef FFlowDetailsAddOnUI::BuildAttachAddOnMenuContent(UObject* EditedObject) +{ + UFlowGraphNode* GraphNode = FindGraphNodeForEditedObject(EditedObject); + UEdGraph* Graph = GetOwningEdGraph(GraphNode); + + if (!IsValid(GraphNode) || !IsValid(Graph)) + { + return SNew(STextBlock) + .Text(LOCTEXT("AttachAddOnUnavailable", "Attach AddOn is unavailable (no owning graph node found).")); + } + + return BuildAttachAddOnMenuContent(Graph, GraphNode); +} + +TSharedRef FFlowDetailsAddOnUI::BuildAttachAddOnMenuContent(UEdGraph* Graph, UFlowGraphNode* GraphNode) +{ + if (!IsValid(Graph) || !IsValid(GraphNode)) + { + return SNew(STextBlock) + .Text(LOCTEXT("AttachAddOnUnavailable2", "Attach AddOn is unavailable.")); + } + + // Wrap for sizing similar to the context menu widget + return SNew(SBox) + .WidthOverride(420.0f) + .HeightOverride(520.0f) + [ + SNew(SGraphEditorActionMenuFlow) + .GraphObj(Graph) + .GraphNode(GraphNode) + .AutoExpandActionMenu(true) + ]; +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowNamedDataPinPropertyCustomization.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowNamedDataPinPropertyCustomization.cpp new file mode 100644 index 000000000..3db153a12 --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowNamedDataPinPropertyCustomization.cpp @@ -0,0 +1,14 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowNamedDataPinPropertyCustomization.h" +#include "Types/FlowNamedDataPinProperty.h" + +FText FFlowNamedDataPinPropertyCustomization::BuildHeaderText() const +{ + if (const FFlowNamedDataPinProperty* FlowNamedDataPinProperty = IFlowExtendedPropertyTypeCustomization::TryGetTypedStructValue(StructPropertyHandle)) + { + return FlowNamedDataPinProperty->BuildHeaderText(); + } + + return Super::BuildHeaderText(); +} diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowNodeAddOn_Details.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowNodeAddOn_Details.cpp new file mode 100644 index 000000000..829477925 --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowNodeAddOn_Details.cpp @@ -0,0 +1,78 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowNodeAddOn_Details.h" + +#include "AddOns/FlowNodeAddOn.h" +#include "DetailCustomizations/FlowDetailsAddOnUI.h" + +#include "DetailCategoryBuilder.h" +#include "DetailLayoutBuilder.h" +#include "DetailWidgetRow.h" +#include "Widgets/Input/SComboButton.h" +#include "Widgets/Text/STextBlock.h" + +#define LOCTEXT_NAMESPACE "FlowNodeAddOnDetails" + +void FFlowNodeAddOn_Details::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) +{ + // hide class properties while editing node addon instance placed in the graph + if (DetailLayout.HasClassDefaultObject() == false) + { + DetailLayout.HideCategory(TEXT("FlowNode")); + DetailLayout.HideCategory(TEXT("FlowNodeAddOn")); + } + + // Cache edited addon + { + TArray> Objects; + DetailLayout.GetObjectsBeingCustomized(Objects); + + EditedAddOn = nullptr; + + for (const TWeakObjectPtr& Obj : Objects) + { + if (UFlowNodeAddOn* AsAddOn = Cast(Obj.Get())) + { + EditedAddOn = AsAddOn; + + break; + } + } + } + + // Add "Attach AddOn..." dropdown (menu button) + if (EditedAddOn.IsValid() && !DetailLayout.HasClassDefaultObject()) + { + IDetailCategoryBuilder& AddOnsCategory = DetailLayout.EditCategory( + TEXT("AddOns"), + LOCTEXT("AddOnsCategory", "AddOns"), + ECategoryPriority::Important); + + AddOnsCategory.AddCustomRow(LOCTEXT("AttachAddOnSearch", "Attach AddOn")) + .WholeRowContent() + [ + SNew(SComboButton) + .ButtonContent() + [ + SNew(STextBlock) + .Text(LOCTEXT("AttachAddOnButton", "Attach AddOn...")) + ] + .ToolTipText(LOCTEXT("AttachAddOnButtonTooltip", "Attach an AddOn to the selected node/addon.")) + .IsEnabled_Lambda([this]() + { + return EditedAddOn.IsValid() && FFlowDetailsAddOnUI::CanAttachAddOn(EditedAddOn.Get()); + }) + .OnGetMenuContent_Lambda([this]() + { + return EditedAddOn.IsValid() + ? FFlowDetailsAddOnUI::BuildAttachAddOnMenuContent(EditedAddOn.Get()) + : SNullWidget::NullWidget; + }) + ]; + } + + // Call base template to set up rebuild delegate wiring + TFlowDataPinValueOwnerCustomization::CustomizeDetails(DetailLayout); +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_ComponentObserverDetails.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_ComponentObserverDetails.cpp similarity index 81% rename from Source/FlowEditor/Private/Nodes/Customizations/FlowNode_ComponentObserverDetails.cpp rename to Source/FlowEditor/Private/DetailCustomizations/FlowNode_ComponentObserverDetails.cpp index dec2f9222..68b0a7729 100644 --- a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_ComponentObserverDetails.cpp +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_ComponentObserverDetails.cpp @@ -1,7 +1,7 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors -#include "FlowNode_ComponentObserverDetails.h" -#include "Nodes/World/FlowNode_ComponentObserver.h" +#include "DetailCustomizations/FlowNode_ComponentObserverDetails.h" +#include "Nodes/Actor/FlowNode_ComponentObserver.h" #include "DetailCategoryBuilder.h" #include "DetailLayoutBuilder.h" diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowNode_CustomEventBaseDetails.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_CustomEventBaseDetails.cpp new file mode 100644 index 000000000..a9743ce13 --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_CustomEventBaseDetails.cpp @@ -0,0 +1,147 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowNode_CustomEventBaseDetails.h" +#include "FlowAsset.h" +#include "Nodes/Graph/FlowNode_CustomEventBase.h" + +#include "DetailCategoryBuilder.h" +#include "DetailLayoutBuilder.h" +#include "DetailWidgetRow.h" +#include "Widgets/Input/SComboBox.h" +#include "Widgets/Text/STextBlock.h" +#include "Widgets/SWidget.h" + +void FFlowNode_CustomEventBaseDetails::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) +{ + // Subclasses must override this function (and call CustomizeDetailsInternal with the localized text) + checkNoEntry(); +} + +void FFlowNode_CustomEventBaseDetails::CustomizeDetailsInternal(IDetailLayoutBuilder& DetailLayout, const FText& CustomRowNameText, const FText& EventNameText) +{ + DetailLayout.GetObjectsBeingCustomized(ObjectsBeingEdited); + + if (ObjectsBeingEdited[0].IsValid()) + { + const UFlowNode_CustomEventBase* EventNode = CastChecked(ObjectsBeingEdited[0]); + CachedEventNameSelected = MakeShared(EventNode->GetEventName()); + } + + IDetailCategoryBuilder& Category = CreateDetailCategory(DetailLayout); + + Category.AddCustomRow(CustomRowNameText) + .NameContent() + [ + SNew(STextBlock) + .Text(EventNameText) + ] + .ValueContent() + .HAlign(HAlign_Fill) + [ + SAssignNew(EventTextListWidget, SComboBox>) + .OptionsSource(&EventNames) + .OnGenerateWidget(this, &FFlowNode_CustomEventBaseDetails::GenerateEventWidget) + .OnComboBoxOpening(this, &FFlowNode_CustomEventBaseDetails::OnComboBoxOpening) + .OnSelectionChanged(this, &FFlowNode_CustomEventBaseDetails::PinSelectionChanged) + [ + SNew(STextBlock) + .Text(this, &FFlowNode_CustomEventBaseDetails::GetSelectedEventText) + ] + ]; +} + +void FFlowNode_CustomEventBaseDetails::OnComboBoxOpening() +{ + RebuildEventNames(); +} + +void FFlowNode_CustomEventBaseDetails::RebuildEventNames() +{ + EventNames.Empty(); + + check(CachedEventNameSelected.IsValid()); + EventNames.Add(CachedEventNameSelected); + + if (ObjectsBeingEdited[0].IsValid() && ObjectsBeingEdited[0].Get()->GetOuter()) + { + const UFlowAsset* FlowAsset = CastChecked(ObjectsBeingEdited[0].Get()->GetOuter()); + TArray SortedNames = BuildEventNames(*FlowAsset); + + if (bExcludeReferencedEvents) + { + for (const TPair& Node : FlowAsset->GetNodes()) + { + if (Node.Value->GetClass()->IsChildOf(UFlowNode_CustomEventBase::StaticClass())) + { + SortedNames.Remove(Cast(Node.Value)->GetEventName()); + } + } + } + + SortedNames.Sort([](const FName& A, const FName& B) + { + return A.LexicalLess(B); + }); + + for (const FName& EventName : SortedNames) + { + const bool bIsCurrentSelection = (EventName == *CachedEventNameSelected); + if (!EventName.IsNone() && !bIsCurrentSelection) + { + EventNames.Add(MakeShared(EventName)); + } + } + } + + if (!IsInEventNames(NAME_None)) + { + EventNames.Add(MakeShared(NAME_None)); + } +} + +bool FFlowNode_CustomEventBaseDetails::IsInEventNames(const FName& EventName) const +{ + const bool bIsInEventNames = EventNames.ContainsByPredicate([&EventName](const TSharedPtr& ExistingName) + { + return *ExistingName == EventName; + }); + + return bIsInEventNames; +} + +TSharedRef FFlowNode_CustomEventBaseDetails::GenerateEventWidget(const TSharedPtr Item) const +{ + return SNew(STextBlock) + .Text(FText::FromName(*Item.Get())); +} + +FText FFlowNode_CustomEventBaseDetails::GetSelectedEventText() const +{ + check(CachedEventNameSelected.IsValid()); + return FText::FromName(*CachedEventNameSelected.Get()); +} + +void FFlowNode_CustomEventBaseDetails::PinSelectionChanged(const TSharedPtr Item, ESelectInfo::Type SelectInfo) +{ + ensure(ObjectsBeingEdited[0].IsValid()); + + UFlowNode_CustomEventBase* Node = Cast(ObjectsBeingEdited[0].Get()); + if (IsValid(Node) && Item) + { + const bool bIsChanged = (*CachedEventNameSelected != *Item); + + if (bIsChanged) + { + CachedEventNameSelected = Item; + + const FName ItemAsName = *CachedEventNameSelected; + Node->SetEventName(ItemAsName); + + if (EventTextListWidget.IsValid()) + { + // Tell UDE to refresh the widget to show the new change + EventTextListWidget->Invalidate(EInvalidateWidgetReason::Paint); + } + } + } +} diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowNode_CustomInputDetails.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_CustomInputDetails.cpp new file mode 100644 index 000000000..5bcb3b60c --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_CustomInputDetails.cpp @@ -0,0 +1,35 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowNode_CustomInputDetails.h" +#include "DetailLayoutBuilder.h" +#include "FlowAsset.h" + +#define LOCTEXT_NAMESPACE "FlowNode_CustomInputDetails" + +FFlowNode_CustomInputDetails::FFlowNode_CustomInputDetails() +{ + bExcludeReferencedEvents = true; +} + +void FFlowNode_CustomInputDetails::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) +{ + // For backward compatability, these localized texts are in FlowNode_CustomInputDetails, + // not FlowNode_CustomNodeBase, so passing them in to a common function. + + static const FText CustomRowNameText = LOCTEXT("CustomRowName", "Event Name"); + static const FText EventNameText = LOCTEXT("EventName", "Event Name"); + + CustomizeDetailsInternal(DetailLayout, CustomRowNameText, EventNameText); +} + +IDetailCategoryBuilder& FFlowNode_CustomInputDetails::CreateDetailCategory(IDetailLayoutBuilder& DetailLayout) const +{ + return DetailLayout.EditCategory("CustomInput", LOCTEXT("CustomInputCategory", "Custom Event")); +} + +TArray FFlowNode_CustomInputDetails::BuildEventNames(const UFlowAsset& FlowAsset) const +{ + return FlowAsset.GetCustomInputs(); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowNode_CustomOutputDetails.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_CustomOutputDetails.cpp new file mode 100644 index 000000000..652e768ae --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_CustomOutputDetails.cpp @@ -0,0 +1,35 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowNode_CustomOutputDetails.h" +#include "DetailLayoutBuilder.h" +#include "FlowAsset.h" + +#define LOCTEXT_NAMESPACE "FlowNode_CustomOutputDetails" + +FFlowNode_CustomOutputDetails::FFlowNode_CustomOutputDetails() +{ + check(bExcludeReferencedEvents == false); +} + +void FFlowNode_CustomOutputDetails::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) +{ + // For backward compatability, these localized texts are in FlowNode_CustomOutputDetails, + // not FlowNode_CustomNodeBase, so passing them in to a common function. + + static const FText CustomRowNameText = LOCTEXT("CustomRowName", "Event Name"); + static const FText EventNameText = LOCTEXT("EventName", "Event Name"); + + CustomizeDetailsInternal(DetailLayout, CustomRowNameText, EventNameText); +} + +IDetailCategoryBuilder& FFlowNode_CustomOutputDetails::CreateDetailCategory(IDetailLayoutBuilder& DetailLayout) const +{ + return DetailLayout.EditCategory("CustomOutput", LOCTEXT("CustomEventsCategory", "Custom Output")); +} + +TArray FFlowNode_CustomOutputDetails::BuildEventNames(const UFlowAsset& FlowAsset) const +{ + return FlowAsset.GetCustomOutputs(); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowNode_Details.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_Details.cpp new file mode 100644 index 000000000..9a699f8b2 --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_Details.cpp @@ -0,0 +1,75 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowNode_Details.h" + +#include "DetailCustomizations/FlowDetailsAddOnUI.h" +#include "Nodes/FlowNode.h" + +#include "DetailCategoryBuilder.h" +#include "DetailLayoutBuilder.h" +#include "DetailWidgetRow.h" +#include "Widgets/Input/SComboButton.h" +#include "Widgets/Text/STextBlock.h" + +#define LOCTEXT_NAMESPACE "FlowNodeDetails" + +void FFlowNode_Details::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) +{ + // Hide class-level category when editing an instance (not the CDO) + if (!DetailLayout.HasClassDefaultObject()) + { + DetailLayout.HideCategory(TEXT("FlowNode")); + } + + // Cache edited object + { + TArray> Objects; + DetailLayout.GetObjectsBeingCustomized(Objects); + + EditedNode = nullptr; + for (const TWeakObjectPtr& Obj : Objects) + { + if (UFlowNode* AsNode = Cast(Obj.Get())) + { + EditedNode = AsNode; + break; + } + } + } + + // Add "Attach AddOn..." dropdown (menu button) + if (EditedNode.IsValid() && !DetailLayout.HasClassDefaultObject()) + { + IDetailCategoryBuilder& AddOnsCategory = DetailLayout.EditCategory( + TEXT("AddOns"), + LOCTEXT("AddOnsCategory", "AddOns"), + ECategoryPriority::Important); + + AddOnsCategory.AddCustomRow(LOCTEXT("AttachAddOnSearch", "Attach AddOn")) + .WholeRowContent() + [ + SNew(SComboButton) + .ButtonContent() + [ + SNew(STextBlock) + .Text(LOCTEXT("AttachAddOnButton", "Attach AddOn...")) + ] + .ToolTipText(LOCTEXT("AttachAddOnButtonTooltip", "Attach an AddOn to the selected node/addon.")) + .IsEnabled_Lambda([this]() + { + return EditedNode.IsValid() && FFlowDetailsAddOnUI::CanAttachAddOn(EditedNode.Get()); + }) + .OnGetMenuContent_Lambda([this]() + { + return EditedNode.IsValid() + ? FFlowDetailsAddOnUI::BuildAttachAddOnMenuContent(EditedNode.Get()) + : SNullWidget::NullWidget; + }) + ]; + } + + // Call base template to set up rebuild delegate wiring + TFlowDataPinValueOwnerCustomization::CustomizeDetails(DetailLayout); +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_PlayLevelSequenceDetails.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_PlayLevelSequenceDetails.cpp similarity index 74% rename from Source/FlowEditor/Private/Nodes/Customizations/FlowNode_PlayLevelSequenceDetails.cpp rename to Source/FlowEditor/Private/DetailCustomizations/FlowNode_PlayLevelSequenceDetails.cpp index f57695a0c..d9ee70957 100644 --- a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_PlayLevelSequenceDetails.cpp +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_PlayLevelSequenceDetails.cpp @@ -1,7 +1,7 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors -#include "FlowNode_PlayLevelSequenceDetails.h" -#include "Nodes/World/FlowNode_PlayLevelSequence.h" +#include "DetailCustomizations/FlowNode_PlayLevelSequenceDetails.h" +#include "Nodes/Actor/FlowNode_PlayLevelSequence.h" #include "DetailCategoryBuilder.h" #include "DetailLayoutBuilder.h" @@ -14,5 +14,7 @@ void FFlowNode_PlayLevelSequenceDetails::CustomizeDetails(IDetailLayoutBuilder& SequenceCategory.AddProperty(GET_MEMBER_NAME_CHECKED(UFlowNode_PlayLevelSequence, bPlayReverse)); SequenceCategory.AddProperty(GET_MEMBER_NAME_CHECKED(UFlowNode_PlayLevelSequence, CameraSettings)); SequenceCategory.AddProperty(GET_MEMBER_NAME_CHECKED(UFlowNode_PlayLevelSequence, bUseGraphOwnerAsTransformOrigin)); + SequenceCategory.AddProperty(GET_MEMBER_NAME_CHECKED(UFlowNode_PlayLevelSequence, bReplicates)); + SequenceCategory.AddProperty(GET_MEMBER_NAME_CHECKED(UFlowNode_PlayLevelSequence, bAlwaysRelevant)); SequenceCategory.AddProperty(GET_MEMBER_NAME_CHECKED(UFlowNode_PlayLevelSequence, bApplyOwnerTimeDilation)); } diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowNode_SubGraphDetails.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_SubGraphDetails.cpp new file mode 100644 index 000000000..d3b80e4d0 --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_SubGraphDetails.cpp @@ -0,0 +1,70 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowNode_SubGraphDetails.h" + +#include "DetailLayoutBuilder.h" +#include "FlowAsset.h" +#include "Nodes/Graph/FlowNode_SubGraph.h" + +void FFlowNode_SubGraphDetails::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) +{ + TArray> ObjectsBeingEdited; + DetailLayout.GetObjectsBeingCustomized(ObjectsBeingEdited); + + if (ObjectsBeingEdited[0].IsValid()) + { + const UFlowNode_SubGraph* SubGraphNode = CastChecked(ObjectsBeingEdited[0]); + + // Generate the list of asset classes allowed or disallowed + TArray FlowAssetClasses; + GetDerivedClasses(UFlowAsset::StaticClass(), FlowAssetClasses, true); + FlowAssetClasses.Add(UFlowAsset::StaticClass()); + + TArray DisallowedClasses; + TArray AllowedClasses; + for (auto FlowAssetClass : FlowAssetClasses) + { + if (const UFlowAsset* DefaultAsset = Cast(FlowAssetClass->GetDefaultObject())) + { + if (DefaultAsset->DeniedInSubgraphNodeClasses.Contains(SubGraphNode->GetClass())) + { + DisallowedClasses.Add(FlowAssetClass); + } + + if (DefaultAsset->AllowedInSubgraphNodeClasses.Contains(SubGraphNode->GetClass())) + { + AllowedClasses.Add(FlowAssetClass); + } + } + } + + DisallowedClasses.Append(SubGraphNode->DeniedAssignedAssetClasses); + + for (auto FlowAssetClass : SubGraphNode->AllowedAssignedAssetClasses) + { + if (!DisallowedClasses.Contains(FlowAssetClass)) + { + AllowedClasses.AddUnique(FlowAssetClass); + } + } + + FString AllowedClassesString; + for (UClass* Class : AllowedClasses) + { + AllowedClassesString.Append(FString::Printf(TEXT("%s,"), *Class->GetClassPathName().ToString())); + } + + FString DisallowedClassesString; + for (UClass* Class : DisallowedClasses) + { + DisallowedClassesString.Append(FString::Printf(TEXT("%s,"), *Class->GetClassPathName().ToString())); + } + + const auto AssetProperty = DetailLayout.GetProperty(FName("Asset"), UFlowNode_SubGraph::StaticClass()); + if (FProperty* MetaDataProperty = AssetProperty->GetMetaDataProperty()) + { + MetaDataProperty->SetMetaData(TEXT("AllowedClasses"), *AllowedClassesString); + MetaDataProperty->SetMetaData(TEXT("DisallowedClasses"), *DisallowedClassesString); + } + } +} diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowPinCustomization.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowPinCustomization.cpp new file mode 100644 index 000000000..e19623835 --- /dev/null +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowPinCustomization.cpp @@ -0,0 +1,41 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DetailCustomizations/FlowPinCustomization.h" +#include "Nodes/FlowPin.h" +#include "IDetailChildrenBuilder.h" +#include "UObject/UnrealType.h" + +void FFlowPinCustomization::CustomizeChildren(TSharedRef InStructPropertyHandle, IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ + // Add all of the child properties as normal, but also bind a callback when the property value changes + uint32 NumChildren = 0; + verify(InStructPropertyHandle->GetNumChildren(NumChildren) == FPropertyAccess::Success); + for (uint32 ChildIdx = 0; ChildIdx < NumChildren; ++ChildIdx) + { + TSharedPtr ChildProperty = InStructPropertyHandle->GetChildHandle(ChildIdx); + if (ChildProperty.IsValid()) + { + ChildProperty->SetOnPropertyValueChanged(FSimpleDelegate::CreateSP(this, &FFlowPinCustomization::OnChildPropertyValueChanged)); + + ChildBuilder.AddProperty(ChildProperty.ToSharedRef()); + } + } +} + +void FFlowPinCustomization::OnChildPropertyValueChanged() +{ + if (FFlowPin* FlowPin = GetFlowPin()) + { + IFlowExtendedPropertyTypeCustomization::OnAnyChildPropertyChanged(); + } +} + +FText FFlowPinCustomization::BuildHeaderText() const +{ + if (const FFlowPin* FlowPin = GetFlowPin()) + { + return FlowPin->BuildHeaderText(); + } + + return Super::BuildHeaderText(); +} diff --git a/Source/FlowEditor/Private/Find/FindInFlow.cpp b/Source/FlowEditor/Private/Find/FindInFlow.cpp new file mode 100644 index 000000000..3f596e39f --- /dev/null +++ b/Source/FlowEditor/Private/Find/FindInFlow.cpp @@ -0,0 +1,964 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Find/FindInFlow.h" +#include "Asset/FlowAssetEditor.h" +#include "Find/SFindInFlowFilterPopup.h" +#include "Graph/FlowGraphEditorSettings.h" +#include "Graph/FlowGraphUtils.h" +#include "Graph/Nodes/FlowGraphNode.h" +#include "FlowAsset.h" +#include "FlowEditorModule.h" +#include "Nodes/FlowNode.h" +#include "Nodes/FlowNodeBase.h" +#include "AddOns/FlowNodeAddOn.h" +#include "Nodes/Graph/FlowNode_SubGraph.h" + +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetRegistry/ARFilter.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphNode.h" +#include "Framework/Application/SlateApplication.h" +#include "Framework/Views/ITypedTableView.h" +#include "GraphEditor.h" +#include "HAL/PlatformMath.h" +#include "Input/Events.h" +#include "Internationalization/Internationalization.h" +#include "Layout/Children.h" +#include "Layout/WidgetPath.h" +#include "Math/Color.h" +#include "Misc/Attribute.h" +#include "Misc/EnumRange.h" +#include "Misc/ScopedSlowTask.h" +#include "SlotBase.h" +#include "Subsystems/AssetEditorSubsystem.h" +#include "Styling/AppStyle.h" +#include "Styling/SlateColor.h" +#include "Templates/Casts.h" +#include "Types/SlateStructs.h" +#include "UObject/Class.h" +#include "UObject/ObjectPtr.h" +#include "UObject/TopLevelAssetPath.h" +#include "Widgets/Images/SImage.h" +#include "Widgets/Input/SSearchBox.h" +#include "Widgets/Input/SComboBox.h" +#include "Widgets/Input/SSpinBox.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Layout/SBorder.h" +#include "Widgets/Layout/SBox.h" +#include "Widgets/SBoxPanel.h" +#include "Widgets/SToolTip.h" +#include "Widgets/Text/STextBlock.h" +#include "Widgets/Views/STableRow.h" + +#define LOCTEXT_NAMESPACE "FindInFlow" + +////////////////////////////////////////////////////////////////////////// +// FFindInFlowCache + +TMap, TMap>> FFindInFlowCache::CategoryStringCache; + +void FFindInFlowCache::OnFlowAssetChanged(UFlowAsset& ChangedFlowAsset) +{ + TArray> EntriesToRemove; + + for (const auto& KV : CategoryStringCache) + { + const TWeakObjectPtr& EdNodePtr = KV.Key; + + UEdGraphNode* EdNode = EdNodePtr.Get(); + + if (!IsValid(EdNode)) + { + EntriesToRemove.Add(EdNodePtr); + + continue; + } + + UEdGraph* EdGraph = ChangedFlowAsset.GetGraph(); + if (EdGraph->Nodes.Contains(EdNode)) + { + EntriesToRemove.Add(EdNodePtr); + } + } + + for (const TWeakObjectPtr& EdNodePtr : EntriesToRemove) + { + CategoryStringCache.Remove(EdNodePtr); + } +} + +////////////////////////////////////////////////////////////////////////// +// FFindInFlowResult + +FFindInFlowResult::FFindInFlowResult(const FString& InValue, UFlowAsset* InOwningFlowAsset) + : Value(InValue) + , OwningFlowAsset(InOwningFlowAsset) +{ +} + +FFindInFlowResult::FFindInFlowResult(const FString& InValue, TSharedPtr InParent, UEdGraphNode* InNode, bool bInIsSubGraphNode, UFlowAsset* InOwningFlowAsset) + : Value(InValue) + , GraphNode(InNode) + , OwningFlowAsset(InOwningFlowAsset) + , Parent(InParent) + , bIsSubGraphNode(bInIsSubGraphNode) +{ +} + +TSharedRef FFindInFlowResult::CreateIcon() const +{ + const FSlateColor IconColor = FSlateColor::UseForeground(); + const FSlateBrush* Brush = FAppStyle::GetBrush(TEXT("GraphEditor.FIB_Event")); + + return SNew(SImage) + .Image(Brush) + .ColorAndOpacity(IconColor); +} + +FReply FFindInFlowResult::OnClick(TWeakPtr FlowAssetEditorPtr) +{ + if (GraphNode.IsValid()) + { + if (UEdGraph* Graph = GraphNode->GetGraph()) + { + if (UFlowAsset* Asset = Cast(Graph->GetOuter())) + { + GEditor->GetEditorSubsystem()->OpenEditorForAsset(Asset); + if (TSharedPtr Editor = FFlowGraphUtils::GetFlowAssetEditor(Graph)) + { + Editor->JumpToNode(GraphNode.Get()); + } + } + } + } + else if (OwningFlowAsset.IsValid()) + { + GEditor->GetEditorSubsystem()->OpenEditorForAsset(OwningFlowAsset.Get()); + } + return FReply::Handled(); +} + +FReply FFindInFlowResult::OnDoubleClick() const +{ + if (bIsSubGraphNode && Parent.IsValid()) + { + if (const UFlowGraphNode* ParentNode = Cast(Parent.Pin()->GraphNode.Get())) + { + if (UFlowNode_SubGraph* SubGraph = Cast(ParentNode->GetFlowNodeBase())) + { + if (UObject* Target = SubGraph->GetAssetToEdit()) + { + GEditor->GetEditorSubsystem()->OpenEditorForAsset(Target); + if (TSharedPtr Editor = FFlowGraphUtils::GetFlowAssetEditor(GraphNode->GetGraph())) + { + Editor->JumpToNode(GraphNode.Get()); + } + } + } + } + } + + return FReply::Handled(); +} + +FString FFindInFlowResult::GetDescriptionText() const +{ + if (const UFlowGraphNode* FlowGraphNode = Cast(GraphNode.Get())) + { + return FlowGraphNode->GetNodeDescription(); + } + + return FString(); +} + +FString FFindInFlowResult::GetCommentText() const +{ + return GraphNode.IsValid() ? GraphNode->NodeComment : FString(); +} + +FString FFindInFlowResult::GetNodeTypeText() const +{ + if (!GraphNode.IsValid()) + { + return FString(); + } + + if (const UFlowGraphNode* FlowGraphNode = Cast(GraphNode.Get())) + { + if (UFlowNodeBase* Base = FlowGraphNode->GetFlowNodeBase()) + { + return Base->GetClass()->GetDisplayNameText().ToString(); + } + } + + return GraphNode->GetClass()->GetDisplayNameText().ToString(); +} + +FText FFindInFlowResult::GetToolTipText() const +{ + FString Tip = GetNodeTypeText() + TEXT("\n") + GetDescriptionText(); + + if (!GetCommentText().IsEmpty()) + { + Tip += TEXT("\n") + GetCommentText(); + } + + if (!MatchedPropertySnippet.IsEmpty()) + { + Tip += TEXT("\n\nMatched: ") + MatchedPropertySnippet; + } + + return FText::FromString(Tip); +} + +FText FFindInFlowResult::GetMatchedSnippet() const +{ + return FText::FromString(MatchedPropertySnippet); +} + +FText FFindInFlowResult::GetMatchedCategoriesText() const +{ + if (MatchedFlags == EFlowSearchFlags::None) + { + return FText::GetEmpty(); + } + + TArray DisplayNames; + + for (EFlowSearchFlags Flag : MakeFlagsRange(EFlowSearchFlags::All)) + { + if (EnumHasAnyFlags(MatchedFlags, Flag)) + { + FText DisplayName = UEnum::GetDisplayValueAsText(Flag); + if (!DisplayName.IsEmpty()) + { + DisplayNames.Add(DisplayName); + } + } + } + + if (DisplayNames.Num() == 0) + { + return FText::GetEmpty(); + } + + return FText::Join(FText::FromString(TEXT(", ")), DisplayNames); +} + +////////////////////////////////////////////////////////////////////////// +// SFindInFlow + +void SFindInFlow::Construct(const FArguments& InArgs, TSharedPtr InFlowAssetEditor) +{ + FlowAssetEditorPtr = InFlowAssetEditor; + SearchResults.Setup(); + + // Load INI settings + const UFlowGraphEditorSettings* Settings = GetDefault(); + if (ensure(Settings)) + { + MaxSearchDepth = Settings->DefaultMaxSearchDepth; + SearchFlags = static_cast(Settings->DefaultSearchFlags); + } + + // Populate scope options + FLOW_ASSERT_ENUM_MAX(EFlowSearchScope, 3); + for (EFlowSearchScope Scope : TEnumRange()) + { + if (FlowEnum::IsValidEnumValue(Scope)) + { + ScopeOptionList.Add(MakeShareable(new EFlowSearchScope(Scope))); + } + } + SelectedScopeOption = ScopeOptionList[0]; + + SAssignNew(SearchTextField, SSearchBox) + .OnTextCommitted(this, &SFindInFlow::OnSearchTextCommitted); + + SAssignNew(SearchButton, SButton) + .Text(LOCTEXT("SearchButton", "Search")) + .OnClicked(this, &SFindInFlow::OnSearchButtonClicked); + + SAssignNew(MaxDepthSpinBox, SSpinBox) + .MinValue(0) + .MaxValue(10) + .Value(MaxSearchDepth) + .OnValueChanged(this, &SFindInFlow::OnMaxDepthChanged) + .ToolTipText(LOCTEXT("MaxDepthTooltip", "Maximum recursion depth when searching inside objects")); + + SAssignNew(TreeView, STreeViewType) + .TreeItemsSource(&SearchResults.ItemsFound) + .OnGenerateRow(this, &SFindInFlow::OnGenerateRow) + .OnGetChildren(this, &SFindInFlow::OnGetChildren) + .OnSelectionChanged(this, &SFindInFlow::OnTreeSelectionChanged) + .OnMouseButtonDoubleClick(this, &SFindInFlow::OnTreeSelectionDoubleClicked); + + ChildSlot + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + .VAlign(VAlign_Center) + [ + SearchTextField.ToSharedRef() + ] + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(4, 0) + [ + SearchButton.ToSharedRef() + ] + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(4, 0) + [ + SNew(STextBlock) + .Text(LOCTEXT("FiltersLabel", "Filters:")) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(4, 0) + [ + SNew(SButton) + .ButtonStyle(FAppStyle::Get(), "HoverHintOnly") + .ToolTipText(LOCTEXT("EditFiltersTooltip", "Edit search filters")) + .OnClicked_Lambda([this]() + { + const FFindInFlowApplyDelegate OnSaveAsDefault = FFindInFlowApplyDelegate::CreateLambda([this](EFlowSearchFlags Flags) + { + if (UFlowGraphEditorSettings* GraphEditorSettings = GetMutableDefault()) + { + GraphEditorSettings->DefaultSearchFlags = static_cast(Flags); + GraphEditorSettings->SaveConfig(); + } + }); + + const TSharedRef FilterPopup = SNew(SFindInFlowFilterPopup) + .OnApply(FFindInFlowApplyDelegate::CreateLambda([this](const EFlowSearchFlags NewSearchFlags) + { + SearchFlags = NewSearchFlags; + InitiateSearch(); + })) + .OnSaveAsDefault(OnSaveAsDefault) + .InitialFlags(SearchFlags); + + FSlateApplication::Get().PushMenu( + AsShared(), + FWidgetPath(), + FilterPopup, + FSlateApplication::Get().GetCursorPos(), + FPopupTransitionEffect::ContextMenu); + + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text_Lambda([this]() + { + int32 ActiveCount = FMath::CountBits(static_cast(SearchFlags)); + return FText::Format(LOCTEXT("ActiveFilters", "{0} Active"), FText::AsNumber(ActiveCount)); + }) + ] + ] + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(4, 0) + [ + SNew(SComboBox>) + .OptionsSource(&ScopeOptionList) + .OnGenerateWidget(this, &SFindInFlow::GenerateScopeWidget) + .OnSelectionChanged(this, &SFindInFlow::OnScopeChanged) + [ + SNew(STextBlock).Text(this, &SFindInFlow::GetCurrentScopeText) + ] + ] + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(4, 0) + [ + SNew(STextBlock) + .Text(LOCTEXT("MaxDepthLabel", "Max Depth:")) + ] + + SHorizontalBox::Slot() + .AutoWidth() + [ + MaxDepthSpinBox.ToSharedRef() + ] + ] + + SVerticalBox::Slot() + .FillHeight(1.0f) + [ + SNew(SBorder) + .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) + [ + TreeView.ToSharedRef() + ] + ] + ]; +} + +void SFindInFlow::FocusForUse() const +{ + if (SearchTextField.IsValid()) + { + FSlateApplication::Get().SetKeyboardFocus(SearchTextField.ToSharedRef()); + SearchTextField->SelectAllText(); + } +} + +void SFindInFlow::OnSearchTextChanged(const FText& Text) +{ + SearchValue = Text.ToString(); +} + +void SFindInFlow::OnSearchTextCommitted(const FText& Text, ETextCommit::Type) +{ + SearchValue = Text.ToString(); + + InitiateSearch(); +} + +FReply SFindInFlow::OnSearchButtonClicked() +{ + InitiateSearch(); + + return FReply::Handled(); +} + +void SFindInFlow::OnScopeChanged(TSharedPtr NewSelection, ESelectInfo::Type) +{ + SelectedScopeOption = NewSelection; + SearchScope = *NewSelection; +} + +void SFindInFlow::OnMaxDepthChanged(int32 NewDepth) +{ + MaxSearchDepth = NewDepth; + + // Save to INI + if (UFlowGraphEditorSettings* GraphEditorSettings = GetMutableDefault()) + { + GraphEditorSettings->DefaultMaxSearchDepth = NewDepth; + GraphEditorSettings->SaveConfig(); + } +} + +TSharedRef SFindInFlow::GenerateScopeWidget(TSharedPtr Item) const +{ + return SNew(STextBlock) + .Text(UEnum::GetDisplayValueAsText(*Item.Get())); +} + +FText SFindInFlow::GetCurrentScopeText() const +{ + return UEnum::GetDisplayValueAsText(*SelectedScopeOption.Get()); +} + +void SFindInFlow::InitiateSearch() +{ + FFlowEditorModule* FlowEditorModule = &FModuleManager::LoadModuleChecked("FlowEditor"); + if (ensure(FlowEditorModule)) + { + FlowEditorModule->RegisterForAssetChanges(); + } + + SearchResults.Reset(); + + HighlightText = FText::FromString(SearchValue); + TreeView->RequestTreeRefresh(); + + if (SearchValue.IsEmpty()) + { + return; + } + + TArray Tokens; + SearchValue.ParseIntoArray(Tokens, TEXT(" "), true); + for (FString& Token : Tokens) + { + Token = Token.ToUpper(); + } + + TSharedPtr Editor = FlowAssetEditorPtr.Pin(); + if (!Editor.IsValid()) + { + return; + } + + UFlowAsset* CurrentAsset = Editor->GetFlowAsset(); + if (!CurrentAsset || !CurrentAsset->GetGraph()) + { + return; + } + + constexpr int32 Depth = 0; + switch (SearchScope) + { + case EFlowSearchScope::ThisAssetOnly: + { + FSearchResult AssetRoot = MakeShareable(new FFindInFlowResult(CurrentAsset->GetName(), CurrentAsset)); + ProcessAsset(CurrentAsset, AssetRoot, Tokens, Depth); + + if (AssetRoot->Children.Num() > 0) + { + SearchResults.ItemsFound.Add(AssetRoot); + + // Auto-expand the current asset's results + TreeView->SetItemExpansion(AssetRoot, true); + } + } + break; + + case EFlowSearchScope::AllOfThisType: + case EFlowSearchScope::AllFlowAssets: + { + FAssetRegistryModule& Registry = FModuleManager::LoadModuleChecked("AssetRegistry"); + TArray Assets; + FARFilter Filter; + Filter.bRecursiveClasses = true; + + if (SearchScope == EFlowSearchScope::AllFlowAssets) + { + Filter.ClassPaths.Add(FTopLevelAssetPath(UFlowAsset::StaticClass()->GetClassPathName())); + } + else + { + Filter.ClassPaths.Add(FTopLevelAssetPath(CurrentAsset->GetClass()->GetClassPathName())); + } + + Registry.Get().GetAssets(Filter, Assets); + + FScopedSlowTask Task(Assets.Num(), LOCTEXT("SearchingAssets", "Searching Flow Assets...")); + Task.MakeDialog(); + + int32 CurrentAssetIndex = 0; + + for (const FAssetData& Data : Assets) + { + UFlowAsset* Asset = Cast(Data.GetAsset()); + if (!IsValid(Asset)) + { + continue; + } + + CurrentAssetIndex++; + + Task.EnterProgressFrame(1, FText::Format(LOCTEXT("SearchingAsset", "Searching {0}/{1}: {2}..."), CurrentAssetIndex, Assets.Num(), FText::FromString(Asset->GetName()))); + + FSearchResult AssetRoot = MakeShareable(new FFindInFlowResult(Asset->GetName(), Asset)); + ProcessAsset(Asset, AssetRoot, Tokens, Depth); + + if (AssetRoot->Children.Num() > 0) + { + SearchResults.ItemsFound.Add(AssetRoot); + + // Auto-expand only the current asset + if (Asset == CurrentAsset) + { + TreeView->SetItemExpansion(AssetRoot, true); + } + } + } + } + break; + + default: + checkNoEntry(); + break; + } + + // Add "No results" placeholder if nothing found + if (SearchResults.ItemsFound.IsEmpty()) + { + FSearchResult NoResults = MakeShareable(new FFindInFlowResult(TEXT("No results found"))); + SearchResults.ItemsFound.Add(NoResults); + } + + TreeView->RequestTreeRefresh(); +} + +bool SFindInFlow::ProcessAsset(UFlowAsset* Asset, FSearchResult ParentResult, const TArray& Tokens, int32 Depth) +{ + if (!Asset || !Asset->GetGraph() || Depth >= MaxSearchDepth || SearchResults.VisitedAssets.Contains(Asset)) + { + return false; + } + + SearchResults.VisitedAssets.Add(Asset); + + bool bAnyMatches = false; + + for (UEdGraphNode* EdNode : Asset->GetGraph()->Nodes) + { + const TMap>* CategoryStrings = BuildCategoryStrings(EdNode, Depth); + + if (!CategoryStrings) + { + continue; + } + + EFlowSearchFlags NodeMatchedFlags = EFlowSearchFlags::None; + + for (const TPair>& Pair : *CategoryStrings) + { + const TSet& StringSet = Pair.Value; + if (EnumHasAnyFlags(SearchFlags, Pair.Key) && StringSetMatchesSearchTokens(Tokens, StringSet)) + { + EnumAddFlags(NodeMatchedFlags, Pair.Key); + } + } + + if (NodeMatchedFlags != EFlowSearchFlags::None) + { + FString Title = EdNode->GetNodeTitle(ENodeTitleType::ListView).ToString(); + if (Title.IsEmpty()) + { + Title = EdNode->GetClass()->GetName(); + } + + FSearchResult Result = MakeShareable(new FFindInFlowResult(Title, ParentResult, EdNode, Depth > 0, Asset)); + Result->MatchedFlags = NodeMatchedFlags; + ParentResult->Children.Add(Result); + + bAnyMatches = true; + } + + bAnyMatches |= RecurseIntoSubgraphsIfEnabled(EdNode, ParentResult, Tokens, Depth); + } + + return bAnyMatches; +} + +bool SFindInFlow::RecurseIntoSubgraphsIfEnabled(UEdGraphNode* EdNode, FSearchResult ParentResult, const TArray& Tokens, int32 Depth) +{ + if (!EnumHasAnyFlags(SearchFlags, EFlowSearchFlags::Subgraphs)) + { + return false; + } + + UFlowGraphNode* FlowGraphNode = Cast(EdNode); + if (!FlowGraphNode || !FlowGraphNode->GetFlowNodeBase()) + { + return false; + } + + UFlowNode_SubGraph* SubGraph = Cast(FlowGraphNode->GetFlowNodeBase()); + if (!SubGraph) + { + return false; + } + + UFlowAsset* SubAsset = Cast(SubGraph->GetAssetToEdit()); + if (!SubAsset) + { + return false; + } + + const FString SubgraphStr = + SearchResults.VisitedAssets.Contains(SubAsset) ? + TEXT(" (repeat subgraph)") : + TEXT(" (Subgraph)"); + + const FString SubTitle = SubAsset->GetName() + SubgraphStr; + FSearchResult SubResult = MakeShareable(new FFindInFlowResult(SubTitle, ParentResult, EdNode, true, SubAsset)); + + // Subgraphs don't count against depth + if (ProcessAsset(SubAsset, SubResult, Tokens, Depth)) + { + ParentResult->Children.Add(SubResult); + + return true; + } + + return false; +} + +const TMap>* SFindInFlow::BuildCategoryStrings(UEdGraphNode* EdNode, int32 Depth) const +{ + if (!IsValid(EdNode)) + { + return nullptr; + } + + // Check cache first + if (const TMap>* Cached = FFindInFlowCache::CategoryStringCache.Find(EdNode)) + { + return Cached; + } + + TMap> NewResultMap; + + UpdateSearchFlagToStringMapForEdGraphNode(*EdNode, NewResultMap, Depth); + + UFlowGraphNode* FlowGraphNode = Cast(EdNode); + if (IsValid(FlowGraphNode)) + { + UFlowNodeBase* FlowNodeBase = FlowGraphNode->GetFlowNodeBase(); + if (IsValid(FlowNodeBase)) + { + UpdateSearchFlagToStringMapForFlowNodeBase(*FlowNodeBase, NewResultMap, Depth); + } + } + + // Now add the new map to the search cache + const TMap>* AddedResultMap = &FFindInFlowCache::CategoryStringCache.Add(EdNode, NewResultMap); + return AddedResultMap; +} + +void SFindInFlow::UpdateSearchFlagToStringMapForEdGraphNode(const UEdGraphNode& EdGraphNode, TMap>& SearchFlagToStringMap, int32 Depth) const +{ + // Comments + if (EnumHasAnyFlags(SearchFlags, EFlowSearchFlags::Comments)) + { + TSet& CommentsSet = SearchFlagToStringMap.FindOrAdd(EFlowSearchFlags::Comments); + CommentsSet.Add(EdGraphNode.NodeComment); + } +} + +void SFindInFlow::UpdateSearchFlagToStringMapForFlowNodeBase(const UFlowNodeBase& FlowNodeBase, TMap>& SearchFlagToStringMap, int32 Depth) const +{ + // Node Titles + if (EnumHasAnyFlags(SearchFlags, EFlowSearchFlags::Titles)) + { + TSet& TitlesSet = SearchFlagToStringMap.FindOrAdd(EFlowSearchFlags::Titles); + TitlesSet.Add(FlowNodeBase.GetNodeTitle().ToString()); + } + + // Tooltips + if (EnumHasAnyFlags(SearchFlags, EFlowSearchFlags::Tooltips)) + { + TSet& TooltipsSet = SearchFlagToStringMap.FindOrAdd(EFlowSearchFlags::Tooltips); + TooltipsSet.Add(FlowNodeBase.GetNodeToolTip().ToString()); + } + + // Classes + if (EnumHasAnyFlags(SearchFlags, EFlowSearchFlags::Classes)) + { + TSet& ClassesSet = SearchFlagToStringMap.FindOrAdd(EFlowSearchFlags::Classes); + + const FString DisplayName = FlowNodeBase.GetClass()->GetDisplayNameText().ToString(); + ClassesSet.Add(DisplayName); + + const FString NativeName = FlowNodeBase.GetClass()->GetName(); + ClassesSet.Add(NativeName); + } + + // Descriptions + if (EnumHasAnyFlags(SearchFlags, EFlowSearchFlags::Descriptions)) + { + TSet& DescriptionsSet = SearchFlagToStringMap.FindOrAdd(EFlowSearchFlags::Descriptions); + + DescriptionsSet.Add(FlowNodeBase.GetNodeDescription()); + } + + // Config Text + if (EnumHasAnyFlags(SearchFlags, EFlowSearchFlags::ConfigText)) + { + TSet& ConfigSet = SearchFlagToStringMap.FindOrAdd(EFlowSearchFlags::ConfigText); + ConfigSet.Add(FlowNodeBase.GetNodeConfigText().ToString()); + } + + // Property-based scouring + if (EnumHasAnyFlags(SearchFlags, EFlowSearchFlags::PropertiesFlags)) + { + AppendPropertyValues(&FlowNodeBase, FlowNodeBase.GetClass(), &FlowNodeBase, SearchFlagToStringMap, Depth); + } + + // AddOns + if (EnumHasAnyFlags(SearchFlags, EFlowSearchFlags::AddOns)) + { + FlowNodeBase.ForEachAddOnConst([this, &SearchFlagToStringMap, &Depth](const UFlowNodeAddOn& AddOn) + { + // No depth penalty for AddOns + UpdateSearchFlagToStringMapForFlowNodeBase(AddOn, SearchFlagToStringMap, Depth); + + return EFlowForEachAddOnFunctionReturnValue::Continue; + }); + } +} + +void SFindInFlow::AppendPropertyValues(const void* Container, const UStruct* Struct, const UObject* ParentObject, TMap>& SearchFlagToStringMap, int32 Depth) const +{ + int32 MaxDepth = 1; + if (const UFlowGraphEditorSettings* Settings = GetDefault()) + { + MaxDepth = Settings->DefaultMaxSearchDepth; + } + + if (!Container || !Struct || !ParentObject || Depth >= MaxDepth) + { + return; + } + + for (TFieldIterator It(Struct, EFieldIteratorFlags::IncludeSuper); It; ++It) + { + FProperty* Prop = *It; + if (!Prop->HasAnyPropertyFlags(CPF_Edit | CPF_SimpleDisplay | CPF_AdvancedDisplay | CPF_BlueprintVisible | CPF_Config)) + { + continue; + } + + const void* ValuePtr = Prop->ContainerPtrToValuePtr(Container); + + if (EnumHasAnyFlags(SearchFlags, EFlowSearchFlags::PropertyNames)) + { + TSet& PropertyNamesSet = SearchFlagToStringMap.FindOrAdd(EFlowSearchFlags::PropertyNames); + + const FString DisplayName = Prop->GetMetaData(TEXT("DisplayName")); + + if (!DisplayName.IsEmpty()) + { + PropertyNamesSet.Add(DisplayName); + } + + PropertyNamesSet.Add(Prop->GetName()); + } + + if (EnumHasAnyFlags(SearchFlags, EFlowSearchFlags::PropertyValues)) + { + TSet& PropertyValuesSet = SearchFlagToStringMap.FindOrAdd(EFlowSearchFlags::PropertyValues); + + FString ValueStr; + UObject* MutableParentObject = const_cast(ParentObject); + Prop->ExportText_InContainer(0, ValueStr, Container, nullptr, MutableParentObject, PPF_None); + ValueStr = ValueStr.Replace(TEXT("\""), TEXT("")).TrimStartAndEnd(); + + PropertyValuesSet.Add(ValueStr); + } + + if (EnumHasAnyFlags(SearchFlags, EFlowSearchFlags::Tooltips)) + { + TSet& TooltipsSet = SearchFlagToStringMap.FindOrAdd(EFlowSearchFlags::Tooltips); + TooltipsSet.Add(Prop->GetMetaData(TEXT("ToolTip"))); + } + + if (FStructProperty* StructProp = CastField(Prop)) + { + // Recurse into structs (no depth penalty) + AppendPropertyValues(ValuePtr, StructProp->Struct, ParentObject, SearchFlagToStringMap, Depth); + } + else if (FObjectProperty* ObjProp = CastField(Prop)) + { + // Recurse into inline objects (incurs a depth penalty) + UObject* Obj = ObjProp->GetObjectPropertyValue(ValuePtr); + if (IsValid(Obj) && !Obj->HasAnyFlags(RF_ClassDefaultObject)) + { + AppendPropertyValues(Obj, Obj->GetClass(), Obj, SearchFlagToStringMap, Depth + 1); + } + } + } +} + +bool SFindInFlow::StringMatchesSearchTokens(const TArray& Tokens, const FString& ComparisonString) +{ + int32 MatchedTokenCount = 0; + const int32 TotalTokenCount = Tokens.Num(); + + // Must match all tokens + for (const FString& Token : Tokens) + { + if (ComparisonString.Contains(Token)) + { + ++MatchedTokenCount; + } + else + { + break; + } + } + + if (MatchedTokenCount == TotalTokenCount) + { + return true; + } + else + { + return false; + } +} + +bool SFindInFlow::StringSetMatchesSearchTokens(const TArray& Tokens, const TSet& StringSet) +{ + for (const FString& StringFromSet : StringSet) + { + if (StringMatchesSearchTokens(Tokens, StringFromSet)) + { + return true; + } + } + + return false; +} + +TSharedRef SFindInFlow::OnGenerateRow(FSearchResult InItem, const TSharedRef& OwnerTable) +{ + return SNew(STableRow, OwnerTable) + .ToolTip(SNew(SToolTip).Text(InItem->GetToolTipText())) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(2, 0) + [ + InItem->CreateIcon() + ] + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .Padding(4, 0) + [ + SNew(STextBlock) + .Text(FText::FromString(InItem->Value)) + .HighlightText(HighlightText) + ] + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .Padding(4, 0) + [ + SNew(STextBlock) + .Text(FText::FromString(InItem->GetNodeTypeText())) + .ColorAndOpacity(FSlateColor(FLinearColor(0.6f, 0.8f, 1.0f))) + ] + + SHorizontalBox::Slot() + .HAlign(HAlign_Right) + .VAlign(VAlign_Center) + .Padding(4, 0) + [ + SNew(STextBlock) + .Text(InItem->GetMatchedCategoriesText()) + .ColorAndOpacity(FSlateColor(FLinearColor(0.8f, 0.8f, 0.8f))) + ] + ]; +} + +void SFindInFlow::OnGetChildren(FSearchResult InItem, TArray& OutChildren) +{ + OutChildren = InItem->Children; +} + +void SFindInFlow::OnTreeSelectionChanged(FSearchResult Item, ESelectInfo::Type) +{ + if (Item.IsValid()) + { + Item->OnClick(FlowAssetEditorPtr); + } +} + +void SFindInFlow::OnTreeSelectionDoubleClicked(FSearchResult Item) +{ + if (Item.IsValid()) + { + Item->OnDoubleClick(); + } +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Find/SFindInFlowFilterPopup.cpp b/Source/FlowEditor/Private/Find/SFindInFlowFilterPopup.cpp new file mode 100644 index 000000000..7735a2d90 --- /dev/null +++ b/Source/FlowEditor/Private/Find/SFindInFlowFilterPopup.cpp @@ -0,0 +1,145 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Find/SFindInFlowFilterPopup.h" +#include "Widgets/Input/SCheckBox.h" +#include "Widgets/Text/STextBlock.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Layout/SScrollBox.h" +#include "Framework/Application/SlateApplication.h" + +#define LOCTEXT_NAMESPACE "FindInFlow" + +void SFindInFlowFilterPopup::Construct(const FArguments& InArgs) +{ + OnApplyDelegate = InArgs._OnApply; + OnSaveAsDefaultDelegate = InArgs._OnSaveAsDefault; + ProposedFlags = InArgs._InitialFlags; + + // Build the checkbox container with slots added during construction + SAssignNew(CheckBoxContainer, SVerticalBox); + + for (EFlowSearchFlags Flag : MakeFlagsRange(EFlowSearchFlags::All)) + { + CheckBoxContainer->AddSlot() + .AutoHeight() + [ + SNew(SCheckBox) + .IsChecked(this, &SFindInFlowFilterPopup::GetCheckState, Flag) + .OnCheckStateChanged_Lambda([this, Flag](ECheckBoxState NewState) + { + if (NewState == ECheckBoxState::Checked) + { + EnumAddFlags(ProposedFlags, Flag); + } + else + { + EnumRemoveFlags(ProposedFlags, Flag); + } + }) + [ + SNew(STextBlock) + .Text(UEnum::GetDisplayValueAsText(Flag)) + ] + ]; + } + + ChildSlot + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + .Padding(10) + [ + SNew(STextBlock) + .Text(LOCTEXT("FilterPopupTitle", "Select Search Filters:")) + .Font(FAppStyle::GetFontStyle("NormalFontBold")) + ] + + SVerticalBox::Slot() + .FillHeight(1.0f) + .Padding(10, 5) + [ + SNew(SScrollBox) + + SScrollBox::Slot() + [ + CheckBoxContainer.ToSharedRef() + ] + ] + + SVerticalBox::Slot() + .AutoHeight() + .Padding(10) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SButton) + .Text(LOCTEXT("ToggleAllFilters", "Toggle All")) + .OnClicked(this, &SFindInFlowFilterPopup::OnToggleAllClicked) + ] + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SButton) + .Text(LOCTEXT("SaveAsDefaultFilters", "Save as Default")) + .OnClicked(this, &SFindInFlowFilterPopup::OnSaveAsDefaultClicked) + ] + ] + + SVerticalBox::Slot() + .AutoHeight() + .Padding(10) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SButton) + .Text(LOCTEXT("CancelFilters", "Cancel")) + .OnClicked(this, &SFindInFlowFilterPopup::OnCancelClicked) + ] + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SButton) + .Text(LOCTEXT("ApplyFilters", "Apply")) + .OnClicked(this, &SFindInFlowFilterPopup::OnApplyClicked) + ] + ] + ]; +} + +FReply SFindInFlowFilterPopup::OnApplyClicked() +{ + OnApplyDelegate.ExecuteIfBound(ProposedFlags); + FSlateApplication::Get().DismissAllMenus(); + return FReply::Handled(); +} + +FReply SFindInFlowFilterPopup::OnCancelClicked() +{ + FSlateApplication::Get().DismissAllMenus(); + return FReply::Handled(); +} + +FReply SFindInFlowFilterPopup::OnToggleAllClicked() +{ + if (ProposedFlags != EFlowSearchFlags::None) + { + ProposedFlags = EFlowSearchFlags::None; + } + else + { + ProposedFlags = EFlowSearchFlags::All; + } + + CheckBoxContainer->Invalidate(EInvalidateWidgetReason::Layout); + + return FReply::Handled(); +} + +FReply SFindInFlowFilterPopup::OnSaveAsDefaultClicked() +{ + OnSaveAsDefaultDelegate.ExecuteIfBound(ProposedFlags); + return FReply::Handled(); +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/FlowEditor/Private/FlowEditorCommands.cpp b/Source/FlowEditor/Private/FlowEditorCommands.cpp index 863c9c75a..bcba22b00 100644 --- a/Source/FlowEditor/Private/FlowEditorCommands.cpp +++ b/Source/FlowEditor/Private/FlowEditorCommands.cpp @@ -1,37 +1,38 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #include "FlowEditorCommands.h" - #include "FlowEditorStyle.h" -#include "Graph/FlowGraphSchema.h" #include "Graph/FlowGraphSchema_Actions.h" #include "Nodes/FlowNode.h" -#include "EditorStyleSet.h" #include "Misc/ConfigCacheIni.h" +#include "Styling/AppStyle.h" #define LOCTEXT_NAMESPACE "FlowGraphCommands" FFlowToolbarCommands::FFlowToolbarCommands() - : TCommands("FlowToolbar", LOCTEXT("FlowToolbar", "Flow Toobar"), NAME_None, FFlowEditorStyle::GetStyleSetName()) + : TCommands("FlowToolbar", LOCTEXT("FlowToolbar", "Flow Toolbar"), NAME_None, FFlowEditorStyle::GetStyleSetName()) { } void FFlowToolbarCommands::RegisterCommands() { - UI_COMMAND(RefreshAsset, "Refresh Asset", "Refresh asset and all nodes", EUserInterfaceActionType::Button, FInputChord()); - UI_COMMAND(GoToMasterInstance, "Go To Master", "Open editor for the Flow Asset that created this Flow instance", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(RefreshAsset, "Refresh", "Refresh asset and all nodes", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(ValidateAsset, "Validate", "Validate asset and all nodes", EUserInterfaceActionType::Button, FInputChord()); + + UI_COMMAND(SearchInAsset, "Search", "Search in the current Flow Graph", EUserInterfaceActionType::Button, FInputChord(EModifierKey::Control | EModifierKey::Shift, EKeys::F)); + UI_COMMAND(EditAssetDefaults, "Asset Defaults", "Edit the FlowAsset default properties", EUserInterfaceActionType::Button, FInputChord()); } FFlowGraphCommands::FFlowGraphCommands() - : TCommands("FlowGraph", LOCTEXT("FlowGraph", "Flow Graph"), NAME_None, FEditorStyle::GetStyleSetName()) + : TCommands("FlowGraph", LOCTEXT("FlowGraph", "Flow Graph"), NAME_None, FAppStyle::GetAppStyleSetName()) { } void FFlowGraphCommands::RegisterCommands() { - UI_COMMAND(RefreshContextPins, "Refresh context pins", "Refresh pins generated from the context asset", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(ReconstructNode, "Reconstruct node", "Reconstruct this node", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(AddInput, "Add Input", "Adds an input to the node", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(AddOutput, "Add Output", "Adds an output to the node", EUserInterfaceActionType::Button, FInputChord()); @@ -43,6 +44,13 @@ void FFlowGraphCommands::RegisterCommands() UI_COMMAND(DisablePinBreakpoint, "Disable Pin Breakpoint", "Disables a breakpoint on the pin", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(TogglePinBreakpoint, "Toggle Pin Breakpoint", "Toggles a breakpoint on the pin", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(EnableAllBreakpoints,"Enable All Breakpoints", "Enable all breakpoints", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(DisableAllBreakpoints, "Disable All Breakpoints", "Disable all breakpoints", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(RemoveAllBreakpoints, "Delete All Breakpoints", "Delete all breakpoints", EUserInterfaceActionType::Button, FInputChord(EModifierKey::Control | EModifierKey::Shift, EKeys::F9)); + + UI_COMMAND(EnableNode, "Enable Node", "Default state, node is fully executed.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(DisableNode, "Disable Node", "No logic executed, any Input Pin activation is ignored. Node instantly enters a deactivated state.", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(SetPassThrough, "Set Pass Through", "Internal node logic not executed. All connected outputs are triggered, node finishes its work.", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(ForcePinActivation, "Force Pin Activation", "Forces execution of the pin in a graph, used to bypass blockers", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(FocusViewport, "Focus Viewport", "Focus viewport on actor assigned to the node", EUserInterfaceActionType::Button, FInputChord()); @@ -50,7 +58,7 @@ void FFlowGraphCommands::RegisterCommands() } FFlowSpawnNodeCommands::FFlowSpawnNodeCommands() - : TCommands(TEXT("FFlowSpawnNodeCommands"), LOCTEXT("FlowGraph_SpawnNodes", "Flow Graph - Spawn Nodes"), NAME_None, FEditorStyle::GetStyleSetName()) + : TCommands(TEXT("FFlowSpawnNodeCommands"), LOCTEXT("FlowGraph_SpawnNodes", "Flow Graph - Spawn Nodes"), NAME_None, FAppStyle::GetAppStyleSetName()) { } @@ -68,7 +76,7 @@ void FFlowSpawnNodeCommands::RegisterCommands() FString ClassName; if (FParse::Value(*NodeSpawns[x], TEXT("Class="), ClassName)) { - UClass* FoundClass = FindObject(ANY_PACKAGE, *ClassName, true); + UClass* FoundClass = FindFirstObject(*ClassName, EFindFirstObjectOptions::ExactClass, ELogVerbosity::Warning, TEXT("looking for SpawnNodes")); if (FoundClass && FoundClass->IsChildOf(UFlowNode::StaticClass())) { NodeClass = FoundClass; @@ -116,13 +124,13 @@ void FFlowSpawnNodeCommands::RegisterCommands() const FText CommandLabelText = FText::FromString(NodeClass->GetName()); const FText Description = FText::Format(LOCTEXT("NodeSpawnDescription", "Hold down the bound keys and left click in the graph panel to spawn a {0} node."), CommandLabelText); - FUICommandInfo::MakeCommandInfo(this->AsShared(), CommandInfo, FName(*NodeSpawns[x]), CommandLabelText, Description, FSlateIcon(FEditorStyle::GetStyleSetName(), *FString::Printf(TEXT("%s.%s"), *this->GetContextName().ToString(), *NodeSpawns[x])), EUserInterfaceActionType::Button, Chord); + FUICommandInfo::MakeCommandInfo(this->AsShared(), CommandInfo, FName(*NodeSpawns[x]), CommandLabelText, Description, FSlateIcon(FAppStyle::GetAppStyleSetName(), *FString::Printf(TEXT("%s.%s"), *this->GetContextName().ToString(), *NodeSpawns[x])), EUserInterfaceActionType::Button, Chord); NodeCommands.Add(NodeClass, CommandInfo); } } -TSharedPtr FFlowSpawnNodeCommands::GetChordByClass(UClass* NodeClass) const +TSharedPtr FFlowSpawnNodeCommands::GetChordByClass(const UClass* NodeClass) const { if (NodeCommands.Contains(NodeClass) && NodeCommands[NodeClass]->GetFirstValidChord()->IsValidChord()) { @@ -132,7 +140,7 @@ TSharedPtr FFlowSpawnNodeCommands::GetChordByClass(UClass* No return nullptr; } -TSharedPtr FFlowSpawnNodeCommands::GetActionByChord(FInputChord& InChord) const +TSharedPtr FFlowSpawnNodeCommands::GetActionByChord(const FInputChord& InChord) const { if (InChord.IsValidChord()) { diff --git a/Source/FlowEditor/Private/FlowEditorLogChannels.cpp b/Source/FlowEditor/Private/FlowEditorLogChannels.cpp new file mode 100644 index 000000000..414286936 --- /dev/null +++ b/Source/FlowEditor/Private/FlowEditorLogChannels.cpp @@ -0,0 +1,5 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "FlowEditorLogChannels.h" + +DEFINE_LOG_CATEGORY(LogFlowEditor); diff --git a/Source/FlowEditor/Private/FlowEditorModule.cpp b/Source/FlowEditor/Private/FlowEditorModule.cpp index 8d83d0340..423b4fefd 100644 --- a/Source/FlowEditor/Private/FlowEditorModule.cpp +++ b/Source/FlowEditor/Private/FlowEditorModule.cpp @@ -3,130 +3,200 @@ #include "FlowEditorModule.h" #include "FlowEditorStyle.h" -#include "Asset/AssetTypeActions_FlowAsset.h" -#include "Asset/FlowAssetDetails.h" #include "Asset/FlowAssetEditor.h" #include "Asset/FlowAssetIndexer.h" #include "Graph/FlowGraphConnectionDrawingPolicy.h" +#include "Graph/FlowGraphPinFactory.h" #include "Graph/FlowGraphSettings.h" -#include "LevelEditor/SLevelEditorFlow.h" +#include "Utils/SLevelEditorFlow.h" #include "MovieScene/FlowTrackEditor.h" #include "Nodes/AssetTypeActions_FlowNodeBlueprint.h" -#include "Nodes/Customizations/FlowNode_Details.h" -#include "Nodes/Customizations/FlowNode_ComponentObserverDetails.h" -#include "Nodes/Customizations/FlowNode_CustomInputDetails.h" -#include "Nodes/Customizations/FlowNode_CustomOutputDetails.h" -#include "Nodes/Customizations/FlowNode_PlayLevelSequenceDetails.h" +#include "Nodes/AssetTypeActions_FlowNodeAddOnBlueprint.h" #include "Pins/SFlowInputPinHandle.h" #include "Pins/SFlowOutputPinHandle.h" +#include "FlowModule.h" + +#include "DetailCustomizations/FlowAssetDetails.h" +#include "DetailCustomizations/FlowNode_Details.h" +#include "DetailCustomizations/FlowNode_ComponentObserverDetails.h" +#include "DetailCustomizations/FlowNode_CustomInputDetails.h" +#include "DetailCustomizations/FlowNode_CustomOutputDetails.h" +#include "DetailCustomizations/FlowNode_PlayLevelSequenceDetails.h" +#include "DetailCustomizations/FlowNode_SubGraphDetails.h" +#include "DetailCustomizations/FlowNodeAddOn_Details.h" +#include "DetailCustomizations/FlowActorOwnerComponentRefCustomization.h" +#include "DetailCustomizations/FlowPinCustomization.h" +#include "DetailCustomizations/FlowNamedDataPinPropertyCustomization.h" +#include "DetailCustomizations/FlowAssetParamsPtrCustomization.h" +#include "DetailCustomizations/FlowDataPinValueOwnerCustomizations.h" +#include "DetailCustomizations/FlowDataPinValueStandardCustomizations.h" + #include "FlowAsset.h" -#include "Nodes/Route/FlowNode_CustomInput.h" -#include "Nodes/Route/FlowNode_CustomOutput.h" -#include "Nodes/World/FlowNode_ComponentObserver.h" -#include "Nodes/World/FlowNode_PlayLevelSequence.h" +#include "AddOns/FlowNodeAddOn.h" +#include "Asset/FlowAssetParamsTypes.h" +#include "Find/FindInFlow.h" +#include "Nodes/Actor/FlowNode_ComponentObserver.h" +#include "Nodes/Actor/FlowNode_PlayLevelSequence.h" +#include "Nodes/Graph/FlowNode_CustomInput.h" +#include "Nodes/Graph/FlowNode_CustomOutput.h" +#include "Nodes/Graph/FlowNode_SubGraph.h" +#include "Types/FlowNamedDataPinProperty.h" #include "AssetToolsModule.h" +#include "AssetRegistry/AssetRegistryModule.h" #include "EdGraphUtilities.h" #include "IAssetSearchModule.h" #include "Framework/MultiBox/MultiBoxBuilder.h" -#include "ISequencerChannelInterface.h" +#include "ISequencerChannelInterface.h" // ignore Rider's false "unused include" warning #include "ISequencerModule.h" -#include "LevelEditor.h" -#include "Modules/ModuleManager.h" +#include "ToolMenus.h" static FName AssetSearchModuleName = TEXT("AssetSearch"); #define LOCTEXT_NAMESPACE "FlowEditorModule" EAssetTypeCategories::Type FFlowEditorModule::FlowAssetCategory = static_cast(0); +FAssetCategoryPath FFlowAssetCategoryPaths::Flow(LOCTEXT("Flow", "Flow")); void FFlowEditorModule::StartupModule() { FFlowEditorStyle::Initialize(); + TrySetFlowNodeDisplayStyleDefaults(); + RegisterAssets(); // register visual utilities FEdGraphUtilities::RegisterVisualPinConnectionFactory(MakeShareable(new FFlowGraphConnectionDrawingPolicyFactory)); + FEdGraphUtilities::RegisterVisualPinFactory(MakeShareable(new FFlowGraphPinFactory())); FEdGraphUtilities::RegisterVisualPinFactory(MakeShareable(new FFlowInputPinHandleFactory())); FEdGraphUtilities::RegisterVisualPinFactory(MakeShareable(new FFlowOutputPinHandleFactory())); // add Flow Toolbar - if (UFlowGraphSettings::Get()->bShowAssetToolbarAboveLevelEditor) + if (GetDefault()->bShowAssetToolbarAboveLevelEditor) { - if (FLevelEditorModule* LevelEditorModule = FModuleManager::GetModulePtr(TEXT("LevelEditor"))) + UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateLambda([this]() { - const TSharedPtr MenuExtender = MakeShareable(new FExtender()); - MenuExtender->AddToolBarExtension("Play", EExtensionHook::After, nullptr, FToolBarExtensionDelegate::CreateRaw(this, &FFlowEditorModule::CreateFlowToolbar)); - LevelEditorModule->GetToolBarExtensibilityManager()->AddExtender(MenuExtender); - } + UToolMenu* ToolbarMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar.User"); + FToolMenuSection& Section = ToolbarMenu->FindOrAddSection("FlowEditor"); + Section.AddEntry(FToolMenuEntry::InitWidget("FlowAssetWidget", SNew(SLevelEditorFlow), FText::GetEmpty())); + })); } // register Flow sequence track ISequencerModule& SequencerModule = FModuleManager::Get().LoadModuleChecked("Sequencer"); FlowTrackCreateEditorHandle = SequencerModule.RegisterTrackEditor(FOnCreateTrackEditor::CreateStatic(&FFlowTrackEditor::CreateTrackEditor)); - RegisterPropertyCustomizations(); - - // register detail customizations - RegisterCustomClassLayout(UFlowAsset::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowAssetDetails::MakeInstance)); - RegisterCustomClassLayout(UFlowNode::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowNode_Details::MakeInstance)); - RegisterCustomClassLayout(UFlowNode_ComponentObserver::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowNode_ComponentObserverDetails::MakeInstance)); - RegisterCustomClassLayout(UFlowNode_CustomInput::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowNode_CustomInputDetails::MakeInstance)); - RegisterCustomClassLayout(UFlowNode_CustomOutput::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowNode_CustomOutputDetails::MakeInstance)); - RegisterCustomClassLayout(UFlowNode_PlayLevelSequence::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowNode_PlayLevelSequenceDetails::MakeInstance)); - - FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked("PropertyEditor"); - PropertyModule.NotifyCustomizationModuleChanged(); + RegisterDetailCustomizations(); // register asset indexers if (FModuleManager::Get().IsModuleLoaded(AssetSearchModuleName)) { RegisterAssetIndexers(); } - ModulesChangedHandle = FModuleManager::Get().OnModulesChanged().AddRaw(this, &FFlowEditorModule::ModulesChangesCallback); + ModulesChangedHandle = FModuleManager::Get().OnModulesChanged().AddStatic(&FFlowEditorModule::ModulesChangesCallback); +} + +void FFlowEditorModule::RegisterForAssetChanges() +{ + if (!bIsRegisteredForAssetChanges) + { + // Register asset change detection for search cache invalidation + const FAssetRegistryModule& AssetRegistry = FModuleManager::LoadModuleChecked("AssetRegistry"); + AssetRegistry.Get().OnAssetUpdated().AddStatic(&FFlowEditorModule::OnAssetUpdated); + AssetRegistry.Get().OnAssetRenamed().AddStatic(&FFlowEditorModule::OnAssetRenamed); + AssetRegistry.Get().OnAssetRemoved().AddStatic(&FFlowEditorModule::OnAssetUpdated); + + bIsRegisteredForAssetChanges = true; + } } void FFlowEditorModule::ShutdownModule() { FFlowEditorStyle::Shutdown(); + UnregisterDetailCustomizations(); + UnregisterAssets(); // unregister track editors ISequencerModule& SequencerModule = FModuleManager::Get().LoadModuleChecked("Sequencer"); SequencerModule.UnRegisterTrackEditor(FlowTrackCreateEditorHandle); - // unregister details customizations - if (FModuleManager::Get().IsModuleLoaded("PropertyEditor")) + FModuleManager::Get().OnModulesChanged().Remove(ModulesChangedHandle); + + if (bIsRegisteredForAssetChanges && FModuleManager::Get().IsModuleLoaded("AssetRegistry")) { - FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked("PropertyEditor"); + // Unregister asset change detection + FAssetRegistryModule& AssetRegistry = FModuleManager::Get().GetModuleChecked("AssetRegistry"); - for (auto It = CustomClassLayouts.CreateConstIterator(); It; ++It) - { - if (It->IsValid()) - { - PropertyModule.UnregisterCustomClassLayout(*It); - } - } + AssetRegistry.Get().OnAssetUpdated().RemoveAll(this); + AssetRegistry.Get().OnAssetRenamed().RemoveAll(this); + AssetRegistry.Get().OnAssetRemoved().RemoveAll(this); + + bIsRegisteredForAssetChanges = false; } - - FModuleManager::Get().OnModulesChanged().Remove(ModulesChangedHandle); +} + +void FFlowEditorModule::TrySetFlowNodeDisplayStyleDefaults() const +{ + // Force the Flow module to be loaded before we try to access the Settings + FModuleManager::LoadModuleChecked("Flow"); + + UFlowGraphSettings* GraphSettings = GetMutableDefault(); + GraphSettings->TryAddDefaultNodeDisplayStyle(FFlowNodeDisplayStyleConfig(FlowNodeStyle::Node, FLinearColor(0.0f, 0.581f, 1.0f, 1.0f))); + GraphSettings->TryAddDefaultNodeDisplayStyle(FFlowNodeDisplayStyleConfig(FlowNodeStyle::Condition, FLinearColor(1.0f, 0.62f, 0.016f, 1.0f))); + GraphSettings->TryAddDefaultNodeDisplayStyle(FFlowNodeDisplayStyleConfig(FlowNodeStyle::Deprecated, FLinearColor(1.0f, 1.0f, 0.0f, 1.0f))); + GraphSettings->TryAddDefaultNodeDisplayStyle(FFlowNodeDisplayStyleConfig(FlowNodeStyle::Developer, FLinearColor(0.7f, 0.2f, 1.0f, 1.0f))); + GraphSettings->TryAddDefaultNodeDisplayStyle(FFlowNodeDisplayStyleConfig(FlowNodeStyle::InOut, FLinearColor(1.0f, 0.0f, 0.008f, 1.0f))); + GraphSettings->TryAddDefaultNodeDisplayStyle(FFlowNodeDisplayStyleConfig(FlowNodeStyle::Latent, FLinearColor(0.0f, 0.770f, 0.375f, 1.0f))); + GraphSettings->TryAddDefaultNodeDisplayStyle(FFlowNodeDisplayStyleConfig(FlowNodeStyle::Logic, FLinearColor(1.0f, 1.0f, 1.0f, 1.0f))); + GraphSettings->TryAddDefaultNodeDisplayStyle(FFlowNodeDisplayStyleConfig(FlowNodeStyle::SubGraph, FLinearColor(1.0f, 0.128f, 0.0f, 1.0f))); + GraphSettings->TryAddDefaultNodeDisplayStyle(FFlowNodeDisplayStyleConfig(FlowNodeStyle::Terminal, FLinearColor(1.0f, 0.0f, 0.008f, 1.0f))); + + GraphSettings->TryAddDefaultNodeDisplayStyle(FFlowNodeDisplayStyleConfig(FlowNodeStyle::AddOn, FLinearColor(0.0f, 0.581f, 1.0f, 1.0f))); + GraphSettings->TryAddDefaultNodeDisplayStyle(FFlowNodeDisplayStyleConfig(FlowNodeStyle::AddOn_PerSpawnedActor, FLinearColor(0.3f, 0.3f, 1.0f, 1.0f))); + GraphSettings->TryAddDefaultNodeDisplayStyle(FFlowNodeDisplayStyleConfig(FlowNodeStyle::AddOn_Predicate, FLinearColor(1.0f, 1.0f, 1.0f, 1.0f))); + GraphSettings->TryAddDefaultNodeDisplayStyle(FFlowNodeDisplayStyleConfig(FlowNodeStyle::AddOn_Predicate_Composite, FLinearColor(1.0f, 1.0f, 1.0f, 1.0f))); } void FFlowEditorModule::RegisterAssets() { IAssetTools& AssetTools = FModuleManager::LoadModuleChecked("AssetTools").Get(); - FlowAssetCategory = AssetTools.RegisterAdvancedAssetCategory(FName(TEXT("Flow")), UFlowGraphSettings::Get()->FlowAssetCategoryName); - const TSharedRef FlowAssetActions = MakeShareable(new FAssetTypeActions_FlowAsset()); - RegisteredAssetActions.Add(FlowAssetActions); - AssetTools.RegisterAssetTypeActions(FlowAssetActions); + // try to merge asset category with a built-in one + { + const FText AssetCategoryText = GetDefault()->FlowAssetCategoryName; + + // Find matching built-in category + if (!AssetCategoryText.IsEmpty()) + { + TArray AllCategories; + AssetTools.GetAllAdvancedAssetCategories(AllCategories); + for (const FAdvancedAssetCategory& ExistingCategory : AllCategories) + { + if (ExistingCategory.CategoryName.EqualTo(AssetCategoryText)) + { + FlowAssetCategory = ExistingCategory.CategoryType; + break; + } + } + } + + if (FlowAssetCategory == EAssetTypeCategories::None) + { + FlowAssetCategory = AssetTools.RegisterAdvancedAssetCategory(FName(TEXT("Flow")), AssetCategoryText); + } + } const TSharedRef FlowNodeActions = MakeShareable(new FAssetTypeActions_FlowNodeBlueprint()); RegisteredAssetActions.Add(FlowNodeActions); AssetTools.RegisterAssetTypeActions(FlowNodeActions); + + const TSharedRef FlowNodeAddOnActions = MakeShareable(new FAssetTypeActions_FlowNodeAddOnBlueprint()); + RegisteredAssetActions.Add(FlowNodeAddOnActions); + AssetTools.RegisterAssetTypeActions(FlowNodeAddOnActions); } void FFlowEditorModule::UnregisterAssets() @@ -143,49 +213,110 @@ void FFlowEditorModule::UnregisterAssets() RegisteredAssetActions.Empty(); } -void FFlowEditorModule::RegisterPropertyCustomizations() const -{ - FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked("PropertyEditor"); - - // notify on customization change - PropertyModule.NotifyCustomizationModuleChanged(); -} - void FFlowEditorModule::RegisterCustomClassLayout(const TSubclassOf Class, const FOnGetDetailCustomizationInstance DetailLayout) { if (Class) { + FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked("PropertyEditor"); + PropertyModule.RegisterCustomClassLayout(Class->GetFName(), DetailLayout); + CustomClassLayouts.Add(Class->GetFName()); + } +} +void FFlowEditorModule::RegisterCustomStructLayout(const UScriptStruct& Struct, const FOnGetPropertyTypeCustomizationInstance DetailLayout) +{ + if (FModuleManager::Get().IsModuleLoaded("PropertyEditor")) + { FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked("PropertyEditor"); - PropertyModule.RegisterCustomClassLayout(Class->GetFName(), DetailLayout); + PropertyModule.RegisterCustomPropertyTypeLayout(Struct.GetFName(), DetailLayout); + + CustomStructLayouts.Add(Struct.GetFName()); } } -void FFlowEditorModule::ModulesChangesCallback(FName ModuleName, EModuleChangeReason ReasonForChange) +void FFlowEditorModule::RegisterDetailCustomizations() { - if (ReasonForChange == EModuleChangeReason::ModuleLoaded && ModuleName == AssetSearchModuleName) + // register detail customizations + if (FModuleManager::Get().IsModuleLoaded("PropertyEditor")) { - RegisterAssetIndexers(); + FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked("PropertyEditor"); + + RegisterCustomClassLayout(UFlowAsset::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowAssetDetails::MakeInstance)); + RegisterCustomClassLayout(UFlowAssetParams::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowAssetParamsCustomization::MakeInstance)); + RegisterCustomClassLayout(UFlowExecutableActorComponent::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowExecutableActorComponentCustomization::MakeInstance)); + RegisterCustomClassLayout(UFlowNodeBase::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowNodeBaseCustomization::MakeInstance)); + RegisterCustomClassLayout(UFlowNode::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowNode_Details::MakeInstance)); + RegisterCustomClassLayout(UFlowNodeAddOn::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowNodeAddOn_Details::MakeInstance)); + RegisterCustomClassLayout(UFlowNode_ComponentObserver::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowNode_ComponentObserverDetails::MakeInstance)); + RegisterCustomClassLayout(UFlowNode_CustomInput::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowNode_CustomInputDetails::MakeInstance)); + RegisterCustomClassLayout(UFlowNode_CustomOutput::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowNode_CustomOutputDetails::MakeInstance)); + RegisterCustomClassLayout(UFlowNode_PlayLevelSequence::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowNode_PlayLevelSequenceDetails::MakeInstance)); + RegisterCustomClassLayout(UFlowNode_SubGraph::StaticClass(), FOnGetDetailCustomizationInstance::CreateStatic(&FFlowNode_SubGraphDetails::MakeInstance)); + RegisterCustomStructLayout(*FFlowActorOwnerComponentRef::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowActorOwnerComponentRefCustomization::MakeInstance)); + RegisterCustomStructLayout(*FFlowPin::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowPinCustomization::MakeInstance)); + RegisterCustomStructLayout(*FFlowNamedDataPinProperty::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowNamedDataPinPropertyCustomization::MakeInstance)); + RegisterCustomStructLayout(*FFlowAssetParamsPtr::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowAssetParamsPtrCustomization::MakeInstance)); + RegisterCustomStructLayout(*FFlowDataPinValue_Bool::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowDataPinValueCustomization_Bool::MakeInstance)); + RegisterCustomStructLayout(*FFlowDataPinValue_Int::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowDataPinValueCustomization_Int::MakeInstance)); + RegisterCustomStructLayout(*FFlowDataPinValue_Int64::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowDataPinValueCustomization_Int64::MakeInstance)); + RegisterCustomStructLayout(*FFlowDataPinValue_Float::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowDataPinValueCustomization_Float::MakeInstance)); + RegisterCustomStructLayout(*FFlowDataPinValue_Double::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowDataPinValueCustomization_Double::MakeInstance)); + RegisterCustomStructLayout(*FFlowDataPinValue_Name::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowDataPinValueCustomization_Name::MakeInstance)); + RegisterCustomStructLayout(*FFlowDataPinValue_String::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowDataPinValueCustomization_String::MakeInstance)); + RegisterCustomStructLayout(*FFlowDataPinValue_Text::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowDataPinValueCustomization_Text::MakeInstance)); + RegisterCustomStructLayout(*FFlowDataPinValue_Enum::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowDataPinValueCustomization_Enum::MakeInstance)); + RegisterCustomStructLayout(*FFlowDataPinValue_Vector::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowDataPinValueCustomization_Vector::MakeInstance)); + RegisterCustomStructLayout(*FFlowDataPinValue_Rotator::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowDataPinValueCustomization_Rotator::MakeInstance)); + RegisterCustomStructLayout(*FFlowDataPinValue_Transform::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowDataPinValueCustomization_Transform::MakeInstance)); + RegisterCustomStructLayout(*FFlowDataPinValue_GameplayTag::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowDataPinValueCustomization_GameplayTag::MakeInstance)); + RegisterCustomStructLayout(*FFlowDataPinValue_GameplayTagContainer::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowDataPinValueCustomization_GameplayTagContainer::MakeInstance)); + RegisterCustomStructLayout(*FFlowDataPinValue_InstancedStruct::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowDataPinValueCustomization_InstancedStruct::MakeInstance)); + RegisterCustomStructLayout(*FFlowDataPinValue_Class::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowDataPinValueCustomization_Class::MakeInstance)); + RegisterCustomStructLayout(*FFlowDataPinValue_Object::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowDataPinValueCustomization_Object::MakeInstance)); + + PropertyModule.NotifyCustomizationModuleChanged(); } } -void FFlowEditorModule::RegisterAssetIndexers() const +void FFlowEditorModule::UnregisterDetailCustomizations() { - /** - * Documentation: https://github.com/MothCocoon/FlowGraph/wiki/Asset-Search - * Uncomment line below, if you made these changes to the engine: https://github.com/EpicGames/UnrealEngine/pull/9070 - */ - //IAssetSearchModule::Get().RegisterAssetIndexer(UFlowAsset::StaticClass(), MakeUnique()); + // unregister details customizations + if (FModuleManager::Get().IsModuleLoaded("PropertyEditor")) + { + FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked("PropertyEditor"); + + for (auto It = CustomClassLayouts.CreateConstIterator(); It; ++It) + { + if (It->IsValid()) + { + PropertyModule.UnregisterCustomClassLayout(*It); + } + } + + for (auto It = CustomStructLayouts.CreateConstIterator(); It; ++It) + { + if (It->IsValid()) + { + PropertyModule.UnregisterCustomPropertyTypeLayout(*It); + } + } + + PropertyModule.NotifyCustomizationModuleChanged(); + } } -void FFlowEditorModule::CreateFlowToolbar(FToolBarBuilder& ToolbarBuilder) const +void FFlowEditorModule::ModulesChangesCallback(const FName ModuleName, const EModuleChangeReason ReasonForChange) { - ToolbarBuilder.BeginSection("Flow"); + if (ReasonForChange == EModuleChangeReason::ModuleLoaded && ModuleName == AssetSearchModuleName) { - ToolbarBuilder.AddWidget(SNew(SLevelEditorFlow)); + RegisterAssetIndexers(); } - ToolbarBuilder.EndSection(); +} + +void FFlowEditorModule::RegisterAssetIndexers() +{ + IAssetSearchModule::Get().RegisterAssetIndexer(UFlowAsset::StaticClass(), MakeUnique()); } TSharedRef FFlowEditorModule::CreateFlowAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr& InitToolkitHost, UFlowAsset* FlowAsset) @@ -195,7 +326,19 @@ TSharedRef FFlowEditorModule::CreateFlowAssetEditor(const EToo return NewFlowAssetEditor; } +void FFlowEditorModule::OnAssetUpdated(const FAssetData& AssetData) +{ + if (UFlowAsset* FlowAsset = Cast(AssetData.GetAsset())) + { + FFindInFlowCache::OnFlowAssetChanged(*FlowAsset); + } +} + +void FFlowEditorModule::OnAssetRenamed(const FAssetData& AssetData, const FString& OldObjectPath) +{ + OnAssetUpdated(AssetData); +} + #undef LOCTEXT_NAMESPACE IMPLEMENT_MODULE(FFlowEditorModule, FlowEditor) -DEFINE_LOG_CATEGORY(LogFlowEditor); diff --git a/Source/FlowEditor/Private/FlowEditorStyle.cpp b/Source/FlowEditor/Private/FlowEditorStyle.cpp index ab4046e05..f37844996 100644 --- a/Source/FlowEditor/Private/FlowEditorStyle.cpp +++ b/Source/FlowEditor/Private/FlowEditorStyle.cpp @@ -5,9 +5,10 @@ #include "Interfaces/IPluginManager.h" #include "Styling/SlateStyleRegistry.h" -#define IMAGE_BRUSH( RelativePath, ... ) FSlateImageBrush( StyleSet->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) -#define BOX_BRUSH( RelativePath, ... ) FSlateBoxBrush( StyleSet->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) #define BORDER_BRUSH( RelativePath, ... ) FSlateBorderBrush( StyleSet->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) +#define BOX_BRUSH( RelativePath, ... ) FSlateBoxBrush( StyleSet->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) +#define IMAGE_BRUSH( RelativePath, ... ) FSlateImageBrush( StyleSet->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) +#define IMAGE_BRUSH_SVG( RelativePath, ... ) FSlateVectorImageBrush(StyleSet->RootToContentDir(RelativePath, TEXT(".svg")), __VA_ARGS__) TSharedPtr FFlowEditorStyle::StyleSet = nullptr; @@ -30,18 +31,18 @@ void FFlowEditorStyle::Initialize() // engine assets StyleSet->SetContentRoot(FPaths::EngineContentDir() / TEXT("Editor/Slate/")); - StyleSet->Set("FlowToolbar.RefreshAsset", new IMAGE_BRUSH("Automation/RefreshTests", Icon40)); - StyleSet->Set("FlowToolbar.RefreshAsset.Small", new IMAGE_BRUSH("Automation/RefreshTests", Icon20)); + StyleSet->Set("FlowToolbar.RefreshAsset", new IMAGE_BRUSH_SVG( "Starship/Common/Apply", Icon20)); + StyleSet->Set("FlowToolbar.ValidateAsset", new IMAGE_BRUSH_SVG( "Starship/Common/Debug", Icon20)); - StyleSet->Set("FlowToolbar.GoToMasterInstance", new IMAGE_BRUSH("Icons/icon_DebugStepOut_40x", Icon40)); - StyleSet->Set("FlowToolbar.GoToMasterInstance.Small", new IMAGE_BRUSH("Icons/icon_DebugStepOut_40x", Icon20)); + StyleSet->Set("FlowToolbar.SearchInAsset", new IMAGE_BRUSH_SVG( "Starship/Common/Search", Icon20)); + StyleSet->Set("FlowToolbar.EditAssetDefaults", new IMAGE_BRUSH_SVG("Starship/Common/Details", Icon20)); StyleSet->Set("FlowGraph.BreakpointEnabled", new IMAGE_BRUSH("Old/Kismet2/Breakpoint_Valid", FVector2D(24.0f, 24.0f))); StyleSet->Set("FlowGraph.BreakpointDisabled", new IMAGE_BRUSH("Old/Kismet2/Breakpoint_Disabled", FVector2D(24.0f, 24.0f))); StyleSet->Set("FlowGraph.BreakpointHit", new IMAGE_BRUSH("Old/Kismet2/IP_Breakpoint", Icon40)); StyleSet->Set("FlowGraph.PinBreakpointHit", new IMAGE_BRUSH("Old/Kismet2/IP_Breakpoint", Icon30)); - StyleSet->Set( "GraphEditor.Sequence_16x", new IMAGE_BRUSH("Icons/icon_Blueprint_Sequence_16x", Icon16)); + StyleSet->Set("GraphEditor.Sequence_16x", new IMAGE_BRUSH("Icons/icon_Blueprint_Sequence_16x", Icon16)); // Flow assets StyleSet->SetContentRoot(IPluginManager::Get().FindPlugin(TEXT("Flow"))->GetBaseDir() / TEXT("Resources")); @@ -64,6 +65,7 @@ void FFlowEditorStyle::Shutdown() StyleSet.Reset(); } -#undef IMAGE_BRUSH -#undef BOX_BRUSH #undef BORDER_BRUSH +#undef BOX_BRUSH +#undef IMAGE_BRUSH +#undef IMAGE_BRUSH_SVG diff --git a/Source/FlowEditor/Private/Graph/FlowGraph.cpp b/Source/FlowEditor/Private/Graph/FlowGraph.cpp index b776077e9..89884346f 100644 --- a/Source/FlowEditor/Private/Graph/FlowGraph.cpp +++ b/Source/FlowEditor/Private/Graph/FlowGraph.cpp @@ -2,49 +2,617 @@ #include "Graph/FlowGraph.h" #include "Graph/FlowGraphSchema.h" +#include "Graph/FlowGraphSchema_Actions.h" #include "Graph/Nodes/FlowGraphNode.h" +#include "Graph/Nodes/FlowGraphNode_Reroute.h" +#include "AddOns/FlowNodeAddOn.h" +#include "Nodes/FlowNode.h" +#include "FlowEditorLogChannels.h" +#include "Editor.h" #include "Kismet2/BlueprintEditorUtils.h" +#include "EdGraph/EdGraphPin.h" +#include "Logging/LogMacros.h" +#include "Runtime/Launch/Resources/Version.h" -void FFlowGraphInterface::OnInputTriggered(UEdGraphNode* GraphNode, const int32 Index) const +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowGraph) + +UFlowGraph::UFlowGraph(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) + , GraphVersion(0) { - CastChecked(GraphNode)->OnInputTriggered(Index); + bLockUpdates = false; + bIsLoadingGraph = false; } -void FFlowGraphInterface::OnOutputTriggered(UEdGraphNode* GraphNode, const int32 Index) const +void UFlowGraph::CreateGraph(UFlowAsset* InFlowAsset) { - CastChecked(GraphNode)->OnOutputTriggered(Index); + return CreateGraph(InFlowAsset, UFlowGraphSchema::StaticClass()); } -UFlowGraph::UFlowGraph(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) +void UFlowGraph::CreateGraph(UFlowAsset* InFlowAsset, TSubclassOf FlowSchema) { - if (!UFlowAsset::GetFlowGraphInterface().IsValid()) + UFlowGraph* NewGraph = CastChecked(FBlueprintEditorUtils::CreateNewGraph(InFlowAsset, NAME_None, StaticClass(), FlowSchema)); + NewGraph->bAllowDeletion = false; + + // Ensure we mapped relation between UFlowNode and UFlowGraphNode classes + // Otherwise generating graph wouldn't assign proper UFlowGraphNode class to default nodes generated below + // Issue only occurred if somebody would generate graph programatically without opening Flow Asset editor at least once + UFlowGraphSchema::GatherNodes(); + + InFlowAsset->FlowGraph = NewGraph; + InFlowAsset->FlowGraph->GetSchema()->CreateDefaultNodesForGraph(*InFlowAsset->FlowGraph); +} + +void UFlowGraph::RefreshGraph() +{ + if (!GEditor || GEditor->PlayWorld) { - UFlowAsset::SetFlowGraphInterface(MakeShared()); + // don't run fixup in PIE + return; + } + + // Locking updates to the graph while we update it + { + LockUpdates(); + + // check if all Graph Nodes have expected, up-to-date type + const UFlowGraphSchema* FlowGraphSchema = CastChecked(GetSchema()); + FlowGraphSchema->GatherNodes(); + + for (const TPair& Node : GetFlowAsset()->GetNodes()) + { + UFlowNode* FlowNode = Node.Value; + if (IsValid(FlowNode)) + { + UFlowGraphNode* const ExistingFlowGraphNode = Cast(FlowNode->GetGraphNode()); + UFlowGraphNode* RefreshedFlowGraphNode = ExistingFlowGraphNode; + + const TSubclassOf ExpectGraphNodeClass = UFlowGraphSchema::GetAssignedGraphNodeClass(FlowNode->GetClass()); + const UClass* ExistingFlowGraphNodeClass = IsValid(ExistingFlowGraphNode) ? ExistingFlowGraphNode->GetClass() : nullptr; + if (ExistingFlowGraphNodeClass != ExpectGraphNodeClass) + { + // Create a new Flow Graph Node of proper type + RefreshedFlowGraphNode = FFlowGraphSchemaAction_NewNode::RecreateNode(this, ExistingFlowGraphNode, FlowNode); + } + + RecursivelyRefreshAddOns(*RefreshedFlowGraphNode); + } + } + + // This function will (eventually) result in all graph nodes being reconstructed + UnlockUpdates(); } } -UEdGraph* UFlowGraph::CreateGraph(UFlowAsset* InFlowAsset) +void UFlowGraph::RecursivelyRefreshAddOns(UFlowGraphNode& FromFlowGraphNode) { - UFlowGraph* NewGraph = CastChecked(FBlueprintEditorUtils::CreateNewGraph(InFlowAsset, NAME_None, StaticClass(), UFlowGraphSchema::StaticClass())); - NewGraph->bAllowDeletion = false; + // Refresh AddOns + const UFlowNodeBase* FromFlowNodeBase = FromFlowGraphNode.GetFlowNodeBase(); - InFlowAsset->FlowGraph = NewGraph; - NewGraph->GetSchema()->CreateDefaultNodesForGraph(*NewGraph); + const TArray FlowNodeAddOnChildren = FromFlowNodeBase->GetFlowNodeAddOnChildren(); + for (UFlowNodeAddOn* AddOn : FlowNodeAddOnChildren) + { + if (!AddOn) + { + UE_LOG( + LogFlowEditor, + Error, + TEXT("Missing AddOn detected for node %s (parent %s)"), + *FromFlowNodeBase->GetName(), + FromFlowGraphNode.GetParentNode() ? + *FromFlowGraphNode.GetParentNode()->GetName() : + TEXT("")); + + continue; + } + + UFlowGraphNode* const AddOnFlowGraphNode = Cast(AddOn->GetGraphNode()); + + const TSubclassOf ExpectAddOnGraphNodeClass = UFlowGraphSchema::GetAssignedGraphNodeClass(AddOn->GetClass()); + UFlowGraphNode* RefreshedAddOnFlowGraphNode = AddOnFlowGraphNode; + const UClass* ExistingAddOnGraphNodeClass = IsValid(AddOnFlowGraphNode) ? AddOnFlowGraphNode->GetClass() : nullptr; - return NewGraph; + if (ExistingAddOnGraphNodeClass != ExpectAddOnGraphNodeClass) + { + // Create a new Flow Graph Node of proper type for the AddOn + RefreshedAddOnFlowGraphNode = FFlowSchemaAction_NewSubNode::RecreateNode(this, AddOnFlowGraphNode, &FromFlowGraphNode, AddOn); + } + + // Recurse for the AddOn's AddOn's + RecursivelyRefreshAddOns(*RefreshedAddOnFlowGraphNode); + } } void UFlowGraph::NotifyGraphChanged() { - GetFlowAsset()->HarvestNodeConnections(); - GetFlowAsset()->MarkPackageDirty(); + if (UFlowAsset* FlowAsset = GetFlowAsset()) + { + FlowAsset->HarvestNodeConnections(); + } Super::NotifyGraphChanged(); } UFlowAsset* UFlowGraph::GetFlowAsset() const { - return CastChecked(GetOuter()); + return GetTypedOuter(); +} + +void UFlowGraph::ValidateAsset(FFlowMessageLog& MessageLog) +{ + if (UFlowAsset* FlowAsset = GetFlowAsset()) + { + FlowAsset->ValidateAsset(MessageLog); + } + + for (UEdGraphNode* Node : Nodes) + { + if (const UFlowGraphNode* FlowGraphNode = Cast(Node)) + { + FlowGraphNode->ValidateGraphNode(MessageLog); + } + } +} + +void UFlowGraph::Serialize(FArchive& Ar) +{ + // Overridden to flags up errors in the behavior tree while cooking. + Super::Serialize(Ar); + + if (Ar.IsSaving() || Ar.IsCooking()) + { + // Logging of errors happens in UpdateDeprecatedClasses + UpdateDeprecatedClasses(); + } +} + +void UFlowGraph::OnCreated() +{ + MarkVersion(); +} + +void UFlowGraph::OnLoaded() +{ + check(GEditor); + + bIsLoadingGraph = true; + + UpdateVersion(); + + UFlowAsset* FlowAsset = GetFlowAsset(); + if (IsValid(FlowAsset)) + { + FlowAsset->SetupForEditing(); + } + + // Setup all the Nodes in the graph for editing + for (UEdGraphNode* Node : Nodes) + { + UFlowGraphNode* FlowGraphNode = Cast(Node); + if (IsValid(FlowGraphNode)) + { + RecursivelySetupAllFlowGraphNodesForEditing(*FlowGraphNode); + } + } + + UpdateDeprecatedClasses(); + + if (UpdateUnknownNodeClasses()) + { + NotifyGraphChanged(); + } + + RefreshGraph(); + + bIsLoadingGraph = false; +} + +void UFlowGraph::OnSave() +{ + bIsSavingGraph = true; + + UpdateAsset(); + + bIsSavingGraph = false; +} + +void UFlowGraph::Initialize() +{ + UpdateVersion(); +} + +void UFlowGraph::UpdateVersion() +{ + if (GraphVersion == CurrentGraphVersion) + { + return; + } + + const int32 PrevGraphVersion = GraphVersion; + MarkVersion(); + Modify(); + + // Insert any Version updating code here + + if (PrevGraphVersion < 2) + { + UpgradeAllFlowNodePins(); + } +} + +void UFlowGraph::UpgradeAllFlowNodePins() +{ + if (UFlowAsset* FlowAsset = GetFlowAsset()) + { + for (TPair>& Node : FlowAsset->Nodes) + { + UFlowNode* FlowNode = Node.Value; + if (IsValid(FlowNode)) + { + FlowNode->FixupDataPinTypes(); + FlowNode->TryUpdateAutoDataPins(); + } + } + } + + for (UEdGraphNode* Node : Nodes) + { + if (UFlowGraphNode* FlowGraphNode = Cast(Node)) + { + FlowGraphNode->MarkNeedsFullReconstruction(); + FlowGraphNode->ReconstructNode(); + } + } +} + +void UFlowGraph::MarkVersion() +{ + GraphVersion = CurrentGraphVersion; +} + +void UFlowGraph::UpdateClassData() +{ + for (UEdGraphNode* Node : Nodes) + { + if (UFlowGraphNode* FlowGraphNode = Cast(Node)) + { + FlowGraphNode->UpdateNodeClassData(); + + for (UFlowGraphNode* SubNode : FlowGraphNode->SubNodes) + { + if (SubNode) + { + SubNode->UpdateNodeClassData(); + } + } + } + } +} + +void UFlowGraph::UpdateAsset(const int32 UpdateFlags) +{ + if (IsLocked()) + { + return; + } + + /* UpdateAsset is called to do any reconciliation from the editor-version of the + * graph to the runtime version of the graph data. + * In our case, it will copy the AddOns from their editor-side UFlowGraphNode containers to + * their runtime UFlowNode and/or UFlowNodeAddOn ::AddOn array entry. */ + for (UEdGraphNode* Node : Nodes) + { + if (UFlowGraphNode* FlowGraphNode = Cast(Node)) + { + constexpr bool bForceReconstructNode = false; + FlowGraphNode->RebuildRuntimeAddOnsFromEditorSubNodes(bForceReconstructNode); + } + } + + // Apply any node reconstructs that were requested while locked or transacting + ProcessPendingNodeReconstructs(); +} + +bool UFlowGraph::UpdateUnknownNodeClasses() +{ + bool bUpdated = false; + + for (UEdGraphNode* Node : Nodes) + { + if (UFlowGraphNode* FlowGraphNode = Cast(Node)) + { + const bool bUpdatedNode = FlowGraphNode->RefreshNodeClass(); + bUpdated = bUpdated || bUpdatedNode; + + for (UFlowGraphNode* SubNode : FlowGraphNode->SubNodes) + { + const bool bUpdatedSubNode = SubNode->RefreshNodeClass(); + bUpdated = bUpdated || bUpdatedSubNode; + } + } + } + + return bUpdated; +} + +void UFlowGraph::UpdateDeprecatedClasses() +{ + // This function sets error messages and logs errors about nodes. + + for (UEdGraphNode* Node : Nodes) + { + if (UFlowGraphNode* FlowGraphNode = Cast(Node)) + { + UpdateFlowGraphNodeErrorMessage(*FlowGraphNode); + + for (UFlowGraphNode* SubNode : FlowGraphNode->SubNodes) + { + if (SubNode) + { + UpdateFlowGraphNodeErrorMessage(*SubNode); + } + } + } + } +} + +void UFlowGraph::UpdateFlowGraphNodeErrorMessage(UFlowGraphNode& Node) +{ + // Broke out setting error message in to own function so it can be reused when iterating nodes collection. + if (Node.GetFlowNodeBase()) + { + Node.ErrorMessage = GetDeprecationMessage(Node.GetFlowNodeBase()->GetClass()); + } + else + { + // Null instance. Do we have any meaningful class data? + FString StoredClassName = Node.NodeInstanceClass.GetAssetName(); + StoredClassName.RemoveFromEnd(TEXT("_C")); + + if (!StoredClassName.IsEmpty()) + { + // There is class data here but the instance was not be created. + static const FString IsMissingClassMessage(" class missing. Referenced by "); + Node.ErrorMessage = StoredClassName + IsMissingClassMessage + Node.GetFullName(); + } + } + + if (Node.HasErrors()) + { + UE_LOG(LogFlowEditor, Error, TEXT("%s"), *Node.ErrorMessage); + } +} + +FString UFlowGraph::GetDeprecationMessage(const UClass* Class) +{ + static FName MetaDeprecated = TEXT("DeprecatedNode"); + static FName MetaDeprecatedMessage = TEXT("DeprecationMessage"); + const FString DefDeprecatedMessage("Please remove it!"); + const FString DeprecatedPrefix("DEPRECATED"); + FString DeprecatedMessage; + + if (Class && Class->HasAnyClassFlags(CLASS_Native) && Class->HasMetaData(MetaDeprecated)) + { + DeprecatedMessage = DeprecatedPrefix + TEXT(": "); + DeprecatedMessage += Class->HasMetaData(MetaDeprecatedMessage) ? Class->GetMetaData(MetaDeprecatedMessage) : DefDeprecatedMessage; + } + + return DeprecatedMessage; +} + +void UFlowGraph::OnSubNodeDropped() +{ + // Historically this only harvested connections. + // Keep behavior, but ensure pending reconstructs are processed as well. + NotifyGraphChanged(); + + ProcessPendingNodeReconstructs(); +} + +void UFlowGraph::RemoveOrphanedNodes() +{ + TSet NodeInstances; + CollectAllNodeInstances(NodeInstances); + + NodeInstances.Remove(nullptr); + + // Obtain a list of all nodes actually in the asset and discard unused nodes + TArray AllInners; + constexpr bool bIncludeNestedObjects = false; + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 8 + GetObjectsWithOuter(GetOuter(), AllInners, bIncludeNestedObjects); +#else + GetObjectsWithOuter(GetOuter(), AllInners, EGetObjectsFlags::IncludeNestedObjects); +#endif + + for (auto InnerIt = AllInners.CreateConstIterator(); InnerIt; ++InnerIt) + { + UObject* TestObject = *InnerIt; + if (!NodeInstances.Contains(TestObject) && CanRemoveNestedObject(TestObject)) + { + OnNodeInstanceRemoved(TestObject); + + TestObject->SetFlags(RF_Transient); + +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 8 + TestObject->Rename(nullptr, GetTransientPackage(), REN_DontCreateRedirectors | REN_NonTransactional | REN_ForceNoResetLoaders); +#else + // from compilation warning + // "Rename will no longer call ResetLoaders making this flag no longer needed. + // Prefer REN_AllowPackageLinkerMismatch if you wish to intentionally allow the linker to contain references to objects whose names no longer match what was loaded from disk." + TestObject->Rename(nullptr, GetTransientPackage(), REN_DontCreateRedirectors | REN_NonTransactional); +#endif + } + } +} + +void UFlowGraph::CollectAllNodeInstances(TSet& NodeInstances) +{ + for (UObject* NodeInstance : NodeInstances) + { + if (UFlowGraphNode* FlowGraphNode = Cast(NodeInstance)) + { + NodeInstances.Add(FlowGraphNode->GetFlowNodeBase()); + + for (const UFlowGraphNode* SubNode : FlowGraphNode->SubNodes) + { + if (SubNode) + { + NodeInstances.Add(SubNode->GetFlowNodeBase()); + } + } + } + } +} + +bool UFlowGraph::CanRemoveNestedObject(UObject* TestObject) const +{ + return !TestObject->IsA(UEdGraphNode::StaticClass()) && + !TestObject->IsA(UEdGraph::StaticClass()) && + !TestObject->IsA(UEdGraphSchema::StaticClass()); +} + +UEdGraphPin* UFlowGraph::FindGraphNodePin(UEdGraphNode* Node, const EEdGraphPinDirection Direction) +{ + UEdGraphPin* Pin = nullptr; + for (int32 Idx = 0; Idx < Node->Pins.Num(); Idx++) + { + if (Node->Pins[Idx]->Direction == Direction) + { + Pin = Node->Pins[Idx]; + break; + } + } + + return Pin; +} + +bool UFlowGraph::IsLocked() const +{ + return bLockUpdates; } + +void UFlowGraph::LockUpdates() +{ + bLockUpdates = true; +} + +void UFlowGraph::UnlockUpdates() +{ + bLockUpdates = false; + + // Apply any deferred reroute type updates first, while we're in a stable post-paste state. + ProcessPendingRerouteTypeFixups(); + + // Apply any deferred node reconstructs next (e.g. subnode changes during paste/transactions). + ProcessPendingNodeReconstructs(); + + // Existing behavior + UpdateAsset(); +} + +void UFlowGraph::EnqueueRerouteTypeFixup(UFlowGraphNode_Reroute* RerouteNode) +{ + if (!IsValid(RerouteNode)) + { + return; + } + + // If not locked, run immediately (keeps behavior responsive outside paste/locked updates) + if (!IsLocked()) + { + RerouteNode->NodeConnectionListChanged(); + return; + } + + PendingRerouteTypeFixups.Add(RerouteNode); +} + +void UFlowGraph::ProcessPendingRerouteTypeFixups() +{ + if (PendingRerouteTypeFixups.Num() == 0) + { + return; + } + + // Move aside so re-entrancy (or new enqueue) doesn't invalidate iteration + TSet> Local = MoveTemp(PendingRerouteTypeFixups); + PendingRerouteTypeFixups.Reset(); + + for (UFlowGraphNode_Reroute* RerouteNode : Local) + { + if (IsValid(RerouteNode)) + { + // This will call into reroute's centralized retype path via ReconfigureFromConnections() + RerouteNode->NodeConnectionListChanged(); + } + } +} + +void UFlowGraph::EnqueueNodeReconstruct(UFlowGraphNode* Node) +{ + if (!IsValid(Node)) + { + return; + } + + // If updates are locked OR we're in a transaction OR saving/loading, defer. + if (IsLocked() || GIsTransacting || IsSavingGraph() || IsLoadingGraph()) + { + PendingNodeReconstructs.Add(Node); + return; + } + + // Otherwise do it now. + Node->MarkNeedsFullReconstruction(); + Node->ReconstructNode(); +} + +void UFlowGraph::ProcessPendingNodeReconstructs() +{ + if (PendingNodeReconstructs.IsEmpty()) + { + return; + } + + TSet> Local = MoveTemp(PendingNodeReconstructs); + PendingNodeReconstructs.Reset(); + + for (UFlowGraphNode* Node : Local) + { + if (IsValid(Node)) + { + Node->MarkNeedsFullReconstruction(); + Node->ReconstructNode(); + } + } +} + +void UFlowGraph::RecursivelySetupAllFlowGraphNodesForEditing(UFlowGraphNode& FromFlowGraphNode) +{ + UFlowNodeBase* FromNodeInstance = FromFlowGraphNode.GetFlowNodeBase(); + if (IsValid(FromNodeInstance)) + { + // Setup all the flow node (and SubNode) instances for editing + FromNodeInstance->SetupForEditing(FromFlowGraphNode); + } + else + { + // Reconstruct the node if the NodeInstance is missing + FromFlowGraphNode.ReconstructNode(); + } + + for (UFlowGraphNode* SubNode : FromFlowGraphNode.SubNodes) + { + // Setup all the flow SubNodes for editing + if (IsValid(SubNode)) + { + SubNode->SetParentNodeForSubNode(&FromFlowGraphNode); + + RecursivelySetupAllFlowGraphNodesForEditing(*SubNode); + } + } +} + diff --git a/Source/FlowEditor/Private/Graph/FlowGraphConnectionDrawingPolicy.cpp b/Source/FlowEditor/Private/Graph/FlowGraphConnectionDrawingPolicy.cpp index 4aa0c4eb0..bc630fc67 100644 --- a/Source/FlowEditor/Private/Graph/FlowGraphConnectionDrawingPolicy.cpp +++ b/Source/FlowEditor/Private/Graph/FlowGraphConnectionDrawingPolicy.cpp @@ -2,8 +2,8 @@ #include "Graph/FlowGraphConnectionDrawingPolicy.h" -#include "Asset/FlowAssetEditor.h" #include "Graph/FlowGraph.h" +#include "Graph/FlowGraphEditor.h" #include "Graph/FlowGraphEditorSettings.h" #include "Graph/FlowGraphSchema.h" #include "Graph/FlowGraphSettings.h" @@ -11,10 +11,14 @@ #include "Graph/Nodes/FlowGraphNode.h" #include "FlowAsset.h" +#include "FlowEditorLogChannels.h" +#include "Graph/Nodes/FlowGraphNode_Reroute.h" #include "Nodes/FlowNode.h" #include "Misc/App.h" +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowGraphConnectionDrawingPolicy) + FConnectionDrawingPolicy* FFlowGraphConnectionDrawingPolicyFactory::CreateConnectionPolicy(const class UEdGraphSchema* Schema, int32 InBackLayerID, int32 InFrontLayerID, float ZoomFactor, const class FSlateRect& InClippingRect, class FSlateWindowElementList& InDrawElements, class UEdGraph* InGraphObj) const { if (Schema->IsA(UFlowGraphSchema::StaticClass())) @@ -31,18 +35,20 @@ FFlowGraphConnectionDrawingPolicy::FFlowGraphConnectionDrawingPolicy(int32 InBac : FConnectionDrawingPolicy(InBackLayerID, InFrontLayerID, ZoomFactor, InClippingRect, InDrawElements) , GraphObj(InGraphObj) { + const UFlowGraphSettings* GraphSettings = GetDefault(); + // Cache off the editor options - RecentWireDuration = UFlowGraphSettings::Get()->RecentWireDuration; + RecentWireDuration = GraphSettings->RecentWireDuration; - InactiveColor = UFlowGraphSettings::Get()->InactiveWireColor; - RecentColor = UFlowGraphSettings::Get()->RecentWireColor; - RecordedColor = UFlowGraphSettings::Get()->RecordedWireColor; - SelectedColor = UFlowGraphSettings::Get()->SelectedWireColor; + InactiveColor = GraphSettings->InactiveWireColor; + RecentColor = GraphSettings->RecentWireColor; + RecordedColor = GraphSettings->RecordedWireColor; + SelectedColor = GraphSettings->SelectedWireColor; - InactiveWireThickness = UFlowGraphSettings::Get()->InactiveWireThickness; - RecentWireThickness = UFlowGraphSettings::Get()->RecentWireThickness; - RecordedWireThickness = UFlowGraphSettings::Get()->RecordedWireThickness; - SelectedWireThickness = UFlowGraphSettings::Get()->SelectedWireThickness; + InactiveWireThickness = GraphSettings->InactiveWireThickness; + RecentWireThickness = GraphSettings->RecentWireThickness; + RecordedWireThickness = GraphSettings->RecordedWireThickness; + SelectedWireThickness = GraphSettings->SelectedWireThickness; // Don't want to draw ending arrowheads ArrowImage = nullptr; @@ -61,6 +67,13 @@ void FFlowGraphConnectionDrawingPolicy::BuildPaths() for (const TPair& Record : Node->GetWireRecords()) { + if (!FlowGraphNode->OutputPins.IsValidIndex(Record.Key)) + { + UE_LOG(LogFlowEditor, Error, TEXT("Flow node '%s' has an invalid pin connection. This is probably an flow editor code bug."), *Node->GetName()); + + continue; + } + if (UEdGraphPin* OutputPin = FlowGraphNode->OutputPins[Record.Key]) { // check if Output pin is connected to anything @@ -78,17 +91,18 @@ void FFlowGraphConnectionDrawingPolicy::BuildPaths() } } - if (GraphObj && (UFlowGraphEditorSettings::Get()->bHighlightInputWiresOfSelectedNodes || UFlowGraphEditorSettings::Get()->bHighlightOutputWiresOfSelectedNodes)) + const UFlowGraphEditorSettings* GraphEditorSettings = GetDefault(); + if (GraphObj && (GraphEditorSettings->bHighlightInputWiresOfSelectedNodes || GraphEditorSettings->bHighlightOutputWiresOfSelectedNodes)) { - const TSharedPtr FlowAssetEditor = FFlowGraphUtils::GetFlowAssetEditor(GraphObj); - if (FlowAssetEditor.IsValid()) + const TSharedPtr FlowGraphEditor = FFlowGraphUtils::GetFlowGraphEditor(GraphObj); + if (FlowGraphEditor.IsValid()) { - for (UFlowGraphNode* SelectedNode : FlowAssetEditor->GetSelectedFlowNodes()) + for (UFlowGraphNode* SelectedNode : FlowGraphEditor->GetSelectedFlowNodes()) { for (UEdGraphPin* Pin : SelectedNode->Pins) { - if ((Pin->Direction == EGPD_Input && UFlowGraphEditorSettings::Get()->bHighlightInputWiresOfSelectedNodes) - || (Pin->Direction == EGPD_Output && UFlowGraphEditorSettings::Get()->bHighlightOutputWiresOfSelectedNodes)) + if ((Pin->Direction == EGPD_Input && GraphEditorSettings->bHighlightInputWiresOfSelectedNodes) + || (Pin->Direction == EGPD_Output && GraphEditorSettings->bHighlightOutputWiresOfSelectedNodes)) { for (UEdGraphPin* LinkedPin : Pin->LinkedTo) { @@ -101,15 +115,15 @@ void FFlowGraphConnectionDrawingPolicy::BuildPaths() } } -void FFlowGraphConnectionDrawingPolicy::DrawConnection(int32 LayerId, const FVector2D& Start, const FVector2D& End, const FConnectionParams& Params) +void FFlowGraphConnectionDrawingPolicy::DrawConnection(int32 LayerId, const FVector2f& Start, const FVector2f& End, const FConnectionParams& Params) { - switch (UFlowGraphSettings::Get()->ConnectionDrawType) + switch (GetDefault()->ConnectionDrawType) { case EFlowConnectionDrawType::Default: FConnectionDrawingPolicy::DrawConnection(LayerId, Start, End, Params); break; case EFlowConnectionDrawType::Circuit: - DrawCircuitSpline(LayerId, Start, End, Params); + DrawCircuitSpline(LayerId, Start, End, Params); break; default: ; } @@ -134,7 +148,12 @@ void FFlowGraphConnectionDrawingPolicy::DetermineWiringStyle(UEdGraphPin* Output { Params.WireColor = Schema->GetPinTypeColor(OutputPin->PinType); - if (InputPin) + if (Cast(OutputPin->GetOwningNode())->GetSignalMode() == EFlowSignalMode::Disabled) + { + Params.WireColor *= 0.5f; + Params.WireThickness = 0.5f; + } + else if (InputPin && FFlowPin::IsExecPinCategory(InputPin->PinType.PinCategory)) { // selected paths if (SelectedPaths.Contains(OutputPin) || SelectedPaths.Contains(InputPin)) @@ -142,32 +161,58 @@ void FFlowGraphConnectionDrawingPolicy::DetermineWiringStyle(UEdGraphPin* Output Params.WireColor = SelectedColor; Params.WireThickness = SelectedWireThickness; Params.bDrawBubbles = false; - return; } - // recent paths - if (RecentPaths.Contains(OutputPin) && RecentPaths[OutputPin] == InputPin) + else if (RecentPaths.Contains(OutputPin) && RecentPaths[OutputPin] == InputPin) { Params.WireColor = RecentColor; Params.WireThickness = RecentWireThickness; Params.bDrawBubbles = true; - return; } - // all paths, showing graph history - if (RecordedPaths.Contains(OutputPin) && RecordedPaths[OutputPin] == InputPin) + else if (RecordedPaths.Contains(OutputPin) && RecordedPaths[OutputPin] == InputPin) { Params.WireColor = RecordedColor; Params.WireThickness = RecordedWireThickness; Params.bDrawBubbles = false; - return; } - // It's not followed, fade it and keep it thin - Params.WireColor = InactiveColor; - Params.WireThickness = InactiveWireThickness; + else + { + Params.WireColor = InactiveColor; + Params.WireThickness = InactiveWireThickness; + } } } + + // If reroute node path goes backwards, we need to flip the direction to make it look nice + // (all of the logic for this is basically same as in FKismetConnectionDrawingPolicy) + { + UEdGraphNode* OutputNode = OutputPin->GetOwningNode(); + UEdGraphNode* InputNode = (InputPin != nullptr) ? InputPin->GetOwningNode() : nullptr; + if (auto* OutputRerouteNode = Cast(OutputNode)) + { + if (ShouldChangeTangentForReroute(OutputRerouteNode)) + { + Params.StartDirection = EGPD_Input; + } + } + + if (auto* InputRerouteNode = Cast(InputNode)) + { + if (ShouldChangeTangentForReroute(InputRerouteNode)) + { + Params.EndDirection = EGPD_Output; + } + } + } + + const bool bDeemphasizeUnhoveredPins = HoveredPins.Num() > 0; + + if (bDeemphasizeUnhoveredPins) + { + ApplyHoverDeemphasis(OutputPin, InputPin, /*inout*/ Params.WireThickness, /*inout*/ Params.WireColor); + } } void FFlowGraphConnectionDrawingPolicy::Draw(TMap, FArrangedWidget>& InPinGeometries, FArrangedChildren& ArrangedNodes) @@ -177,14 +222,14 @@ void FFlowGraphConnectionDrawingPolicy::Draw(TMap, FArranged FConnectionDrawingPolicy::Draw(InPinGeometries, ArrangedNodes); } -void FFlowGraphConnectionDrawingPolicy::DrawCircuitSpline(const int32& LayerId, const FVector2D& Start, const FVector2D& End, const FConnectionParams& Params) const +void FFlowGraphConnectionDrawingPolicy::DrawCircuitSpline(const int32& LayerId, const FVector2f& Start, const FVector2f& End, const FConnectionParams& Params) const { - const FVector2D StartingPoint = FVector2D(Start.X + UFlowGraphSettings::Get()->CircuitConnectionSpacing.X, Start.Y); - const FVector2D EndPoint = FVector2D(End.X - UFlowGraphSettings::Get()->CircuitConnectionSpacing.Y, End.Y); - const FVector2D ControlPoint = GetControlPoint(StartingPoint, EndPoint); + const FVector2f StartingPoint = FVector2f(Start.X + GetDefault()->CircuitConnectionSpacing.X, Start.Y); + const FVector2f EndPoint = FVector2f(End.X - GetDefault()->CircuitConnectionSpacing.Y, End.Y); + const FVector2f ControlPoint = GetControlPoint(StartingPoint, EndPoint); - const FVector2D StartDirection = (Params.StartDirection == EGPD_Output) ? FVector2D(1.0f, 0.0f) : FVector2D(-1.0f, 0.0f); - const FVector2D EndDirection = (Params.EndDirection == EGPD_Input) ? FVector2D(1.0f, 0.0f) : FVector2D(-1.0f, 0.0f); + const FVector2f StartDirection = (Params.StartDirection == EGPD_Output) ? FVector2f(1.0f, 0.0f) : FVector2f(-1.0f, 0.0f); + const FVector2f EndDirection = (Params.EndDirection == EGPD_Input) ? FVector2f(1.0f, 0.0f) : FVector2f(-1.0f, 0.0f); DrawCircuitConnection(LayerId, Start, StartDirection, StartingPoint, EndDirection, Params); DrawCircuitConnection(LayerId, StartingPoint, StartDirection, ControlPoint, EndDirection, Params); @@ -192,7 +237,7 @@ void FFlowGraphConnectionDrawingPolicy::DrawCircuitSpline(const int32& LayerId, DrawCircuitConnection(LayerId, EndPoint, StartDirection, End, EndDirection, Params); } -void FFlowGraphConnectionDrawingPolicy::DrawCircuitConnection(const int32& LayerId, const FVector2D& Start, const FVector2D& StartDirection, const FVector2D& End, const FVector2D& EndDirection, const FConnectionParams& Params) const +void FFlowGraphConnectionDrawingPolicy::DrawCircuitConnection(const int32& LayerId, const FVector2f& Start, const FVector2f& StartDirection, const FVector2f& End, const FVector2f& EndDirection, const FConnectionParams& Params) const { FSlateDrawElement::MakeDrawSpaceSpline(DrawElementsList, LayerId, Start, StartDirection, End, EndDirection, Params.WireThickness, ESlateDrawEffect::None, Params.WireColor); @@ -207,7 +252,7 @@ void FFlowGraphConnectionDrawingPolicy::DrawCircuitConnection(const int32& Layer { const float BubbleSpacing = 64.f * ZoomFactor; const float BubbleSpeed = 192.f * ZoomFactor; - const FVector2D BubbleSize = BubbleImage->ImageSize * ZoomFactor * 0.2f * Params.WireThickness; + const FVector2f BubbleSize = BubbleImage->ImageSize * ZoomFactor * 0.2f * Params.WireThickness; const float Time = (FPlatformTime::Seconds() - GStartTime); const float BubbleOffset = FMath::Fmod(Time * BubbleSpeed, BubbleSpacing); @@ -218,7 +263,7 @@ void FFlowGraphConnectionDrawingPolicy::DrawCircuitConnection(const int32& Layer if (Distance < SplineLength) { const float Alpha = SplineReparamTable.Eval(Distance, 0.f); - FVector2D BubblePos = FMath::CubicInterp(Start, StartDirection, End, EndDirection, Alpha); + FVector2f BubblePos = FMath::CubicInterp(Start, StartDirection, End, EndDirection, Alpha); BubblePos -= (BubbleSize * 0.5f); FSlateDrawElement::MakeBox(DrawElementsList, LayerId, FPaintGeometry(BubblePos, BubbleSize, ZoomFactor), BubbleImage, ESlateDrawEffect::None, Params.WireColor); @@ -228,10 +273,10 @@ void FFlowGraphConnectionDrawingPolicy::DrawCircuitConnection(const int32& Layer } } -FVector2D FFlowGraphConnectionDrawingPolicy::GetControlPoint(const FVector2D& Source, const FVector2D& Target) +FVector2f FFlowGraphConnectionDrawingPolicy::GetControlPoint(const FVector2f& Source, const FVector2f& Target) { - const FVector2D Delta = Target - Source; - const float Tangent = FMath::Tan(UFlowGraphSettings::Get()->CircuitConnectionAngle * (PI / 180.f)); + const FVector2f Delta = Target - Source; + const float Tangent = FMath::Tan(GetDefault()->CircuitConnectionAngle * (PI / 180.f)); const float DeltaX = FMath::Abs(Delta.X); const float DeltaY = FMath::Abs(Delta.Y); @@ -239,7 +284,7 @@ FVector2D FFlowGraphConnectionDrawingPolicy::GetControlPoint(const FVector2D& So const float SlopeWidth = DeltaY / Tangent; if (DeltaX > SlopeWidth) { - return Delta.X > 0.f ? FVector2D(Target.X - SlopeWidth, Source.Y) : FVector2D(Source.X - SlopeWidth, Target.Y); + return Delta.X > 0.f ? FVector2f(Target.X - SlopeWidth, Source.Y) : FVector2f(Source.X - SlopeWidth, Target.Y); } const float SlopeHeight = DeltaX * Tangent; @@ -247,14 +292,100 @@ FVector2D FFlowGraphConnectionDrawingPolicy::GetControlPoint(const FVector2D& So { if (Delta.Y > 0.f) { - return Delta.X < 0.f ? FVector2D(Source.X, Target.Y - SlopeHeight) : FVector2D(Target.X, Source.Y + SlopeHeight); + return Delta.X < 0.f ? FVector2f(Source.X, Target.Y - SlopeHeight) : FVector2f(Target.X, Source.Y + SlopeHeight); } if (Delta.X < 0.f) { - return FVector2D(Source.X, Target.Y + SlopeHeight); + return FVector2f(Source.X, Target.Y + SlopeHeight); } } - return FVector2D(Target.X, Source.Y - SlopeHeight); + return FVector2f(Target.X, Source.Y - SlopeHeight); } + +bool FFlowGraphConnectionDrawingPolicy::ShouldChangeTangentForReroute(UFlowGraphNode_Reroute* Reroute) +{ + if (const bool* pResult = RerouteToReversedDirectionMap.Find(Reroute)) + { + return *pResult; + } + else + { + bool bPinReversed = false; + + FVector2D AverageLeftPin; + FVector2D AverageRightPin; + FVector2D CenterPin = FVector2D::ZeroVector; + const bool bCenterValid = Reroute->OutputPins.Num() == 0 ? false : FindPinCenter(Reroute->OutputPins[0], /*out*/ CenterPin); + const bool bLeftValid = GetAverageConnectedPosition(Reroute, EGPD_Input, /*out*/ AverageLeftPin); + const bool bRightValid = GetAverageConnectedPosition(Reroute, EGPD_Output, /*out*/ AverageRightPin); + + if (bLeftValid && bRightValid) + { + bPinReversed = AverageRightPin.X < AverageLeftPin.X; + } + else if (bCenterValid) + { + if (bLeftValid) + { + bPinReversed = CenterPin.X < AverageLeftPin.X; + } + else if (bRightValid) + { + bPinReversed = AverageRightPin.X < CenterPin.X; + } + } + + RerouteToReversedDirectionMap.Add(Reroute, bPinReversed); + + return bPinReversed; + } +} + +bool FFlowGraphConnectionDrawingPolicy::FindPinCenter(const UEdGraphPin* Pin, FVector2D& OutCenter) const +{ + if (const TSharedPtr* PinWidget = PinToPinWidgetMap.Find(Pin)) + { + if (const FArrangedWidget* PinEntry = PinGeometries->Find((*PinWidget).ToSharedRef())) + { + OutCenter = FGeometryHelper::CenterOf(PinEntry->Geometry); + return true; + } + } + + return false; +} + +bool FFlowGraphConnectionDrawingPolicy::GetAverageConnectedPosition(UFlowGraphNode_Reroute* Reroute, EEdGraphPinDirection Direction, FVector2D& OutPos) const +{ + FVector2D Result = FVector2D::ZeroVector; + int32 ResultCount = 0; + + if(Reroute->InputPins.Num() == 0 || Reroute->OutputPins.Num() == 0) + { + return false; + } + + UEdGraphPin* Pin = (Direction == EGPD_Input) ? Reroute->InputPins[0] : Reroute->OutputPins[0]; + for (const UEdGraphPin* LinkedPin : Pin->LinkedTo) + { + FVector2D CenterPoint; + if (FindPinCenter(LinkedPin, /*out*/ CenterPoint)) + { + Result += CenterPoint; + ResultCount++; + } + } + + if (ResultCount > 0) + { + OutPos = Result * (1.0f / ResultCount); + return true; + } + else + { + return false; + } +} + diff --git a/Source/FlowEditor/Private/Graph/FlowGraphEditor.cpp b/Source/FlowEditor/Private/Graph/FlowGraphEditor.cpp new file mode 100644 index 000000000..84380bec3 --- /dev/null +++ b/Source/FlowEditor/Private/Graph/FlowGraphEditor.cpp @@ -0,0 +1,1589 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Graph/FlowGraphEditor.h" + +#include "Asset/FlowAssetEditor.h" +#include "FlowEditorCommands.h" +#include "Graph/FlowGraphSchema_Actions.h" +#include "Graph/Nodes/FlowGraphNode.h" + +#include "Debugger/FlowDebuggerSubsystem.h" + +#include "EdGraphUtilities.h" +#include "Editor/UnrealEdEngine.h" +#include "Framework/Application/SlateApplication.h" +#include "Framework/Commands/GenericCommands.h" +#include "GraphEditorActions.h" +#include "HAL/PlatformApplicationMisc.h" +#include "IDetailsView.h" +#include "LevelEditor.h" +#include "Modules/ModuleManager.h" +#include "ScopedTransaction.h" +#include "Runtime/Launch/Resources/Version.h" +#include "ToolMenu.h" +#include "ToolMenuDelegates.h" +#include "ToolMenus.h" +#include "UnrealEdGlobals.h" +#include "Widgets/Docking/SDockTab.h" +#include "Algo/AnyOf.h" + +#define LOCTEXT_NAMESPACE "FlowGraphEditor" + +void SFlowGraphEditor::Construct(const FArguments& InArgs, const TSharedPtr InAssetEditor) +{ + FlowAssetEditor = InAssetEditor; + FlowAsset = FlowAssetEditor.Pin()->GetFlowAsset(); + DetailsView = InArgs._DetailsView; + + DebuggerSubsystem = GEngine->GetEngineSubsystem(); + + BindGraphCommands(); + CreateDebugMenu(); + + SGraphEditor::FArguments Arguments; + Arguments._AdditionalCommands = CommandList; + Arguments._Appearance = TAttribute::CreateSP(this, &SFlowGraphEditor::GetGraphAppearanceInfo); + Arguments._GraphToEdit = FlowAsset->GetGraph(); + Arguments._GraphEvents = InArgs._GraphEvents; + Arguments._AutoExpandActionMenu = true; + Arguments._GraphEvents.OnSelectionChanged = FOnSelectionChanged::CreateSP(this, &SFlowGraphEditor::OnSelectedNodesChanged); + Arguments._GraphEvents.OnNodeDoubleClicked = FSingleNodeEvent::CreateSP(this, &SFlowGraphEditor::OnNodeDoubleClicked); + Arguments._GraphEvents.OnTextCommitted = FOnNodeTextCommitted::CreateSP(this, &SFlowGraphEditor::OnNodeTitleCommitted); + Arguments._GraphEvents.OnSpawnNodeByShortcutAtLocation = FOnSpawnNodeByShortcutAtLocation::CreateStatic(&SFlowGraphEditor::OnSpawnGraphNodeByShortcut, static_cast(FlowAsset->GetGraph())); + + SGraphEditor::Construct(Arguments); +} + +bool SFlowGraphEditor::GetValidExecBreakpointPinContext(const UEdGraphPin* Pin, FGuid& OutNodeGuid, FName& OutPinName) +{ + if (!Pin) + { + return false; + } + + if (!FFlowPin::IsExecPinCategory(Pin->PinType.PinCategory)) + { + return false; + } + + // - If the owning node is not a UFlowGraphNode, allow it. + // - If it is a UFlowGraphNode, require it to allow breakpoints. + const UEdGraphNode* EdNode = Pin->GetOwningNode(); + if (!EdNode) + { + return false; + } + + const UFlowGraphNode* FlowNode = Cast(EdNode); + if (FlowNode && !FlowNode->CanPlaceBreakpoints()) + { + return false; + } + + OutNodeGuid = EdNode->NodeGuid; + OutPinName = Pin->PinName; + return true; +} + +const FFlowBreakpoint* SFlowGraphEditor::FindPinBreakpoint(UFlowDebuggerSubsystem* InDebuggerSubsystem, const UEdGraphPin* Pin) +{ + if (!InDebuggerSubsystem) + { + return nullptr; + } + + FGuid NodeGuid; + FName PinName; + if (!GetValidExecBreakpointPinContext(Pin, NodeGuid, PinName)) + { + return nullptr; + } + + return InDebuggerSubsystem->FindBreakpoint(NodeGuid, PinName); +} + +bool SFlowGraphEditor::HasPinBreakpoint(UFlowDebuggerSubsystem* InDebuggerSubsystem, const UEdGraphPin* Pin) +{ + return FindPinBreakpoint(InDebuggerSubsystem, Pin) != nullptr; +} + +bool SFlowGraphEditor::HasEnabledPinBreakpoint(UFlowDebuggerSubsystem* InDebuggerSubsystem, const UEdGraphPin* Pin) +{ + if (const FFlowBreakpoint* BP = FindPinBreakpoint(InDebuggerSubsystem, Pin)) + { + return BP->IsEnabled(); + } + + return false; +} + +void SFlowGraphEditor::BindGraphCommands() +{ + FGraphEditorCommands::Register(); + FFlowGraphCommands::Register(); + FFlowSpawnNodeCommands::Register(); + + const FGenericCommands& GenericCommands = FGenericCommands::Get(); + const FGraphEditorCommandsImpl& GraphEditorCommands = FGraphEditorCommands::Get(); + const FFlowGraphCommands& FlowGraphCommands = FFlowGraphCommands::Get(); + + CommandList = MakeShareable(new FUICommandList); + + // Graph commands + CommandList->MapAction(GraphEditorCommands.CreateComment, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnCreateComment), + FCanExecuteAction::CreateStatic(&SFlowGraphEditor::CanEdit)); + + CommandList->MapAction(GraphEditorCommands.StraightenConnections, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnStraightenConnections)); + + CommandList->MapAction(GraphEditorCommands.DeleteAndReconnectNodes, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::DeleteSelectedNodes), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanDeleteNodes)); + + // Generic Node commands + CommandList->MapAction(GenericCommands.Undo, + FExecuteAction::CreateStatic(&SFlowGraphEditor::UndoGraphAction), + FCanExecuteAction::CreateStatic(&SFlowGraphEditor::CanEdit)); + + CommandList->MapAction(GenericCommands.Redo, + FExecuteAction::CreateStatic(&SFlowGraphEditor::RedoGraphAction), + FCanExecuteAction::CreateStatic(&SFlowGraphEditor::CanEdit)); + + CommandList->MapAction(GenericCommands.SelectAll, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::SelectAllNodes), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanSelectAllNodes)); + + CommandList->MapAction(GenericCommands.Delete, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::DeleteSelectedNodes), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanDeleteNodes)); + + CommandList->MapAction(GenericCommands.Copy, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::CopySelectedNodes), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanCopyNodes)); + + CommandList->MapAction(GenericCommands.Cut, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::CutSelectedNodes), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanCutNodes)); + + CommandList->MapAction(GenericCommands.Paste, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::PasteNodes), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanPasteNodes)); + + CommandList->MapAction(GenericCommands.Duplicate, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::DuplicateNodes), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanDuplicateNodes)); + + // Pin commands + CommandList->MapAction(FlowGraphCommands.ReconstructNode, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::ReconstructNode), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanReconstructNode)); + + CommandList->MapAction(FlowGraphCommands.AddInput, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::AddInput), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanAddInput)); + + CommandList->MapAction(FlowGraphCommands.AddOutput, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::AddOutput), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanAddOutput)); + + CommandList->MapAction(FlowGraphCommands.RemovePin, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::RemovePin), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanRemovePin)); + + // Breakpoint commands + CommandList->MapAction(GraphEditorCommands.AddBreakpoint, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnAddBreakpoint), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanAddBreakpoint), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanAddBreakpoint) + ); + + CommandList->MapAction(GraphEditorCommands.RemoveBreakpoint, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnRemoveBreakpoint), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanRemoveBreakpoint), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanRemoveBreakpoint) + ); + + CommandList->MapAction(GraphEditorCommands.EnableBreakpoint, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnEnableBreakpoint), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanEnableBreakpoint), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanEnableBreakpoint) + ); + + CommandList->MapAction(GraphEditorCommands.DisableBreakpoint, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnDisableBreakpoint), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanDisableBreakpoint), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanDisableBreakpoint) + ); + + CommandList->MapAction(GraphEditorCommands.ToggleBreakpoint, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnToggleBreakpoint), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanToggleBreakpoint), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanToggleBreakpoint) + ); + + // Pin Breakpoint commands + CommandList->MapAction(FlowGraphCommands.AddPinBreakpoint, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnAddPinBreakpoint), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanAddPinBreakpoint), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanAddPinBreakpoint) + ); + + CommandList->MapAction(FlowGraphCommands.RemovePinBreakpoint, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnRemovePinBreakpoint), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanRemovePinBreakpoint), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanRemovePinBreakpoint) + ); + + CommandList->MapAction(FlowGraphCommands.EnablePinBreakpoint, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnEnablePinBreakpoint), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanEnablePinBreakpoint), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanEnablePinBreakpoint) + ); + + CommandList->MapAction(FlowGraphCommands.DisablePinBreakpoint, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnDisablePinBreakpoint), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanDisablePinBreakpoint), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanDisablePinBreakpoint) + ); + + CommandList->MapAction(FlowGraphCommands.TogglePinBreakpoint, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnTogglePinBreakpoint), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanTogglePinBreakpoint), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanTogglePinBreakpoint) + ); + + CommandList->MapAction(FlowGraphCommands.EnableAllBreakpoints, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::EnableAllBreakpoints), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::HasAnyDisabledBreakpoints)); + + CommandList->MapAction(FlowGraphCommands.DisableAllBreakpoints, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::DisableAllBreakpoints), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::HasAnyEnabledBreakpoints)); + + CommandList->MapAction(FlowGraphCommands.RemoveAllBreakpoints, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::RemoveAllBreakpoints), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::HasAnyBreakpoints)); + + // Execution Override commands + CommandList->MapAction(FlowGraphCommands.EnableNode, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::SetSignalMode, EFlowSignalMode::Enabled), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanSetSignalMode, EFlowSignalMode::Enabled), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanSetSignalMode, EFlowSignalMode::Enabled) + ); + + CommandList->MapAction(FlowGraphCommands.DisableNode, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::SetSignalMode, EFlowSignalMode::Disabled), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanSetSignalMode, EFlowSignalMode::Disabled), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanSetSignalMode, EFlowSignalMode::Disabled) + ); + + CommandList->MapAction(FlowGraphCommands.SetPassThrough, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::SetSignalMode, EFlowSignalMode::PassThrough), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanSetSignalMode, EFlowSignalMode::PassThrough), + FIsActionChecked(), + FIsActionButtonVisible::CreateSP(this, &SFlowGraphEditor::CanSetSignalMode, EFlowSignalMode::PassThrough) + ); + + CommandList->MapAction(FlowGraphCommands.ForcePinActivation, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnForcePinActivation), + FCanExecuteAction::CreateStatic(&SFlowGraphEditor::IsPIE), + FIsActionChecked(), + FIsActionButtonVisible::CreateStatic(&SFlowGraphEditor::IsPIE) + ); + + // Jump commands + CommandList->MapAction(FlowGraphCommands.FocusViewport, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::FocusViewport), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanFocusViewport)); + + CommandList->MapAction(FlowGraphCommands.JumpToNodeDefinition, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::JumpToNodeDefinition), + FCanExecuteAction::CreateSP(this, &SFlowGraphEditor::CanJumpToNodeDefinition)); + + // Organisation Commands + CommandList->MapAction(GraphEditorCommands.AlignNodesTop, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnAlignTop)); + + CommandList->MapAction(GraphEditorCommands.AlignNodesMiddle, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnAlignMiddle)); + + CommandList->MapAction(GraphEditorCommands.AlignNodesBottom, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnAlignBottom)); + + CommandList->MapAction(GraphEditorCommands.AlignNodesLeft, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnAlignLeft)); + + CommandList->MapAction(GraphEditorCommands.AlignNodesCenter, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnAlignCenter)); + + CommandList->MapAction(GraphEditorCommands.AlignNodesRight, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnAlignRight)); + + CommandList->MapAction(GraphEditorCommands.StraightenConnections, + FExecuteAction::CreateSP(this, &SFlowGraphEditor::OnStraightenConnections)); +} + +void SFlowGraphEditor::CreateDebugMenu() +{ + const FNewToolMenuDelegate AddDebugSection = FNewToolMenuDelegate::CreateLambda([this](UToolMenu* InMenu) + { + FToolMenuSection& Section = InMenu->AddSection("Breakpoints", LOCTEXT("Breakpoints", "Breakpoints")); + const FFlowGraphCommands& FlowGraphCommands = FFlowGraphCommands::Get(); + + Section.AddMenuEntryWithCommandList(FlowGraphCommands.EnableAllBreakpoints, CommandList); + Section.AddMenuEntryWithCommandList(FlowGraphCommands.DisableAllBreakpoints, CommandList); + Section.AddMenuEntryWithCommandList(FlowGraphCommands.RemoveAllBreakpoints, CommandList); + }); + + FToolMenuSection& MainSection = UToolMenus::Get()->ExtendMenu("AssetEditor.FlowEditor.MainMenu")->FindOrAddSection(NAME_None); + FToolMenuEntry& DebugEntry = MainSection.AddSubMenu( + "Debug", + LOCTEXT("Debug", "Debug"), + LOCTEXT("DebugTooltip", "Open the debug menu"), + AddDebugSection + ); + + DebugEntry.InsertPosition = FToolMenuInsert("Edit", EToolMenuInsertType::After); +} + +FGraphAppearanceInfo SFlowGraphEditor::GetGraphAppearanceInfo() const +{ + FGraphAppearanceInfo AppearanceInfo; + AppearanceInfo.CornerText = GetCornerText(); + AppearanceInfo.PIENotifyText = GetPIENotifyText(); + return AppearanceInfo; +} + +FText SFlowGraphEditor::GetCornerText() const +{ + return LOCTEXT("AppearanceCornerText_FlowAsset", "FLOW"); +} + +FText SFlowGraphEditor::GetPIENotifyText() const +{ + if (const UFlowAsset* InspectedInstance = FlowAsset->GetInspectedInstance()) + { + if (const UWorld* InspectedWorld = InspectedInstance->GetWorld()) + { + return FText::FromString(GetDebugStringForWorld(InspectedWorld)); + } + } + + return LOCTEXT("PIENotifyText_FlowAsset", "Template"); +} + +void SFlowGraphEditor::UndoGraphAction() +{ + GEditor->UndoTransaction(); +} + +void SFlowGraphEditor::RedoGraphAction() +{ + GEditor->RedoTransaction(); +} + +FReply SFlowGraphEditor::OnSpawnGraphNodeByShortcut(FInputChord InChord, const FVector2f& InPosition, UEdGraph* InGraph) +{ + UEdGraph* Graph = InGraph; + + if (FFlowSpawnNodeCommands::IsRegistered()) + { + const TSharedPtr Action = FFlowSpawnNodeCommands::Get().GetActionByChord(InChord); + if (Action.IsValid()) + { + TArray DummyPins; + + Action->PerformAction(Graph, DummyPins, InPosition); + return FReply::Handled(); + } + } + + return FReply::Unhandled(); +} + +void SFlowGraphEditor::OnCreateComment() const +{ + FFlowGraphSchemaAction_NewComment CommentAction; + CommentAction.PerformAction(FlowAsset->GetGraph(), nullptr, GetPasteLocation2f()); +} + +bool SFlowGraphEditor::IsTabFocused() const +{ + return FlowAssetEditor.Pin()->IsTabFocused(FFlowAssetEditor::GraphTab); +} + +bool SFlowGraphEditor::CanEdit() +{ + return GEditor->PlayWorld == nullptr; +} + +bool SFlowGraphEditor::IsPIE() +{ + return GEditor->PlayWorld != nullptr; +} + +bool SFlowGraphEditor::IsPlaySessionPaused() +{ + bool bPaused = true; + + for (const FWorldContext& PieContext : GUnrealEd->GetWorldContexts()) + { + const UWorld* PlayWorld = PieContext.World(); + if (PlayWorld && PlayWorld->IsGameWorld()) + { + bPaused = bPaused && PlayWorld->bDebugPauseExecution; + } + } + + return bPaused; +} + +void SFlowGraphEditor::SelectSingleNode(UEdGraphNode* Node) +{ + ClearSelectionSet(); + SetNodeSelection(Node, true); +} + +void SFlowGraphEditor::OnSelectedNodesChanged(const TSet& Nodes) +{ + TArray SelectedObjects; + + if (Nodes.Num() > 0) + { + FlowAssetEditor.Pin()->SetUISelectionState(FFlowAssetEditor::GraphTab); + + for (TSet::TConstIterator SetIt(Nodes); SetIt; ++SetIt) + { + if (const UFlowGraphNode* GraphNode = Cast(*SetIt)) + { + SelectedObjects.Add(GraphNode->GetFlowNodeBase()); + } + else + { + SelectedObjects.Add(*SetIt); + } + } + } + else + { + FlowAssetEditor.Pin()->SetUISelectionState(NAME_None); + SelectedObjects.Add(FlowAsset.Get()); + } + + if (DetailsView.IsValid()) + { + DetailsView->SetObjects(SelectedObjects); + } + + OnSelectionChangedEvent.ExecuteIfBound(Nodes); +} + +TSet SFlowGraphEditor::GetSelectedFlowNodes() const +{ + TSet Result; + + const FGraphPanelSelectionSet SelectedNodes = GetSelectedNodes(); + for (FGraphPanelSelectionSet::TConstIterator NodeIt(SelectedNodes); NodeIt; ++NodeIt) + { + if (UFlowGraphNode* SelectedNode = Cast(*NodeIt)) + { + Result.Emplace(SelectedNode); + } + } + + return Result; +} + +void SFlowGraphEditor::ReconnectExecPins(const UFlowGraphNode* Node) +{ + if (Node == nullptr) + { + return; + } + + UEdGraphPin* InputPin = nullptr; + UEdGraphPin* OutputPin = nullptr; + + for (UEdGraphPin* Pin : Node->InputPins) + { + if (Pin->HasAnyConnections()) + { + if (InputPin) + { + // more than one connected input pins - do not reconnect anything + return; + } + + if (Pin) + { + InputPin = Pin; + } + } + else if (InputPin == nullptr) + { + // first pin doesn't have any connections - do not reconnect anything, because we probably don't know expected result for user + return; + } + } + + for (UEdGraphPin* Pin : Node->OutputPins) + { + if (Pin->HasAnyConnections()) + { + if (OutputPin) + { + // more than one connected output pins - do not reconnect anything + return; + } + + if (Pin) + { + OutputPin = Pin; + } + } + else if (OutputPin == nullptr) + { + // first pin doesn't have any connections - do not reconnect anything, because we probably don't know expected result for user + return; + } + } + + if (InputPin && OutputPin) + { + // Make a connection from every incoming exec pin to every outgoing then pin + for (UEdGraphPin* const IncomingConnectionPin : InputPin->LinkedTo) + { + if (IncomingConnectionPin) + { + for (UEdGraphPin* const ConnectedCompletePin : OutputPin->LinkedTo) + { + IncomingConnectionPin->MakeLinkTo(ConnectedCompletePin); + } + } + } + } +} + +void SFlowGraphEditor::DeleteSelectedNodes() +{ + const FScopedTransaction Transaction(LOCTEXT("DeleteSelectedNode", "Delete Selected Node")); + GetCurrentGraph()->Modify(); + FlowAsset->Modify(); + + const FGraphPanelSelectionSet SelectedNodes = GetSelectedNodes(); + FlowAssetEditor.Pin()->SetUISelectionState(NAME_None); + + for (FGraphPanelSelectionSet::TConstIterator NodeIt(SelectedNodes); NodeIt; ++NodeIt) + { + UEdGraphNode* Node = CastChecked(*NodeIt); + if (Node->CanUserDeleteNode()) + { + if (DebuggerSubsystem.IsValid()) + { + DebuggerSubsystem->RemoveAllBreakpoints(Node->NodeGuid); + } + + if (const UFlowGraphNode* FlowGraphNode = Cast(Node)) + { + if (const UFlowNode* FlowNode = Cast(FlowGraphNode->GetFlowNodeBase())) + { + // If the user is pressing shift then try and reconnect the pins + if (FSlateApplication::Get().GetModifierKeys().IsShiftDown()) + { + ReconnectExecPins(FlowGraphNode); + } + + GetCurrentGraph()->GetSchema()->BreakNodeLinks(*Node); + Node->DestroyNode(); + + FlowAsset->UnregisterNode(FlowNode->GetGuid()); + continue; + } + } + + GetCurrentGraph()->GetSchema()->BreakNodeLinks(*Node); + Node->DestroyNode(); + } + } +} + +void SFlowGraphEditor::DeleteSelectedDuplicableNodes() +{ + // Cache off the old selection + const FGraphPanelSelectionSet OldSelectedNodes = GetSelectedNodes(); + + // Clear the selection and only select the nodes that can be duplicated + FGraphPanelSelectionSet RemainingNodes; + ClearSelectionSet(); + + for (FGraphPanelSelectionSet::TConstIterator SelectedIt(OldSelectedNodes); SelectedIt; ++SelectedIt) + { + if (UEdGraphNode* Node = Cast(*SelectedIt)) + { + if (Node->CanDuplicateNode()) + { + SetNodeSelection(Node, true); + } + else + { + RemainingNodes.Add(Node); + } + } + } + + // Delete the duplicable nodes + DeleteSelectedNodes(); + + for (FGraphPanelSelectionSet::TConstIterator SelectedIt(RemainingNodes); SelectedIt; ++SelectedIt) + { + if (UEdGraphNode* Node = Cast(*SelectedIt)) + { + SetNodeSelection(Node, true); + } + } +} + +bool SFlowGraphEditor::CanDeleteNodes() const +{ + if (CanEdit() && IsTabFocused()) + { + const FGraphPanelSelectionSet SelectedNodes = GetSelectedNodes(); + for (FGraphPanelSelectionSet::TConstIterator NodeIt(SelectedNodes); NodeIt; ++NodeIt) + { + if (const UEdGraphNode* Node = Cast(*NodeIt)) + { + if (Node->CanUserDeleteNode()) + { + return true; + } + } + } + } + + return false; +} + +void SFlowGraphEditor::CutSelectedNodes() +{ + CopySelectedNodes(); + + // Cut should only delete nodes that can be duplicated + DeleteSelectedDuplicableNodes(); +} + +bool SFlowGraphEditor::CanCutNodes() const +{ + return CanCopyNodes() && CanDeleteNodes(); +} + +void SFlowGraphEditor::CopySelectedNodes() const +{ + // Export the selected nodes and place the text on the clipboard + FGraphPanelSelectionSet SelectedNodes = GetSelectedNodes(); + FGraphPanelSelectionSet NewSelectedNodes; + + for (FGraphPanelSelectionSet::TIterator SelectedIter(SelectedNodes); SelectedIter; ++SelectedIter) + { + if (UFlowGraphNode* FlowGraphNode = Cast(*SelectedIter)) + { + constexpr int32 RootEdNodeParentIndex = INDEX_NONE; + PrepareFlowGraphNodeForCopy(*FlowGraphNode, RootEdNodeParentIndex, NewSelectedNodes); + } + else + { + NewSelectedNodes.Add(*SelectedIter); + } + } + + FString ExportedText; + + FEdGraphUtilities::ExportNodesToText(NewSelectedNodes, ExportedText); + FPlatformApplicationMisc::ClipboardCopy(*ExportedText); + + for (FGraphPanelSelectionSet::TIterator SelectedIter(NewSelectedNodes); SelectedIter; ++SelectedIter) + { + if (UFlowGraphNode* FlowGraphNode = Cast(*SelectedIter)) + { + FlowGraphNode->PostCopyNode(); + } + } +} + +void SFlowGraphEditor::PrepareFlowGraphNodeForCopy(UFlowGraphNode& FlowGraphNode, const int32 ParentEdNodeIndex, FGraphPanelSelectionSet& NewSelectedNodes) +{ + const int32 ThisFlowGraphNodeIndex = NewSelectedNodes.Num(); + bool bAlreadyInSet = false; + NewSelectedNodes.Add(&FlowGraphNode, &bAlreadyInSet); + + if (bAlreadyInSet) + { + return; + } + + FlowGraphNode.PrepareForCopying(); + FlowGraphNode.CopySubNodeParentIndex = ParentEdNodeIndex; + FlowGraphNode.CopySubNodeIndex = ThisFlowGraphNodeIndex; + + // append all subnodes for selection + for (UFlowGraphNode* SubNode : FlowGraphNode.SubNodes) + { + if (SubNode) + { + PrepareFlowGraphNodeForCopy(*SubNode, ThisFlowGraphNodeIndex, NewSelectedNodes); + } + } +} + +bool SFlowGraphEditor::CanCopyNodes() const +{ + if (CanEdit() && IsTabFocused()) + { + const FGraphPanelSelectionSet SelectedNodes = GetSelectedNodes(); + for (FGraphPanelSelectionSet::TConstIterator SelectedIt(SelectedNodes); SelectedIt; ++SelectedIt) + { + const UEdGraphNode* Node = Cast(*SelectedIt); + if (Node && Node->CanDuplicateNode()) + { + return true; + } + } + } + + return false; +} + +void SFlowGraphEditor::PasteNodes() +{ + PasteNodesHere(GetPasteLocation2f()); +} + +void SFlowGraphEditor::PasteNodesHere(const FVector2D& Location) +{ + // Undo/Redo support + const FScopedTransaction Transaction(LOCTEXT("PasteNode", "Paste Node")); + UFlowGraph* FlowGraph = CastChecked(FlowAsset->GetGraph()); + FlowGraph->Modify(); + FlowAsset->Modify(); + + FlowGraph->LockUpdates(); + + const TArray PasteTargetNodes = DerivePasteTargetNodesFromSelectedNodes(); + if (Algo::AnyOf(PasteTargetNodes, [](const UFlowGraphNode* Node) { return Node && !Node->SubNodes.IsEmpty(); })) + { + checkf(PasteTargetNodes.Num() <= 1, TEXT("This should be enforced in CanPasteNodes()")); + } + + UFlowGraphNode* PasteTargetNode = !PasteTargetNodes.IsEmpty() ? PasteTargetNodes.Top() : nullptr; + + FString TextToImport; + const TSet NodesToPaste = ImportNodesToPasteFromClipboard(*FlowGraph, TextToImport); + + // Clear the selection set (newly pasted stuff will be selected) + ClearSelectionSet(); + FlowAssetEditor.Pin()->SetUISelectionState(NAME_None); + + //Average position of nodes so we can move them while still maintaining relative distances to each other + FVector2D AvgNodePosition(0.0f, 0.0f); + + // Number of nodes used to calculate AvgNodePosition + int32 AvgCount = 0; + + for (TSet::TConstIterator It(NodesToPaste); It; ++It) + { + UEdGraphNode* EdNode = *It; + UFlowGraphNode* FlowGraphNode = Cast(EdNode); + if (EdNode && (FlowGraphNode == nullptr || !FlowGraphNode->IsSubNode())) + { + AvgNodePosition.X += EdNode->NodePosX; + AvgNodePosition.Y += EdNode->NodePosY; + ++AvgCount; + } + } + + if (AvgCount > 0) + { + float InvNumNodes = 1.0f / static_cast(AvgCount); + AvgNodePosition.X *= InvNumNodes; + AvgNodePosition.Y *= InvNumNodes; + } + + TMap EdNodeCopyIndexMap; + for (TSet::TConstIterator It(NodesToPaste); It; ++It) + { + UEdGraphNode* PastedNode = *It; + + UFlowGraphNode* PastedFlowGraphNode = Cast(PastedNode); + if (PastedFlowGraphNode) + { + EdNodeCopyIndexMap.Add(PastedFlowGraphNode->CopySubNodeIndex, PastedFlowGraphNode); + } + + if (PastedNode && (PastedFlowGraphNode == nullptr || !PastedFlowGraphNode->IsSubNode())) + { + // Select the newly pasted stuff + constexpr bool bSelectNodes = true; + SetNodeSelection(PastedNode, bSelectNodes); + + PastedNode->NodePosX = (PastedNode->NodePosX - AvgNodePosition.X) + Location.X; + PastedNode->NodePosY = (PastedNode->NodePosY - AvgNodePosition.Y) + Location.Y; + + PastedNode->SnapToGrid(16); + + // Give new node a different Guid from the old one + PastedNode->CreateNewGuid(); + } + + if (PastedFlowGraphNode) + { + if (UFlowNode* FlowNode = Cast(PastedFlowGraphNode->GetFlowNodeBase())) + { + // Only full FlowNodes are registered with the Asset + // (for now? perhaps we register AddOns in the future?) + FlowAsset->RegisterNode(PastedNode->NodeGuid, FlowNode); + } + + PastedFlowGraphNode->RemoveAllSubNodes(); + } + } + + for (TSet::TConstIterator It(NodesToPaste); It; ++It) + { + UFlowGraphNode* PasteNode = Cast(*It); + if (PasteNode && PasteNode->IsSubNode()) + { + PasteNode->NodePosX = 0; + PasteNode->NodePosY = 0; + + // remove subnode from graph, it will be referenced from parent node + PasteNode->DestroyNode(); + + if (PasteNode->CopySubNodeParentIndex == INDEX_NONE) + { + // INDEX_NONE parent index indicates we should set the parent to the PasteTargetNode + if (PasteTargetNode) + { + PasteTargetNode->AddSubNode(PasteNode, FlowGraph); + } + } + else if (UFlowGraphNode* PastedParentNode = EdNodeCopyIndexMap.FindRef(PasteNode->CopySubNodeParentIndex)) + { + PastedParentNode->AddSubNode(PasteNode, FlowGraph); + } + } + } + + if (FlowGraph) + { + FlowGraph->UpdateClassData(); + FlowGraph->OnNodesPasted(TextToImport); + FlowGraph->UnlockUpdates(); + } + + // Update UI + NotifyGraphChanged(); + + if (UObject* GraphOwner = FlowGraph->GetOuter()) + { + GraphOwner->PostEditChange(); + GraphOwner->MarkPackageDirty(); + } +} + +TSet SFlowGraphEditor::ImportNodesToPasteFromClipboard(UFlowGraph& FlowGraph, FString& OutTextToImport) +{ + // Grab the text to paste from the clipboard. + FPlatformApplicationMisc::ClipboardPaste(OutTextToImport); + + // Import the nodes + TSet NodesToPaste; + FEdGraphUtilities::ImportNodesFromText(&FlowGraph, OutTextToImport, /*out*/ NodesToPaste); + + return NodesToPaste; +} + +TArray SFlowGraphEditor::DerivePasteTargetNodesFromSelectedNodes() const +{ + TArray PasteTargetNodes; + const FGraphPanelSelectionSet SelectedNodes = GetSelectedNodes(); + for (FGraphPanelSelectionSet::TConstIterator SelectedIter(SelectedNodes); SelectedIter; ++SelectedIter) + { + UFlowGraphNode* Node = Cast(*SelectedIter); + if (IsValid(Node)) + { + PasteTargetNodes.Add(Node); + } + } + + return PasteTargetNodes; +} + +bool SFlowGraphEditor::CanPasteNodes() const +{ + if (!CanEdit() || !IsTabFocused()) + { + return false; + } + + FString ClipboardContent; + FPlatformApplicationMisc::ClipboardPaste(ClipboardContent); + + UFlowGraph* FlowGraph = CastChecked(FlowAsset->GetGraph()); + if (!ensure(IsValid(FlowGraph))) + { + // We expect to have a legal FlowGraph pointer at this point + return false; + } + + const bool bIsPastePossible = FEdGraphUtilities::CanImportNodesFromText(FlowGraph, ClipboardContent); + if (!bIsPastePossible) + { + return false; + } + + // Disallow paste when multiple target nodes are selected, and if there are subnodes involved. + const TArray PasteTargetNodes = DerivePasteTargetNodesFromSelectedNodes(); + const bool bHasSubNodes = Algo::AnyOf(PasteTargetNodes, [](const UFlowGraphNode* Node) { return Node && !Node->SubNodes.IsEmpty(); }); + + if (bHasSubNodes && PasteTargetNodes.Num() > 1) + { + // NOTE (gtaylor) It's possible we could support multi-paste, but we'd need to rework PasteNodesHere() + // to understand how to paste copies onto each target node. + return false; + } + + FString TextToImport; + const TSet NodesToPaste = ImportNodesToPasteFromClipboard(*FlowGraph, TextToImport); + + if (NodesToPaste.IsEmpty()) + { + // Must have at least one node to paste + return false; + } + + ON_SCOPE_EXIT + { + // We need to clean up the nodes we built to test the paste operation + for (TSet::TConstIterator It(NodesToPaste); It; ++It) + { + UEdGraphNode* NodeToPaste = *It; + if (IsValid(NodeToPaste)) + { + NodeToPaste->ClearFlags(RF_Public); + NodeToPaste->SetFlags(RF_Transient); + + const FString NewNameStr = MakeUniqueObjectName(NodeToPaste->GetOuter(), NodeToPaste->GetClass()).ToString(); + + // This will remove the node from its graph + NodeToPaste->DestroyNode(); + + // Rename and garbage the node so that it can't be found by name if the same clipboard is re-pasted +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 8 + NodeToPaste->Rename(*NewNameStr, nullptr, REN_NonTransactional | REN_DontCreateRedirectors | REN_ForceNoResetLoaders); +#else + // from compilation warning + // "Rename will no longer call ResetLoaders making this flag no longer needed. + // Prefer REN_AllowPackageLinkerMismatch if you wish to intentionally allow the linker to contain references to objects whose names no longer match what was loaded from disk." + NodeToPaste->Rename(*NewNameStr, nullptr, REN_NonTransactional | REN_DontCreateRedirectors); +#endif + + NodeToPaste->MarkAsGarbage(); + } + } + }; + + // If pasting onto a selected node, confirm that the paste operation is legal + if (bHasSubNodes && PasteTargetNodes.Num() >= 1) + { + checkf(PasteTargetNodes.Num() == 1, TEXT("This is enforced earlier in this function, just confirming the code stays that way here.")); + + const UFlowGraphNode* PasteTargetNode = PasteTargetNodes.Top(); + if (!CanPasteNodesAsSubNodes(NodesToPaste, *PasteTargetNode)) + { + return false; + } + } + + return true; +} + +bool SFlowGraphEditor::CanPasteNodesAsSubNodes(const TSet& NodesToPaste, const UFlowGraphNode& PasteTargetNode) +{ + TSet AllRootSubNodesToPaste; + for (TSet::TConstIterator It(NodesToPaste); It; ++It) + { + const UFlowGraphNode* NodeToPaste = Cast(*It); + if (!ensure(IsValid(NodeToPaste))) + { + return false; + } + + if (!NodeToPaste->IsSubNode()) + { + // Only SubNodes can be pasted onto other nodes + + return false; + } + + // Only concerned with the 'root' subnodes + // (we assume the rest of the subnode tree is valid when put into the copy buffer) + if (NodeToPaste->CopySubNodeParentIndex != INDEX_NONE) + { + // a non-INDEX_NONE parent index indicates the subnode is a non-root subnode in the NodesToPaste set + + continue; + } + + AllRootSubNodesToPaste.Add(NodeToPaste); + } + + for (TSet::TConstIterator It(AllRootSubNodesToPaste); It; ++It) + { + const UFlowGraphNode* NodeToPaste = Cast(*It); + + if (!PasteTargetNode.CanAcceptSubNodeAsChild(*NodeToPaste, AllRootSubNodesToPaste)) + { + // This node cannot accept the SubNode as a child + + return false; + } + } + + return true; +} + +void SFlowGraphEditor::DuplicateNodes() +{ + CopySelectedNodes(); + PasteNodes(); +} + +bool SFlowGraphEditor::CanDuplicateNodes() const +{ + return CanCopyNodes(); +} + +void SFlowGraphEditor::OnNodeDoubleClicked(class UEdGraphNode* Node) const +{ + if (const UFlowGraphNode* FlowGraphNode = Cast(Node)) + { + FlowGraphNode->OnNodeDoubleClicked(); + } +} + +void SFlowGraphEditor::OnNodeTitleCommitted(const FText& NewText, ETextCommit::Type CommitInfo, UEdGraphNode* NodeBeingChanged) +{ + if (NodeBeingChanged) + { + const FScopedTransaction Transaction(LOCTEXT("RenameNode", "Rename Node")); + NodeBeingChanged->Modify(); + NodeBeingChanged->OnRenameNode(NewText.ToString()); + } +} + +void SFlowGraphEditor::ReconstructNode() const +{ + for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + SelectedNode->ReconstructNode(); + } +} + +bool SFlowGraphEditor::CanReconstructNode() const +{ + if (CanEdit() && GetSelectedFlowNodes().Num() == 1) + { + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + return SelectedNode->SupportsContextPins(); + } + } + + return false; +} + +void SFlowGraphEditor::AddInput() const +{ + for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + SelectedNode->AddUserInput(); + } +} + +bool SFlowGraphEditor::CanAddInput() const +{ + if (CanEdit() && GetSelectedFlowNodes().Num() == 1) + { + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + return SelectedNode->CanUserAddInput(); + } + } + + return false; +} + +void SFlowGraphEditor::AddOutput() const +{ + for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + SelectedNode->AddUserOutput(); + } +} + +bool SFlowGraphEditor::CanAddOutput() const +{ + if (CanEdit() && GetSelectedFlowNodes().Num() == 1) + { + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + return SelectedNode->CanUserAddOutput(); + } + } + + return false; +} + +void SFlowGraphEditor::RemovePin() +{ + if (UEdGraphPin* SelectedPin = GetGraphPinForMenu()) + { + if (UFlowGraphNode* SelectedNode = Cast(SelectedPin->GetOwningNode())) + { + SelectedNode->RemoveInstancePin(SelectedPin); + } + } +} + +bool SFlowGraphEditor::CanRemovePin() +{ + if (CanEdit() && GetSelectedFlowNodes().Num() == 1) + { + if (const UEdGraphPin* Pin = GetGraphPinForMenu()) + { + if (const UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) + { + if (Pin->Direction == EGPD_Input) + { + return GraphNode->CanUserRemoveInput(Pin); + } + else + { + return GraphNode->CanUserRemoveOutput(Pin); + } + } + } + } + + return false; +} + +void SFlowGraphEditor::OnAddBreakpoint() const +{ + check(DebuggerSubsystem.IsValid()); + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + if (SelectedNode->CanPlaceBreakpoints()) + { + DebuggerSubsystem->AddBreakpoint(SelectedNode->NodeGuid); + } + } +} + +void SFlowGraphEditor::OnAddPinBreakpoint() +{ + check(DebuggerSubsystem.IsValid()); + if (const UEdGraphPin* Pin = GetGraphPinForMenu()) + { + FGuid NodeGuid; + FName PinName; + if (!GetValidExecBreakpointPinContext(Pin, NodeGuid, PinName)) + { + return; + } + + DebuggerSubsystem->AddBreakpoint(NodeGuid, PinName); + } +} + +bool SFlowGraphEditor::CanAddBreakpoint() const +{ + check(DebuggerSubsystem.IsValid()); + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + if (SelectedNode->CanPlaceBreakpoints()) + { + if (DebuggerSubsystem->FindBreakpoint(SelectedNode->NodeGuid) == nullptr) + { + return true; + } + } + } + + return false; +} + +bool SFlowGraphEditor::CanAddPinBreakpoint() +{ + check(DebuggerSubsystem.IsValid()); + if (const UEdGraphPin* Pin = GetGraphPinForMenu()) + { + FGuid NodeGuid; + FName PinName; + if (!GetValidExecBreakpointPinContext(Pin, NodeGuid, PinName)) + { + return false; + } + + return DebuggerSubsystem->FindBreakpoint(NodeGuid, PinName) == nullptr; + } + + return false; +} + +void SFlowGraphEditor::OnRemoveBreakpoint() const +{ + check(DebuggerSubsystem.IsValid()); + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + if (SelectedNode->CanPlaceBreakpoints()) + { + DebuggerSubsystem->RemoveNodeBreakpoint(SelectedNode->NodeGuid); + } + } +} + +void SFlowGraphEditor::OnRemovePinBreakpoint() +{ + check(DebuggerSubsystem.IsValid()); + if (const UEdGraphPin* Pin = GetGraphPinForMenu()) + { + FGuid NodeGuid; + FName PinName; + if (!GetValidExecBreakpointPinContext(Pin, NodeGuid, PinName)) + { + return; + } + + DebuggerSubsystem->RemovePinBreakpoint(NodeGuid, PinName); + } +} + +bool SFlowGraphEditor::CanRemoveBreakpoint() const +{ + check(DebuggerSubsystem.IsValid()); + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + if (SelectedNode->CanPlaceBreakpoints()) + { + if (DebuggerSubsystem->FindBreakpoint(SelectedNode->NodeGuid) != nullptr) + { + return true; + } + } + } + + return false; +} + +bool SFlowGraphEditor::CanRemovePinBreakpoint() +{ + check(DebuggerSubsystem.IsValid()); + return HasPinBreakpoint(DebuggerSubsystem.Get(), GetGraphPinForMenu()); +} + +void SFlowGraphEditor::OnEnableBreakpoint() const +{ + check(DebuggerSubsystem.IsValid()); + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + if (SelectedNode->CanPlaceBreakpoints()) + { + DebuggerSubsystem->SetBreakpointEnabled(SelectedNode->NodeGuid, true); + } + } +} + +void SFlowGraphEditor::OnEnablePinBreakpoint() +{ + check(DebuggerSubsystem.IsValid()); + if (const UEdGraphPin* Pin = GetGraphPinForMenu()) + { + FGuid NodeGuid; + FName PinName; + if (!GetValidExecBreakpointPinContext(Pin, NodeGuid, PinName)) + { + return; + } + + DebuggerSubsystem->SetBreakpointEnabled(NodeGuid, PinName, true); + } +} + +bool SFlowGraphEditor::CanEnableBreakpoint() const +{ + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + if (SelectedNode->CanPlaceBreakpoints()) + { + const FFlowBreakpoint* Breakpoint = DebuggerSubsystem->FindBreakpoint(SelectedNode->NodeGuid); + if (Breakpoint && !Breakpoint->IsEnabled()) + { + return true; + } + } + } + + return false; +} + +bool SFlowGraphEditor::CanEnablePinBreakpoint() +{ + return HasPinBreakpoint(DebuggerSubsystem.Get(), GetGraphPinForMenu()) + && !HasEnabledPinBreakpoint(DebuggerSubsystem.Get(), GetGraphPinForMenu()); +} + +void SFlowGraphEditor::OnDisableBreakpoint() const +{ + check(DebuggerSubsystem.IsValid()); + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + if (SelectedNode->CanPlaceBreakpoints()) + { + DebuggerSubsystem->SetBreakpointEnabled(SelectedNode->NodeGuid, false); + } + } +} + +void SFlowGraphEditor::OnDisablePinBreakpoint() +{ + check(DebuggerSubsystem.IsValid()); + if (const UEdGraphPin* Pin = GetGraphPinForMenu()) + { + FGuid NodeGuid; + FName PinName; + if (!GetValidExecBreakpointPinContext(Pin, NodeGuid, PinName)) + { + return; + } + + DebuggerSubsystem->SetBreakpointEnabled(NodeGuid, PinName, false); + } +} + +bool SFlowGraphEditor::CanDisableBreakpoint() const +{ + check(DebuggerSubsystem.IsValid()); + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + if (SelectedNode->CanPlaceBreakpoints()) + { + const FFlowBreakpoint* Breakpoint = DebuggerSubsystem->FindBreakpoint(SelectedNode->NodeGuid); + if (Breakpoint && Breakpoint->IsEnabled()) + { + return true; + } + } + } + + return false; +} + +bool SFlowGraphEditor::CanDisablePinBreakpoint() +{ + check(DebuggerSubsystem.IsValid()); + return HasEnabledPinBreakpoint(DebuggerSubsystem.Get(), GetGraphPinForMenu()); +} + +void SFlowGraphEditor::OnToggleBreakpoint() const +{ + check(DebuggerSubsystem.IsValid()); + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + if (SelectedNode->CanPlaceBreakpoints()) + { + DebuggerSubsystem->ToggleBreakpoint(SelectedNode->NodeGuid); + } + } +} + +void SFlowGraphEditor::OnTogglePinBreakpoint() +{ + check(DebuggerSubsystem.IsValid()); + if (const UEdGraphPin* Pin = GetGraphPinForMenu()) + { + FGuid NodeGuid; + FName PinName; + if (!GetValidExecBreakpointPinContext(Pin, NodeGuid, PinName)) + { + return; + } + + DebuggerSubsystem->ToggleBreakpoint(NodeGuid, PinName); + } +} + +bool SFlowGraphEditor::CanToggleBreakpoint() const +{ + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + if (SelectedNode->CanPlaceBreakpoints()) + { + return true; + } + } + + return false; +} + +bool SFlowGraphEditor::CanTogglePinBreakpoint() +{ + if (const UEdGraphPin* Pin = GetGraphPinForMenu()) + { + FGuid NodeGuid; + FName PinName; + return GetValidExecBreakpointPinContext(Pin, NodeGuid, PinName); + } + + return false; +} + +void SFlowGraphEditor::EnableAllBreakpoints() const +{ + if (DebuggerSubsystem.IsValid()) + { + DebuggerSubsystem->SetAllBreakpointsEnabled(FlowAsset, true); + } +} + +bool SFlowGraphEditor::HasAnyDisabledBreakpoints() const +{ + return DebuggerSubsystem.IsValid() ? DebuggerSubsystem->HasAnyBreakpointsDisabled(FlowAsset) : false; +} + +void SFlowGraphEditor::DisableAllBreakpoints() const +{ + if (DebuggerSubsystem.IsValid()) + { + DebuggerSubsystem->SetAllBreakpointsEnabled(FlowAsset, false); + } +} + +bool SFlowGraphEditor::HasAnyEnabledBreakpoints() const +{ + return DebuggerSubsystem.IsValid() ? DebuggerSubsystem->HasAnyBreakpointsEnabled(FlowAsset) : false; +} + +void SFlowGraphEditor::RemoveAllBreakpoints() const +{ + if (DebuggerSubsystem.IsValid()) + { + DebuggerSubsystem->RemoveAllBreakpoints(FlowAsset); + } +} + +bool SFlowGraphEditor::HasAnyBreakpoints() const +{ + return DebuggerSubsystem.IsValid() ? DebuggerSubsystem->HasAnyBreakpoints(FlowAsset) : false; +} + +void SFlowGraphEditor::SetSignalMode(const EFlowSignalMode Mode) const +{ + const FScopedTransaction Transaction(LOCTEXT("SetSignalMode", "Set Signal Mode")); + + for (UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + SelectedNode->SetSignalMode(Mode); + } + + FlowAsset->Modify(); +} + +bool SFlowGraphEditor::CanSetSignalMode(const EFlowSignalMode Mode) const +{ + if (IsPIE()) + { + return false; + } + + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + return SelectedNode->CanSetSignalMode(Mode); + } + + return false; +} + +void SFlowGraphEditor::OnForcePinActivation() +{ + if (UEdGraphPin* Pin = GetGraphPinForMenu()) + { + if (const UFlowGraphNode* GraphNode = Cast(Pin->GetOwningNode())) + { + GraphNode->ForcePinActivation(Pin); + } + } +} + +void SFlowGraphEditor::FocusViewport() const +{ + // Iterator used but should only contain one node + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + const UFlowNode* FlowNode = Cast(SelectedNode->GetFlowNodeBase()); + if (UFlowNode* InspectedInstance = FlowNode->GetInspectedInstance()) + { + if (AActor* ActorToFocus = InspectedInstance->GetActorToFocus()) + { + GEditor->SelectNone(false, false, false); + GEditor->SelectActor(ActorToFocus, true, true, true); + GEditor->NoteSelectionChange(); + + GEditor->MoveViewportCamerasToActor(*ActorToFocus, false); + + const FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked("LevelEditor"); + const TSharedPtr LevelEditorTab = LevelEditorModule.GetLevelEditorInstanceTab().Pin(); + if (LevelEditorTab.IsValid()) + { + LevelEditorTab->DrawAttention(); + } + } + } + + return; + } +} + +bool SFlowGraphEditor::CanFocusViewport() const +{ + return GetSelectedFlowNodes().Num() == 1; +} + +void SFlowGraphEditor::JumpToNodeDefinition() const +{ + // Iterator used but should only contain one node + for (const UFlowGraphNode* SelectedNode : GetSelectedFlowNodes()) + { + SelectedNode->JumpToDefinition(); + return; + } +} + +bool SFlowGraphEditor::CanJumpToNodeDefinition() const +{ + return GetSelectedFlowNodes().Num() == 1; +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Graph/FlowGraphEditorSettings.cpp b/Source/FlowEditor/Private/Graph/FlowGraphEditorSettings.cpp index a09ae135b..9c20e9b00 100644 --- a/Source/FlowEditor/Private/Graph/FlowGraphEditorSettings.cpp +++ b/Source/FlowEditor/Private/Graph/FlowGraphEditorSettings.cpp @@ -1,17 +1,32 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #include "Graph/FlowGraphEditorSettings.h" +#include "Graph/FlowGraphSchema.h" -#include "FlowAsset.h" +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowGraphEditorSettings) -UFlowGraphEditorSettings::UFlowGraphEditorSettings(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) - , NodeDoubleClickTarget(EFlowNodeDoubleClickTarget::PrimaryAsset) +UFlowGraphEditorSettings::UFlowGraphEditorSettings() + : NodeDoubleClickTarget(EFlowNodeDoubleClickTarget::PrimaryAssetOrNodeDefinition) , bShowNodeClass(false) + , bShowNodeDescriptionWhilePlaying(true) + , bShowAddonDescriptions(true) + , bEnforceFriendlyPinNames(false) , bShowSubGraphPreview(true) , bShowSubGraphPath(true) , SubGraphPreviewSize(FVector2D(640.f, 360.f)) - , bHighlightInputWiresOfSelectedNodes(true) + , bHighlightInputWiresOfSelectedNodes(false) , bHighlightOutputWiresOfSelectedNodes(false) { } + +#if WITH_EDITOR +void UFlowGraphEditorSettings::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + if (PropertyChangedEvent.GetMemberPropertyName() == GET_MEMBER_NAME_CHECKED(UFlowGraphEditorSettings, bShowNodeClass)) + { + GetDefault()->ForceVisualizationCacheClear(); + } +} +#endif diff --git a/Source/FlowEditor/Private/Graph/FlowGraphNodesPolicy.cpp b/Source/FlowEditor/Private/Graph/FlowGraphNodesPolicy.cpp new file mode 100644 index 000000000..29ea839fe --- /dev/null +++ b/Source/FlowEditor/Private/Graph/FlowGraphNodesPolicy.cpp @@ -0,0 +1,54 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Graph/FlowGraphNodesPolicy.h" +#include "Nodes/FlowNodeBase.h" +#include "Graph/FlowGraphSettings.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowGraphNodesPolicy) + +#if WITH_EDITOR +EFlowGraphPolicyResult FFlowGraphNodesPolicy::IsNodeAllowedByPolicy(const UFlowNodeBase* FlowNodeBase) const +{ + if (!IsValid(FlowNodeBase)) + { + return EFlowGraphPolicyResult::TentativeForbidden; + } + + const FString NodeCategoryString = UFlowGraphSettings::GetNodeCategoryForNode(*FlowNodeBase); + + const bool bIsInAllowedCategory = !AllowedCategories.IsEmpty() && IsAnySubcategory(NodeCategoryString, AllowedCategories); + if (bIsInAllowedCategory) + { + return EFlowGraphPolicyResult::Allowed; + } + + const bool bIsInDisallowedCategory = !DisallowedCategories.IsEmpty() && IsAnySubcategory(NodeCategoryString, DisallowedCategories); + if (bIsInDisallowedCategory) + { + return EFlowGraphPolicyResult::Forbidden; + } + + if (AllowedCategories.IsEmpty()) + { + // If the AllowedCategories is empty, then we consider any node that isn't disallowed, as allowed + return EFlowGraphPolicyResult::TentativeAllowed; + } + else + { + return EFlowGraphPolicyResult::TentativeForbidden; + } +} + +bool FFlowGraphNodesPolicy::IsAnySubcategory(const FString& CheckCategory, const TArray& Categories) +{ + for (const FString& Category : Categories) + { + if (CheckCategory.StartsWith(Category, ESearchCase::IgnoreCase)) + { + return true; + } + } + + return false; +} +#endif \ No newline at end of file diff --git a/Source/FlowEditor/Private/Graph/FlowGraphPinFactory.cpp b/Source/FlowEditor/Private/Graph/FlowGraphPinFactory.cpp new file mode 100644 index 000000000..8072421d5 --- /dev/null +++ b/Source/FlowEditor/Private/Graph/FlowGraphPinFactory.cpp @@ -0,0 +1,79 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Graph/FlowGraphPinFactory.h" +#include "Graph/FlowGraphSchema.h" +#include "Graph/FlowGraphSettings.h" +#include "Graph/Nodes/FlowGraphNode.h" +#include "Graph/Widgets/SFlowGraphNode.h" +#include "Nodes/FlowNode.h" +#include "Nodes/FlowPin.h" + +#include "NodeFactory.h" +#include "SGraphPin.h" + +////////////////////////////////////////////////////////////////////////// +// FFlowGraphPinFactory + +TSharedPtr FFlowGraphPinFactory::CreatePin(UEdGraphPin* InPin) const +{ + if (!InPin->GetSchema()->IsA()) + { + // Limit pin widget creation to FlowGraph schemas + return nullptr; + } + + const UFlowGraphNode* FlowGraphNode = Cast(InPin->GetOwningNode()); + + // Create the widget for a Flow 'Exec'-style pin + if (FlowGraphNode && FFlowPin::IsExecPinCategory(InPin->PinType.PinCategory)) + { + const TSharedPtr NewPinWidget = SNew(SFlowGraphPinExec, InPin); + + const UFlowNode* FlowNode = Cast(FlowGraphNode->GetFlowNodeBase()); + + if (!GetDefault()->bShowDefaultPinNames && IsValid(FlowNode)) + { + if (InPin->Direction == EGPD_Input) + { + // Pin array can have pins with name None, which will not be created. We need to check if array have only one valid pin + if (GatherValidPinsCount(FlowNode->GetInputPins()) == 1 && InPin->PinName == UFlowNode::DefaultInputPin.PinName) + { + NewPinWidget->SetShowLabel(false); + } + } + else + { + // Pin array can have pins with name None, which will not be created. We need to check if array have only one valid pin + if (GatherValidPinsCount(FlowNode->GetOutputPins()) == 1 && InPin->PinName == UFlowNode::DefaultOutputPin.PinName) + { + NewPinWidget->SetShowLabel(false); + } + } + } + + return NewPinWidget; + } + + // For data pins, give the K2 (blueprint) node factory an opportunity to create the widget + TSharedPtr K2PinWidget = FNodeFactory::CreateK2PinWidget(InPin); + if (K2PinWidget.IsValid()) + { + return K2PinWidget; + } + + return nullptr; +} + +int32 FFlowGraphPinFactory::GatherValidPinsCount(const TArray& Pins) +{ + int32 Count = 0; + for (const FFlowPin& Pin : Pins) + { + if (Pin.IsValid()) + { + ++Count; + } + } + + return Count; +} diff --git a/Source/FlowEditor/Private/Graph/FlowGraphSchema.cpp b/Source/FlowEditor/Private/Graph/FlowGraphSchema.cpp index 43aebb508..296d34442 100644 --- a/Source/FlowEditor/Private/Graph/FlowGraphSchema.cpp +++ b/Source/FlowEditor/Private/Graph/FlowGraphSchema.cpp @@ -2,45 +2,211 @@ #include "Graph/FlowGraphSchema.h" -#include "Asset/FlowAssetEditor.h" #include "Graph/FlowGraph.h" +#include "Graph/FlowGraphEditor.h" +#include "Graph/FlowGraphEditorSettings.h" #include "Graph/FlowGraphSchema_Actions.h" #include "Graph/FlowGraphSettings.h" #include "Graph/FlowGraphUtils.h" #include "Graph/Nodes/FlowGraphNode.h" #include "FlowAsset.h" +#include "FlowEditorLogChannels.h" +#include "FlowPinSubsystem.h" +#include "FlowSettings.h" +#include "AddOns/FlowNodeAddOn.h" +#include "Graph/Nodes/FlowGraphNode_Reroute.h" #include "Nodes/FlowNode.h" -#include "Nodes/Route/FlowNode_Start.h" +#include "Nodes/FlowNodeAddOnBlueprint.h" +#include "Nodes/FlowNodeBlueprint.h" +#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 "AssetRegistryModule.h" -#include "Developer/ToolMenus/Public/ToolMenus.h" +#include "AssetRegistry/AssetRegistryModule.h" #include "EdGraph/EdGraph.h" +#include "EdGraphSchema_K2.h" +#include "Editor.h" +#include "Engine/MemberReference.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Runtime/Engine/Internal/Kismet/BlueprintTypeConversions.h" #include "ScopedTransaction.h" -#include "UObject/UObjectIterator.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowGraphSchema) #define LOCTEXT_NAMESPACE "FlowGraphSchema" +bool UFlowGraphSchema::bInitialGatherPerformed = false; TArray UFlowGraphSchema::NativeFlowNodes; +TArray UFlowGraphSchema::NativeFlowNodeAddOns; TMap UFlowGraphSchema::BlueprintFlowNodes; -TMap UFlowGraphSchema::AssignedGraphNodeClasses; +TMap UFlowGraphSchema::BlueprintFlowNodeAddOns; +TMap, TSubclassOf> UFlowGraphSchema::GraphNodesByFlowNodes; bool UFlowGraphSchema::bBlueprintCompilationPending; +int32 UFlowGraphSchema::CurrentCacheRefreshID = 0; FFlowGraphSchemaRefresh UFlowGraphSchema::OnNodeListChanged; +namespace FlowGraphSchema::Private +{ + // Adapted from UE::EdGraphSchemaK2::Private, because it's Private + + template + constexpr bool TAlwaysFalse = false; + + template + UClass* GetAuthoritativeClass(const TProperty& Property) + { + UClass* PropertyClass = nullptr; + if constexpr (std::is_same_v) + { + PropertyClass = Property.PropertyClass; + } + else if constexpr (std::is_same_v) + { + PropertyClass = Property.PropertyClass; + } + else if constexpr (std::is_same_v) + { + PropertyClass = Property.InterfaceClass; + } + else if constexpr (std::is_same_v) + { + PropertyClass = Property.MetaClass; + } + else if constexpr (std::is_same_v) + { + PropertyClass = Property.MetaClass; + } + else + { + static_assert(TAlwaysFalse, "Invalid property used."); + } + + if (PropertyClass && PropertyClass->ClassGeneratedBy) + { + PropertyClass = PropertyClass->GetAuthoritativeClass(); + } + + if (PropertyClass && FKismetEditorUtilities::IsClassABlueprintSkeleton(PropertyClass)) + { + UE_LOG(LogFlowEditor, Warning, TEXT("'%s' is a skeleton class. SubCategoryObject will serialize to a null value."), *PropertyClass->GetFullName()); + } + + return PropertyClass; + } + + static UClass* GetOriginalClassToFixCompatibility(const UClass* InClass) + { + const UBlueprint* BP = InClass ? Cast(InClass->ClassGeneratedBy) : nullptr; + return BP ? BP->OriginalClass : nullptr; + } + + // During compilation, pins are moved around for node expansion and the Blueprints may still inherit from REINST_ classes + // which causes problems for IsChildOf. Because we do not want to modify IsChildOf we must use a separate function + // that can check to see if classes have an AuthoritativeClass that IsChildOf a Target class. + static bool IsAuthoritativeChildOf(const UStruct* InSourceStruct, const UStruct* InTargetStruct) + { + bool bResult = false; + bool bIsNonNativeClass = false; + if (const UClass* TargetAsClass = Cast(InTargetStruct)) + { + InTargetStruct = TargetAsClass->GetAuthoritativeClass(); + } + if (UClass* SourceAsClass = const_cast(Cast(InSourceStruct))) + { + if (SourceAsClass->ClassGeneratedBy) + { + // We have a non-native (Blueprint) class which means it can exist in a semi-compiled state and inherit from a REINST_ class. + bIsNonNativeClass = true; + while (SourceAsClass) + { + if (SourceAsClass->GetAuthoritativeClass() == InTargetStruct) + { + bResult = true; + break; + } + SourceAsClass = SourceAsClass->GetSuperClass(); + } + } + } + + // We have a native (C++) class, do a normal IsChildOf check + if (!bIsNonNativeClass) + { + bResult = InSourceStruct && InSourceStruct->IsChildOf(InTargetStruct); + } + + return bResult; + } + + static bool ExtendedIsChildOf(const UClass* Child, const UClass* Parent) + { + if (Child && Child->IsChildOf(Parent)) + { + return true; + } + + const UClass* OriginalChild = GetOriginalClassToFixCompatibility(Child); + if (OriginalChild && OriginalChild->IsChildOf(Parent)) + { + return true; + } + + const UClass* OriginalParent = GetOriginalClassToFixCompatibility(Parent); + if (OriginalParent && Child && Child->IsChildOf(OriginalParent)) + { + return true; + } + + return false; + } + + static bool ExtendedImplementsInterface(const UClass* Class, const UClass* Interface) + { + if (Class->ImplementsInterface(Interface)) + { + return true; + } + + const UClass* OriginalClass = GetOriginalClassToFixCompatibility(Class); + if (OriginalClass && OriginalClass->ImplementsInterface(Interface)) + { + return true; + } + + const UClass* OriginalInterface = GetOriginalClassToFixCompatibility(Interface); + if (OriginalInterface && Class->ImplementsInterface(OriginalInterface)) + { + return true; + } + + return false; + } +} + UFlowGraphSchema::UFlowGraphSchema(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { + if (HasAnyFlags(RF_ClassDefaultObject)) + { + GetMutableDefault()->OnAdaptiveNodeTitlesChanged.BindLambda([]() + { + GetDefault()->ForceVisualizationCacheClear(); + }); + } } void UFlowGraphSchema::SubscribeToAssetChanges() { const FAssetRegistryModule& AssetRegistry = FModuleManager::LoadModuleChecked(AssetRegistryConstants::ModuleName); - AssetRegistry.Get().OnFilesLoaded().AddStatic(&UFlowGraphSchema::GatherFlowNodes); + AssetRegistry.Get().OnFilesLoaded().AddStatic(&UFlowGraphSchema::GatherNodes); AssetRegistry.Get().OnAssetAdded().AddStatic(&UFlowGraphSchema::OnAssetAdded); AssetRegistry.Get().OnAssetRemoved().AddStatic(&UFlowGraphSchema::OnAssetRemoved); + AssetRegistry.Get().OnAssetRenamed().AddStatic(&UFlowGraphSchema::OnAssetRenamed); FCoreUObjectDelegates::ReloadCompleteDelegate.AddStatic(&UFlowGraphSchema::OnHotReload); @@ -51,18 +217,18 @@ void UFlowGraphSchema::SubscribeToAssetChanges() } } -void UFlowGraphSchema::GetPaletteActions(FGraphActionMenuBuilder& ActionMenuBuilder, const UClass* AssetClass, const FString& CategoryName) +void UFlowGraphSchema::GetPaletteActions(FGraphActionMenuBuilder& ActionMenuBuilder, const UFlowAsset* EditedFlowAsset, const FString& CategoryName) { - GetFlowNodeActions(ActionMenuBuilder, AssetClass->GetDefaultObject(), CategoryName); + GetFlowNodeActions(ActionMenuBuilder, EditedFlowAsset, CategoryName); GetCommentAction(ActionMenuBuilder); } void UFlowGraphSchema::GetGraphContextActions(FGraphContextMenuBuilder& ContextMenuBuilder) const { - GetFlowNodeActions(ContextMenuBuilder, GetAssetClassDefaults(ContextMenuBuilder.CurrentGraph), FString()); + GetFlowNodeActions(ContextMenuBuilder, GetEditedAssetOrClassDefault(ContextMenuBuilder.CurrentGraph), FString()); GetCommentAction(ContextMenuBuilder, ContextMenuBuilder.CurrentGraph); - if (!ContextMenuBuilder.FromPin && FFlowGraphUtils::GetFlowAssetEditor(ContextMenuBuilder.CurrentGraph)->CanPasteNodes()) + if (!ContextMenuBuilder.FromPin && FFlowGraphUtils::GetFlowGraphEditor(ContextMenuBuilder.CurrentGraph)->CanPasteNodes()) { const TSharedPtr NewAction(new FFlowGraphSchemaAction_Paste(FText::GetEmpty(), LOCTEXT("PasteHereAction", "Paste here"), FText::GetEmpty(), 0)); ContextMenuBuilder.AddAction(NewAction); @@ -71,17 +237,255 @@ void UFlowGraphSchema::GetGraphContextActions(FGraphContextMenuBuilder& ContextM void UFlowGraphSchema::CreateDefaultNodesForGraph(UEdGraph& Graph) const { + const UFlowAsset* AssetClassDefaults = GetEditedAssetOrClassDefault(&Graph); + static const FVector2f NodeOffsetIncrement = FVector2f(0, 128); + FVector2f NodeOffset = FVector2f::ZeroVector; + // Start node - UFlowGraphNode* NewGraphNode = FFlowGraphSchemaAction_NewNode::CreateNode(&Graph, nullptr, UFlowNode_Start::StaticClass(), FVector2D::ZeroVector); + CreateDefaultNode(Graph, UFlowNode_Start::StaticClass(), NodeOffset, AssetClassDefaults->bStartNodePlacedAsGhostNode); + + // Add default nodes for all the CustomInputs + if (IsValid(AssetClassDefaults)) + { + for (const FName& CustomInputName : AssetClassDefaults->CustomInputs) + { + NodeOffset += NodeOffsetIncrement; + const UFlowGraphNode* NewFlowGraphNode = CreateDefaultNode(Graph, UFlowNode_CustomInput::StaticClass(), NodeOffset, true); + + UFlowNode_CustomInput* CustomInputNode = CastChecked(NewFlowGraphNode->GetFlowNodeBase()); + CustomInputNode->SetEventName(CustomInputName); + } + } + + UFlowAsset* FlowAsset = CastChecked(&Graph)->GetFlowAsset(); + FlowAsset->HarvestNodeConnections(); +} + +UFlowGraphNode* UFlowGraphSchema::CreateDefaultNode(UEdGraph& Graph, const TSubclassOf& NodeClass, const FVector2f& Offset, const bool bPlacedAsGhostNode) +{ + UFlowGraphNode* NewGraphNode = FFlowGraphSchemaAction_NewNode::CreateNode(&Graph, nullptr, NodeClass, Offset); SetNodeMetaData(NewGraphNode, FNodeMetadata::DefaultGraphNode); - const UFlowAsset* AssetClassDefaults = GetAssetClassDefaults(&Graph); - if (AssetClassDefaults && AssetClassDefaults->bStartNodePlacedAsGhostNode) + if (bPlacedAsGhostNode) { NewGraphNode->MakeAutomaticallyPlacedGhostNode(); } - CastChecked(&Graph)->GetFlowAsset()->HarvestNodeConnections(); + return NewGraphNode; +} + +bool UFlowGraphSchema::ArePinsCompatible(const UEdGraphPin* PinA, const UEdGraphPin* PinB, const UClass* CallingContext, bool bIgnoreArray) const +{ + // First, pins must be direction-compatible (and we need stable Input/Output ordering). + const UEdGraphPin* InputPin = nullptr; + const UEdGraphPin* OutputPin = nullptr; + + if (!CategorizePinsByDirection(PinA, PinB, /*out*/ InputPin, /*out*/ OutputPin)) + { + return false; + } + + check(InputPin); + check(OutputPin); + + const bool bInvolvesReroute = + (Cast(PinA->GetOwningNode()) != nullptr) || + (Cast(PinB->GetOwningNode()) != nullptr); + + if (bInvolvesReroute) + { + // Exec pins remain strict; defer to canonical exec/type logic. + const bool bAnyExec = + FFlowPin::IsExecPinCategory(InputPin->PinType.PinCategory) || + FFlowPin::IsExecPinCategory(OutputPin->PinType.PinCategory); + + // Data pins: allow any type when a reroute is involved (reroute will adapt after connection). + if (!bAnyExec) + { + return true; + } + } + + return ArePinTypesCompatible(*OutputPin, *InputPin, CallingContext, bIgnoreArray); +} + +bool UFlowGraphSchema::ArePinTypesCompatible( + 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) + { + // Exec pins must match exactly (exec ↔ exec only). + return (bIsInputExecPin && bIsOutputExecPin); + } + + const UFlowAsset* FlowAsset = GetFlowAssetForPin(OutputPin); + if (!IsValid(FlowAsset)) + { + UE_LOG(LogFlowEditor, Error, TEXT("Could not find the FlowAsset when trying to check ArePinTypesCompatible!")); + return false; + } + + // Get the PinConnectionPolicy from the FlowAsset + const FFlowPinConnectionPolicy& PinConnectionPolicy = FlowAsset->GetPinConnectionPolicy(); + if (!PinConnectionPolicy.CanConnectPinTypeNames(OutputPinType.PinCategory, InputPinType.PinCategory)) + { + // 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); + + if (bRequirePinCategoryMemberReferenceMatch && + OutputPinType.PinSubCategoryMemberReference != InputPinType.PinSubCategoryMemberReference) + { + // Pin category member reference mismatch. + return false; + } + + // Container type (Single/Array, etc.) + const bool bRequireContainerTypeMatch = + EnumHasAnyFlags(FoundPinTypeMatchPolicy->PinTypeMatchRules, EFlowPinTypeMatchRules::RequireContainerTypeMatch); + + if (bRequireContainerTypeMatch && OutputPinType.ContainerType != InputPinType.ContainerType) + { + const bool bIsAnyArray = + OutputPinType.ContainerType == EPinContainerType::Array || + InputPinType.ContainerType == EPinContainerType::Array; + + if (!bIgnoreArray || !bIsAnyArray) + { + // Mismatched container type (and array mismatch is not being ignored). + return false; + } + } + + const bool bRequirePinSubCategoryObjectMatch = + EnumHasAnyFlags(FoundPinTypeMatchPolicy->PinTypeMatchRules, EFlowPinTypeMatchRules::RequirePinSubCategoryObjectMatch); + + if (bRequirePinSubCategoryObjectMatch) + { + const UStruct* OutputStruct = Cast(OutputPinType.PinSubCategoryObject.Get()); + const UStruct* InputStruct = Cast(InputPinType.PinSubCategoryObject.Get()); + + // ArePinSubCategoryObjectsCompatible() expects to fill an OutConnectionResponse on failure, + // but since we only return bool here, we intentionally discard it. + FPinConnectionResponse DiscardedResponse; + if (!ArePinSubCategoryObjectsCompatible(OutputStruct, InputStruct, *FoundPinTypeMatchPolicy, DiscardedResponse)) + { + // SubCategoryObject types are not compatible per policy. + return false; + } + } + + return true; +} + +bool UFlowGraphSchema::ArePinSubCategoryObjectsCompatible( + const UStruct* OutputStruct, + const UStruct* InputStruct, + const FFlowPinTypeMatchPolicy& PinTypeMatchPolicy, + FPinConnectionResponse& OutConnectionResponse) const +{ + if (!IsValid(InputStruct)) + { + // Assume "InputStruct's SubCategoryObject == null", means any SubCategoryObject is acceptable + return true; + } + + if (!IsValid(OutputStruct)) + { + // null objects are the norm for many PinCategories, so long as they match + return true; + } + + // Exact match + if (OutputStruct == InputStruct) + { + return true; + } + + using namespace FlowGraphSchema::Private; + + // Only allow a match if the input is a superclass of the output + const bool bAllowSubCategoryObjectSubclasses = EnumHasAnyFlags(PinTypeMatchPolicy.PinTypeMatchRules, EFlowPinTypeMatchRules::AllowSubCategoryObjectSubclasses); + if (bAllowSubCategoryObjectSubclasses && IsAuthoritativeChildOf(OutputStruct, InputStruct)) + { + return true; + } + + UClass const* OutputClass = Cast(OutputStruct); + UClass const* InputClass = Cast(InputStruct); + + // Class specifics + if (IsValid(InputClass) && IsValid(OutputClass)) + { + // Only allow a match if the input is a superclass of the output + if (bAllowSubCategoryObjectSubclasses && ExtendedIsChildOf(OutputClass, InputClass)) + { + return true; + } + + OutConnectionResponse = + FPinConnectionResponse( + CONNECT_RESPONSE_DISALLOW, + FString::Printf( + TEXT("Output %s must be subclass of input %s"), + *OutputClass->GetName(), + *InputClass->GetName())); + + return false; + } + + if (!IsValid(InputClass) && !IsValid(OutputClass)) + { + const bool bAllowSubCategoryObjectSameLayout = EnumHasAnyFlags(PinTypeMatchPolicy.PinTypeMatchRules, EFlowPinTypeMatchRules::AllowSubCategoryObjectSameLayout); + const bool bSameLayoutMustMatchPropertyNames = EnumHasAnyFlags(PinTypeMatchPolicy.PinTypeMatchRules, EFlowPinTypeMatchRules::SameLayoutMustMatchPropertyNames); + + // Allow structs with the same layout + if (bAllowSubCategoryObjectSameLayout + && FStructUtils::TheSameLayout(OutputStruct, InputStruct, bSameLayoutMustMatchPropertyNames)) + { + return true; + } + + using namespace UE::Kismet::BlueprintTypeConversions; + + // Allow convertable ScriptStructs + const UScriptStruct* InputScriptStruct = Cast(InputStruct); + const UScriptStruct* OutputScriptStruct = Cast(OutputStruct); + if (IsValid(InputScriptStruct) && IsValid(OutputScriptStruct)) + { + const bool bAreConvertibleStructs = + FStructConversionTable::Get().GetConversionFunction(OutputScriptStruct, InputScriptStruct).IsSet(); + + if (bAreConvertibleStructs) + { + return true; + } + } + } + + OutConnectionResponse = + FPinConnectionResponse( + CONNECT_RESPONSE_DISALLOW, + FString::Printf( + TEXT("Output %s is not compatible with input %s"), + *OutputStruct->GetName(), + *InputStruct->GetName())); + + return false; } const FPinConnectionResponse UFlowGraphSchema::CanCreateConnection(const UEdGraphPin* PinA, const UEdGraphPin* PinB) const @@ -95,7 +499,7 @@ const FPinConnectionResponse UFlowGraphSchema::CanCreateConnection(const UEdGrap } // Make sure the pins are not on the same node - if (PinA->GetOwningNode() == PinB->GetOwningNode()) + if (OwningNodeA == OwningNodeB) { return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, TEXT("Both are on the same node")); } @@ -105,6 +509,18 @@ const FPinConnectionResponse UFlowGraphSchema::CanCreateConnection(const UEdGrap return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, TEXT("Cannot make new connections to orphaned pin")); } + FString NodeResponseMessage; + + // Node can disallow the connection + if (OwningNodeA->IsConnectionDisallowed(PinA, PinB, NodeResponseMessage)) + { + return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, NodeResponseMessage); + } + if (OwningNodeB->IsConnectionDisallowed(PinB, PinA, NodeResponseMessage)) + { + return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, NodeResponseMessage); + } + // Compare the directions const UEdGraphPin* InputPin = nullptr; const UEdGraphPin* OutputPin = nullptr; @@ -114,106 +530,542 @@ const FPinConnectionResponse UFlowGraphSchema::CanCreateConnection(const UEdGrap return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, TEXT("Directions are not compatible")); } - // Break existing connections on outputs only - multiple input connections are acceptable - if (OutputPin->LinkedTo.Num() > 0) - { - const ECanCreateConnectionResponse ReplyBreakInputs = (OutputPin == PinA ? CONNECT_RESPONSE_BREAK_OTHERS_A : CONNECT_RESPONSE_BREAK_OTHERS_B); - return FPinConnectionResponse(ReplyBreakInputs, TEXT("Replace existing connections")); - } + check(InputPin); + check(OutputPin); + + // Use the owning flow node's *template* class as the CallingContext. + // (Avoid GetFlowNodeBase() here: it may return inspected PIE instances.) + const UClass* CallingContext = nullptr; + if (const UFlowNodeBase* NodeTemplate = OwningNodeA->GetNodeTemplate()) + { + CallingContext = NodeTemplate->GetClass(); + } + + // Compare the pin types + constexpr bool bIgnoreArray = false; + const bool bArePinsCompatible = ArePinsCompatible(OutputPin, InputPin, CallingContext, bIgnoreArray); + if (!bArePinsCompatible) + { + return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, TEXT("Pins are not compatible")); + } + + FPinConnectionResponse ConnectionResponse = DetermineConnectionResponseOfCompatibleTypedPins(PinA, PinB, InputPin, OutputPin); + if (ConnectionResponse.Message.IsEmpty()) + { + ConnectionResponse.Message = FText::FromString(NodeResponseMessage); + } + else if (!NodeResponseMessage.IsEmpty()) + { + ConnectionResponse.Message = FText::Format( + LOCTEXT("MultiMsgConnectionResponse", "{0} - {1}"), + ConnectionResponse.Message, + FText::FromString(NodeResponseMessage)); + } + + return ConnectionResponse; +} + +const FPinConnectionResponse UFlowGraphSchema::DetermineConnectionResponseOfCompatibleTypedPins( + const UEdGraphPin* PinA, + const UEdGraphPin* PinB, + const UEdGraphPin* InputPin, + const UEdGraphPin* OutputPin) const +{ + const bool bIsExistingConnection = PinA->LinkedTo.Contains(PinB); + if (bIsExistingConnection) + { + // Don't error for queries about existing connections + return FPinConnectionResponse(CONNECT_RESPONSE_MAKE, TEXT("")); + } + + checkf(!PinB->LinkedTo.Contains(PinA), TEXT("This should be caught with the bIsExistingConnection test above")); + + const bool bInvolvesReroute = + (Cast(PinA->GetOwningNode()) != nullptr) || + (Cast(PinB->GetOwningNode()) != nullptr); + + // Break existing connections on outputs for Exec Pins + const bool bIsExecPin = FFlowPin::IsExecPinCategory(InputPin->PinType.PinCategory); + if (bIsExecPin && OutputPin->LinkedTo.Num() > 0) + { + const ECanCreateConnectionResponse ReplyBreakOutputs = + (OutputPin == PinA ? CONNECT_RESPONSE_BREAK_OTHERS_A : CONNECT_RESPONSE_BREAK_OTHERS_B); + + return FPinConnectionResponse(ReplyBreakOutputs, TEXT("Replace existing exec connection")); + } + + // Break existing connections on inputs for Data Pins + if (!bIsExecPin && InputPin->LinkedTo.Num() > 0) + { + const ECanCreateConnectionResponse ReplyBreakInputs = + (InputPin == PinA ? CONNECT_RESPONSE_BREAK_OTHERS_A : CONNECT_RESPONSE_BREAK_OTHERS_B); + + return FPinConnectionResponse( + ReplyBreakInputs, + bInvolvesReroute ? TEXT("Replace existing data connection (reroute will adapt)") : TEXT("Replace existing data connection")); + } + + return FPinConnectionResponse(CONNECT_RESPONSE_MAKE, TEXT("")); +} + +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()) + { + return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, TEXT("The Play-in-Editor is simulating")); + } + + // Make sure the nodes are not the same + if (NodeA == NodeB) + { + return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, TEXT("Both are the same node")); + } + + const UFlowGraphNode* FlowGraphNodeA = Cast(NodeA); + const UFlowGraphNode* FlowGraphNodeB = Cast(NodeB); + + FString ReasonString; + if (FlowGraphNodeA && FlowGraphNodeB) + { + const TSet OtherGraphNodes; + if (!FlowGraphNodeB->CanAcceptSubNodeAsChild(*FlowGraphNodeA, OtherGraphNodes, &ReasonString)) + { + return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, ReasonString); + } + else + { + return FPinConnectionResponse(CONNECT_RESPONSE_MAKE, ReasonString); + } + } + else + { + return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW, TEXT("Incompatible graph node types")); + } +} + +bool UFlowGraphSchema::TryCreateConnection(UEdGraphPin* PinA, UEdGraphPin* PinB) const +{ + const bool bModified = UEdGraphSchema::TryCreateConnection(PinA, PinB); + + if (bModified) + { + UFlowGraphNode* FlowGraphNodeA = Cast(PinA->GetOwningNode()); + UFlowGraphNode* FlowGraphNodeB = Cast(PinB->GetOwningNode()); + + UEdGraph* EdGraph = FlowGraphNodeA ? FlowGraphNodeA->GetGraph() : nullptr; + + // If either side is a reroute, re-type it based on the "other" pin and break incompatible links. + UFlowGraphNode_Reroute* RerouteNode = Cast(PinA->GetOwningNode()); + UEdGraphPin* OtherPin = PinB; + + if (!RerouteNode) + { + RerouteNode = Cast(PinB->GetOwningNode()); + OtherPin = PinA; + } + + if (RerouteNode) + { + check(OtherPin); + + RerouteNode->ApplyTypeFromConnectedPin(*OtherPin); + + constexpr bool bForInputPins = true; + BreakIncompatibleConnections(RerouteNode, RerouteNode->InputPins, *OtherPin); + + constexpr bool bForOutputPins = false; + BreakIncompatibleConnections(RerouteNode, RerouteNode->OutputPins, *OtherPin); + } + + if (EdGraph) + { + NotifyNodesChanged(FlowGraphNodeA, FlowGraphNodeB, EdGraph); + } + } + + return bModified; +} + +template +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) + { + TArray ConnectionsToBreak; + for (UEdGraphPin* LinkedPin : Pin->LinkedTo) + { + bool bIsCompatible; + + if constexpr (bIsInputPins) + { + // LinkedPin (output) to NewType (input) + bIsCompatible = ArePinTypesCompatible(*LinkedPin, TypeFromPin, nullptr); + } + else + { + // NewType (output) to LinkedPin (input) + bIsCompatible = ArePinTypesCompatible(TypeFromPin, *LinkedPin, nullptr); + } + + if (!bIsCompatible) + { + ConnectionsToBreak.Add(LinkedPin); + } + } + + for (UEdGraphPin* PinToBreak : ConnectionsToBreak) + { + PinToBreak->BreakLinkTo(Pin); + } + } +} + +void UFlowGraphSchema::NotifyNodesChanged(UFlowGraphNode* NodeA, UFlowGraphNode* NodeB, UEdGraph* Graph) const +{ + Graph->NotifyNodeChanged(NodeA); + Graph->NotifyNodeChanged(NodeB); +} + +bool UFlowGraphSchema::ShouldHidePinDefaultValue(UEdGraphPin* Pin) const +{ + return true; +} + +FLinearColor UFlowGraphSchema::GetPinTypeColor(const FEdGraphPinType& PinType) const +{ + if (const FFlowPinType* FlowPinType = LookupDataPinTypeForPinCategory(PinType.PinCategory)) + { + return FlowPinType->GetPinColor(); + } + + return FLinearColor(1.0f, 1.0f, 1.0f, 1.0f); +} + +FText UFlowGraphSchema::GetPinDisplayName(const UEdGraphPin* Pin) const +{ + FText ResultPinName; + check(Pin != nullptr); + if (Pin->PinFriendlyName.IsEmpty()) + { + // We don't want to display "None" for no name + if (Pin->PinName.IsNone()) + { + return FText::GetEmpty(); + } + + // this option is only difference between this override and UEdGraphSchema::GetPinDisplayName + if (GetDefault()->bEnforceFriendlyPinNames) + { + ResultPinName = FText::FromString(FName::NameToDisplayString(Pin->PinName.ToString(), true)); + } + else + { + ResultPinName = FText::FromName(Pin->PinName); + } + } + else + { + ResultPinName = Pin->PinFriendlyName; + + bool bShouldUseLocalizedNodeAndPinNames = false; + GConfig->GetBool(TEXT("Internationalization"), TEXT("ShouldUseLocalizedNodeAndPinNames"), bShouldUseLocalizedNodeAndPinNames, GEditorSettingsIni); + + if (!bShouldUseLocalizedNodeAndPinNames) + { + ResultPinName = FText::FromString(ResultPinName.BuildSourceString()); + } + } + + return ResultPinName; +} + +void UFlowGraphSchema::ConstructBasicPinTooltip(const UEdGraphPin& Pin, const FText& PinDescription, FString& TooltipOut) const +{ + if (Pin.bWasTrashed) + { + return; + } + + FFormatNamedArguments Args; + Args.Add(TEXT("PinType"), UEdGraphSchema_K2::TypeToText(Pin.PinType)); + + if (UEdGraphNode* PinNode = Pin.GetOwningNode()) + { + UEdGraphSchema_K2 const* const K2Schema = Cast(PinNode->GetSchema()); + if (ensure(K2Schema != nullptr)) // ensure that this node belongs to this schema + { + Args.Add(TEXT("DisplayName"), GetPinDisplayName(&Pin)); + Args.Add(TEXT("LineFeed1"), FText::FromString(TEXT("\n"))); + } + } + else + { + Args.Add(TEXT("DisplayName"), FText::GetEmpty()); + Args.Add(TEXT("LineFeed1"), FText::GetEmpty()); + } + + + if (!PinDescription.IsEmpty()) + { + Args.Add(TEXT("Description"), PinDescription); + Args.Add(TEXT("LineFeed2"), FText::FromString(TEXT("\n\n"))); + } + else + { + Args.Add(TEXT("Description"), FText::GetEmpty()); + Args.Add(TEXT("LineFeed2"), FText::GetEmpty()); + } + + TooltipOut = FText::Format(LOCTEXT("PinTooltip", "{DisplayName}{LineFeed1}{PinType}{LineFeed2}{Description}"), Args).ToString(); +} + +bool UFlowGraphSchema::CanShowDataTooltipForPin(const UEdGraphPin& Pin) const +{ + return !FFlowPin::IsExecPinCategory(Pin.PinType.PinCategory); +} + +const FFlowPinType* UFlowGraphSchema::LookupDataPinTypeForPinCategory(const FName& PinCategory) +{ + UFlowPinSubsystem* PinSubsystem = UFlowPinSubsystem::Get(); + if (!PinSubsystem) + { + UE_LOG(LogFlowEditor, Error, TEXT("Could not find the FlowPinSubsystem!")); + + return nullptr; + } + + // Flow uses the PinTypeName as the PinCategory for UEdGraphPin purposes + const FFlowPinTypeName PinTypeName(PinCategory); + const FFlowPinType* PinType = PinSubsystem->FindPinType(PinTypeName); + return PinType; +} + +bool UFlowGraphSchema::IsTitleBarPin(const UEdGraphPin& Pin) const +{ + return FFlowPin::IsExecPinCategory(Pin.PinType.PinCategory); +} + +void UFlowGraphSchema::BreakNodeLinks(UEdGraphNode& TargetNode) const +{ + Super::BreakNodeLinks(TargetNode); +} + +void UFlowGraphSchema::BreakPinLinks(UEdGraphPin& TargetPin, bool bSendsNodeNotification) const +{ + const FScopedTransaction Transaction(LOCTEXT("GraphEd_BreakPinLinks", "Break Pin Links")); + + TArray CachedLinkedTo = TargetPin.LinkedTo; + + UFlowGraphNode* OwningFlowGraphNode = Cast(TargetPin.GetOwningNodeUnchecked()); + UEdGraph* EdGraph = (OwningFlowGraphNode) ? OwningFlowGraphNode->GetGraph() : nullptr; + + Super::BreakPinLinks(TargetPin, bSendsNodeNotification); + + if (TargetPin.bOrphanedPin) + { + if (OwningFlowGraphNode) + { + // this calls NotifyNodeChanged() + OwningFlowGraphNode->RemoveOrphanedPin(&TargetPin); + } + } + else if (bSendsNodeNotification) + { + if (IsValid(EdGraph)) + { + EdGraph->NotifyNodeChanged(OwningFlowGraphNode); + } + } + + for (UEdGraphPin* OtherPin : CachedLinkedTo) + { + UFlowGraphNode* OtherOwningFlowGraphNode = Cast(OtherPin->GetOwningNodeUnchecked()); + if (IsValid(OtherOwningFlowGraphNode)) + { + if (OtherPin->bOrphanedPin) + { + // this calls NotifyNodeChanged() + OtherOwningFlowGraphNode->RemoveOrphanedPin(OtherPin); + } + else if (bSendsNodeNotification) + { + EdGraph->NotifyNodeChanged(OtherOwningFlowGraphNode); + } + } + } +} + +int32 UFlowGraphSchema::GetNodeSelectionCount(const UEdGraph* Graph) const +{ + return FFlowGraphUtils::GetFlowGraphEditor(Graph)->GetNumberOfSelectedNodes(); +} - return FPinConnectionResponse(CONNECT_RESPONSE_MAKE, TEXT("")); +TSharedPtr UFlowGraphSchema::GetCreateCommentAction() const +{ + return TSharedPtr(static_cast(new FFlowGraphSchemaAction_NewComment)); } -bool UFlowGraphSchema::TryCreateConnection(UEdGraphPin* PinA, UEdGraphPin* PinB) const +void UFlowGraphSchema::OnPinConnectionDoubleCicked(UEdGraphPin* PinA, UEdGraphPin* PinB, const FVector2f& GraphPosition) const { - const bool bModified = UEdGraphSchema::TryCreateConnection(PinA, PinB); + const FScopedTransaction Transaction(LOCTEXT("CreateFlowRerouteNodeOnWire", "Create Flow Reroute Node")); - if (bModified) + const FVector2f NodeSpacerSize(42.0f, 24.0f); + const FVector2f KnotTopLeft = GraphPosition - (NodeSpacerSize * 0.5f); + + UEdGraph* ParentGraph = PinA->GetOwningNode()->GetGraph(); + UFlowGraphNode* NewEdNode = FFlowGraphSchemaAction_NewNode::CreateNode(ParentGraph, nullptr, UFlowNode_Reroute::StaticClass(), KnotTopLeft, false); + UFlowGraphNode_Reroute* NewRerouteEdNode = Cast(NewEdNode); + + if (PinA->Direction == EGPD_Output) { - PinA->GetOwningNode()->GetGraph()->NotifyGraphChanged(); + check(PinB->Direction == EGPD_Input && PinA->Direction == EGPD_Output); + NewRerouteEdNode->ConfigureRerouteNodeFromPinConnections(*PinB, *PinA); + } + else + { + check(PinA->Direction == EGPD_Input && PinB->Direction == EGPD_Output); + NewRerouteEdNode->ConfigureRerouteNodeFromPinConnections(*PinA, *PinB); } - - return bModified; } -bool UFlowGraphSchema::ShouldHidePinDefaultValue(UEdGraphPin* Pin) const +bool UFlowGraphSchema::IsCacheVisualizationOutOfDate(int32 InVisualizationCacheID) const { - return true; + return CurrentCacheRefreshID != InVisualizationCacheID; } -FLinearColor UFlowGraphSchema::GetPinTypeColor(const FEdGraphPinType& PinType) const +int32 UFlowGraphSchema::GetCurrentVisualizationCacheID() const { - return FLinearColor::White; + return CurrentCacheRefreshID; } -void UFlowGraphSchema::BreakNodeLinks(UEdGraphNode& TargetNode) const +void UFlowGraphSchema::ForceVisualizationCacheClear() const { - Super::BreakNodeLinks(TargetNode); - - TargetNode.GetGraph()->NotifyGraphChanged(); + ++CurrentCacheRefreshID; } -void UFlowGraphSchema::BreakPinLinks(UEdGraphPin& TargetPin, bool bSendsNodeNotification) const +void UFlowGraphSchema::UpdateGeneratedDisplayNames() { - const FScopedTransaction Transaction(LOCTEXT("GraphEd_BreakPinLinks", "Break Pin Links")); - - Super::BreakPinLinks(TargetPin, bSendsNodeNotification); + for (UClass* FlowNodeClass : NativeFlowNodes) + { + UpdateGeneratedDisplayName(FlowNodeClass, true); + } - if (TargetPin.bOrphanedPin) + for (UClass* FlowNodeAddOnClass : NativeFlowNodeAddOns) { - // this calls NotifyGraphChanged() - Cast(TargetPin.GetOwningNode())->RemoveOrphanedPin(&TargetPin); + UpdateGeneratedDisplayName(FlowNodeAddOnClass, true); } - else if (bSendsNodeNotification) + + for (const TPair& AssetData : BlueprintFlowNodes) { - TargetPin.GetOwningNode()->GetGraph()->NotifyGraphChanged(); + if (UBlueprint* Blueprint = Cast(AssetData.Value.GetAsset())) + { + UClass* NodeClass = Blueprint->GeneratedClass; + UpdateGeneratedDisplayName(NodeClass, true); + } } -} -int32 UFlowGraphSchema::GetNodeSelectionCount(const UEdGraph* Graph) const -{ - return FFlowGraphUtils::GetFlowAssetEditor(Graph)->GetNumberOfSelectedNodes(); -} + for (const TPair& AssetData : BlueprintFlowNodeAddOns) + { + if (UBlueprint* Blueprint = Cast(AssetData.Value.GetAsset())) + { + UClass* NodeAddOnClass = Blueprint->GeneratedClass; + UpdateGeneratedDisplayName(NodeAddOnClass, true); + } + } + + OnNodeListChanged.Broadcast(); -TSharedPtr UFlowGraphSchema::GetCreateCommentAction() const -{ - return TSharedPtr(static_cast(new FFlowGraphSchemaAction_NewComment)); + // Refresh node titles + GetDefault()->ForceVisualizationCacheClear(); } -void UFlowGraphSchema::OnPinConnectionDoubleCicked(UEdGraphPin* PinA, UEdGraphPin* PinB, const FVector2D& GraphPosition) const +void UFlowGraphSchema::UpdateGeneratedDisplayName(UClass* NodeClass, bool bBatch) { - const FScopedTransaction Transaction(LOCTEXT("CreateFlowRerouteNodeOnWire", "Create Flow Reroute Node")); + static const FName NAME_GeneratedDisplayName("GeneratedDisplayName"); - const FVector2D NodeSpacerSize(42.0f, 24.0f); - const FVector2D KnotTopLeft = GraphPosition - (NodeSpacerSize * 0.5f); + if (NodeClass->IsChildOf(UFlowNodeBase::StaticClass()) == false) + { + return; + } - UEdGraph* ParentGraph = PinA->GetOwningNode()->GetGraph(); - UFlowGraphNode* NewReroute = FFlowGraphSchemaAction_NewNode::CreateNode(ParentGraph, nullptr, UFlowNode_Reroute::StaticClass(), KnotTopLeft, false); + FString NameWithoutPrefix = FFlowGraphUtils::RemovePrefixFromNodeText(NodeClass->GetDisplayNameText()); + NodeClass->SetMetaData(NAME_GeneratedDisplayName, *NameWithoutPrefix); + + if (!bBatch) + { + OnNodeListChanged.Broadcast(); - PinA->BreakLinkTo(PinB); - PinA->MakeLinkTo((PinA->Direction == EGPD_Output) ? NewReroute->InputPins[0] : NewReroute->OutputPins[0]); - PinB->MakeLinkTo((PinB->Direction == EGPD_Output) ? NewReroute->InputPins[0] : NewReroute->OutputPins[0]); + // Refresh node titles + GetDefault()->ForceVisualizationCacheClear(); + } } TArray> UFlowGraphSchema::GetFlowNodeCategories() { - if (NativeFlowNodes.Num() == 0) + if (!bInitialGatherPerformed) { - GatherFlowNodes(); + GatherNodes(); } TSet UnsortedCategories; - for (const UClass* FlowNodeClass : NativeFlowNodes) + for (const TSubclassOf FlowNodeClass : NativeFlowNodes) { if (const UFlowNode* DefaultObject = FlowNodeClass->GetDefaultObject()) { - UnsortedCategories.Emplace(DefaultObject->GetNodeCategory()); + const FString NodeCategoryString = UFlowGraphSettings::GetNodeCategoryForNode(*DefaultObject); + UnsortedCategories.Emplace(NodeCategoryString); + } + } + + for (const TSubclassOf FlowNodeAddOnClass : NativeFlowNodeAddOns) + { + if (const UFlowNodeAddOn* DefaultObject = FlowNodeAddOnClass->GetDefaultObject()) + { + const FString NodeCategoryString = UFlowGraphSettings::GetNodeCategoryForNode(*DefaultObject); + UnsortedCategories.Emplace(NodeCategoryString); } } for (const TPair& AssetData : BlueprintFlowNodes) { - if (const UBlueprint* Blueprint = GetPlaceableNodeBlueprint(AssetData.Value)) + if (const UBlueprint* Blueprint = GetPlaceableNodeOrAddOnBlueprint(AssetData.Value)) + { + UnsortedCategories.Emplace(Blueprint->BlueprintCategory); + } + } + + for (const TPair& AssetData : BlueprintFlowNodeAddOns) + { + if (const UBlueprint* Blueprint = GetPlaceableNodeOrAddOnBlueprint(AssetData.Value)) { UnsortedCategories.Emplace(Blueprint->BlueprintCategory); } @@ -235,82 +1087,248 @@ TArray> UFlowGraphSchema::GetFlowNodeCategories() return Result; } -UClass* UFlowGraphSchema::GetAssignedGraphNodeClass(const UClass* FlowNodeClass) +TSubclassOf UFlowGraphSchema::GetAssignedGraphNodeClass(const TSubclassOf& FlowNodeClass) { - if (UClass* AssignedGraphNode = AssignedGraphNodeClasses.FindRef(FlowNodeClass)) + TArray> FoundParentClasses; + UClass* ReturnClass = nullptr; + + // Collect all possible parents and their corresponding GraphNodeClasses + for (const TPair, TSubclassOf>& GraphNodeByFlowNode : GraphNodesByFlowNodes) { - return AssignedGraphNode; - } + if (FlowNodeClass == GraphNodeByFlowNode.Key) + { + return GraphNodeByFlowNode.Value; + } - return UFlowGraphNode::StaticClass(); -} + if (FlowNodeClass->IsChildOf(GraphNodeByFlowNode.Key)) + { + FoundParentClasses.Add(GraphNodeByFlowNode.Key); + } + } -bool UFlowGraphSchema::IsClassContained(const TArray> Classes, const UClass* Class) -{ - for (const UClass* CurrentClass : Classes) + // Of only one parent found set the return to its GraphNodeClass + if (FoundParentClasses.Num() == 1) + { + ReturnClass = GraphNodesByFlowNodes.FindRef(FoundParentClasses[0]); + } + // If multiple parents found, find the closest one and set the return to its GraphNodeClass + else if (FoundParentClasses.Num() > 1) { - if (Class->IsChildOf(CurrentClass)) + TPair ClosestParentMatch = {1000, nullptr}; + for (const auto& ParentClass : FoundParentClasses) { - return true; + int32 StepsTillExactMatch = 0; + const UClass* LocalParentClass = FlowNodeClass; + + while (IsValid(LocalParentClass) && LocalParentClass != ParentClass && LocalParentClass != UFlowNode::StaticClass()) + { + StepsTillExactMatch++; + LocalParentClass = LocalParentClass->GetSuperClass(); + } + + if (StepsTillExactMatch != 0 && StepsTillExactMatch < ClosestParentMatch.Key) + { + ClosestParentMatch = {StepsTillExactMatch, ParentClass}; + } } + + ReturnClass = GraphNodesByFlowNodes.FindRef(ClosestParentMatch.Value); } - return false; + return IsValid(ReturnClass) ? ReturnClass : UFlowGraphNode::StaticClass(); } -void UFlowGraphSchema::GetFlowNodeActions(FGraphActionMenuBuilder& ActionMenuBuilder, const UFlowAsset* AssetClassDefaults, const FString& CategoryName) +void UFlowGraphSchema::ApplyNodeOrAddOnFilter(const UFlowAsset* EditedFlowAsset, const UClass* FlowNodeClass, TArray& FilteredNodes) { - if (NativeFlowNodes.Num() == 0) + if (FlowNodeClass == nullptr) + { + return; + } + + if (EditedFlowAsset == nullptr) + { + return; + } + + if (!EditedFlowAsset->IsNodeOrAddOnClassAllowed(FlowNodeClass)) + { + return; + } + + const UFlowGraphSettings* GraphSettings = GetDefault(); + if (GraphSettings->NodesHiddenFromPalette.Contains(FlowNodeClass)) + { + return; + } + + using namespace EFlowGraphPolicyResult_Classifiers; + + UFlowNodeBase* FlowNodeBaseCDO = FlowNodeClass->GetDefaultObject(); + UClass* FlowAssetClass = EditedFlowAsset->GetClass(); + + // Crawl up the superclass parentage until we find a strict result, otherwise accept the best tentative result + EFlowGraphPolicyResult BestResult = EFlowGraphPolicyResult::TentativeAllowed; + while (IsValid(FlowAssetClass) && FlowAssetClass->IsChildOf()) + { + if (const FFlowGraphNodesPolicy* FlowAssetPolicy = GraphSettings->PerAssetSubclassFlowNodePolicies.Find(FSoftClassPath(FlowAssetClass))) + { + const EFlowGraphPolicyResult PolicyResult = FlowAssetPolicy->IsNodeAllowedByPolicy(FlowNodeBaseCDO); + + // Choose the most applicable result for this class + BestResult = MergePolicyResult(BestResult, PolicyResult); + + if (IsStrictPolicyResult(BestResult)) + { + // A strict policy stops the crawl up the superclass parentage + break; + } + } + + FlowAssetClass = FlowAssetClass->GetSuperClass(); + } + + if (IsAnyAllowedPolicyResult(BestResult)) { - GatherFlowNodes(); + FilteredNodes.Emplace(FlowNodeBaseCDO); } +} - TArray FlowNodes; - FlowNodes.Reserve(NativeFlowNodes.Num() + BlueprintFlowNodes.Num()); +void UFlowGraphSchema::GetFlowNodeActions(FGraphActionMenuBuilder& ActionMenuBuilder, const UFlowAsset* EditedFlowAsset, const FString& CategoryName) +{ + const TArray FilteredNodes = GetFilteredPlaceableNodesOrAddOns(EditedFlowAsset, NativeFlowNodes, BlueprintFlowNodes); - for (const UClass* FlowNodeClass : NativeFlowNodes) + const UFlowGraphSettings& GraphSettings = *GetDefault(); + for (const UFlowNodeBase* FlowNodeBase : FilteredNodes) { - // Flow Asset type might limit which nodes are placeable - if (IsClassContained(AssetClassDefaults->DeniedNodeClasses, FlowNodeClass)) + // TODO (gtaylor) This should really be integrated into GetFilteredPlaceableNodesOrAddOns, + // but it needs the schema instance, so we need to do a bit more refactoring + const FString NodeCategoryString = UFlowGraphSettings::GetNodeCategoryForNode(*FlowNodeBase); + const bool bAllowedForSchemaCategory = (CategoryName.IsEmpty() || CategoryName.Equals(NodeCategoryString)); + if (!bAllowedForSchemaCategory) { continue; } - if (IsClassContained(AssetClassDefaults->AllowedNodeClasses, FlowNodeClass)) + TSharedPtr NewNodeAction(new FFlowGraphSchemaAction_NewNode(FlowNodeBase, GraphSettings)); + ActionMenuBuilder.AddAction(NewNodeAction); + } +} + +TArray UFlowGraphSchema::GetFilteredPlaceableNodesOrAddOns(const UFlowAsset* EditedFlowAsset, const TArray& InNativeNodesOrAddOns, const TMap& InBlueprintNodesOrAddOns) +{ + if (!bInitialGatherPerformed) + { + GatherNodes(); + } + + // Flow Asset type might limit which nodes or addons are placeable + TArray FilteredNodes; + + FilteredNodes.Reserve(InNativeNodesOrAddOns.Num() + BlueprintFlowNodes.Num()); + + for (const UClass* FlowNodeClass : InNativeNodesOrAddOns) + { + ApplyNodeOrAddOnFilter(EditedFlowAsset, FlowNodeClass, FilteredNodes); + } + + for (const TPair& AssetData : InBlueprintNodesOrAddOns) + { + if (const UBlueprint* Blueprint = GetPlaceableNodeOrAddOnBlueprint(AssetData.Value)) { - FlowNodes.Emplace(FlowNodeClass->GetDefaultObject()); + ApplyNodeOrAddOnFilter(EditedFlowAsset, Blueprint->GeneratedClass, FilteredNodes); } } - for (const TPair& AssetData : BlueprintFlowNodes) + + FilteredNodes.Shrink(); + + return FilteredNodes; +} + +void UFlowGraphSchema::GetGraphNodeContextActions(FGraphContextMenuBuilder& ContextMenuBuilder, int32 SubNodeFlags) const +{ + UEdGraph* Graph = const_cast(ContextMenuBuilder.CurrentGraph); + UClass* GraphNodeClass = UFlowGraphNode::StaticClass(); + + const UFlowAsset* EditedFlowAsset = GetEditedAssetOrClassDefault(ContextMenuBuilder.CurrentGraph); + + TArray FilteredNodes = GetFilteredPlaceableNodesOrAddOns(EditedFlowAsset, NativeFlowNodeAddOns, BlueprintFlowNodeAddOns); + + for (UFlowNodeBase* FlowNodeBase : FilteredNodes) { - if (const UBlueprint* Blueprint = GetPlaceableNodeBlueprint(AssetData.Value)) + UFlowNodeAddOn* FlowNodeAddOnTemplate = CastChecked(FlowNodeBase); + + // Add-Ons are futher filtered by what they are potentially being attached to + // (in addition to the filtering in GetFilteredPlaceableNodesOrAddOns) + const bool bAllowAddOn = IsAddOnAllowedForSelectedObjects(ContextMenuBuilder.SelectedObjects, FlowNodeAddOnTemplate); + if (!bAllowAddOn) { - for (const UClass* AllowedClass : AssetClassDefaults->AllowedNodeClasses) - { - if (Blueprint->GeneratedClass->IsChildOf(AllowedClass)) - { - FlowNodes.Emplace(Blueprint->GeneratedClass->GetDefaultObject()); - } - } + continue; } + + UFlowGraphNode* OpNode = NewObject(Graph, GraphNodeClass); + OpNode->NodeInstanceClass = FlowNodeAddOnTemplate->GetClass(); + + const FString NodeCategoryString = UFlowGraphSettings::GetNodeCategoryForNode(*FlowNodeBase); + TSharedPtr AddOpAction = + FFlowSchemaAction_NewSubNode::AddNewSubNodeAction( + ContextMenuBuilder, + FText::FromString(NodeCategoryString), + FlowNodeBase->GetNodeTitle(), + FlowNodeBase->GetNodeToolTip()); + + AddOpAction->ParentNode = Cast(ContextMenuBuilder.SelectedObjects[0]); + AddOpAction->NodeTemplate = OpNode; } - FlowNodes.Shrink(); +} - for (const UFlowNode* FlowNode : FlowNodes) +bool UFlowGraphSchema::IsAddOnAllowedForSelectedObjects(const TArray& SelectedObjects, const UFlowNodeAddOn* AddOnTemplate) +{ + FLOW_ASSERT_ENUM_MAX(EFlowAddOnAcceptResult, 3); + + // An empty array of other addons to consider to use with CheckAcceptFlowNodeAddOnChild() below + const TArray OtherAddOns; + + EFlowAddOnAcceptResult CombinedResult = EFlowAddOnAcceptResult::Undetermined; + + for (const UObject* SelectedObject : SelectedObjects) { - if ((CategoryName.IsEmpty() || CategoryName.Equals(FlowNode->GetNodeCategory())) && !UFlowGraphSettings::Get()->NodesHiddenFromPalette.Contains(FlowNode->GetClass())) + const UFlowGraphNode* FlowGraphNode = Cast(SelectedObject); + if (!IsValid(FlowGraphNode)) + { + return false; + } + + const UFlowNodeBase* FlowNodeOuter = Cast(FlowGraphNode->GetFlowNodeBase()); + if (!IsValid(FlowNodeOuter)) + { + continue; + } + + const EFlowAddOnAcceptResult SelectedObjectResult = FlowNodeOuter->CheckAcceptFlowNodeAddOnChild(AddOnTemplate, OtherAddOns); + + CombinedResult = CombineFlowAddOnAcceptResult(SelectedObjectResult, CombinedResult); + if (CombinedResult == EFlowAddOnAcceptResult::Reject) { - TSharedPtr NewNodeAction(new FFlowGraphSchemaAction_NewNode(FlowNode)); - ActionMenuBuilder.AddAction(NewNodeAction); + // Any Rejection rejects the entire operation + return false; } } + + if (CombinedResult == EFlowAddOnAcceptResult::TentativeAccept) + { + return true; + } + else + { + return false; + } } void UFlowGraphSchema::GetCommentAction(FGraphActionMenuBuilder& ActionMenuBuilder, const UEdGraph* CurrentGraph /*= nullptr*/) { if (!ActionMenuBuilder.FromPin) { - const bool bIsManyNodesSelected = CurrentGraph ? (FFlowGraphUtils::GetFlowAssetEditor(CurrentGraph)->GetNumberOfSelectedNodes() > 0) : false; + const bool bIsManyNodesSelected = CurrentGraph ? (FFlowGraphUtils::GetFlowGraphEditor(CurrentGraph)->GetNumberOfSelectedNodes() > 0) : false; const FText MenuDescription = bIsManyNodesSelected ? LOCTEXT("CreateCommentAction", "Create Comment from Selection") : LOCTEXT("AddCommentAction", "Add Comment..."); const FText ToolTip = LOCTEXT("CreateCommentToolTip", "Creates a comment."); @@ -319,14 +1337,14 @@ void UFlowGraphSchema::GetCommentAction(FGraphActionMenuBuilder& ActionMenuBuild } } -bool UFlowGraphSchema::IsFlowNodePlaceable(const UClass* Class) +bool UFlowGraphSchema::IsFlowNodeOrAddOnPlaceable(const UClass* Class) { - if (Class->HasAnyClassFlags(CLASS_Abstract | CLASS_NotPlaceable | CLASS_Deprecated)) + if (Class == nullptr || Class->HasAnyClassFlags(CLASS_Abstract | CLASS_NotPlaceable | CLASS_Deprecated)) { return false; } - if (const UFlowNode* DefaultObject = Class->GetDefaultObject()) + if (const UFlowNodeBase* DefaultObject = Class->GetDefaultObject()) { return !DefaultObject->bNodeDeprecated; } @@ -336,7 +1354,7 @@ bool UFlowGraphSchema::IsFlowNodePlaceable(const UClass* Class) void UFlowGraphSchema::OnBlueprintPreCompile(UBlueprint* Blueprint) { - if (Blueprint && Blueprint->GeneratedClass && Blueprint->GeneratedClass->IsChildOf(UFlowNode::StaticClass())) + if (Blueprint && Blueprint->GeneratedClass && Blueprint->GeneratedClass->IsChildOf(UFlowNodeBase::StaticClass())) { bBlueprintCompilationPending = true; } @@ -346,69 +1364,85 @@ void UFlowGraphSchema::OnBlueprintCompiled() { if (bBlueprintCompilationPending) { - GatherFlowNodes(); + GatherNodes(); } bBlueprintCompilationPending = false; } -void UFlowGraphSchema::GatherFlowNodes() +void UFlowGraphSchema::OnHotReload(EReloadCompleteReason ReloadCompleteReason) { - // prevent asset crunching during PIE - if (GEditor && GEditor->PlayWorld) + GatherNodes(); +} + +void UFlowGraphSchema::GatherNativeNodesOrAddOns(const TSubclassOf& FlowNodeBaseClass, TArray& InOutNodesOrAddOnsArray) +{ + // collect C++ Nodes or AddOns once per editor session + if (InOutNodesOrAddOnsArray.Num() > 0) { return; } - // collect C++ nodes once per editor session - if (NativeFlowNodes.Num() == 0) + TArray FlowNodesOrAddOns; + GetDerivedClasses(FlowNodeBaseClass, FlowNodesOrAddOns); + for (UClass* Class : FlowNodesOrAddOns) { - TArray FlowNodes; - GetDerivedClasses(UFlowNode::StaticClass(), FlowNodes); - for (UClass* Class : FlowNodes) + if (Class->ClassGeneratedBy == nullptr && IsFlowNodeOrAddOnPlaceable(Class)) { - if (Class->ClassGeneratedBy == nullptr && IsFlowNodePlaceable(Class)) - { - NativeFlowNodes.Emplace(Class); - } + InOutNodesOrAddOnsArray.Emplace(Class); } + } - TArray GraphNodes; - GetDerivedClasses(UFlowGraphNode::StaticClass(), GraphNodes); - for (UClass* Class : GraphNodes) + TArray GraphNodes; + GetDerivedClasses(UFlowGraphNode::StaticClass(), GraphNodes); + for (UClass* GraphNodeClass : GraphNodes) + { + const UFlowGraphNode* GraphNodeCDO = GraphNodeClass->GetDefaultObject(); + for (UClass* AssignedClass : GraphNodeCDO->AssignedNodeClasses) { - const UFlowGraphNode* DefaultObject = Class->GetDefaultObject(); - for (UClass* AssignedClass : DefaultObject->AssignedNodeClasses) + if (AssignedClass->IsChildOf(FlowNodeBaseClass)) { - if (AssignedClass->IsChildOf(UFlowNode::StaticClass())) - { - AssignedGraphNodeClasses.Emplace(AssignedClass, Class); - } + GraphNodesByFlowNodes.Emplace(AssignedClass, GraphNodeClass); } } } +} - // retrieve all blueprint nodes - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(AssetRegistryConstants::ModuleName); +void UFlowGraphSchema::GatherNodes() +{ + // prevent asset crunching during PIE + if (GEditor && GEditor->PlayWorld) + { + return; + } + + // prevent adding assets while compiling blueprints + // (because adding assets can cause blueprint compiles to be queued as a side effect (via GetPlaceableNodeOrAddOnBlueprint)) + if (GCompilingBlueprint) + { + return; + } + + bInitialGatherPerformed = true; + + GatherNativeNodesOrAddOns(UFlowNode::StaticClass(), NativeFlowNodes); + GatherNativeNodesOrAddOns(UFlowNodeAddOn::StaticClass(), NativeFlowNodeAddOns); + // retrieve all blueprint nodes & addons FARFilter Filter; - Filter.ClassNames.Add(UBlueprint::StaticClass()->GetFName()); - Filter.ClassNames.Add(UBlueprintGeneratedClass::StaticClass()->GetFName()); + Filter.ClassPaths.Add(UFlowNodeBlueprint::StaticClass()->GetClassPathName()); + Filter.ClassPaths.Add(UFlowNodeAddOnBlueprint::StaticClass()->GetClassPathName()); Filter.bRecursiveClasses = true; TArray FoundAssets; + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(AssetRegistryConstants::ModuleName); AssetRegistryModule.Get().GetAssets(Filter, FoundAssets); for (const FAssetData& AssetData : FoundAssets) { AddAsset(AssetData, true); } - OnNodeListChanged.Broadcast(); -} - -void UFlowGraphSchema::OnHotReload(EReloadCompleteReason ReloadCompleteReason) -{ - GatherFlowNodes(); + UpdateGeneratedDisplayNames(); } void UFlowGraphSchema::OnAssetAdded(const FAssetData& AssetData) @@ -418,41 +1452,64 @@ void UFlowGraphSchema::OnAssetAdded(const FAssetData& AssetData) void UFlowGraphSchema::AddAsset(const FAssetData& AssetData, const bool bBatch) { - if (!BlueprintFlowNodes.Contains(AssetData.PackageName)) + const bool bIsAssetAlreadyKnown = + BlueprintFlowNodes.Contains(AssetData.PackageName) || + BlueprintFlowNodeAddOns.Contains(AssetData.PackageName); + + if (bIsAssetAlreadyKnown) { - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(AssetRegistryConstants::ModuleName); - if (AssetRegistryModule.Get().IsLoadingAssets()) - { - return; - } + return; + } + + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(AssetRegistryConstants::ModuleName); + if (AssetRegistryModule.Get().IsLoadingAssets()) + { + return; + } + + bool bAddedToMap = false; + if (ShouldAddToBlueprintFlowNodesMap(AssetData, UFlowNodeBlueprint::StaticClass(), UFlowNode::StaticClass())) + { + BlueprintFlowNodes.Emplace(AssetData.PackageName, AssetData); + bAddedToMap = true; + } + else if (ShouldAddToBlueprintFlowNodesMap(AssetData, UFlowNodeAddOnBlueprint::StaticClass(), UFlowNodeAddOn::StaticClass())) + { + BlueprintFlowNodeAddOns.Emplace(AssetData.PackageName, AssetData); + bAddedToMap = true; + } - TArray AncestorClassNames; - AssetRegistryModule.Get().GetAncestorClassNames(AssetData.AssetClass, AncestorClassNames); - if (!AncestorClassNames.Contains(UBlueprintCore::StaticClass()->GetFName())) + if (bAddedToMap && !bBatch) + { + if (UBlueprint* Blueprint = Cast(AssetData.GetAsset())) { - return; + UClass* NodeClass = Blueprint->GeneratedClass; + UpdateGeneratedDisplayName(NodeClass, false); } + OnNodeListChanged.Broadcast(); + } +} - FString NativeParentClassPath; - AssetData.GetTagValue(FBlueprintTags::NativeParentClassPath, NativeParentClassPath); - if (!NativeParentClassPath.IsEmpty()) - { - UObject* Outer = nullptr; - ResolveName(Outer, NativeParentClassPath, false, false); - const UClass* NativeParentClass = FindObject(ANY_PACKAGE, *NativeParentClassPath); +bool UFlowGraphSchema::ShouldAddToBlueprintFlowNodesMap(const FAssetData& AssetData, const TSubclassOf& BlueprintClass, const TSubclassOf& FlowNodeBaseClass) +{ + if (!AssetData.GetClass()->IsChildOf(BlueprintClass)) + { + return false; + } - // accept only Flow Node blueprints - if (NativeParentClass && NativeParentClass->IsChildOf(UFlowNode::StaticClass())) - { - BlueprintFlowNodes.Emplace(AssetData.PackageName, AssetData); + const UBlueprint* Blueprint = GetPlaceableNodeOrAddOnBlueprint(AssetData); + if (!IsValid(Blueprint)) + { + return false; + } - if (!bBatch) - { - OnNodeListChanged.Broadcast(); - } - } - } + UClass* GeneratedClass = Blueprint->GeneratedClass; + if (!GeneratedClass || !GeneratedClass->IsChildOf(FlowNodeBaseClass)) + { + return false; } + + return true; } void UFlowGraphSchema::OnAssetRemoved(const FAssetData& AssetData) @@ -464,12 +1521,41 @@ void UFlowGraphSchema::OnAssetRemoved(const FAssetData& AssetData) OnNodeListChanged.Broadcast(); } + else if (BlueprintFlowNodeAddOns.Contains(AssetData.PackageName)) + { + BlueprintFlowNodeAddOns.Remove(AssetData.PackageName); + BlueprintFlowNodeAddOns.Shrink(); + + OnNodeListChanged.Broadcast(); + } +} + +void UFlowGraphSchema::OnAssetRenamed(const FAssetData& AssetData, const FString& OldObjectPath) +{ + FString OldPackageName; + FString OldAssetName; + if (OldObjectPath.Split(TEXT("."), &OldPackageName, &OldAssetName)) + { + const FName NAME_OldPackageName{OldPackageName}; + if (BlueprintFlowNodes.Contains(NAME_OldPackageName)) + { + BlueprintFlowNodes.Remove(NAME_OldPackageName); + BlueprintFlowNodes.Shrink(); + } + else if (BlueprintFlowNodeAddOns.Contains(NAME_OldPackageName)) + { + BlueprintFlowNodeAddOns.Remove(NAME_OldPackageName); + BlueprintFlowNodeAddOns.Shrink(); + } + } + + AddAsset(AssetData, false); } -UBlueprint* UFlowGraphSchema::GetPlaceableNodeBlueprint(const FAssetData& AssetData) +UBlueprint* UFlowGraphSchema::GetPlaceableNodeOrAddOnBlueprint(const FAssetData& AssetData) { UBlueprint* Blueprint = Cast(AssetData.GetAsset()); - if (Blueprint && IsFlowNodePlaceable(Blueprint->GeneratedClass)) + if (Blueprint && IsFlowNodeOrAddOnPlaceable(Blueprint->GeneratedClass)) { return Blueprint; } @@ -477,18 +1563,19 @@ UBlueprint* UFlowGraphSchema::GetPlaceableNodeBlueprint(const FAssetData& AssetD return nullptr; } -const UFlowAsset* UFlowGraphSchema::GetAssetClassDefaults(const UEdGraph* Graph) +const UFlowAsset* UFlowGraphSchema::GetEditedAssetOrClassDefault(const UEdGraph* EdGraph) { - const UClass* AssetClass = UFlowAsset::StaticClass(); - - if (Graph) + if (const UFlowGraph* FlowGraph = Cast(EdGraph)) { - if (const UFlowAsset* FlowAsset = Graph->GetTypedOuter()) + UFlowAsset* FlowAsset = FlowGraph->GetFlowAsset(); + + if (FlowAsset) { - AssetClass = FlowAsset->GetClass(); + return FlowGraph->GetFlowAsset(); } } + const UClass* AssetClass = UFlowAsset::StaticClass(); return AssetClass->GetDefaultObject(); } diff --git a/Source/FlowEditor/Private/Graph/FlowGraphSchema_Actions.cpp b/Source/FlowEditor/Private/Graph/FlowGraphSchema_Actions.cpp index 386baa6ac..f8e410773 100644 --- a/Source/FlowEditor/Private/Graph/FlowGraphSchema_Actions.cpp +++ b/Source/FlowEditor/Private/Graph/FlowGraphSchema_Actions.cpp @@ -2,28 +2,30 @@ #include "Graph/FlowGraphSchema_Actions.h" -#include "Asset/FlowAssetEditor.h" #include "Graph/FlowGraph.h" +#include "Graph/FlowGraphEditor.h" +#include "Graph/FlowGraphSettings.h" #include "Graph/FlowGraphSchema.h" #include "Graph/FlowGraphUtils.h" #include "Graph/Nodes/FlowGraphNode.h" #include "FlowAsset.h" +#include "AddOns/FlowNodeAddOn.h" #include "Nodes/FlowNode.h" -#include "Developer/ToolMenus/Public/ToolMenus.h" #include "EdGraph/EdGraph.h" #include "EdGraphNode_Comment.h" #include "Editor.h" -#include "Layout/SlateRect.h" #include "ScopedTransaction.h" +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowGraphSchema_Actions) + #define LOCTEXT_NAMESPACE "FlowGraphSchema_Actions" ///////////////////////////////////////////////////// // Flow Node -UEdGraphNode* FFlowGraphSchemaAction_NewNode::PerformAction(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, const FVector2D Location, bool bSelectNewNode /* = true*/) +UEdGraphNode* FFlowGraphSchemaAction_NewNode::PerformAction(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, const FVector2f& Location, bool bSelectNewNode /* = true*/) { // prevent adding new nodes while playing if (GEditor->PlayWorld != nullptr) @@ -39,7 +41,7 @@ UEdGraphNode* FFlowGraphSchemaAction_NewNode::PerformAction(class UEdGraph* Pare return nullptr; } -UFlowGraphNode* FFlowGraphSchemaAction_NewNode::CreateNode(UEdGraph* ParentGraph, UEdGraphPin* FromPin, UClass* NodeClass, const FVector2D Location, const bool bSelectNewNode /*= true*/) +UFlowGraphNode* FFlowGraphSchemaAction_NewNode::CreateNode(UEdGraph* ParentGraph, UEdGraphPin* FromPin, const UClass* NodeClass, const FVector2f Location, const bool bSelectNewNode /*= true*/) { check(NodeClass); @@ -54,51 +56,235 @@ UFlowGraphNode* FFlowGraphSchemaAction_NewNode::CreateNode(UEdGraph* ParentGraph UFlowAsset* FlowAsset = CastChecked(ParentGraph)->GetFlowAsset(); FlowAsset->Modify(); - const UClass* GraphNodeClass = UFlowGraphSchema::GetAssignedGraphNodeClass(NodeClass); + // create new Flow Graph node + const TSubclassOf FlowNodeBaseClass = const_cast(NodeClass); + const TSubclassOf GraphNodeClass = UFlowGraphSchema::GetAssignedGraphNodeClass(FlowNodeBaseClass); UFlowGraphNode* NewGraphNode = NewObject(ParentGraph, GraphNodeClass, NAME_None, RF_Transactional); + + // register to the graph NewGraphNode->CreateNewGuid(); + ParentGraph->AddNode(NewGraphNode, false, bSelectNewNode); + + // link editor and runtime nodes together + UFlowNode* FlowNode = FlowAsset->CreateNode(NodeClass, NewGraphNode); + NewGraphNode->SetNodeTemplate(FlowNode); + // create pins and connections + NewGraphNode->ReconstructNode(); + NewGraphNode->AutowireNewNode(FromPin); + + // set position NewGraphNode->NodePosX = Location.X; NewGraphNode->NodePosY = Location.Y; - ParentGraph->AddNode(NewGraphNode, false, bSelectNewNode); - - UFlowNode* NewNode = FlowAsset->CreateNode(NodeClass, NewGraphNode); - NewGraphNode->SetFlowNode(NewNode); + // call notifies NewGraphNode->PostPlacedNewNode(); + ParentGraph->NotifyNodeChanged(NewGraphNode); + + FlowAsset->PostEditChange(); + + // select in editor UI + if (bSelectNewNode) + { + const TSharedPtr FlowGraphEditor = FFlowGraphUtils::GetFlowGraphEditor(ParentGraph); + if (FlowGraphEditor.IsValid()) + { + FlowGraphEditor->SelectSingleNode(NewGraphNode); + } + } + + return NewGraphNode; +} + +UFlowGraphNode* FFlowGraphSchemaAction_NewNode::RecreateNode(UEdGraph* ParentGraph, UEdGraphNode* OldInstance, UFlowNode* FlowNode) +{ + check(FlowNode); + + ParentGraph->Modify(); + + UFlowAsset* FlowAsset = CastChecked(ParentGraph)->GetFlowAsset(); + FlowAsset->Modify(); + + // create new Flow Graph node + const TSubclassOf GraphNodeClass = UFlowGraphSchema::GetAssignedGraphNodeClass(FlowNode->GetClass()); + UFlowGraphNode* NewGraphNode = NewObject(ParentGraph, GraphNodeClass, NAME_None, RF_Transactional); + + // register to the graph + NewGraphNode->NodeGuid = FlowNode->GetGuid(); + ParentGraph->AddNode(NewGraphNode, false, false); + + // link editor and runtime nodes together + FlowNode->SetGraphNode(NewGraphNode); + NewGraphNode->SetNodeTemplate(FlowNode); + + // move links from the old node NewGraphNode->AllocateDefaultPins(); + if (OldInstance) + { + for (UEdGraphPin* OldPin : OldInstance->Pins) + { + if (OldPin->LinkedTo.Num() == 0) + { + continue; + } + + for (UEdGraphPin* NewPin : NewGraphNode->Pins) + { + if (NewPin->Direction == OldPin->Direction && NewPin->PinName == OldPin->PinName) + { + TArray Connections = OldPin->LinkedTo; + for (UEdGraphPin* ConnectedPin : Connections) + { + ConnectedPin->BreakLinkTo(OldPin); + ConnectedPin->MakeLinkTo(NewPin); + } + } + } + } + } + + // keep old position + NewGraphNode->NodePosX = OldInstance ? OldInstance->NodePosX : 0; + NewGraphNode->NodePosY = OldInstance ? OldInstance->NodePosY : 0; + + // remove leftover + if (OldInstance) + { + OldInstance->DestroyNode(); + } + + // call notifies + NewGraphNode->PostPlacedNewNode(); + ParentGraph->NotifyGraphChanged(); + + return NewGraphNode; +} + +UFlowGraphNode* FFlowGraphSchemaAction_NewNode::ImportNode(UEdGraph* ParentGraph, UEdGraphPin* FromPin, const UClass* NodeClass, const FGuid& NodeGuid, const FVector2D Location) +{ + check(NodeClass); + + ParentGraph->Modify(); + if (FromPin) + { + FromPin->Modify(); + } + UFlowAsset* FlowAsset = CastChecked(ParentGraph)->GetFlowAsset(); + FlowAsset->Modify(); + + // create new Flow Graph node + TSubclassOf FlowNodeBaseClass = const_cast(NodeClass); + const TSubclassOf GraphNodeClass = UFlowGraphSchema::GetAssignedGraphNodeClass(FlowNodeBaseClass); + UFlowGraphNode* NewGraphNode = NewObject(ParentGraph, GraphNodeClass, NAME_None, RF_Transactional); + + // register to the graph + NewGraphNode->NodeGuid = NodeGuid; + ParentGraph->AddNode(NewGraphNode, false, false); + + // link editor and runtime nodes together + UFlowNode* FlowNode = FlowAsset->CreateNode(NodeClass, NewGraphNode); + NewGraphNode->SetNodeTemplate(FlowNode); + + // create pins and connections + NewGraphNode->AllocateDefaultPins(); NewGraphNode->AutowireNewNode(FromPin); - + + // set position + NewGraphNode->NodePosX = Location.X; + NewGraphNode->NodePosY = Location.Y; + + // call notifies + NewGraphNode->PostPlacedNewNode(); ParentGraph->NotifyGraphChanged(); - const TSharedPtr FlowEditor = FFlowGraphUtils::GetFlowAssetEditor(ParentGraph); - if (FlowEditor.IsValid()) + return NewGraphNode; +} + +FText FFlowGraphSchemaAction_NewNode::GetNodeCategory(const UFlowNodeBase* Node, const UFlowGraphSettings& GraphSettings) +{ + const FString NodeCategoryString = UFlowGraphSettings::GetNodeCategoryForNode(*Node); + return FText::FromString(NodeCategoryString); +} + +///////////////////////////////////////////////////// +// New SubNode (AddOn) + +UEdGraphNode* FFlowSchemaAction_NewSubNode::PerformAction(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, const FVector2D Location, bool bSelectNewNode) +{ + ParentNode->AddSubNode(NodeTemplate, ParentGraph); + return nullptr; +} + +UEdGraphNode* FFlowSchemaAction_NewSubNode::PerformAction(class UEdGraph* ParentGraph, TArray& FromPins, const FVector2D Location, bool bSelectNewNode) +{ + return PerformAction(ParentGraph, nullptr, Location, bSelectNewNode); +} + +void FFlowSchemaAction_NewSubNode::AddReferencedObjects(FReferenceCollector& Collector) +{ + FEdGraphSchemaAction::AddReferencedObjects(Collector); + + // These don't get saved to disk, but we want to make sure the objects don't get GC'd while the action array is around + Collector.AddReferencedObject(NodeTemplate); + Collector.AddReferencedObject(ParentNode); +} + +UFlowGraphNode* FFlowSchemaAction_NewSubNode::RecreateNode(UEdGraph* ParentGraph, UEdGraphNode* OldInstance, UFlowGraphNode* ParentFlowGraphNode, UFlowNodeAddOn* FlowNodeAddOn) +{ + check(FlowNodeAddOn); + + UFlowAsset* FlowAsset = CastChecked(ParentGraph)->GetFlowAsset(); + FlowAsset->Modify(); + + // create new Flow Graph node + const TSubclassOf GraphNodeClass = UFlowGraphSchema::GetAssignedGraphNodeClass(FlowNodeAddOn->GetClass()); + UFlowGraphNode* NewGraphNode = NewObject(ParentGraph, GraphNodeClass, NAME_None, RF_Transactional); + + // link editor and runtime nodes together + FlowNodeAddOn->SetGraphNode(NewGraphNode); + NewGraphNode->SetNodeTemplate(FlowNodeAddOn); + + // remove leftover + if (OldInstance) { - FlowEditor->SelectSingleNode(NewGraphNode); + OldInstance->DestroyNode(); } - FlowAsset->PostEditChange(); - FlowAsset->MarkPackageDirty(); + ParentFlowGraphNode->AddSubNode(NewGraphNode, ParentGraph); + + // call notifies + ParentGraph->NotifyGraphChanged(); + NewGraphNode->PostPlacedNewNode(); return NewGraphNode; } -UEdGraphNode* FFlowGraphSchemaAction_Paste::PerformAction(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, const FVector2D Location, bool bSelectNewNode/* = true*/) +TSharedPtr FFlowSchemaAction_NewSubNode::AddNewSubNodeAction(FGraphActionListBuilderBase& ContextMenuBuilder, const FText& Category, const FText& MenuDesc, const FText& Tooltip) +{ + TSharedPtr NewAction = MakeShared(Category, MenuDesc, Tooltip, 0); + ContextMenuBuilder.AddAction(NewAction); + return NewAction; +} + +///////////////////////////////////////////////////// +// Paste + +UEdGraphNode* FFlowGraphSchemaAction_Paste::PerformAction(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, const FVector2D Location, const bool bSelectNewNode/* = true*/) { // prevent adding new nodes while playing if (GEditor->PlayWorld == nullptr) { - FFlowGraphUtils::GetFlowAssetEditor(ParentGraph)->PasteNodesHere(Location); + FFlowGraphUtils::GetFlowGraphEditor(ParentGraph)->PasteNodesHere(Location); } return nullptr; } ///////////////////////////////////////////////////// -// Comment Node +// New Comment -UEdGraphNode* FFlowGraphSchemaAction_NewComment::PerformAction(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, const FVector2D Location, bool bSelectNewNode/* = true*/) +UEdGraphNode* FFlowGraphSchemaAction_NewComment::PerformAction(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, const FVector2D Location, const bool bSelectNewNode/* = true*/) { // prevent adding new nodes while playing if (GEditor->PlayWorld != nullptr) @@ -109,12 +295,16 @@ UEdGraphNode* FFlowGraphSchemaAction_NewComment::PerformAction(class UEdGraph* P UEdGraphNode_Comment* CommentTemplate = NewObject(); FVector2D SpawnLocation = Location; - FSlateRect Bounds; - if (FFlowGraphUtils::GetFlowAssetEditor(ParentGraph)->GetBoundsForSelectedNodes(Bounds, 50.0f)) + const TSharedPtr FlowGraphEditor = FFlowGraphUtils::GetFlowGraphEditor(ParentGraph); + if (FlowGraphEditor.IsValid()) { - CommentTemplate->SetBounds(Bounds); - SpawnLocation.X = CommentTemplate->NodePosX; - SpawnLocation.Y = CommentTemplate->NodePosY; + FSlateRect Bounds; + if (FlowGraphEditor->GetBoundsForSelectedNodes(Bounds, 50.0f)) + { + CommentTemplate->SetBounds(Bounds); + SpawnLocation.X = CommentTemplate->NodePosX; + SpawnLocation.Y = CommentTemplate->NodePosY; + } } return FEdGraphSchemaAction_NewNode::SpawnNodeFromTemplate(ParentGraph, CommentTemplate, SpawnLocation); diff --git a/Source/FlowEditor/Private/Graph/FlowGraphSettings.cpp b/Source/FlowEditor/Private/Graph/FlowGraphSettings.cpp index ea44964da..ac7e439db 100644 --- a/Source/FlowEditor/Private/Graph/FlowGraphSettings.cpp +++ b/Source/FlowEditor/Private/Graph/FlowGraphSettings.cpp @@ -3,6 +3,14 @@ #include "Graph/FlowGraphSettings.h" #include "FlowAsset.h" +#include "FlowTags.h" +#include "Graph/FlowGraphSchema.h" +#include "Types/FlowGameplayTagMapUtils.h" + +#include "Framework/Notifications/NotificationManager.h" +#include "Widgets/Notifications/SNotificationList.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowGraphSettings) #define LOCTEXT_NAMESPACE "FlowGraphSettings" @@ -12,6 +20,7 @@ UFlowGraphSettings::UFlowGraphSettings(const FObjectInitializer& ObjectInitializ , bExposeFlowNodeCreation(true) , bShowAssetToolbarAboveLevelEditor(true) , FlowAssetCategoryName(LOCTEXT("FlowAssetCategory", "Flow")) + , DefaultFlowAssetClass(UFlowAsset::StaticClass()) , WorldAssetClass(UFlowAsset::StaticClass()) , bShowDefaultPinNames(false) , ExecPinColorModifier(0.75f, 0.75f, 0.75f, 1.0f) @@ -31,12 +40,143 @@ UFlowGraphSettings::UFlowGraphSettings(const FObjectInitializer& ObjectInitializ , SelectedWireColor(FLinearColor(0.984f, 0.482f, 0.010f, 1.0f)) , SelectedWireThickness(1.5f) { - NodeTitleColors.Emplace(EFlowNodeStyle::Condition, FLinearColor(1.0f, 0.62f, 0.016f, 1.0f)); - NodeTitleColors.Emplace(EFlowNodeStyle::Default, FLinearColor(-0.728f, 0.581f, 1.0f, 1.0f)); - NodeTitleColors.Emplace(EFlowNodeStyle::InOut, FLinearColor(1.0f, 0.0f, 0.008f, 1.0f)); - NodeTitleColors.Emplace(EFlowNodeStyle::Latent, FLinearColor(0.0f, 0.770f, 0.375f, 1.0f)); - NodeTitleColors.Emplace(EFlowNodeStyle::Logic, FLinearColor(1.0f, 1.0f, 1.0f, 1.0f)); - NodeTitleColors.Emplace(EFlowNodeStyle::SubGraph, FLinearColor(1.0f, 0.128f, 0.0f, 1.0f)); + NodePrefixesToRemove.Emplace("FN"); + NodePrefixesToRemove.Emplace("FlowNode"); + NodePrefixesToRemove.Emplace("FlowNodeAddOn"); +} + +void UFlowGraphSettings::PostInitProperties() +{ + Super::PostInitProperties(); + + NodePrefixesToRemove.Sort(TGreater{}); +} + +#if WITH_EDITOR + +void UFlowGraphSettings::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + const FName MemberPropertyName = PropertyChangedEvent.GetMemberPropertyName(); + + if (MemberPropertyName == GET_MEMBER_NAME_CHECKED( UFlowGraphSettings, NodePrefixesToRemove )) + { + // + // We need to sort items in array, because unsorted array can cause only partial prefix removal. + // For example, we have a NodeName = "UFlowNode_Custom" and these elements in the array: + // NodePrefixesToRemove = {"FN", "Flow", "FlowNode"}; + // Note: Prefix "U" is removed by Unreal + // + // First prefix does not match start of NodeName, so nothing changes. + // Second prefix match start of the name and is removed, NodeName becomes "Node_Custom" + // Third prefix does not match start of NodeName, so nothing changes. + // After complete process NodeName == "Node_Custom", but expected result is "Custom" + // + // If NodePrefixesToRemove = {"FN", "FlowNode", "Flow"} instead, everything will be removed as expected. + // + + if (FlowArray::TrySortAndRemoveDuplicatesFromArrayInPlace(NodePrefixesToRemove)) + { + // error notification + FNotificationInfo Info(LOCTEXT("FlowGraphSettings_DuplicatePrefixError", "Added prefix already exists in array.")); + Info.ExpireDuration = 3.0f; + FSlateNotificationManager::Get().AddNotification(Info)->SetCompletionState(SNotificationItem::CS_Fail); + } + else + { + UFlowGraphSchema::UpdateGeneratedDisplayNames(); + } + } + else if (MemberPropertyName == GET_MEMBER_NAME_CHECKED(UFlowGraphSettings, NodeDisplayStyles)) + { + if (FlowArray::TrySortAndRemoveDuplicatesFromArrayInPlace(NodeDisplayStyles)) + { + // error notification + FNotificationInfo Info(LOCTEXT("FlowGraphSettings_DuplicateNodeDisplayStyleError", "Added NodeDisplayStyle already exists in array.")); + Info.ExpireDuration = 3.0f; + FSlateNotificationManager::Get().AddNotification(Info)->SetCompletionState(SNotificationItem::CS_Fail); + } + } } +FString UFlowGraphSettings::GetNodeCategoryForNode(const UFlowNodeBase& FlowNodeBase) +{ + const UFlowGraphSettings* GraphSettings = GetDefault(); + if (const FString* CategoryOverridenByUser = GraphSettings->OverridenNodeCategories.Find(FlowNodeBase.GetClass())) + { + return *CategoryOverridenByUser; + } + + return FlowNodeBase.GetNodeCategory(); +} + +const TMap& UFlowGraphSettings::EnsureNodeDisplayStylesMap() +{ + if (NodeDisplayStylesAuthoredTags.Num() != NodeDisplayStyles.Num()) + { + NodeDisplayStylesAuthoredTags.Reset(); + + // Create an expanded GameplayTag map that will allow the settings to be looked up by subtag + TMap UnexpandedMap; + UnexpandedMap.Reserve(NodeDisplayStyles.Num()); + + for (const FFlowNodeDisplayStyleConfig& Config : NodeDisplayStyles) + { + UnexpandedMap.Add(Config.Tag, Config); + NodeDisplayStylesAuthoredTags.AddTag(Config.Tag); + } + + // Expand the map + NodeDisplayStylesMap.Empty(); + FlowMap::PatchGameplayTagMap(UnexpandedMap, NodeDisplayStylesMap); + } + + return NodeDisplayStylesMap; +} + +void UFlowGraphSettings::TryAddDefaultNodeDisplayStyle(const FFlowNodeDisplayStyleConfig& StyleConfig) +{ + const int32 FoundIndex = + NodeDisplayStyles.FindLastByPredicate( + [&StyleConfig](const FFlowNodeDisplayStyleConfig& CurConfig) + { + if (CurConfig.Tag == StyleConfig.Tag) + { + return true; + } + + return false; + }); + + if (FoundIndex != INDEX_NONE) + { + // Keep the existing config + return; + } + + NodeDisplayStyles.Add(StyleConfig); + return; +} + +const FLinearColor* UFlowGraphSettings::LookupNodeTitleColorForNode(const UFlowNodeBase& FlowNodeBase) +{ + if (const FLinearColor* NodeSpecificColor = NodeSpecificColors.Find(FlowNodeBase.GetClass())) + { + return NodeSpecificColor; + } + + const FGameplayTag& StyleTag = FlowNodeBase.GetNodeDisplayStyle(); + const TMap& StyleMap = EnsureNodeDisplayStylesMap(); + + if (const FFlowNodeDisplayStyleConfig* Config = FlowMap::TryLookupGameplayTagKey(StyleTag, StyleMap, FlowNodeStyle::CategoryName)) + { + return &Config->TitleColor; + } + + return nullptr; +} + +#endif + #undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Graph/FlowGraphUtils.cpp b/Source/FlowEditor/Private/Graph/FlowGraphUtils.cpp index 81cecdd66..4e04b3dc5 100644 --- a/Source/FlowEditor/Private/Graph/FlowGraphUtils.cpp +++ b/Source/FlowEditor/Private/Graph/FlowGraphUtils.cpp @@ -3,23 +3,64 @@ #include "Graph/FlowGraphUtils.h" #include "Asset/FlowAssetEditor.h" #include "Graph/FlowGraph.h" +#include "Graph/FlowGraphSettings.h" #include "FlowAsset.h" #include "Toolkits/ToolkitManager.h" -TSharedPtr FFlowGraphUtils::GetFlowAssetEditor(const UObject* ObjectToFocusOn) +TSharedPtr FFlowGraphUtils::GetFlowAssetEditor(const UEdGraph* Graph) { - check(ObjectToFocusOn); + check(Graph); TSharedPtr FlowAssetEditor; - if (UFlowAsset* FlowAsset = Cast(ObjectToFocusOn)->GetFlowAsset()) + if (const UFlowAsset* FlowAsset = Cast(Graph)->GetFlowAsset()) { - const TSharedPtr FoundAssetEditor = FToolkitManager::Get().FindEditorForAsset(FlowAsset); - if (FoundAssetEditor.IsValid()) + FlowAssetEditor = GetFlowAssetEditor(FlowAsset); + } + return FlowAssetEditor; +} + +TSharedPtr FFlowGraphUtils::GetFlowAssetEditor(const UFlowAsset* FlowAsset) +{ + check(FlowAsset); + + TSharedPtr FlowAssetEditor; + const TSharedPtr FoundAssetEditor = FToolkitManager::Get().FindEditorForAsset(FlowAsset); + if (FoundAssetEditor.IsValid()) + { + FlowAssetEditor = StaticCastSharedPtr(FoundAssetEditor); + } + return FlowAssetEditor; +} + +TSharedPtr FFlowGraphUtils::GetFlowGraphEditor(const UEdGraph* Graph) +{ + TSharedPtr FlowGraphEditor; + + const TSharedPtr FlowEditor = GetFlowAssetEditor(Graph); + if (FlowEditor.IsValid()) + { + FlowGraphEditor = FlowEditor->GetFlowGraph(); + } + + return FlowGraphEditor; +} + +FString FFlowGraphUtils::RemovePrefixFromNodeText(const FText& Source) +{ + FString SourceString = Source.ToString(); + TArray NodePrefixes = GetDefault()->NodePrefixesToRemove; + + for (FString Prefix : NodePrefixes) + { + Prefix = FName::NameToDisplayString(Prefix, false); + if (SourceString.StartsWith(Prefix)) { - FlowAssetEditor = StaticCastSharedPtr(FoundAssetEditor); + SourceString.MidInline(Prefix.Len(), MAX_int32, EAllowShrinking::No); + SourceString = SourceString.TrimStart(); } } - return FlowAssetEditor; + + return SourceString; } diff --git a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp index b1b5b14d6..c97531e42 100644 --- a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp +++ b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp @@ -2,126 +2,83 @@ #include "Graph/Nodes/FlowGraphNode.h" -#include "Asset/FlowDebugger.h" +#include "FlowAsset.h" +#include "AddOns/FlowNodeAddOn.h" +#include "Nodes/FlowNode.h" + +#include "Debugger/FlowDebuggerSubsystem.h" + #include "FlowEditorCommands.h" #include "Graph/FlowGraph.h" #include "Graph/FlowGraphEditorSettings.h" #include "Graph/FlowGraphSchema.h" #include "Graph/FlowGraphSettings.h" +#include "Graph/Nodes/FlowGraphNode_Reroute.h" #include "Graph/Widgets/SFlowGraphNode.h" +#include "Graph/Widgets/SGraphEditorActionMenuFlow.h" +#include "Interfaces/FlowDataPinValueSupplierInterface.h" +#include "Types/FlowDataPinValue.h" -#include "FlowAsset.h" -#include "Nodes/FlowNode.h" - +#include "BlueprintNodeHelpers.h" #include "Developer/ToolMenus/Public/ToolMenus.h" -#include "EdGraph/EdGraphSchema.h" -#include "EdGraphSchema_K2.h" +#include "DiffResults.h" #include "Editor.h" -#include "Editor/EditorEngine.h" +#include "FlowLogChannels.h" #include "Framework/Commands/GenericCommands.h" +#include "GraphDiffControl.h" #include "GraphEditorActions.h" +#include "HAL/FileManager.h" #include "Kismet2/KismetEditorUtilities.h" #include "ScopedTransaction.h" #include "SourceCodeNavigation.h" +#include "Subsystems/AssetEditorSubsystem.h" #include "Textures/SlateIcon.h" #include "ToolMenuSection.h" -#include "UnrealEd.h" - -#define LOCTEXT_NAMESPACE "FlowGraphNode" - -////////////////////////////////////////////////////////////////////////// -// Flow Breakpoint - -void FFlowBreakpoint::AddBreakpoint() -{ - if (!bHasBreakpoint) - { - bHasBreakpoint = true; - bBreakpointEnabled = true; - } -} - -void FFlowBreakpoint::RemoveBreakpoint() -{ - if (bHasBreakpoint) - { - bHasBreakpoint = false; - bBreakpointEnabled = false; - } -} - -bool FFlowBreakpoint::HasBreakpoint() const -{ - return bHasBreakpoint; -} - -void FFlowBreakpoint::EnableBreakpoint() -{ - if (bHasBreakpoint && !bBreakpointEnabled) - { - bBreakpointEnabled = true; - } -} - -bool FFlowBreakpoint::CanEnableBreakpoint() const -{ - return bHasBreakpoint && !bBreakpointEnabled; -} - -void FFlowBreakpoint::DisableBreakpoint() -{ - if (bHasBreakpoint && bBreakpointEnabled) - { - bBreakpointEnabled = false; - } -} +#include "Editor/Transactor.h" -bool FFlowBreakpoint::IsBreakpointEnabled() const -{ - return bHasBreakpoint && bBreakpointEnabled; -} - -void FFlowBreakpoint::ToggleBreakpoint() -{ - if (bHasBreakpoint) - { - bHasBreakpoint = false; - bBreakpointEnabled = false; - } - else - { - bHasBreakpoint = true; - bBreakpointEnabled = true; - } -} +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowGraphNode) -////////////////////////////////////////////////////////////////////////// -// Flow Graph Node +#define LOCTEXT_NAMESPACE "FlowGraphNode" UFlowGraphNode::UFlowGraphNode(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) - , FlowNode(nullptr) + , NodeInstance(nullptr) , bBlueprintCompilationPending(false) + , bIsReconstructingNode(false) + , bIsDestroyingNode(false) , bNeedsFullReconstruction(false) { OrphanedPinSaveMode = ESaveOrphanPinMode::SaveAll; } -void UFlowGraphNode::SetFlowNode(UFlowNode* InFlowNode) +void UFlowGraphNode::SetNodeTemplate(UFlowNodeBase* InNodeInstance) +{ + ensure(InNodeInstance); + NodeInstance = InNodeInstance; + NodeInstanceClass = InNodeInstance->GetClass(); +} + +const UFlowNodeBase* UFlowGraphNode::GetNodeTemplate() const { - FlowNode = InFlowNode; + return NodeInstance; } -UFlowNode* UFlowGraphNode::GetFlowNode() const +UFlowNodeBase* UFlowGraphNode::GetFlowNodeBase() const { - if (FlowNode) + if (NodeInstance) { - if (const UFlowAsset* InspectedInstance = FlowNode->GetFlowAsset()->GetInspectedInstance()) + if (const UFlowNode* FlowNode = Cast(NodeInstance)) { - return InspectedInstance->GetNode(FlowNode->GetGuid()); + if (const UFlowAsset* FlowAsset = FlowNode->GetFlowAsset()) + { + if (const UFlowAsset* InspectedInstance = FlowAsset->GetInspectedInstance()) + { + return InspectedInstance->GetNode(FlowNode->GetGuid()); + } + } } - return FlowNode; + return NodeInstance; } return nullptr; @@ -131,13 +88,13 @@ void UFlowGraphNode::PostLoad() { Super::PostLoad(); - if (FlowNode) + if (NodeInstance) { - FlowNode->FixNode(this); // fix already created nodes + NodeInstance->FixNode(this); // fix already created nodes SubscribeToExternalChanges(); } - ReconstructNode(); + RebuildPinArraysOnLoad(); } void UFlowGraphNode::PostDuplicate(bool bDuplicateForPIE) @@ -148,6 +105,7 @@ void UFlowGraphNode::PostDuplicate(bool bDuplicateForPIE) { CreateNewGuid(); + UFlowNode* FlowNode = Cast(NodeInstance); if (FlowNode && FlowNode->GetFlowAsset()) { FlowNode->GetFlowAsset()->RegisterNode(NodeGuid, FlowNode); @@ -161,6 +119,14 @@ void UFlowGraphNode::PostEditImport() PostCopyNode(); SubscribeToExternalChanges(); + + // Reset the owning graph after an edit import + ResetNodeOwner(); + UpdateNodeClassData(); + if (NodeInstance) + { + InitializeInstance(); + } } void UFlowGraphNode::PostPlacedNewNode() @@ -168,75 +134,107 @@ void UFlowGraphNode::PostPlacedNewNode() Super::PostPlacedNewNode(); SubscribeToExternalChanges(); + + // note: NodeInstance can be already spawned by paste operation, don't override it + if (NodeInstanceClass.IsPending()) + { + NodeInstanceClass.LoadSynchronous(); + } + + if (NodeInstance == nullptr) + { + if (const UClass* NodeClass = NodeInstanceClass.Get()) + { + const UEdGraph* Graph = GetGraph(); + if (Graph && Graph->GetOuter()) + { + NodeInstance = NewObject(Graph->GetOuter(), NodeClass); + NodeInstance->SetFlags(RF_Transactional); + + InitializeInstance(); + } + } + } } void UFlowGraphNode::PrepareForCopying() { Super::PrepareForCopying(); - if (FlowNode) + if (NodeInstance) { - // Temporarily take ownership of the FlowNode, so that it is not deleted when cutting - FlowNode->Rename(nullptr, this, REN_DontCreateRedirectors); + // Temporarily take ownership of the node instance, so that it is not deleted when cutting + NodeInstance->Rename(nullptr, this, REN_DontCreateRedirectors | REN_DoNotDirty); } } +void UFlowGraphNode::PostPasteNode() +{ + Super::PostPasteNode(); + //prep reconstruct the node, necessary for copy-paste to handle the reconstruct. + bNeedsFullReconstruction = true; +} + void UFlowGraphNode::PostCopyNode() { - // Make sure this FlowNode is owned by the FlowAsset it's being pasted into - if (FlowNode) + // Make sure this NodeInstance is owned by the FlowAsset it's being pasted into + if (NodeInstance) { - UFlowAsset* FlowAsset = CastChecked(GetGraph())->GetFlowAsset(); + UFlowAsset* FlowAsset = GetFlowAsset(); - if (FlowNode->GetOuter() != FlowAsset) + if (NodeInstance->GetOuter() != FlowAsset) { - // Ensures FlowNode is owned by the FlowAsset - FlowNode->Rename(nullptr, FlowAsset, REN_DontCreateRedirectors); + // Ensures NodeInstance is owned by the FlowAsset + NodeInstance->Rename(nullptr, FlowAsset, REN_DontCreateRedirectors | REN_DoNotDirty); } - FlowNode->SetGraphNode(this); + NodeInstance->SetGraphNode(this); } + + // Reset the node's owning graph prior to copying + ResetNodeOwner(); } void UFlowGraphNode::SubscribeToExternalChanges() { - if (FlowNode) + if (NodeInstance) { - FlowNode->OnReconstructionRequested.BindUObject(this, &UFlowGraphNode::OnExternalChange); + NodeInstance->OnReconstructionRequested.BindUObject(this, &UFlowGraphNode::OnExternalChange); - // blueprint nodes - if (FlowNode->GetClass()->ClassGeneratedBy && GEditor) + for (UFlowGraphNode* SubNode : SubNodes) { - GEditor->OnBlueprintPreCompile().AddUObject(this, &UFlowGraphNode::OnBlueprintPreCompile); - GEditor->OnBlueprintCompiled().AddUObject(this, &UFlowGraphNode::OnBlueprintCompiled); + if (SubNode->NodeInstance) + { + SubNode->NodeInstance->OnAddOnRequestedParentReconstruction.BindUObject(this, &UFlowGraphNode::OnExternalChange); + } } } } -void UFlowGraphNode::OnBlueprintPreCompile(UBlueprint* Blueprint) +void UFlowGraphNode::OnExternalChange() { - if (Blueprint && Blueprint == FlowNode->GetClass()->ClassGeneratedBy) + if (bIsReconstructingNode) { - bBlueprintCompilationPending = true; + return; } -} -void UFlowGraphNode::OnBlueprintCompiled() -{ - if (bBlueprintCompilationPending) - { - OnExternalChange(); - } + // Do not create transaction here, since this function triggers from modifying UFlowNode's property, which itself already made inside of transaction. + Modify(); + + bNeedsFullReconstruction = true; + ReconstructNode(); - bBlueprintCompilationPending = false; + GetGraph()->NotifyNodeChanged(this); } -void UFlowGraphNode::OnExternalChange() +void UFlowGraphNode::OnGraphRefresh() { - bNeedsFullReconstruction = true; - ReconstructNode(); - GetGraph()->NotifyGraphChanged(); +} + +bool UFlowGraphNode::CanPlaceBreakpoints() const +{ + return true; } bool UFlowGraphNode::CanCreateUnderSpecifiedSchema(const UEdGraphSchema* Schema) const @@ -317,38 +315,64 @@ void UFlowGraphNode::InsertNewNode(UEdGraphPin* FromPin, UEdGraphPin* NewLinkPin void UFlowGraphNode::ReconstructNode() { - // Store old pins - TArray OldPins(Pins); + if (!CanReconstructNode()) + { + return; + } + + TGuardValue GuardIsResonstructingNode(bIsReconstructingNode, true); - // Reset pin arrays - Pins.Reset(); - InputPins.Reset(); - OutputPins.Reset(); + FScopedTransaction Transaction(LOCTEXT("ReconstructNode", "Reconstruct Node"), !GUndo); - // Recreate pins - if (SupportsContextPins() && (FlowNode->CanRefreshContextPinsOnLoad() || bNeedsFullReconstruction)) + if (UFlowNode* FlowNode = Cast(NodeInstance)) { - RefreshContextPins(false); + FlowNode->SetupForEditing(*this); } - AllocateDefaultPins(); - RewireOldPinsToNewPins(OldPins); - // Destroy old pins - for (UEdGraphPin* OldPin : OldPins) + const bool bAnyPinsUpdated = TryUpdateNodePins(); // Updates all pins of the Flow Node (native pins, meta auto pins, and context pins which include data pins for now) + const bool bAreGraphPinsMismatched = !CheckGraphPinsMatchNodePins(); // This must be called last since it checks the existing graph node against the cleaned up Flow Node instance + + // Does Graph Node requires reconstruction? + if (bNeedsFullReconstruction || bAnyPinsUpdated || bAreGraphPinsMismatched) { - OldPin->Modify(); - OldPin->BreakAllPinLinks(); - DestroyPin(OldPin); + Modify(); + + TArray OldPins(Pins); + + Pins.Reset(); + InputPins.Reset(); + OutputPins.Reset(); + + AllocateDefaultPins(); + RewireOldPinsToNewPins(OldPins); + + // Destroy old pins + for (UEdGraphPin* OldPin : OldPins) + { + OldPin->Modify(); + OldPin->BreakAllPinLinks(); + DestroyPin(OldPin); + } + + // Clear breakpoints for destroyed pins + if (UFlowDebuggerSubsystem* DebuggerSubsystem = GEngine->GetEngineSubsystem()) + { + DebuggerSubsystem->RemoveObsoletePinBreakpoints(this); + } + + bNeedsFullReconstruction = false; } - bNeedsFullReconstruction = false; + // This ensures the graph editor 'Refresh' button still rebuilds all the graph widgets even if the FlowGraphNode has nothing to update + // Ideally we could get rid of the 'Refresh' button, but I think it will keep being useful, esp. for users making rough custom widgets + (void)OnReconstructNodeCompleted.ExecuteIfBound(); } void UFlowGraphNode::AllocateDefaultPins() { check(Pins.Num() == 0); - if (FlowNode) + if (UFlowNode* FlowNode = Cast(NodeInstance)) { for (const FFlowPin& InputPin : FlowNode->InputPins) { @@ -417,7 +441,7 @@ void UFlowGraphNode::RewireOldPinsToNewPins(TArray& InOldPins) OldPin->bOrphanedPin = true; OldPin->bNotConnectable = true; OrphanedOldPins.Add(OldPin); - InOldPins.RemoveAt(OldPinIndex, 1, false); + InOldPins.RemoveAt(OldPinIndex, 1, EAllowShrinking::No); } } @@ -428,6 +452,17 @@ void UFlowGraphNode::RewireOldPinsToNewPins(TArray& InOldPins) if (OrphanedPin->ParentPin == nullptr) { Pins.Add(OrphanedPin); + + switch (OrphanedPin->Direction) + { + case EGPD_Input: + InputPins.Add(OrphanedPin); + break; + case EGPD_Output: + OutputPins.Add(OrphanedPin); + break; + default: ; + } } } } @@ -438,16 +473,6 @@ void UFlowGraphNode::ReconstructSinglePin(UEdGraphPin* NewPin, UEdGraphPin* OldP // Copy over modified persistent data NewPin->MovePersistentDataFromOldPin(*OldPin); - - // Update the in breakpoints as the old pin will be going the way of the dodo - for (TPair& PinBreakpoint : PinBreakpoints) - { - if (PinBreakpoint.Key.Get() == OldPin) - { - PinBreakpoint.Key = NewPin; - break; - } - } } void UFlowGraphNode::GetNodeContextMenuActions(class UToolMenu* Menu, class UGraphNodeContextMenuContext* Context) const @@ -491,18 +516,29 @@ void UFlowGraphNode::GetNodeContextMenuActions(class UToolMenu* Menu, class UGra } else if (Context->Node) { + { + FToolMenuSection& Section = Menu->AddSection("FlowGraphNodeAddOns", LOCTEXT("NodeAddOnsMenuHeader", "AddOns")); + Section.AddSubMenu( + "AttachAddOn", + LOCTEXT("AttachAddOn", "Attach AddOn..."), + LOCTEXT("AttachAddOnTooltip", "Attaches an AddOn to the Node"), + FNewToolMenuDelegate::CreateUObject(this, &UFlowGraphNode::CreateAttachAddOnSubMenu, static_cast(Context->Graph)) + ); + } + { FToolMenuSection& Section = Menu->AddSection("FlowGraphNodeActions", LOCTEXT("NodeActionsMenuHeader", "Node Actions")); Section.AddMenuEntry(GenericCommands.Delete); Section.AddMenuEntry(GenericCommands.Cut); Section.AddMenuEntry(GenericCommands.Copy); Section.AddMenuEntry(GenericCommands.Duplicate); + Section.AddMenuEntry(GenericCommands.Paste); Section.AddMenuEntry(GraphCommands.BreakNodeLinks); if (SupportsContextPins()) { - Section.AddMenuEntry(FlowGraphCommands.RefreshContextPins); + Section.AddMenuEntry(FlowGraphCommands.ReconstructNode); } if (CanUserAddInput()) @@ -524,6 +560,22 @@ void UFlowGraphNode::GetNodeContextMenuActions(class UToolMenu* Menu, class UGra Section.AddMenuEntry(GraphCommands.ToggleBreakpoint); } + { + FToolMenuSection& Section = Menu->AddSection("FlowGraphNodeExecutionOverride", LOCTEXT("NodeExecutionOverrideMenuHeader", "Execution Override")); + if (CanSetSignalMode(EFlowSignalMode::Enabled)) + { + Section.AddMenuEntry(FlowGraphCommands.EnableNode); + } + if (CanSetSignalMode(EFlowSignalMode::Disabled)) + { + Section.AddMenuEntry(FlowGraphCommands.DisableNode); + } + if (CanSetSignalMode(EFlowSignalMode::PassThrough)) + { + Section.AddMenuEntry(FlowGraphCommands.SetPassThrough); + } + } + { FToolMenuSection& Section = Menu->AddSection("FlowGraphNodeJumps", LOCTEXT("NodeJumpsMenuHeader", "Jumps")); if (CanFocusViewport()) @@ -535,17 +587,77 @@ void UFlowGraphNode::GetNodeContextMenuActions(class UToolMenu* Menu, class UGra Section.AddMenuEntry(FlowGraphCommands.JumpToNodeDefinition); } } + + { + FToolMenuSection& Section = Menu->AddSection("FlowGraphNodeOrganisation", LOCTEXT("NodeOrganisation", "Organisation")); + Section.AddSubMenu("Alignment", LOCTEXT("AlignmentHeader", "Alignment"), FText(), FNewToolMenuDelegate::CreateLambda([](UToolMenu* SubMenu) + { + FToolMenuSection& SubMenuSection = SubMenu->AddSection("EdGraphSchemaAlignment", LOCTEXT("AlignHeader", "Align")); + SubMenuSection.AddMenuEntry(FGraphEditorCommands::Get().AlignNodesTop); + SubMenuSection.AddMenuEntry(FGraphEditorCommands::Get().AlignNodesMiddle); + SubMenuSection.AddMenuEntry(FGraphEditorCommands::Get().AlignNodesBottom); + SubMenuSection.AddMenuEntry(FGraphEditorCommands::Get().AlignNodesLeft); + SubMenuSection.AddMenuEntry(FGraphEditorCommands::Get().AlignNodesCenter); + SubMenuSection.AddMenuEntry(FGraphEditorCommands::Get().AlignNodesRight); + SubMenuSection.AddMenuEntry(FGraphEditorCommands::Get().StraightenConnections); + })); + } } } +void UFlowGraphNode::CreateAttachAddOnSubMenu(UToolMenu* Menu, UEdGraph* Graph) const +{ + UFlowGraphNode* MutableThis = const_cast(this); + + const TSharedRef Widget = + SNew(SGraphEditorActionMenuFlow) + .GraphObj(Graph) + .GraphNode(MutableThis) + .AutoExpandActionMenu(true); + + Menu->AddMenuEntry("Section", FToolMenuEntry::InitWidget("Widget", Widget, FText(), true)); +} + bool UFlowGraphNode::CanUserDeleteNode() const { - return FlowNode ? FlowNode->bCanDelete : Super::CanUserDeleteNode(); + return NodeInstance ? NodeInstance->bCanDelete : Super::CanUserDeleteNode(); } bool UFlowGraphNode::CanDuplicateNode() const { - return FlowNode ? FlowNode->bCanDuplicate : Super::CanDuplicateNode(); + if (NodeInstance) + { + return NodeInstance->bCanDuplicate; + } + + // support code paths calling this method on CDO, where there's no Flow Node Instance + if (AssignedNodeClasses.Num() > 0) + { + // we simply allow action if any Assigned Node Class accepts it, as the action is disallowed in special node likes StartNode + for (const UClass* Class : AssignedNodeClasses) + { + const UFlowNode* NodeDefaults = Class->GetDefaultObject(); + if (NodeDefaults && NodeDefaults->bCanDuplicate) + { + return true; + } + } + + return false; + } + + return true; +} + +bool UFlowGraphNode::CanPasteHere(const UEdGraph* TargetGraph) const +{ + const UFlowGraph* FlowGraph = Cast(TargetGraph); + if (FlowGraph == nullptr) + { + return false; + } + + return Super::CanPasteHere(TargetGraph) && FlowGraph->GetFlowAsset()->IsNodeOrAddOnClassAllowed(NodeInstanceClass.Get()); } TSharedPtr UFlowGraphNode::CreateVisualWidget() @@ -555,29 +667,29 @@ TSharedPtr UFlowGraphNode::CreateVisualWidget() FText UFlowGraphNode::GetNodeTitle(ENodeTitleType::Type TitleType) const { - if (FlowNode) + if (NodeInstance) { - if (UFlowGraphEditorSettings::Get()->bShowNodeClass) + if (GetDefault()->bShowNodeClass) { FString CleanAssetName; - if (FlowNode->GetClass()->ClassGeneratedBy) + if (NodeInstance->GetClass()->ClassGeneratedBy) { - FlowNode->GetClass()->GetPathName(nullptr, CleanAssetName); + NodeInstance->GetClass()->GetPathName(nullptr, CleanAssetName); const int32 SubStringIdx = CleanAssetName.Find(".", ESearchCase::IgnoreCase, ESearchDir::FromEnd); CleanAssetName.LeftInline(SubStringIdx); } else { - CleanAssetName = FlowNode->GetClass()->GetName(); + CleanAssetName = NodeInstance->GetClass()->GetName(); } FFormatNamedArguments Args; - Args.Add(TEXT("NodeTitle"), FlowNode->GetNodeTitle()); + Args.Add(TEXT("NodeTitle"), NodeInstance->GetNodeTitle()); Args.Add(TEXT("AssetName"), FText::FromString(CleanAssetName)); return FText::Format(INVTEXT("{NodeTitle}\n{AssetName}"), Args); } - return FlowNode->GetNodeTitle(); + return NodeInstance->GetNodeTitle(); } return Super::GetNodeTitle(TitleType); @@ -585,20 +697,15 @@ FText UFlowGraphNode::GetNodeTitle(ENodeTitleType::Type TitleType) const FLinearColor UFlowGraphNode::GetNodeTitleColor() const { - if (FlowNode) + if (NodeInstance) { FLinearColor DynamicColor; - if (FlowNode->GetDynamicTitleColor(DynamicColor)) + if (NodeInstance->GetDynamicTitleColor(DynamicColor)) { return DynamicColor; } - UFlowGraphSettings* GraphSettings = UFlowGraphSettings::Get(); - if (const FLinearColor* NodeSpecificColor = GraphSettings->NodeSpecificColors.Find(FlowNode->GetClass())) - { - return *NodeSpecificColor; - } - if (const FLinearColor* StyleColor = GraphSettings->NodeTitleColors.Find(FlowNode->GetNodeStyle())) + if (const FLinearColor* StyleColor = GetMutableDefault()->LookupNodeTitleColorForNode(*NodeInstance)) { return *StyleColor; } @@ -615,9 +722,9 @@ FSlateIcon UFlowGraphNode::GetIconAndTint(FLinearColor& OutColor) const FText UFlowGraphNode::GetTooltipText() const { FText Tooltip; - if (FlowNode) + if (NodeInstance) { - Tooltip = FlowNode->GetClass()->GetToolTipText(); + Tooltip = NodeInstance->GetNodeToolTip(); } if (Tooltip.IsEmpty()) { @@ -628,21 +735,42 @@ FText UFlowGraphNode::GetTooltipText() const FString UFlowGraphNode::GetNodeDescription() const { - return FlowNode ? FlowNode->GetNodeDescription() : FString(); + if (NodeInstance && (GEditor->PlayWorld == nullptr || GetDefault()->bShowNodeDescriptionWhilePlaying)) + { + const UFlowGraphEditorSettings* GraphEditorSettings = GetDefault(); + if (GEditor->PlayWorld == nullptr || GraphEditorSettings->bShowNodeDescriptionWhilePlaying) + { + FString Result = NodeInstance->GetNodeDescription(); + + if (GraphEditorSettings->bShowAddonDescriptions) + { + FString AddonDescriptions = NodeInstance->GetAddOnDescriptions(); + if (!AddonDescriptions.IsEmpty()) + { + return Result.Append(LINE_TERMINATOR).Append(AddonDescriptions); + } + } + + return Result; + } + } + + return FString(); } UFlowNode* UFlowGraphNode::GetInspectedNodeInstance() const { + const UFlowNode* FlowNode = Cast(NodeInstance); return FlowNode ? FlowNode->GetInspectedInstance() : nullptr; } EFlowNodeState UFlowGraphNode::GetActivationState() const { - if (FlowNode) + if (const UFlowNode* FlowNode = Cast(NodeInstance)) { - if (const UFlowNode* NodeInstance = FlowNode->GetInspectedInstance()) + if (const UFlowNode* InspectedInstance = FlowNode->GetInspectedInstance()) { - return NodeInstance->GetActivationState(); + return InspectedInstance->GetActivationState(); } } @@ -651,11 +779,11 @@ EFlowNodeState UFlowGraphNode::GetActivationState() const FString UFlowGraphNode::GetStatusString() const { - if (FlowNode) + if (const UFlowNode* FlowNode = Cast(NodeInstance)) { - if (const UFlowNode* NodeInstance = FlowNode->GetInspectedInstance()) + if (const UFlowNode* InspectedInstance = FlowNode->GetInspectedInstance()) { - return NodeInstance->GetStatusString(); + return InspectedInstance->GetStatusStringForNodeAndAddOns(); } } @@ -664,28 +792,28 @@ FString UFlowGraphNode::GetStatusString() const FLinearColor UFlowGraphNode::GetStatusBackgroundColor() const { - if (FlowNode) + if (const UFlowNode* FlowNode = Cast(NodeInstance)) { - if (const UFlowNode* NodeInstance = FlowNode->GetInspectedInstance()) + if (const UFlowNode* InspectedInstance = FlowNode->GetInspectedInstance()) { FLinearColor ObtainedColor; - if (NodeInstance->GetStatusBackgroundColor(ObtainedColor)) + if (InspectedInstance->GetStatusBackgroundColor(ObtainedColor)) { return ObtainedColor; } } } - return UFlowGraphSettings::Get()->NodeStatusBackground; + return GetDefault()->NodeStatusBackground; } bool UFlowGraphNode::IsContentPreloaded() const { - if (FlowNode) + if (const UFlowNode* FlowNode = Cast(NodeInstance)) { - if (const UFlowNode* NodeInstance = FlowNode->GetInspectedInstance()) + if (const UFlowNode* InspectedInstance = FlowNode->GetInspectedInstance()) { - return NodeInstance->bPreloaded; + return InspectedInstance->IsContentPreloaded(); } } @@ -694,24 +822,24 @@ bool UFlowGraphNode::IsContentPreloaded() const bool UFlowGraphNode::CanFocusViewport() const { + UFlowNode* FlowNode = Cast(NodeInstance); return FlowNode ? (GEditor->bIsSimulatingInEditor && FlowNode->GetActorToFocus()) : false; } bool UFlowGraphNode::CanJumpToDefinition() const { - return FlowNode != nullptr; + return NodeInstance != nullptr; } void UFlowGraphNode::JumpToDefinition() const { - if (FlowNode) + if (NodeInstance) { - if (FlowNode->GetClass()->IsNative()) + if (NodeInstance->GetClass()->IsNative()) { - if (FSourceCodeNavigation::CanNavigateToClass(FlowNode->GetClass())) + if (FSourceCodeNavigation::CanNavigateToClass(NodeInstance->GetClass())) { - const bool bSucceeded = FSourceCodeNavigation::NavigateToClass(FlowNode->GetClass()); - if (bSucceeded) + if (FSourceCodeNavigation::NavigateToClass(NodeInstance->GetClass())) { return; } @@ -719,8 +847,7 @@ void UFlowGraphNode::JumpToDefinition() const // Failing that, fall back to the older method which will still get the file open assuming it exists FString NativeParentClassHeaderPath; - const bool bFileFound = FSourceCodeNavigation::FindClassHeaderPath(FlowNode->GetClass(), NativeParentClassHeaderPath) && (IFileManager::Get().FileSize(*NativeParentClassHeaderPath) != INDEX_NONE); - if (bFileFound) + if (FSourceCodeNavigation::FindClassHeaderPath(NodeInstance->GetClass(), NativeParentClassHeaderPath) && (IFileManager::Get().FileSize(*NativeParentClassHeaderPath) != INDEX_NONE)) { const FString AbsNativeParentClassHeaderPath = FPaths::ConvertRelativePathToFull(NativeParentClassHeaderPath); FSourceCodeNavigation::OpenSourceFile(AbsNativeParentClassHeaderPath); @@ -728,7 +855,58 @@ void UFlowGraphNode::JumpToDefinition() const } else { - FKismetEditorUtilities::BringKismetToFocusAttentionOnObject(FlowNode->GetClass()); + FKismetEditorUtilities::BringKismetToFocusAttentionOnObject(NodeInstance->GetClass()); + } + } +} + +bool UFlowGraphNode::SupportsCommentBubble() const +{ + if (IsSubNode()) + { + return false; + } + + return Super::SupportsCommentBubble(); +} + +void UFlowGraphNode::OnNodeDoubleClicked() const +{ + UFlowNodeBase* FlowNodeBase = GetFlowNodeBase(); + if (IsValid(FlowNodeBase)) + { + const EFlowNodeDoubleClickTarget DoubleClickTarget = GetDefault()->NodeDoubleClickTarget; + if (DoubleClickTarget == EFlowNodeDoubleClickTarget::NodeDefinition) + { + JumpToDefinition(); + } + else + { + FString AssetPath; + UObject* AssetToEdit = nullptr; + if (UFlowNode* FlowNode = Cast(FlowNodeBase)) + { + AssetPath = FlowNode->GetAssetPath(); + AssetToEdit = FlowNode->GetAssetToEdit(); + } + + if (!AssetPath.IsEmpty()) + { + GEditor->GetEditorSubsystem()->OpenEditorForAsset(AssetPath); + } + else if (AssetToEdit) + { + GEditor->GetEditorSubsystem()->OpenEditorForAsset(AssetToEdit); + + if (GEditor->PlayWorld != nullptr) + { + OnNodeDoubleClickedInPIE(); + } + } + else if (DoubleClickTarget == EFlowNodeDoubleClickTarget::PrimaryAssetOrNodeDefinition) + { + JumpToDefinition(); + } } } } @@ -740,8 +918,11 @@ void UFlowGraphNode::CreateInputPin(const FFlowPin& FlowPin, const int32 Index / return; } - const FEdGraphPinType PinType = FEdGraphPinType(UEdGraphSchema_K2::PC_Exec, FName(NAME_None), nullptr, EPinContainerType::None, false, FEdGraphTerminalType()); - UEdGraphPin* NewPin = CreatePin(EGPD_Input, PinType, FlowPin.PinName, Index); + const FEdGraphPinType EdGraphPinType = FlowPin.BuildEdGraphPinType(); + + check(!EdGraphPinType.PinCategory.IsNone()); + + UEdGraphPin* NewPin = CreatePin(EGPD_Input, EdGraphPinType, FlowPin.PinName, Index); check(NewPin); if (!FlowPin.PinFriendlyName.IsEmpty()) @@ -749,7 +930,7 @@ void UFlowGraphNode::CreateInputPin(const FFlowPin& FlowPin, const int32 Index / NewPin->bAllowFriendlyName = true; NewPin->PinFriendlyName = FlowPin.PinFriendlyName; } - + NewPin->PinToolTip = FlowPin.PinToolTip; InputPins.Emplace(NewPin); @@ -762,8 +943,10 @@ void UFlowGraphNode::CreateOutputPin(const FFlowPin& FlowPin, const int32 Index return; } - const FEdGraphPinType PinType = FEdGraphPinType(UEdGraphSchema_K2::PC_Exec, FName(NAME_None), nullptr, EPinContainerType::None, false, FEdGraphTerminalType()); - UEdGraphPin* NewPin = CreatePin(EGPD_Output, PinType, FlowPin.PinName, Index); + const FEdGraphPinType EdGraphPinType = FlowPin.BuildEdGraphPinType(); + check(!EdGraphPinType.PinCategory.IsNone()); + + UEdGraphPin* NewPin = CreatePin(EGPD_Output, EdGraphPinType, FlowPin.PinName, Index); check(NewPin); if (!FlowPin.PinFriendlyName.IsEmpty()) @@ -782,67 +965,96 @@ void UFlowGraphNode::RemoveOrphanedPin(UEdGraphPin* Pin) const FScopedTransaction Transaction(LOCTEXT("RemoveOrphanedPin", "Remove Orphaned Pin")); Modify(); - PinBreakpoints.Remove(Pin); + if (UFlowDebuggerSubsystem* DebuggerSubsystem = GEngine->GetEngineSubsystem()) + { + DebuggerSubsystem->RemovePinBreakpoint(NodeGuid, Pin->PinName); + } Pin->MarkAsGarbage(); Pins.Remove(Pin); ReconstructNode(); - GetGraph()->NotifyGraphChanged(); + + GetGraph()->NotifyNodeChanged(this); } bool UFlowGraphNode::SupportsContextPins() const { - return FlowNode && FlowNode->SupportsContextPins(); + return NodeInstance && NodeInstance->SupportsContextPins(); } bool UFlowGraphNode::CanUserAddInput() const { + const UFlowNode* FlowNode = Cast(NodeInstance); return FlowNode && FlowNode->CanUserAddInput() && InputPins.Num() < 256; } bool UFlowGraphNode::CanUserAddOutput() const { + const UFlowNode* FlowNode = Cast(NodeInstance); return FlowNode && FlowNode->CanUserAddOutput() && OutputPins.Num() < 256; } bool UFlowGraphNode::CanUserRemoveInput(const UEdGraphPin* Pin) const { - return FlowNode && FlowNode->InputPins.Num() > FlowNode->GetClass()->GetDefaultObject()->InputPins.Num(); + const UFlowNode* FlowNode = Cast(NodeInstance); + return FlowNode && !FlowNode->GetClass()->GetDefaultObject()->InputPins.Contains(Pin->PinName); } bool UFlowGraphNode::CanUserRemoveOutput(const UEdGraphPin* Pin) const { - return FlowNode && FlowNode->OutputPins.Num() > FlowNode->GetClass()->GetDefaultObject()->OutputPins.Num(); + const UFlowNode* FlowNode = Cast(NodeInstance); + return FlowNode && !FlowNode->GetClass()->GetDefaultObject()->OutputPins.Contains(Pin->PinName); } void UFlowGraphNode::AddUserInput() { - AddInstancePin(EGPD_Input, *FString::FromInt(InputPins.Num())); + const UFlowNode* FlowNode = Cast(NodeInstance); + AddInstancePin(EGPD_Input, FlowNode->CountNumberedInputs()); } void UFlowGraphNode::AddUserOutput() { - AddInstancePin(EGPD_Output, *FString::FromInt(OutputPins.Num())); + const UFlowNode* FlowNode = Cast(NodeInstance); + AddInstancePin(EGPD_Output, FlowNode->CountNumberedOutputs()); } -void UFlowGraphNode::AddInstancePin(const EEdGraphPinDirection Direction, const FName& PinName) +void UFlowGraphNode::AddInstancePin(const EEdGraphPinDirection Direction, const uint8 NumberedPinsAmount) { const FScopedTransaction Transaction(LOCTEXT("AddInstancePin", "Add Instance Pin")); Modify(); + const FFlowPin PinName = FFlowPin(FString::FromInt(NumberedPinsAmount)); + + UFlowNode* FlowNode = Cast(NodeInstance); if (Direction == EGPD_Input) { - FlowNode->InputPins.Emplace(PinName); - CreateInputPin(FlowNode->InputPins.Last()); + if (FlowNode->InputPins.IsValidIndex(NumberedPinsAmount)) + { + FlowNode->InputPins.Insert(PinName, NumberedPinsAmount); + } + else + { + FlowNode->InputPins.Add(PinName); + } + + CreateInputPin(PinName, NumberedPinsAmount); } else { - FlowNode->OutputPins.Emplace(PinName); - CreateOutputPin(FlowNode->OutputPins.Last()); + if (FlowNode->OutputPins.IsValidIndex(NumberedPinsAmount)) + { + FlowNode->OutputPins.Insert(PinName, NumberedPinsAmount); + } + else + { + FlowNode->OutputPins.Add(PinName); + } + + CreateOutputPin(PinName, FlowNode->InputPins.Num() + NumberedPinsAmount); } - GetGraph()->NotifyGraphChanged(); + GetGraph()->NotifyNodeChanged(this); } void UFlowGraphNode::RemoveInstancePin(UEdGraphPin* Pin) @@ -850,14 +1062,18 @@ void UFlowGraphNode::RemoveInstancePin(UEdGraphPin* Pin) const FScopedTransaction Transaction(LOCTEXT("RemoveInstancePin", "Remove Instance Pin")); Modify(); - PinBreakpoints.Remove(Pin); + if (UFlowDebuggerSubsystem* DebuggerSubsystem = GEngine->GetEngineSubsystem()) + { + DebuggerSubsystem->RemovePinBreakpoint(NodeGuid, Pin->PinName); + } + UFlowNode* FlowNode = Cast(NodeInstance); if (Pin->Direction == EGPD_Input) { if (InputPins.Contains(Pin)) { InputPins.Remove(Pin); - FlowNode->RemoveUserInput(); + FlowNode->RemoveUserInput(Pin->PinName); Pin->MarkAsGarbage(); Pins.Remove(Pin); @@ -868,7 +1084,7 @@ void UFlowGraphNode::RemoveInstancePin(UEdGraphPin* Pin) if (OutputPins.Contains(Pin)) { OutputPins.Remove(Pin); - FlowNode->RemoveUserOutput(); + FlowNode->RemoveUserOutput(Pin->PinName); Pin->MarkAsGarbage(); Pins.Remove(Pin); @@ -876,32 +1092,7 @@ void UFlowGraphNode::RemoveInstancePin(UEdGraphPin* Pin) } ReconstructNode(); - GetGraph()->NotifyGraphChanged(); -} - -void UFlowGraphNode::RefreshContextPins(const bool bReconstructNode) -{ - if (SupportsContextPins()) - { - const FScopedTransaction Transaction(LOCTEXT("RefreshContextPins", "Refresh Context Pins")); - Modify(); - - const UFlowNode* NodeDefaults = FlowNode->GetClass()->GetDefaultObject(); - - // recreate inputs - FlowNode->InputPins = NodeDefaults->InputPins; - FlowNode->AddInputPins(FlowNode->GetContextInputs()); - - // recreate outputs - FlowNode->OutputPins = NodeDefaults->OutputPins; - FlowNode->AddOutputPins(FlowNode->GetContextOutputs()); - - if (bReconstructNode) - { - ReconstructNode(); - GetGraph()->NotifyGraphChanged(); - } - } + GetGraph()->NotifyNodeChanged(this); } void UFlowGraphNode::GetPinHoverText(const UEdGraphPin& Pin, FString& HoverTextOut) const @@ -909,8 +1100,10 @@ void UFlowGraphNode::GetPinHoverText(const UEdGraphPin& Pin, FString& HoverTextO // start with the default hover text (from the pin's tool-tip) Super::GetPinHoverText(Pin, HoverTextOut); + const bool bHasValidPlayWorld = IsValid(GEditor->PlayWorld); + // add information on pin activations - if (GEditor->PlayWorld) + if (bHasValidPlayWorld) { if (const UFlowNode* InspectedNodeInstance = GetInspectedNodeInstance()) { @@ -920,11 +1113,7 @@ void UFlowGraphNode::GetPinHoverText(const UEdGraphPin& Pin, FString& HoverTextO } const TArray& PinRecords = InspectedNodeInstance->GetPinRecords(Pin.PinName, Pin.Direction); - if (PinRecords.Num() == 0) - { - HoverTextOut.Append(FPinRecord::NoActivations); - } - else + if (PinRecords.Num() > 0) { HoverTextOut.Append(FPinRecord::PinActivations); for (int32 i = 0; i < PinRecords.Num(); i++) @@ -932,99 +1121,1002 @@ void UFlowGraphNode::GetPinHoverText(const UEdGraphPin& Pin, FString& HoverTextO HoverTextOut.Append(LINE_TERMINATOR); HoverTextOut.Appendf(TEXT("%d) %s"), i + 1, *PinRecords[i].HumanReadableTime); - if (PinRecords[i].bForcedActivation) + switch (PinRecords[i].ActivationType) { + case EFlowPinActivationType::Default: + break; + case EFlowPinActivationType::Forced: HoverTextOut.Append(FPinRecord::ForcedActivation); + break; + case EFlowPinActivationType::PassThrough: + HoverTextOut.Append(FPinRecord::PassThroughActivation); + break; + default:; } } } } } -} -void UFlowGraphNode::OnInputTriggered(const int32 Index) -{ - if (InputPins.IsValidIndex(Index) && PinBreakpoints.Contains(InputPins[Index])) + // add information on data pin values (only for data pins) + const bool bIsDataPinCategory = !FFlowPin::IsExecPinCategory(Pin.PinType.PinCategory); + if (bIsDataPinCategory) { - PinBreakpoints[InputPins[Index]].bBreakpointHit = true; - TryPausingSession(true); - } + const UEdGraphPin* GraphPinObj = &Pin; - TryPausingSession(false); -} + // Prefer showing runtime values when PIE (consistent with activation history) + const UFlowNodeBase* FlowNodeBase = GetFlowNodeBase(); -void UFlowGraphNode::OnOutputTriggered(const int32 Index) -{ - if (OutputPins.IsValidIndex(Index) && PinBreakpoints.Contains(OutputPins[Index])) - { - PinBreakpoints[OutputPins[Index]].bBreakpointHit = true; - TryPausingSession(true); - } + if (bHasValidPlayWorld) + { + FlowNodeBase = GetInspectedNodeInstance(); + } - TryPausingSession(false); -} + FFlowDataPinResult DataResult(EFlowDataPinResolveResult::FailedNullFlowNodeBase); -void UFlowGraphNode::TryPausingSession(bool bPauseSession) -{ - // Node breakpoints waits on any pin triggered - if (NodeBreakpoint.IsBreakpointEnabled()) - { - NodeBreakpoint.bBreakpointHit = true; - bPauseSession = true; - } + if (IsValid(FlowNodeBase)) + { + DataResult = FlowNodeBase->TryResolveDataPin(GraphPinObj->PinName); + } - if (bPauseSession) - { - FEditorDelegates::ResumePIE.AddUObject(this, &UFlowGraphNode::OnResumePIE); - FEditorDelegates::EndPIE.AddUObject(this, &UFlowGraphNode::OnEndPIE); + FString ValueString; - FFlowDebugger::PausePlaySession(); - } + if (FlowPinType::IsSuccess(DataResult.Result) && DataResult.ResultValue.IsValid()) + { + const FFlowDataPinValue& Value = DataResult.ResultValue.Get(); + if (!Value.TryConvertValuesToString(ValueString)) + { + ValueString = TEXT(""); + } + } + else + { + ValueString = TEXT(""); + } + + if (!HoverTextOut.IsEmpty()) + { + HoverTextOut.Append(LINE_TERMINATOR).Append(LINE_TERMINATOR); + } + + HoverTextOut.Appendf(TEXT("Value: %s"), *ValueString); + } } -void UFlowGraphNode::OnResumePIE(const bool bIsSimulating) +void UFlowGraphNode::ForcePinActivation(const FEdGraphPinReference PinReference) const { - ResetBreakpoints(); + UFlowNode* InspectedNodeInstance = GetInspectedNodeInstance(); + if (InspectedNodeInstance == nullptr) + { + return; + } + + if (const UEdGraphPin* FoundPin = PinReference.Get()) + { + switch (FoundPin->Direction) + { + case EGPD_Input: + InspectedNodeInstance->TriggerInput(FoundPin->PinName, EFlowPinActivationType::Forced); + break; + case EGPD_Output: + InspectedNodeInstance->TriggerOutput(FoundPin->PinName, false, EFlowPinActivationType::Forced); + break; + default: ; + } + } } -void UFlowGraphNode::OnEndPIE(const bool bIsSimulating) +void UFlowGraphNode::SetSignalMode(const EFlowSignalMode Mode) { - ResetBreakpoints(); + UFlowNode* FlowNode = Cast(NodeInstance); + if (FlowNode && FlowNode->SignalMode != Mode) + { + FlowNode->Modify(); + FlowNode->SignalMode = Mode; + OnSignalModeChanged.ExecuteIfBound(); + } } -void UFlowGraphNode::ResetBreakpoints() +EFlowSignalMode UFlowGraphNode::GetSignalMode() const { - FEditorDelegates::ResumePIE.RemoveAll(this); - FEditorDelegates::EndPIE.RemoveAll(this); + if (IsSubNode()) + { + // SubNodes count as enabled for signal mode queries in the editor + return EFlowSignalMode::Enabled; + } - NodeBreakpoint.bBreakpointHit = false; - for (TPair& PinBreakpoint : PinBreakpoints) + const UFlowNode* FlowNode = Cast(NodeInstance); + if (IsValid(FlowNode)) + { + return FlowNode->SignalMode; + } + else { - PinBreakpoint.Value.bBreakpointHit = false; + return EFlowSignalMode::Disabled; } } -void UFlowGraphNode::ForcePinActivation(const FEdGraphPinReference PinReference) const +bool UFlowGraphNode::CanSetSignalMode(const EFlowSignalMode Mode) const { - UFlowNode* InspectedNodeInstance = GetInspectedNodeInstance(); - if (InspectedNodeInstance == nullptr) + const UFlowNode* FlowNode = Cast(NodeInstance); + return FlowNode ? (FlowNode->AllowedSignalModes.Contains(Mode) && FlowNode->SignalMode != Mode) : false; +} + +void UFlowGraphNode::InitializeInstance() +{ + check(NodeInstance); + + // link editor and runtime nodes together + NodeInstance->SetGraphNode(this); +} + +void UFlowGraphNode::PostEditUndo() +{ + UEdGraphNode::PostEditUndo(); + ResetNodeOwner(); + + if (ParentNode) { + ParentNode->SubNodes.AddUnique(this); + + ParentNode->RebuildRuntimeAddOnsFromEditorSubNodes(); + } + else + { + RebuildRuntimeAddOnsFromEditorSubNodes(); + } +} + +UFlowAsset* UFlowGraphNode::GetFlowAsset() const +{ + if (const UFlowGraph* FlowGraph = GetFlowGraph()) + { + if (UFlowAsset* FlowAsset = FlowGraph->GetFlowAsset()) + { + return FlowAsset; + } + } + + return nullptr; +} + +void UFlowGraphNode::LogError(const FString& MessageToLog, const UFlowNodeBase* FlowNodeBase) const +{ + if (const UFlowAsset* FlowAsset = GetFlowAsset()) + { + FlowAsset->LogError(MessageToLog, FlowNodeBase); + } +} + +void UFlowGraphNode::ResetNodeOwner() +{ + if (NodeInstance) + { + const UEdGraph* Graph = GetGraph(); + UObject* GraphOwner = Graph ? Graph->GetOuter() : nullptr; + + NodeInstance->Rename(nullptr, GraphOwner, REN_DontCreateRedirectors | REN_DoNotDirty); + NodeInstance->ClearFlags(RF_Transient); + + for (const TObjectPtr& SubNode : SubNodes) + { + SubNode->ResetNodeOwner(); + } + } +} + +FText UFlowGraphNode::GetDescription() const +{ + FString StoredClassName = NodeInstanceClass.GetAssetName(); + StoredClassName.RemoveFromEnd(TEXT("_C")); + + return FText::Format(LOCTEXT("NodeClassError", "Class {0} not found, make sure it's saved!"), FText::FromString(StoredClassName)); +} + +UEdGraphPin* UFlowGraphNode::GetInputPin(int32 InputIndex) const +{ + check(InputIndex >= 0); + + for (int32 PinIndex = 0, FoundInputs = 0; PinIndex < Pins.Num(); PinIndex++) + { + if (Pins[PinIndex]->Direction == EGPD_Input) + { + if (InputIndex == FoundInputs) + { + return Pins[PinIndex]; + } + else + { + FoundInputs++; + } + } + } + + return nullptr; +} + +UEdGraphPin* UFlowGraphNode::GetOutputPin(int32 InputIndex) const +{ + check(InputIndex >= 0); + + for (int32 PinIndex = 0, FoundInputs = 0; PinIndex < Pins.Num(); PinIndex++) + { + if (Pins[PinIndex]->Direction == EGPD_Output) + { + if (InputIndex == FoundInputs) + { + return Pins[PinIndex]; + } + else + { + FoundInputs++; + } + } + } + + return nullptr; +} + +UFlowGraph* UFlowGraphNode::GetFlowGraph() const +{ + return CastChecked(GetGraph()); +} + +bool UFlowGraphNode::IsSubNode() const +{ + return bIsSubNode || (ParentNode != nullptr); +} + +void UFlowGraphNode::NodeConnectionListChanged() +{ + Super::NodeConnectionListChanged(); + + if (UFlowGraph* Graph = GetFlowGraph()) + { + Graph->GetFlowAsset()->HarvestNodeConnections(Cast(GetFlowNodeBase())); + Graph->NotifyNodeChanged(this); + } +} + +FString UFlowGraphNode::GetPropertyNameAndValueForDiff(const FProperty* Prop, const uint8* PropertyAddr) const +{ + return BlueprintNodeHelpers::DescribeProperty(Prop, PropertyAddr); +} + +void UFlowGraphNode::SetParentNodeForSubNode(UFlowGraphNode* InParentNode) +{ + if (InParentNode) + { + // Once a SubNode, always a SubNode + bIsSubNode = true; + } + + ParentNode = InParentNode; +} + +UFlowGraphNode* UFlowGraphNode::GetRootFlowGraphNode() const +{ + UFlowGraphNode* Root = const_cast(this); + while (IsValid(Root) && Root->ParentNode) + { + Root = Root->ParentNode; + } + + return Root; +} + +void UFlowGraphNode::RequestReconstructOnRootFlowNode() const +{ + // Preferred path: ask the runtime AddOn to request reconstruction on its owning FlowNode. + // This is important because it resolves the correct owning FlowNode even for AddOn-inside-AddOn. + if (const UFlowNodeAddOn* ThisAsAddOn = Cast(NodeInstance)) + { + ThisAsAddOn->RequestReconstructionOnOwningFlowNode(); + return; } - if (const UEdGraphPin* FoundPin = PinReference.Get()) + // Fallback: if we're already a root FlowNode, reconstruct directly. + UFlowGraphNode* RootGraphNode = GetRootFlowGraphNode(); + if (!IsValid(RootGraphNode)) { - switch (FoundPin->Direction) + return; + } + + if (Cast(RootGraphNode->NodeInstance)) + { + RootGraphNode->MarkNeedsFullReconstruction(); + RootGraphNode->ReconstructNode(); + + if (UEdGraph* Graph = RootGraphNode->GetGraph()) + { + Graph->NotifyNodeChanged(RootGraphNode); + } + } +} + +void UFlowGraphNode::RebuildRuntimeAddOnsFromEditorSubNodes(bool bForceReconstructNode) +{ + // Whenever we change the SubNodes array, we need to mirror the changes + // across to the AddOns array in the runtime instance data + + if (IsValid(NodeInstance)) + { + TArray& NodeInstanceAddOns = NodeInstance->GetFlowNodeAddOnChildrenByEditor(); + NodeInstanceAddOns.Reset(); + NodeInstanceAddOns.Reserve(SubNodes.Num()); + + for (const UFlowGraphNode* SubNode : SubNodes) + { + if (!IsValid(SubNode)) + { + LogError(FString::Printf(TEXT("%s: Has unexpectedly null SubNode"), *GetName()), NodeInstance); + + continue; + } + + // Add the runtime AddOn to its runtime UFlowNode or UFlowNodeAddOn container + UFlowNodeAddOn* AddOnSubNodeInstance = Cast(SubNode->NodeInstance); + if (IsValid(AddOnSubNodeInstance)) + { + NodeInstanceAddOns.AddUnique(AddOnSubNodeInstance); + } + else + { + LogError(FString::Printf(TEXT("%s: SubNode is missing an AddOn NodeInstance"), *GetName()), NodeInstance); + } + } + } + + // Update the SubNodes as well + for (UFlowGraphNode* SubNode : SubNodes) + { + if (IsValid(SubNode)) + { + SubNode->RebuildRuntimeAddOnsFromEditorSubNodes(); + } + } + + // Reconstruct the context pins for all flow nodes after their AddOns have been processed + if (IsValid(NodeInstance) && NodeInstance->IsA()) + { + static thread_local bool bIsRebuildingForThisThread = false; + + if (!bIsRebuildingForThisThread) + { + TGuardValue GuardIsRebuilding(bIsRebuildingForThisThread, true); + + if (bForceReconstructNode) + { + MarkNeedsFullReconstruction(); + } + + // Now rebuild the EdGraphNode pins to match the updated FlowNode state. + ReconstructNode(); + } + } +} + +void UFlowGraphNode::FindDiffs(UEdGraphNode* OtherNode, FDiffResults& Results) +{ + Super::FindDiffs(OtherNode, Results); + + const UFlowGraphNode* OtherGraphNode = Cast(OtherNode); + if (!IsValid(OtherGraphNode)) + { + return; + } + + if (NodeInstance && OtherGraphNode->NodeInstance) + { + FDiffSingleResult Diff; + Diff.Diff = EDiffType::NODE_PROPERTY; + Diff.Node1 = this; + Diff.Node2 = OtherNode; + Diff.Object1 = NodeInstance; + Diff.Object2 = OtherGraphNode->NodeInstance; + Diff.ToolTip = LOCTEXT("DIF_NodeInstancePropertyToolTip", "A property of the node instance has changed"); + Diff.Category = EDiffType::MODIFICATION; + + DiffProperties(NodeInstance->GetClass(), OtherGraphNode->NodeInstance->GetClass(), NodeInstance, OtherGraphNode->NodeInstance, Results, Diff); + } + + DiffSubNodes(LOCTEXT("AddOnDiffDisplayName", "AddOn"), SubNodes, OtherGraphNode->SubNodes, Results); +} + +void UFlowGraphNode::DiffSubNodes(const FText& NodeTypeDisplayName, const TArray& LhsSubNodes, const TArray& RhsSubNodes, FDiffResults& Results) +{ + TArray NodeMatches; + TSet MatchedRhsNodes; + + FGraphDiffControl::FNodeDiffContext AdditiveDiffContext; + AdditiveDiffContext.NodeTypeDisplayName = NodeTypeDisplayName; + AdditiveDiffContext.bIsRootNode = false; + + // march through the all the nodes in the rhs and look for matches + for (UEdGraphNode* RhsSubNode : RhsSubNodes) + { + FGraphDiffControl::FNodeMatch NodeMatch; + NodeMatch.NewNode = RhsSubNode; + + // Do two passes, exact and soft + for (UEdGraphNode* LhsSubNode : LhsSubNodes) + { + if (FGraphDiffControl::IsNodeMatch(LhsSubNode, RhsSubNode, true, &NodeMatches)) + { + NodeMatch.OldNode = LhsSubNode; + break; + } + } + + if (NodeMatch.NewNode == nullptr) + { + for (UEdGraphNode* LhsSubNode : LhsSubNodes) + { + if (FGraphDiffControl::IsNodeMatch(LhsSubNode, RhsSubNode, false, &NodeMatches)) + { + NodeMatch.OldNode = LhsSubNode; + break; + } + } + } + + // if we found a corresponding node in the lhs graph, track it (so we can prevent future matches with the same nodes) + if (NodeMatch.IsValid()) + { + NodeMatches.Add(NodeMatch); + MatchedRhsNodes.Add(NodeMatch.OldNode); + } + + NodeMatch.Diff(AdditiveDiffContext, Results); + } + + FGraphDiffControl::FNodeDiffContext SubtractiveDiffContext = AdditiveDiffContext; + SubtractiveDiffContext.DiffMode = FGraphDiffControl::EDiffMode::Subtractive; + SubtractiveDiffContext.DiffFlags = FGraphDiffControl::EDiffFlags::NodeExistance; + + // go through the lhs nodes to catch ones that may have been missing from the rhs graph + for (UEdGraphNode* LhsSubNode : LhsSubNodes) + { + // if this node has already been matched, move on + if (!LhsSubNode || MatchedRhsNodes.Find(LhsSubNode)) + { + continue; + } + + // There can't be a matching node in RhsGraph because it would have been found above + FGraphDiffControl::FNodeMatch NodeMatch; + NodeMatch.NewNode = LhsSubNode; + + NodeMatch.Diff(SubtractiveDiffContext, Results); + } +} + +void UFlowGraphNode::AddSubNode(UFlowGraphNode* SubNode, class UEdGraph* ParentGraph) +{ + const FScopedTransaction Transaction(LOCTEXT("AddNode", "Add Node")); + ParentGraph->Modify(); + Modify(); + + SubNode->SetFlags(RF_Transactional); + + // set outer to be the graph so it doesn't go away + SubNode->Rename(nullptr, ParentGraph, REN_NonTransactional); + SubNode->SetParentNodeForSubNode(this); + + SubNode->CreateNewGuid(); + SubNode->PostPlacedNewNode(); + + SubNode->AllocateDefaultPins(); + SubNode->AutowireNewNode(nullptr); + + SubNode->NodePosX = 0; + SubNode->NodePosY = 0; + + SubNodes.Add(SubNode); + if (SubNode->NodeInstance) + { + SubNode->NodeInstance->OnAddOnRequestedParentReconstruction.BindUObject(this, &UFlowGraphNode::OnExternalChange); + } + OnSubNodeAdded(SubNode); + + ParentGraph->NotifyGraphChanged(); + GetFlowGraph()->UpdateAsset(); + + // Ensure pin rebuild bubbles to the owning FlowNode (important for AddOn-inside-AddOn). + // Avoid doing extra work while pasting/locked updates; UnlockUpdates will reconcile and rebuild. + if (const UFlowGraph* FlowGraph = GetFlowGraph()) + { + if (!FlowGraph->IsLocked()) + { + RequestReconstructOnRootFlowNode(); + } + } + + // NOTE - We do not need to RebuildRuntimeAddOnsFromEditorSubNodes here, because UpdateAsset() will do it +} + +void UFlowGraphNode::OnSubNodeAdded(UFlowGraphNode* SubNode) +{ + // Empty in base class +} + +void UFlowGraphNode::RemoveSubNode(UFlowGraphNode* SubNode) +{ + Modify(); + + if (SubNode && SubNode->NodeInstance) + { + SubNode->NodeInstance->OnAddOnRequestedParentReconstruction.Unbind(); + } + + SubNodes.RemoveSingle(SubNode); + + RebuildRuntimeAddOnsFromEditorSubNodes(); + + // Critical for nested AddOn trees: removing an AddOn can change the root FlowNode's auto/context pins. + RequestReconstructOnRootFlowNode(); + + OnSubNodeRemoved(SubNode); +} + +void UFlowGraphNode::RemoveAllSubNodes() +{ + for (UFlowGraphNode* SubNode : SubNodes) + { + if (SubNode && SubNode->NodeInstance) + { + SubNode->NodeInstance->OnAddOnRequestedParentReconstruction.Unbind(); + } + } + + SubNodes.Reset(); + + RebuildRuntimeAddOnsFromEditorSubNodes(); + + // Critical for nested AddOn trees: structural changes can change the root FlowNode's auto/context pins. + RequestReconstructOnRootFlowNode(); +} + +void UFlowGraphNode::OnSubNodeRemoved(UFlowGraphNode* SubNode) +{ + // Empty in base class +} + +int32 UFlowGraphNode::FindSubNodeDropIndex(UFlowGraphNode* SubNode) const +{ + const int32 InsertIndex = SubNodes.IndexOfByKey(SubNode); + return InsertIndex; +} + +void UFlowGraphNode::InsertSubNodeAt(UFlowGraphNode* SubNode, const int32 DropIndex) +{ + if (DropIndex > -1) + { + SubNodes.Insert(SubNode, DropIndex); + } + else + { + SubNodes.Add(SubNode); + } + + RebuildRuntimeAddOnsFromEditorSubNodes(); + + // Reparent/reorder can change the owning FlowNode's auto/context pins (esp. cross-parent drag/drop). + RequestReconstructOnRootFlowNode(); +} + +void UFlowGraphNode::DestroyNode() +{ + bIsDestroyingNode = true; + + if (ParentNode) + { + ParentNode->RemoveSubNode(this); + + ParentNode->RebuildRuntimeAddOnsFromEditorSubNodes(); + } + else + { + RebuildRuntimeAddOnsFromEditorSubNodes(); + } + + UEdGraphNode::DestroyNode(); + + bIsDestroyingNode = false; +} + +bool UFlowGraphNode::UsesBlueprint() const +{ + return NodeInstance && NodeInstance->GetClass()->HasAnyClassFlags(CLASS_CompiledFromBlueprint); +} + +bool UFlowGraphNode::RefreshNodeClass() +{ + bool bUpdated = false; + if (NodeInstance == nullptr) + { + if (NodeInstanceClass.IsPending()) + { + NodeInstanceClass.LoadSynchronous(); + } + + if (NodeInstanceClass.IsValid()) + { + PostPlacedNewNode(); + + bUpdated = (NodeInstance != nullptr); + } + } + + return bUpdated; +} + +void UFlowGraphNode::UpdateNodeClassData() +{ + if (NodeInstance) + { + NodeInstanceClass = NodeInstance->GetClass(); + } +} + +bool UFlowGraphNode::HasErrors() const +{ + return ErrorMessage.Len() > 0 || !IsValid(NodeInstance); +} + +void UFlowGraphNode::ValidateGraphNode(FFlowMessageLog& MessageLog) const +{ + // Verify that all input data pin connections are legal + + if (!NodeInstance) + { + // Missing the node instance! + MessageLog.Error(TEXT("FlowGraphNode is missing its UFlowNode instance!"), nullptr); + return; + } + + const UFlowGraphSchema* Schema = CastChecked(GetSchema()); + for (const UEdGraphPin* EdGraphPin : InputPins) + { + if (FFlowPin::IsExecPinCategory(EdGraphPin->PinType.PinCategory)) + { + continue; + } + + if (!EdGraphPin->HasAnyConnections()) + { + continue; + } + + for (UEdGraphPin* const ConnectedPin : EdGraphPin->LinkedTo) + { + const FPinConnectionResponse Response = Schema->CanCreateConnection(ConnectedPin, EdGraphPin); + + if (!Response.CanSafeConnect()) + { + MessageLog.Error(*FString::Printf(TEXT("Pin %s has invalid connection: %s"), *EdGraphPin->GetName(), *Response.Message.ToString()), NodeInstance); + } + } + } +} + +bool UFlowGraphNode::CanReconstructNode() const +{ + // Global states that should prevent ReconstructNode from running + if (GIsTransacting || bIsReconstructingNode || bIsDestroyingNode) + { + return false; + } + + // This should never happen + if (!ensureMsgf(IsValid(NodeInstance), TEXT("FlowGraphNode has no NodeInstance, graph may be corrupt! Flow Asset: %s"), *GetFlowAsset()->GetName())) + { + return false; + } + + // This should never happen + if (!ensureMsgf(IsValid(GetGraph()), TEXT("FlowGraphNode has no owner graph, graph may be corrupt! Flow Node Instance: %s"), *NodeInstance->GetName())) + { + return false; + } + + // Don't do anything if the Flow Graph is preventing it + if (const UFlowGraph* FlowGraph = GetFlowGraph()) + { + if (FlowGraph->IsSavingGraph()) + { + return false; + } + + if (FlowGraph->IsLocked()) + { + return false; + } + } + + return true; +} + +void CleanInvalidPins(TArray& Array) +{ + for (int i = Array.Num() - 1; i >= 0; --i) + { + if (!Array[i].IsValid()) + { + Array.RemoveAtSwap(i, EAllowShrinking::No); + } + } +} + +void CleanInvalidPins(TArray& Array) +{ + for (int i = Array.Num() - 1; i >= 0; --i) + { + if (Array[i]->bOrphanedPin) + { + Array.RemoveAtSwap(i, EAllowShrinking::No); + } + } +} + +bool CheckPinsMatch(const TArray& LeftPins, const TArray& RightPins) +{ + if (LeftPins.Num() != RightPins.Num()) + { + return false; + } + + for (const FFlowPin& Left : LeftPins) + { + // Do a deep pin match (not a simple name-only match) + auto PinsAreEqualPredicate = [&Left](const FFlowPin& Right) + { + return Left.DeepIsEqual(Right); + }; + + // For each required pin, make sure the existing pins array contains a pin that matches by name and type + if (!RightPins.ContainsByPredicate(PinsAreEqualPredicate)) + { + // Something didn't match! + return false; + } + } + + return true; +} + +bool CheckPinsMatch(const TArray& GraphPins, const TArray& NodePins) +{ + // Compare valid pin counts + if (GraphPins.Num() != NodePins.Num()) + { + return false; + } + + // Compare valid pin names + for (const FFlowPin& FlowNodePin : NodePins) + { + if (!GraphPins.ContainsByPredicate([&FlowNodePin](const UEdGraphPin* GraphNodePin) + { + return GraphNodePin->PinName == FlowNodePin.PinName && GraphNodePin->PinFriendlyName.EqualTo(FlowNodePin.PinFriendlyName); + })) + { + // Could not match the pin from the flow node with any of the EdPins array. + // we have a mismatch between the ed graph pins and the flow node, something changed. + return false; + } + } + + return true; +} + +bool UFlowGraphNode::TryUpdateNodePins() const +{ + UFlowNode* FlowNodeInstance = Cast(NodeInstance); + if (!IsValid(FlowNodeInstance)) + { + // default to having changed because we don't have a way to confirm that the pins have remained intact. + return true; + } + + // Ensure the AddOns for this FlowNode have their FlowNode pointer set + FlowNodeInstance->EnsureAddOnFlowNodePointersForEditor(); + + // Attempt to update auto-generated pins + // This must be called first, it updates the underlying data for data pins of the Flow Node + const bool bAutoDataPinsChanged = FlowNodeInstance->TryUpdateAutoDataPins(); + + // these check would be all ignored if a full reconstruction has been requested + if (!bNeedsFullReconstruction) + { + bool bLoadingGraph = false; + if (const UFlowGraph* FlowGraph = GetFlowGraph()) + { + bLoadingGraph = FlowGraph->IsLoadingGraph(); + } + + // Confirm that we should be refreshing context pins + const bool bShouldRefreshContextPins = SupportsContextPins() && (!bLoadingGraph || NodeInstance->CanRefreshContextPinsOnLoad() || bAutoDataPinsChanged); + if (!bShouldRefreshContextPins) + { + return false; + } + } + + // ------------ + // Get all pins of the FlowNode itself + const UFlowNode* FlowNodeCDO = FlowNodeInstance->GetClass()->GetDefaultObject(); + check(IsValid(FlowNodeCDO)); + + // Fix up old pins on the CDO + UFlowNode* MutableCDO = const_cast(FlowNodeCDO); + MutableCDO->EnsureAddOnFlowNodePointersForEditor(); + MutableCDO->FixupDataPinTypes(); + + const bool bIsRerouteGraphNode = (Cast(this) != nullptr); + + // We grab basic built-in input/output pins from: + // - CDO for regular nodes + // - INSTANCE for reroute nodes (reroute pins are adaptive and may legitimately differ from CDO defaults) + TArray RequiredNodeInputPins = bIsRerouteGraphNode ? FlowNodeInstance->GetInputPins() : FlowNodeCDO->GetInputPins(); + RequiredNodeInputPins.Append(FlowNodeInstance->GetContextInputs()); + CleanInvalidPins(RequiredNodeInputPins); + + TArray RequiredNodeOutputPins = bIsRerouteGraphNode ? FlowNodeInstance->GetOutputPins() : FlowNodeCDO->GetOutputPins(); + RequiredNodeOutputPins.Append(FlowNodeInstance->GetContextOutputs()); + CleanInvalidPins(RequiredNodeOutputPins); + + // ------------ + // Get all existing pins of the flow node instance + TArray ExistingNodeInputPins = FlowNodeInstance->GetInputPins(); + CleanInvalidPins(ExistingNodeInputPins); + + TArray ExistingNodeOutputPins = FlowNodeInstance->GetOutputPins(); + CleanInvalidPins(ExistingNodeOutputPins); + + // ------------ + // If required pins don't match existing pins, brute force replace them + // (unless the node allows user added inputs/outputs, in which case we cannot destroy them) + + bool bPinsChanged = false; + + if (!FlowNodeInstance->CanUserAddInput() && !CheckPinsMatch(RequiredNodeInputPins, ExistingNodeInputPins)) + { + FlowNodeInstance->Modify(); + + FlowNodeInstance->InputPins.Empty(RequiredNodeInputPins.Num()); + FlowNodeInstance->AddInputPins(RequiredNodeInputPins); // We could just copy it, but this function could do more things one day + + bPinsChanged = true; + } + + if (!FlowNodeInstance->CanUserAddOutput() && !CheckPinsMatch(RequiredNodeOutputPins, ExistingNodeOutputPins)) + { + FlowNodeInstance->Modify(); + + FlowNodeInstance->OutputPins.Empty(RequiredNodeOutputPins.Num()); + FlowNodeInstance->AddOutputPins(RequiredNodeOutputPins); // We could just copy it, but this function could do more things one day + + bPinsChanged = true; + } + + return bPinsChanged; +} + +bool UFlowGraphNode::CheckGraphPinsMatchNodePins() const +{ + const UFlowNode* FlowNodeInstance = Cast(NodeInstance); + if (!IsValid(FlowNodeInstance)) + { + return false; + } + + // Get the existing node pins - invalid pins need to be stripped from the check + TArray ExistingNodePins = FlowNodeInstance->GetInputPins(); + ExistingNodePins.Append(FlowNodeInstance->GetOutputPins()); + CleanInvalidPins(ExistingNodePins); + + // Get the current FlowGraphNode pins list - orphaned pins need to be stripped from the check + TArray AllGraphNodePins = Pins; + CleanInvalidPins(AllGraphNodePins); + + return CheckPinsMatch(AllGraphNodePins, ExistingNodePins); +} + +bool UFlowGraphNode::IsAncestorNode(const UFlowGraphNode& OtherNode) const +{ + const UFlowGraphNode* CurParentNode = ParentNode; + while (CurParentNode) + { + if (CurParentNode == &OtherNode) + { + return true; + } + + CurParentNode = CurParentNode->ParentNode; + } + + return false; +} + +void UFlowGraphNode::RebuildPinArraysOnLoad() +{ + for (UEdGraphPin* Pin : Pins) + { + switch (Pin->Direction) { case EGPD_Input: - InspectedNodeInstance->TriggerInput(FoundPin->PinName, true); + InputPins.Add(Pin); break; case EGPD_Output: - InspectedNodeInstance->TriggerOutput(FoundPin->PinName, false, true); + OutputPins.Add(Pin); break; - default: ; + default: + UE_LOG(LogFlow, Error, TEXT("Encountered Pin with invalid direction!")); + } + } +} + +bool UFlowGraphNode::CanAcceptSubNodeAsChild(const UFlowGraphNode& OtherSubNode, const TSet& AllRootSubNodesToPaste, FString* OutReasonString) const +{ + const UFlowNodeBase* OtherFlowNodeSubNode = OtherSubNode.NodeInstance; + + if (!OtherFlowNodeSubNode) + { + if (OutReasonString) + { + *OutReasonString = TEXT("Editor node is missing a runtime AddOn instance"); + } + + return false; + } + + if (IsAncestorNode(OtherSubNode)) + { + if (OutReasonString) + { + *OutReasonString = TEXT("Cannot be a AddOn of one of our own AddOns"); } + + return false; } + + check(OtherFlowNodeSubNode); + const UFlowNodeAddOn* AddOnToConsider = Cast(OtherFlowNodeSubNode); + + // Build the array of other root AddOns that will also be added as children as an atomic operation (eg, multi-paste) + TArray OtherAddOnsToPaste; + + for (TSet::TConstIterator It(AllRootSubNodesToPaste); It; ++It) + { + const UFlowGraphNode* NodeToPaste = Cast(*It); + UFlowNodeAddOn* AddOnToPaste = Cast(NodeToPaste->NodeInstance); + + if (IsValid(AddOnToPaste) && AddOnToPaste != AddOnToConsider) + { + OtherAddOnsToPaste.Add(AddOnToPaste); + } + } + + const UFlowNodeBase* ThisFlowNodeBase = NodeInstance; + const EFlowAddOnAcceptResult AcceptResult = ThisFlowNodeBase->CheckAcceptFlowNodeAddOnChild(AddOnToConsider, OtherAddOnsToPaste); + + // Undetermined and Reject both count as Rejection, only TentativeAccept is an 'accept' result + + if (AcceptResult == EFlowAddOnAcceptResult::TentativeAccept) + { + FLOW_ASSERT_ENUM_MAX(EFlowAddOnAcceptResult, 3); + + return true; + } + + if (OutReasonString) + { + *OutReasonString = FString::Printf(TEXT("%s cannot accept AddOn type %s"), *ThisFlowNodeBase->GetClass()->GetName(), *OtherFlowNodeSubNode->GetClass()->GetName()); + } + + return false; } -#undef LOCTEXT_NAMESPACE +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Branch.cpp b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Branch.cpp new file mode 100644 index 000000000..ace07bbb7 --- /dev/null +++ b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Branch.cpp @@ -0,0 +1,19 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Graph/Nodes/FlowGraphNode_Branch.h" +#include "Nodes/Route/FlowNode_Branch.h" +#include "Nodes/Route/FlowNode_Switch.h" + +#include "Textures/SlateIcon.h" + +UFlowGraphNode_Branch::UFlowGraphNode_Branch(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + AssignedNodeClasses = { UFlowNode_Branch::StaticClass(), UFlowNode_Switch::StaticClass() }; +} + +FSlateIcon UFlowGraphNode_Branch::GetIconAndTint(FLinearColor& OutColor) const +{ + static FSlateIcon Icon("FlowEditorStyle", "GraphEditor.Branch_16x"); + return Icon; +} diff --git a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_ExecutionSequence.cpp b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_ExecutionSequence.cpp index 5ba3e06f2..bcea74fe1 100644 --- a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_ExecutionSequence.cpp +++ b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_ExecutionSequence.cpp @@ -6,6 +6,8 @@ #include "Textures/SlateIcon.h" +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowGraphNode_ExecutionSequence) + UFlowGraphNode_ExecutionSequence::UFlowGraphNode_ExecutionSequence(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { diff --git a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Finish.cpp b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Finish.cpp index 1092260e4..34ebc8098 100644 --- a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Finish.cpp +++ b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Finish.cpp @@ -3,7 +3,9 @@ #include "Graph/Nodes/FlowGraphNode_Finish.h" #include "Graph/Widgets/SFlowGraphNode_Finish.h" -#include "Nodes/Route/FlowNode_Finish.h" +#include "Nodes/Graph/FlowNode_Finish.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowGraphNode_Finish) UFlowGraphNode_Finish::UFlowGraphNode_Finish(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) diff --git a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Reroute.cpp b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Reroute.cpp index cac81f6d4..67b34d23d 100644 --- a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Reroute.cpp +++ b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Reroute.cpp @@ -3,12 +3,17 @@ #include "Graph/Nodes/FlowGraphNode_Reroute.h" #include "SGraphNodeKnot.h" +#include "Graph/FlowGraph.h" +#include "Graph/Nodes/FlowGraphNode.h" +#include "Nodes/FlowNode.h" #include "Nodes/Route/FlowNode_Reroute.h" +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowGraphNode_Reroute) + UFlowGraphNode_Reroute::UFlowGraphNode_Reroute(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { - AssignedNodeClasses = {UFlowNode_Reroute::StaticClass()}; + AssignedNodeClasses = { UFlowNode_Reroute::StaticClass() }; } TSharedPtr UFlowGraphNode_Reroute::CreateVisualWidget() @@ -22,3 +27,171 @@ bool UFlowGraphNode_Reroute::ShouldDrawNodeAsControlPointOnly(int32& OutInputPin OutOutputPinIndex = 1; return true; } + +bool UFlowGraphNode_Reroute::CanPlaceBreakpoints() const +{ + return false; +} + +void UFlowGraphNode_Reroute::ConfigureRerouteNodeFromPinConnections(UEdGraphPin& InPin, UEdGraphPin& OutPin) +{ + UFlowNode_Reroute* RerouteTemplate = Cast(NodeInstance); + if (!IsValid(RerouteTemplate)) + { + return; + } + + // IMPORTANT: + // Use editor templates for "ConnectedNode" context. + // GetFlowNodeBase() may return the inspected PIE instance, which we do not want to use for editor graph mutation. + const UFlowGraphNode* FlowGraphNodeIn = Cast(InPin.GetOwningNode()); + const UFlowNode* NodeInTemplate = FlowGraphNodeIn ? Cast(FlowGraphNodeIn->GetNodeTemplate()) : nullptr; + + const UFlowGraphNode* FlowGraphNodeOut = Cast(OutPin.GetOwningNode()); + const UFlowNode* NodeOutTemplate = FlowGraphNodeOut ? Cast(FlowGraphNodeOut->GetNodeTemplate()) : nullptr; + + // Break existing wire first (we're inserting ourselves in between) + InPin.BreakLinkTo(&OutPin); + + // Canonical reroute type: pick one type for BOTH pins. + // Prefer the "source" (OutPin) type since it is the value provider. + const FEdGraphPinType CanonicalType = OutPin.PinType; + + // Apply to our graph pins (visuals/wire coloring) + if (InputPins.Num() > 0 && InputPins[0]) + { + InputPins[0]->PinType = CanonicalType; + } + if (OutputPins.Num() > 0 && OutputPins[0]) + { + OutputPins[0]->PinType = CanonicalType; + } + + // Apply to our template pins (future allocations) + { + // If possible, configure with context of the connected nodes; otherwise fall back to self. + const UFlowNode& InContext = NodeInTemplate ? *NodeInTemplate : *RerouteTemplate; + const UFlowNode& OutContext = NodeOutTemplate ? *NodeOutTemplate : *RerouteTemplate; + + RerouteTemplate->ConfigureInputPin(InContext, CanonicalType); + RerouteTemplate->ConfigureOutputPin(OutContext, CanonicalType); + } + + // Restore reroute wiring + if (OutputPins.Num() > 0 && OutputPins[0]) + { + InPin.MakeLinkTo(OutputPins[0]); + } + if (InputPins.Num() > 0 && InputPins[0]) + { + OutPin.MakeLinkTo(InputPins[0]); + } + + // Nudge visuals + if (UEdGraph* Graph = GetGraph()) + { + Graph->NotifyNodeChanged(this); + } +} + +void UFlowGraphNode_Reroute::NodeConnectionListChanged() +{ + Super::NodeConnectionListChanged(); + ReconfigureFromConnections(); +} + +void UFlowGraphNode_Reroute::ApplyTypeFromConnectedPin(const UEdGraphPin& OtherPin) +{ + if (InputPins.Num() == 0 || OutputPins.Num() == 0) + { + return; + } + + UEdGraphPin* const InputPin = InputPins[0]; + UEdGraphPin* const OutputPin = OutputPins[0]; + if (!InputPin || !OutputPin) + { + return; + } + + UFlowNode_Reroute* RerouteTemplate = Cast(NodeInstance); + if (!IsValid(RerouteTemplate)) + { + return; + } + + const FEdGraphPinType NewType = OtherPin.PinType; + + // Nothing to do? + if (InputPin->PinType == NewType && OutputPin->PinType == NewType) + { + return; + } + + // Apply to graph pins (visual + connection coloring) + InputPin->PinType = NewType; + OutputPin->PinType = NewType; + + // Update template pins too, so future reconstructions allocate correct pin types. + // Pass "connected node" context if possible; fall back to self. + const UFlowNode* ConnectedTemplate = nullptr; + if (const UFlowGraphNode* OtherGraphNode = Cast(OtherPin.GetOwningNode())) + { + ConnectedTemplate = Cast(OtherGraphNode->GetNodeTemplate()); + } + const UFlowNode& ConnectedNodeRef = ConnectedTemplate ? *ConnectedTemplate : *RerouteTemplate; + + RerouteTemplate->ConfigureInputPin(ConnectedNodeRef, NewType); + RerouteTemplate->ConfigureOutputPin(ConnectedNodeRef, NewType); + + // Avoid reconstruct storms (esp. during paste). PinType changes are enough for visuals/wire colors. + // If the graph is locked, defer the retype pass until UnlockUpdates(). + if (UFlowGraph* FlowGraph = Cast(GetGraph())) + { + if (FlowGraph->IsLocked()) + { + FlowGraph->EnqueueRerouteTypeFixup(this); + return; + } + } + + if (UEdGraph* Graph = GetGraph()) + { + Graph->NotifyNodeChanged(this); + } +} + +void UFlowGraphNode_Reroute::ReconfigureFromConnections() +{ + if (InputPins.Num() == 0 || OutputPins.Num() == 0) + { + return; + } + + UEdGraphPin* const InputPin = InputPins[0]; + UEdGraphPin* const OutputPin = OutputPins[0]; + if (!InputPin || !OutputPin) + { + return; + } + + // Determine desired type from whichever side is connected + const UEdGraphPin* TypeSourceLinkedPin = nullptr; + + if (InputPin->LinkedTo.Num() > 0 && InputPin->LinkedTo[0]) + { + TypeSourceLinkedPin = InputPin->LinkedTo[0]; + } + else if (OutputPin->LinkedTo.Num() > 0 && OutputPin->LinkedTo[0]) + { + TypeSourceLinkedPin = OutputPin->LinkedTo[0]; + } + + if (!TypeSourceLinkedPin) + { + // No connections => don't reset type here. Keep last known type. + return; + } + + ApplyTypeFromConnectedPin(*TypeSourceLinkedPin); +} diff --git a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Start.cpp b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Start.cpp index 19fde8ba8..63a555b43 100644 --- a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Start.cpp +++ b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_Start.cpp @@ -3,7 +3,9 @@ #include "Graph/Nodes/FlowGraphNode_Start.h" #include "Graph/Widgets/SFlowGraphNode_Start.h" -#include "Nodes/Route/FlowNode_Start.h" +#include "Nodes/Graph/FlowNode_Start.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowGraphNode_Start) UFlowGraphNode_Start::UFlowGraphNode_Start(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) diff --git a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_SubGraph.cpp b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_SubGraph.cpp index 2e22bc80b..6cd615b1b 100644 --- a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_SubGraph.cpp +++ b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode_SubGraph.cpp @@ -3,7 +3,10 @@ #include "Graph/Nodes/FlowGraphNode_SubGraph.h" #include "Graph/Widgets/SFlowGraphNode_SubGraph.h" -#include "Nodes/Route/FlowNode_SubGraph.h" +#include "FlowAsset.h" +#include "Nodes/Graph/FlowNode_SubGraph.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowGraphNode_SubGraph) UFlowGraphNode_SubGraph::UFlowGraphNode_SubGraph(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) @@ -15,3 +18,15 @@ TSharedPtr UFlowGraphNode_SubGraph::CreateVisualWidget() { return SNew(SFlowGraphNode_SubGraph, this); } + +void UFlowGraphNode_SubGraph::OnNodeDoubleClickedInPIE() const +{ + UFlowNode_SubGraph* SubGraphNode = Cast(GetFlowNodeBase()); + ensureAlways(SubGraphNode); + + const TWeakObjectPtr SubFlowInstance = GetFlowAsset()->GetFlowInstance(SubGraphNode); + if (SubFlowInstance.IsValid()) + { + SubGraphNode->GetFlowAsset()->GetTemplateAsset()->SetInspectedInstance(SubFlowInstance); + } +} diff --git a/Source/FlowEditor/Private/Graph/Widgets/DragFlowGraphNode.cpp b/Source/FlowEditor/Private/Graph/Widgets/DragFlowGraphNode.cpp new file mode 100644 index 000000000..1e033dfbf --- /dev/null +++ b/Source/FlowEditor/Private/Graph/Widgets/DragFlowGraphNode.cpp @@ -0,0 +1,35 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "DragFlowGraphNode.h" +#include "Framework/Application/SlateApplication.h" +#include "Graph/Nodes/FlowGraphNode.h" + +TSharedRef FDragFlowGraphNode::New(const TSharedRef& InGraphPanel, const TSharedRef& InDraggedNode) +{ + TSharedRef Operation = MakeShareable(new FDragFlowGraphNode); + + Operation->StartTime = FPlatformTime::Seconds(); + Operation->GraphPanel = InGraphPanel; + Operation->DraggedNodes.Add(InDraggedNode); + // adjust the decorator away from the current mouse location a small amount based on cursor size + Operation->DecoratorAdjust = FSlateApplication::Get().GetCursorSize(); + Operation->Construct(); + + return Operation; +} + +TSharedRef FDragFlowGraphNode::New(const TSharedRef& InGraphPanel, const TArray< TSharedRef >& InDraggedNodes) +{ + TSharedRef Operation = MakeShareable(new FDragFlowGraphNode); + Operation->StartTime = FPlatformTime::Seconds(); + Operation->GraphPanel = InGraphPanel; + Operation->DraggedNodes = InDraggedNodes; + Operation->DecoratorAdjust = FSlateApplication::Get().GetCursorSize(); + Operation->Construct(); + return Operation; +} + +UFlowGraphNode* FDragFlowGraphNode::GetDropTargetNode() const +{ + return Cast(GetHoveredNode()); +} diff --git a/Source/FlowEditor/Private/Graph/Widgets/DragFlowGraphNode.h b/Source/FlowEditor/Private/Graph/Widgets/DragFlowGraphNode.h new file mode 100644 index 000000000..c57afc324 --- /dev/null +++ b/Source/FlowEditor/Private/Graph/Widgets/DragFlowGraphNode.h @@ -0,0 +1,26 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "Editor/GraphEditor/Private/DragNode.h" +#include "Templates/SharedPointer.h" + +class SGraphPanel; +class UFlowGraphNode; + +// Adapted from FDragAIGraphNode +class FDragFlowGraphNode : public FDragNode +{ +public: + DRAG_DROP_OPERATOR_TYPE(FDragFlowGraphNode, FDragNode) + + static TSharedRef New(const TSharedRef& InGraphPanel, const TSharedRef& InDraggedNode); + static TSharedRef New(const TSharedRef& InGraphPanel, const TArray>& InDraggedNodes); + + UFlowGraphNode* GetDropTargetNode() const; + + double StartTime; + +protected: + typedef FDragNode Super; +}; \ No newline at end of file diff --git a/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode.cpp b/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode.cpp index f906f6781..d8c73bfa2 100644 --- a/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode.cpp +++ b/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode.cpp @@ -1,38 +1,48 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #include "Graph/Widgets/SFlowGraphNode.h" +#include "DragFlowGraphNode.h" #include "FlowEditorStyle.h" -#include "Graph/FlowGraphEditorSettings.h" +#include "Graph/FlowGraph.h" #include "Graph/FlowGraphSettings.h" -#include "FlowAsset.h" #include "Nodes/FlowNode.h" +#include "Debugger/FlowDebuggerSubsystem.h" + #include "EdGraph/EdGraphPin.h" #include "Editor.h" #include "GraphEditorSettings.h" #include "IDocumentation.h" #include "Input/Reply.h" +#include "Internationalization/BreakIterator.h" #include "Layout/Margin.h" #include "Misc/Attribute.h" +#include "NodeFactory.h" #include "SCommentBubble.h" +#include "ScopedTransaction.h" #include "SGraphNode.h" +#include "SGraphPanel.h" #include "SGraphPin.h" #include "SlateOptMacros.h" #include "SLevelOfDetailBranchNode.h" #include "SNodePanel.h" #include "Styling/SlateColor.h" #include "TutorialMetaData.h" +#include "Styling/SlateStyleRegistry.h" #include "Widgets/Images/SImage.h" +#include "Widgets/Input/SButton.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/SBoxPanel.h" #include "Widgets/SOverlay.h" +#include "Widgets/SToolTip.h" +#include "Widgets/Text/SInlineEditableTextBlock.h" #define LOCTEXT_NAMESPACE "SFlowGraphNode" SFlowGraphPinExec::SFlowGraphPinExec() { - PinColorModifier = UFlowGraphSettings::Get()->ExecPinColorModifier; + PinColorModifier = GetDefault()->ExecPinColorModifier; } void SFlowGraphPinExec::Construct(const FArguments& InArgs, UEdGraphPin* InPin) @@ -41,21 +51,41 @@ void SFlowGraphPinExec::Construct(const FArguments& InArgs, UEdGraphPin* InPin) bUsePinColorForText = true; } +const FLinearColor SFlowGraphNode::UnselectedNodeTint = FLinearColor(1.0f, 1.0f, 1.0f, 0.5f); +const FLinearColor SFlowGraphNode::ConfigBoxColor = FLinearColor(0.04f, 0.04f, 0.04f, 1.0f); + void SFlowGraphNode::Construct(const FArguments& InArgs, UFlowGraphNode* InNode) { GraphNode = InNode; FlowGraphNode = InNode; + DebuggerSubsystem = GEngine->GetEngineSubsystem(); + + check(FlowGraphNode); + FlowGraphNode->OnSignalModeChanged.BindRaw(this, &SFlowGraphNode::UpdateGraphNode); + FlowGraphNode->OnReconstructNodeCompleted.BindRaw(this, &SFlowGraphNode::UpdateGraphNode); + SetCursor(EMouseCursor::CardinalCross); UpdateGraphNode(); + + bDragMarkerVisible = false; +} + +SFlowGraphNode::~SFlowGraphNode() +{ + check(FlowGraphNode); + FlowGraphNode->OnSignalModeChanged.Unbind(); + FlowGraphNode->OnReconstructNodeCompleted.Unbind(); + + FlowGraphNode = nullptr; } void SFlowGraphNode::GetNodeInfoPopups(FNodeInfoContext* Context, TArray& Popups) const { - const FString Description = FlowGraphNode->GetNodeDescription(); + const FString& Description = FlowGraphNode->GetNodeDescription(); if (!Description.IsEmpty()) { - const FGraphInformationPopupInfo DescriptionPopup = FGraphInformationPopupInfo(nullptr, UFlowGraphSettings::Get()->NodeDescriptionBackground, Description); + const FGraphInformationPopupInfo DescriptionPopup = FGraphInformationPopupInfo(nullptr, GetDefault()->NodeDescriptionBackground, Description); Popups.Add(DescriptionPopup); } @@ -69,7 +99,7 @@ void SFlowGraphNode::GetNodeInfoPopups(FNodeInfoContext* Context, TArrayIsContentPreloaded()) { - const FGraphInformationPopupInfo DescriptionPopup = FGraphInformationPopupInfo(nullptr, UFlowGraphSettings::Get()->NodeStatusBackground, TEXT("Preloaded")); + const FGraphInformationPopupInfo DescriptionPopup = FGraphInformationPopupInfo(nullptr, GetDefault()->NodeStatusBackground, TEXT("Preloaded")); Popups.Add(DescriptionPopup); } } @@ -95,21 +125,23 @@ const FSlateBrush* SFlowGraphNode::GetShadowBrush(bool bSelected) const return SGraphNode::GetShadowBrush(bSelected); } -void SFlowGraphNode::GetOverlayBrushes(bool bSelected, const FVector2D WidgetSize, TArray& Brushes) const +void SFlowGraphNode::GetOverlayBrushes(bool bSelected, const FVector2f& WidgetSize, TArray& Brushes) const { + check(DebuggerSubsystem.IsValid()); + // Node breakpoint - if (FlowGraphNode->NodeBreakpoint.bHasBreakpoint) + if (const FFlowBreakpoint* NodeBreakpoint = DebuggerSubsystem->FindBreakpoint(FlowGraphNode->NodeGuid)) { FOverlayBrushInfo NodeBrush; - if (FlowGraphNode->NodeBreakpoint.bBreakpointHit) + if (NodeBreakpoint->IsHit()) { NodeBrush.Brush = FFlowEditorStyle::Get()->GetBrush(TEXT("FlowGraph.BreakpointHit")); NodeBrush.OverlayOffset.X = WidgetSize.X - 12.0f; } else { - NodeBrush.Brush = FFlowEditorStyle::Get()->GetBrush(FlowGraphNode->NodeBreakpoint.bBreakpointEnabled ? TEXT("FlowGraph.BreakpointEnabled") : TEXT("FlowGraph.BreakpointDisabled")); + NodeBrush.Brush = FFlowEditorStyle::Get()->GetBrush(NodeBreakpoint->IsEnabled() ? TEXT("FlowGraph.BreakpointEnabled") : TEXT("FlowGraph.BreakpointDisabled")); NodeBrush.OverlayOffset.X = WidgetSize.X; } @@ -119,41 +151,115 @@ void SFlowGraphNode::GetOverlayBrushes(bool bSelected, const FVector2D WidgetSiz } // Pin breakpoints - for (const TPair& PinBreakpoint : FlowGraphNode->PinBreakpoints) + for (UEdGraphPin* Pin : FlowGraphNode->Pins) { - if (PinBreakpoint.Key.Get()->Direction == EGPD_Input) + if (const FFlowBreakpoint* PinBreakpoint = DebuggerSubsystem->FindBreakpoint(Pin->GetOwningNode()->NodeGuid, Pin->PinName)) { - GetPinBrush(true, WidgetSize.X, FlowGraphNode->InputPins.IndexOfByKey(PinBreakpoint.Key.Get()), PinBreakpoint.Value, Brushes); + if (Pin->Direction == EGPD_Input) + { + GetPinBrush(true, WidgetSize.X, FlowGraphNode->InputPins.IndexOfByKey(Pin), PinBreakpoint, Brushes); + } + else + { + GetPinBrush(false, WidgetSize.X, FlowGraphNode->OutputPins.IndexOfByKey(Pin), PinBreakpoint, Brushes); + } } - else + } + + // Node custom overlay icons + if (const UFlowNodeBase* FlowNodeBase = FlowGraphNode->GetFlowNodeBase()) + { + FName CornerIconBrushName = NAME_None; + FName CornerIconStyleSetName = NAME_None; + if (FlowNodeBase->GetCornerIcon(CornerIconBrushName, CornerIconStyleSetName)) { - GetPinBrush(false, WidgetSize.X, FlowGraphNode->OutputPins.IndexOfByKey(PinBreakpoint.Key.Get()), PinBreakpoint.Value, Brushes); + if (const FSlateBrush* CornerIconBrush = GetSlateBrush(CornerIconBrushName, CornerIconStyleSetName)) + { + FOverlayBrushInfo CornerIconInfo; + CornerIconInfo.Brush = CornerIconBrush; + CornerIconInfo.OverlayOffset.X = WidgetSize.X - (CornerIconBrush->ImageSize.X * .5f); + CornerIconInfo.OverlayOffset.Y = -CornerIconBrush->ImageSize.Y * .5f; + Brushes.Add(CornerIconInfo); + } + } + + TArray OverlayIcons; + FlowNodeBase->GetOverlayIcons(OverlayIcons, WidgetSize); + for (const FFlowNodeOverlayIcon& OverlayIcon : OverlayIcons) + { + if (OverlayIcon.BrushName.IsNone()) + { + continue; + } + + if (const FSlateBrush* IconBrush = GetSlateBrush(OverlayIcon.BrushName, OverlayIcon.StyleSetName)) + { + FOverlayBrushInfo IconBrushInfo; + IconBrushInfo.Brush = IconBrush; + IconBrushInfo.OverlayOffset = OverlayIcon.Offset; + Brushes.Add(IconBrushInfo); + } } } } -void SFlowGraphNode::GetPinBrush(const bool bLeftSide, const float WidgetWidth, const int32 PinIndex, const FFlowBreakpoint& Breakpoint, TArray& Brushes) const +const FSlateBrush* SFlowGraphNode::GetSlateBrush(const FName BrushName, const FName StyleSetName) const { - if (Breakpoint.bHasBreakpoint) + if (!StyleSetName.IsNone()) { - FOverlayBrushInfo PinBrush; + // If we have a specific Style Set Name try and find the brush there. + if (const ISlateStyle* AppStyle = FSlateStyleRegistry::FindSlateStyle(StyleSetName)) + { + const FSlateBrush* SlateBrush = AppStyle->GetBrush(BrushName); + if (SlateBrush != nullptr && SlateBrush != AppStyle->GetDefaultBrush()) + { + return SlateBrush; + } + } + } + else + { + // If we do not have a specific StyleSet Name then first search the default Flow Editor StyleSet + // and finally fallback to the default Unreal StyleSet. + + const FSlateBrush* SlateBrush = FFlowEditorStyle::Get()->GetBrush(BrushName); - if (Breakpoint.bBreakpointHit) + if (SlateBrush != nullptr && SlateBrush != FFlowEditorStyle::Get()->GetDefaultBrush()) { - PinBrush.Brush = FFlowEditorStyle::Get()->GetBrush(TEXT("FlowGraph.PinBreakpointHit")); - PinBrush.OverlayOffset.X = bLeftSide ? 0.0f : (WidgetWidth - 36.0f); - PinBrush.OverlayOffset.Y = 12.0f + PinIndex * 28.0f; + return SlateBrush; } else { - PinBrush.Brush = FFlowEditorStyle::Get()->GetBrush(Breakpoint.bBreakpointEnabled ? TEXT("FlowGraph.BreakpointEnabled") : TEXT("FlowGraph.BreakpointDisabled")); - PinBrush.OverlayOffset.X = bLeftSide ? -24.0f : WidgetWidth; - PinBrush.OverlayOffset.Y = 16.0f + PinIndex * 28.0f; + SlateBrush = FAppStyle::GetBrush(BrushName); + if (SlateBrush != nullptr && SlateBrush != FAppStyle::GetDefaultBrush()) + { + return SlateBrush; + } } + } - PinBrush.AnimationEnvelope = FVector2D(0.f, 10.f); - Brushes.Add(PinBrush); + return nullptr; +} + +void SFlowGraphNode::GetPinBrush(const bool bLeftSide, const float WidgetWidth, const int32 PinIndex, const FFlowBreakpoint* Breakpoint, TArray& Brushes) const +{ + FOverlayBrushInfo PinBrush; + + if (Breakpoint->IsHit()) + { + PinBrush.Brush = FFlowEditorStyle::Get()->GetBrush(TEXT("FlowGraph.PinBreakpointHit")); + PinBrush.OverlayOffset.X = bLeftSide ? 0.0f : (WidgetWidth - 36.0f); + PinBrush.OverlayOffset.Y = 12.0f + PinIndex * 28.0f; + } + else + { + PinBrush.Brush = FFlowEditorStyle::Get()->GetBrush(Breakpoint->IsEnabled() ? TEXT("FlowGraph.BreakpointEnabled") : TEXT("FlowGraph.BreakpointDisabled")); + PinBrush.OverlayOffset.X = bLeftSide ? -24.0f : WidgetWidth; + PinBrush.OverlayOffset.Y = 16.0f + PinIndex * 28.0f; } + + PinBrush.AnimationEnvelope = FVector2D(0.f, 10.f); + Brushes.Add(PinBrush); } BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION @@ -163,7 +269,7 @@ void SFlowGraphNode::UpdateGraphNode() InputPins.Empty(); OutputPins.Empty(); - // Reset variables that are going to be exposed, in case we are refreshing an already setup node. + // Reset variables that are going to be exposed, in case we are refreshing an already set node. RightNodeBox.Reset(); LeftNodeBox.Reset(); @@ -190,6 +296,9 @@ void SFlowGraphNode::UpdateGraphNode() IconBrush = GraphNode->GetIconAndTint(IconColor).GetOptionalIcon(); } + // Compute the SubNode padding indent based on the parentage depth for this node + const FMargin NodePadding = ComputeSubNodeChildIndentPaddingMargin(); + const TSharedRef DefaultTitleAreaWidget = SNew(SOverlay) + SOverlay::Slot() .HAlign(HAlign_Fill) @@ -203,7 +312,7 @@ void SFlowGraphNode::UpdateGraphNode() .BorderImage(FFlowEditorStyle::GetBrush("Flow.Node.Title")) // The extra margin on the right is for making the color spill stretch well past the node title .Padding(FMargin(10, 5, 30, 3)) - .BorderBackgroundColor(this, &SGraphNode::GetNodeTitleColor) + .BorderBackgroundColor(this, &SFlowGraphNode::GetBorderBackgroundColor) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() @@ -213,7 +322,7 @@ void SFlowGraphNode::UpdateGraphNode() [ SNew(SImage) .Image(IconBrush) - .ColorAndOpacity(this, &SGraphNode::GetNodeTitleIconColor) + .ColorAndOpacity(this, &SFlowGraphNode::GetNodeTitleIconColor) ] + SHorizontalBox::Slot() [ @@ -250,7 +359,7 @@ void SFlowGraphNode::UpdateGraphNode() DefaultTitleAreaWidget ]; - // Setup a meta tag for this node + // Set up a meta tag for this node FGraphNodeMetaData TagMeta(TEXT("FlowGraphNode")); PopulateMetaTag(&TagMeta); @@ -300,6 +409,7 @@ void SFlowGraphNode::UpdateGraphNode() SAssignNew(MainVerticalBox, SVerticalBox) + SVerticalBox::Slot() .AutoHeight() + .Padding(FMargin(NodePadding.Left, 0.0f, NodePadding.Right, 0.0f)) [ SNew(SOverlay) .AddMetaData(TagMeta) @@ -308,7 +418,7 @@ void SFlowGraphNode::UpdateGraphNode() [ SNew(SImage) .Image(GetNodeBodyBrush()) - .ColorAndOpacity(this, &SGraphNode::GetNodeBodyColor) + .ColorAndOpacity(this, &SFlowGraphNode::GetNodeBodyColor) ] + SOverlay::Slot() [ @@ -336,8 +446,8 @@ void SFlowGraphNode::UpdateGraphNode() .IsGraphNodeHovered(this, &SGraphNode::IsHovered); GetOrAddSlot(ENodeZone::TopCenter) - .SlotOffset(TAttribute(CommentBubble.Get(), &SCommentBubble::GetOffset)) - .SlotSize(TAttribute(CommentBubble.Get(), &SCommentBubble::GetSize)) + .SlotOffset2f(TAttribute(CommentBubble.Get(), &SCommentBubble::GetOffset2f)) + .SlotSize2f(TAttribute(CommentBubble.Get(), &SCommentBubble::GetSize2f)) .AllowScaling(TAttribute(CommentBubble.Get(), &SCommentBubble::IsScalingAllowed)) .VAlign(VAlign_Top) [ @@ -353,14 +463,211 @@ void SFlowGraphNode::UpdateGraphNode() CreateAdvancedViewArrow(InnerVerticalBox); } +FSlateColor SFlowGraphNode::GetBorderBackgroundColor() const +{ + return SGraphNode::GetNodeTitleColor(); +} + +FSlateColor SFlowGraphNode::GetConfigBoxBackgroundColor() const +{ + FLinearColor NodeColor = ConfigBoxColor; + + if (FlowGraphNode && !IsFlowGraphNodeSelected(FlowGraphNode)) + { + NodeColor *= UnselectedNodeTint; + } + + return NodeColor; +} + +void SFlowGraphNode::CreateBelowPinControls(const TSharedPtr InnerVerticalBox) +{ + static const FMargin ConfigBoxPadding = FMargin(2.0f, 0.0f, 1.0f, 0.0f); + + // Add a box to wrap around the Config Text area to make it a more visually distinct part of the node + TSharedPtr BelowPinsBox; + InnerVerticalBox->AddSlot() + .AutoHeight() + .Padding(ConfigBoxPadding) + [ + SNew(SBorder) + .BorderImage(FAppStyle::GetBrush("Graph.StateNode.Body")) + .BorderBackgroundColor(this, &SFlowGraphNode::GetConfigBoxBackgroundColor) + .Visibility(this, &SFlowGraphNode::GetNodeConfigTextVisibility) + [ + SAssignNew(BelowPinsBox, SVerticalBox) + ] + ]; + + CreateConfigText(BelowPinsBox); + + CreateOrRebuildSubNodeBox(InnerVerticalBox); +} + +void SFlowGraphNode::AddSubNodeWidget(const TSharedPtr& NewSubNodeWidget) +{ + if (OwnerGraphPanelPtr.IsValid()) + { + NewSubNodeWidget->SetOwner(OwnerGraphPanelPtr.Pin().ToSharedRef()); + OwnerGraphPanelPtr.Pin()->AttachGraphEvents(NewSubNodeWidget); + } + NewSubNodeWidget->UpdateGraphNode(); + + AddSubNode(NewSubNodeWidget); +} + +FMargin SFlowGraphNode::ComputeSubNodeChildIndentPaddingMargin() const +{ + if (!IsValid(FlowGraphNode) || !FlowGraphNode->IsSubNode()) + { + return FMargin(); + } + + const UFlowGraphNode* CurrentAncestor = FlowGraphNode->GetParentNode(); + + // Compute the parent depth, so it can be used to determine the indent level for this subnode + int32 ParentDepth = 0; + while (IsValid(CurrentAncestor)) + { + ++ParentDepth; + + CurrentAncestor = CurrentAncestor->GetParentNode(); + } + + constexpr float VerticalDefaultPadding = 2.0f; + constexpr float HorizontalDefaultPadding = 2.0f; + constexpr float IndentedHorizontalPadding = 6.0f; + constexpr float RightPadding = HorizontalDefaultPadding; + + float LeftPadding; + if (ParentDepth > 0) + { + // Increase the padding by the parent depth for this node + LeftPadding = IndentedHorizontalPadding * ParentDepth; + } + else + { + LeftPadding = 0.0f; + } + + return FMargin(LeftPadding, VerticalDefaultPadding, RightPadding, VerticalDefaultPadding); +} + +void SFlowGraphNode::CreateConfigText(const TSharedPtr& InnerVerticalBox) +{ + static const FMargin ConfigTextPadding = FMargin(2.0f, 0.0f, 0.0f, 3.0f); + + InnerVerticalBox->AddSlot() + .AutoHeight() + .Padding(ConfigTextPadding) + [ + SAssignNew(ConfigTextBlock, STextBlock) + .AutoWrapText(true) + .LineBreakPolicy(FBreakIterator::CreateWordBreakIterator()) + .Text(this, &SFlowGraphNode::GetNodeConfigText) + ]; +} + +FText SFlowGraphNode::GetNodeConfigText() const +{ + if (const UFlowNodeBase* FlowNodeBase = FlowGraphNode->GetFlowNodeBase()) + { + return FlowNodeBase->GetNodeConfigText(); + } + + return FText::GetEmpty(); +} + +EVisibility SFlowGraphNode::GetNodeConfigTextVisibility() const +{ + // Hide in lower LODs + const TSharedPtr OwnerPanel = GetOwnerPanel(); + if (!OwnerPanel.IsValid() || OwnerPanel->GetCurrentLOD() >= EGraphRenderingLOD::MediumDetail) + { + if (ConfigTextBlock && !ConfigTextBlock->GetText().IsEmptyOrWhitespace()) + { + return EVisibility::Visible; + } + } + + return EVisibility::Collapsed; +} + +void SFlowGraphNode::CreateOrRebuildSubNodeBox(const TSharedPtr& InnerVerticalBox) +{ + if (SubNodeBox.IsValid()) + { + SubNodeBox->ClearChildren(); + } + else + { + SAssignNew(SubNodeBox, SVerticalBox); + } + + SubNodes.Reset(); + + if (FlowGraphNode) + { + for (UFlowGraphNode* SubNode : FlowGraphNode->SubNodes) + { + TSharedPtr NewNode = FNodeFactory::CreateNodeWidget(SubNode); + AddSubNodeWidget(NewNode); + } + } + + InnerVerticalBox->AddSlot() + .AutoHeight() + [ + SubNodeBox.ToSharedRef() + ]; +} + +bool SFlowGraphNode::IsFlowGraphNodeSelected(UFlowGraphNode* Node) const +{ + return GetOwnerPanel().IsValid() && GetOwnerPanel()->SelectionManager.SelectedNodes.Contains(Node); +} + void SFlowGraphNode::UpdateErrorInfo() { - if (const UFlowNode* FlowNode = FlowGraphNode->GetFlowNode()) + if (const UFlowNodeBase* FlowNodeBase = FlowGraphNode->GetFlowNodeBase()) { - if (FlowNode->GetClass()->HasAnyClassFlags(CLASS_Deprecated) || FlowNode->bNodeDeprecated) + if (FlowNodeBase->ValidationLog.Messages.Num() > 0) { - ErrorMsg = FlowNode->ReplacedBy ? FString::Printf(TEXT(" REPLACED BY: %s "), *FlowNode->ReplacedBy->GetName()) : FString(TEXT(" DEPRECATED! ")); - ErrorColor = FEditorStyle::GetColor("ErrorReporting.WarningBackgroundColor"); + EMessageSeverity::Type MaxSeverity = EMessageSeverity::Info; + for (const TSharedRef& Message : FlowNodeBase->ValidationLog.Messages) + { + if (Message->GetSeverity() < MaxSeverity) + { + MaxSeverity = Message->GetSeverity(); + } + } + + switch(MaxSeverity) + { + case EMessageSeverity::Error: + ErrorMsg = FString(TEXT("ERROR!")); + ErrorColor = FAppStyle::GetColor("ErrorReporting.BackgroundColor"); + break; + case EMessageSeverity::PerformanceWarning: + case EMessageSeverity::Warning: + ErrorMsg = FString(TEXT("WARNING!")); + ErrorColor = FAppStyle::GetColor("ErrorReporting.WarningBackgroundColor"); + break; + case EMessageSeverity::Info: + ErrorMsg = FString(TEXT("NOTE")); + ErrorColor = FAppStyle::GetColor("InfoReporting.BackgroundColor"); + break; + default: + break; + } + + return; + } + + if (FlowNodeBase->GetClass()->HasAnyClassFlags(CLASS_Deprecated) || FlowNodeBase->bNodeDeprecated) + { + ErrorMsg = FlowNodeBase->ReplacedBy ? FString::Printf(TEXT(" REPLACED BY: %s "), *FlowNodeBase->ReplacedBy->GetName()) : FString(TEXT(" DEPRECATED! ")); + ErrorColor = FAppStyle::GetColor("ErrorReporting.WarningBackgroundColor"); return; } } @@ -368,10 +675,24 @@ void SFlowGraphNode::UpdateErrorInfo() SGraphNode::UpdateErrorInfo(); } +TSharedRef SFlowGraphNode::CreateTitleWidget(TSharedPtr NodeTitle) +{ + SAssignNew(InlineEditableText, SInlineEditableTextBlock) + .Style(FAppStyle::Get(), "Graph.Node.NodeTitleInlineEditableText") + .Text(NodeTitle.Get(), &SNodeTitle::GetHeadTitle) + .OnVerifyTextChanged(this, &SFlowGraphNode::OnVerifyNameTextChanged) + .OnTextCommitted(this, &SFlowGraphNode::OnNameTextCommited) + .IsReadOnly(this, &SFlowGraphNode::IsNameReadOnly) + .IsSelected(this, &SFlowGraphNode::IsSelectedExclusively); + InlineEditableText->SetColorAndOpacity(TAttribute::Create(TAttribute::FGetter::CreateSP(this, &SFlowGraphNode::GetNodeTitleTextColor))); + + return InlineEditableText.ToSharedRef(); +} + TSharedRef SFlowGraphNode::CreateNodeContentArea() { return SNew(SBorder) - .BorderImage(FEditorStyle::GetBrush("NoBorder")) + .BorderImage(FAppStyle::GetBrush("NoBorder")) .HAlign(HAlign_Fill) .VAlign(VAlign_Fill) [ @@ -396,29 +717,106 @@ const FSlateBrush* SFlowGraphNode::GetNodeBodyBrush() const return FFlowEditorStyle::GetBrush("Flow.Node.Body"); } -void SFlowGraphNode::CreateStandardPinWidget(UEdGraphPin* Pin) +FSlateColor SFlowGraphNode::GetNodeTitleColor() const { - const TSharedPtr NewPin = SNew(SFlowGraphPinExec, Pin); + FLinearColor ReturnTitleColor = GraphNode->IsDeprecated() ? FLinearColor::Red : GetNodeObj()->GetNodeTitleColor(); - if (!UFlowGraphSettings::Get()->bShowDefaultPinNames && FlowGraphNode->GetFlowNode()) + if (FlowGraphNode->GetSignalMode() == EFlowSignalMode::Enabled) { - if (Pin->Direction == EGPD_Input) - { - if (FlowGraphNode->GetFlowNode()->GetInputPins().Num() == 1 && Pin->PinName == UFlowNode::DefaultInputPin.PinName) - { - NewPin->SetShowLabel(false); - } - } - else - { - if (FlowGraphNode->GetFlowNode()->GetOutputPins().Num() == 1 && Pin->PinName == UFlowNode::DefaultOutputPin.PinName) - { - NewPin->SetShowLabel(false); - } - } + ReturnTitleColor.A = FadeCurve.GetLerp(); + } + else + { + ReturnTitleColor *= FLinearColor(0.5f, 0.5f, 0.5f, 0.4f); + } + + if (!IsFlowGraphNodeSelected(FlowGraphNode) && FlowGraphNode->IsSubNode()) + { + ReturnTitleColor *= UnselectedNodeTint; } - this->AddPin(NewPin.ToSharedRef()); + return ReturnTitleColor; +} + +FSlateColor SFlowGraphNode::GetNodeBodyColor() const +{ + FLinearColor ReturnBodyColor = GraphNode->GetNodeBodyTintColor(); + if (FlowGraphNode->GetSignalMode() != EFlowSignalMode::Enabled) + { + ReturnBodyColor *= FLinearColor(1.0f, 1.0f, 1.0f, 0.5f); + } + else if (!IsFlowGraphNodeSelected(FlowGraphNode) && FlowGraphNode->IsSubNode()) + { + ReturnBodyColor *= UnselectedNodeTint; + } + + return ReturnBodyColor; +} + +FSlateColor SFlowGraphNode::GetNodeTitleIconColor() const +{ + FLinearColor ReturnIconColor = IconColor; + if (FlowGraphNode->GetSignalMode() != EFlowSignalMode::Enabled) + { + ReturnIconColor *= FLinearColor(1.0f, 1.0f, 1.0f, 0.3f); + } + else if (!IsFlowGraphNodeSelected(FlowGraphNode) && FlowGraphNode->IsSubNode()) + { + ReturnIconColor *= UnselectedNodeTint; + } + + return ReturnIconColor; +} + +FLinearColor SFlowGraphNode::GetNodeTitleTextColor() const +{ + FLinearColor ReturnTextColor = FLinearColor::White; + if (FlowGraphNode->GetSignalMode() != EFlowSignalMode::Enabled) + { + ReturnTextColor *= FLinearColor(1.0f, 1.0f, 1.0f, 0.3f); + } + else if (!IsFlowGraphNodeSelected(FlowGraphNode) && FlowGraphNode->IsSubNode()) + { + ReturnTextColor *= UnselectedNodeTint; + } + + return ReturnTextColor; +} + +TSharedPtr SFlowGraphNode::GetEnabledStateWidget() const +{ + if (FlowGraphNode->IsSubNode()) + { + // SubNodes don't get enabled/disabled on their own, + // they follow the enabled/disabled setting of their owning flow node + + return TSharedPtr(); + } + + if (FlowGraphNode->GetSignalMode() != EFlowSignalMode::Enabled && !GraphNode->IsAutomaticallyPlacedGhostNode()) + { + const bool bPassThrough = FlowGraphNode->GetSignalMode() == EFlowSignalMode::PassThrough; + const FText StatusMessage = bPassThrough ? LOCTEXT("PassThrough", "Pass Through") : LOCTEXT("DisabledNode", "Disabled"); + const FText StatusMessageTooltip = bPassThrough ? + LOCTEXT("PassThroughTooltip", "This node won't execute internal logic, but it will trigger all connected outputs") : + LOCTEXT("DisabledNodeTooltip", "This node is disabled and will not be executed"); + + return SNew(SBorder) + .BorderImage(FAppStyle::GetBrush(bPassThrough ? "Graph.Node.DevelopmentBanner" : "Graph.Node.DisabledBanner")) + .HAlign(HAlign_Fill) + .VAlign(VAlign_Fill) + [ + SNew(STextBlock) + .Text(StatusMessage) + .ToolTipText(StatusMessageTooltip) + .Justification(ETextJustify::Center) + .ColorAndOpacity(FLinearColor::White) + .ShadowOffset(FVector2D::UnitVector) + .Visibility(EVisibility::Visible) + ]; + } + + return TSharedPtr(); } END_SLATE_FUNCTION_BUILD_OPTIMIZATION @@ -428,55 +826,55 @@ TSharedPtr SFlowGraphNode::GetComplexTooltip() return IDocumentation::Get()->CreateToolTip(TAttribute(this, &SGraphNode::GetNodeTooltip), nullptr, GraphNode->GetDocumentationLink(), GraphNode->GetDocumentationExcerptName()); } -void SFlowGraphNode::CreateInputSideAddButton(TSharedPtr OutputBox) +void SFlowGraphNode::CreateInputSideAddButton(const TSharedPtr OutputBox) { if (FlowGraphNode->CanUserAddInput()) { TSharedPtr AddPinWidget; SAssignNew(AddPinWidget, SHorizontalBox) - +SHorizontalBox::Slot() - .AutoWidth() - . VAlign(VAlign_Center) - . Padding( 0,0,7,0 ) - [ - SNew(SImage) - .Image(FEditorStyle::GetBrush(TEXT("Icons.PlusCircle"))) - ] - +SHorizontalBox::Slot() - .AutoWidth() - .HAlign(HAlign_Left) - [ - SNew(STextBlock) - .Text(LOCTEXT("FlowNodeAddPinButton", "Add pin")) - .ColorAndOpacity(FLinearColor::White) - ]; + +SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(0, 0, 7, 0) + [ + SNew(SImage) + .Image(FAppStyle::GetBrush(TEXT("Icons.PlusCircle"))) + ] + +SHorizontalBox::Slot() + .AutoWidth() + .HAlign(HAlign_Left) + [ + SNew(STextBlock) + .Text(LOCTEXT("FlowNodeAddPinButton", "Add pin")) + .ColorAndOpacity(FLinearColor::White) + ]; AddPinButton(OutputBox, AddPinWidget.ToSharedRef(), EGPD_Input); } } -void SFlowGraphNode::CreateOutputSideAddButton(TSharedPtr OutputBox) +void SFlowGraphNode::CreateOutputSideAddButton(const TSharedPtr OutputBox) { if (FlowGraphNode->CanUserAddOutput()) { TSharedPtr AddPinWidget; SAssignNew(AddPinWidget, SHorizontalBox) - +SHorizontalBox::Slot() - .AutoWidth() - .HAlign(HAlign_Left) - [ - SNew(STextBlock) - .Text(LOCTEXT("FlowNodeAddPinButton", "Add pin")) - .ColorAndOpacity(FLinearColor::White) - ] - +SHorizontalBox::Slot() - .AutoWidth() - .VAlign(VAlign_Center) - .Padding(7,0,0,0) - [ - SNew(SImage) - .Image(FEditorStyle::GetBrush(TEXT("Icons.PlusCircle"))) - ]; + +SHorizontalBox::Slot() + .AutoWidth() + .HAlign(HAlign_Left) + [ + SNew(STextBlock) + .Text(LOCTEXT("FlowNodeAddPinButton", "Add pin")) + .ColorAndOpacity(FLinearColor::White) + ] + +SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(7, 0, 0, 0) + [ + SNew(SImage) + .Image(FAppStyle::GetBrush(TEXT("Icons.PlusCircle"))) + ]; AddPinButton(OutputBox, AddPinWidget.ToSharedRef(), EGPD_Output); } @@ -497,16 +895,16 @@ void SFlowGraphNode::AddPinButton(TSharedPtr OutputBox, const TSha } const TSharedRef AddPinButton = SNew(SButton) - .ContentPadding(0.0f) - .ButtonStyle(FEditorStyle::Get(), "NoBorder") - .OnClicked(this, &SFlowGraphNode::OnAddFlowPin, Direction) - .IsEnabled(this, &SFlowGraphNode::IsNodeEditable) - .ToolTipText(PinTooltipText) - .ToolTip(Tooltip) - .Visibility(this, &SFlowGraphNode::IsAddPinButtonVisible) - [ - ButtonContent - ]; + .ContentPadding(0.0f) + .ButtonStyle(FAppStyle::Get(), "NoBorder") + .OnClicked(this, &SFlowGraphNode::OnAddFlowPin, Direction) + .IsEnabled(this, &SFlowGraphNode::IsNodeEditable) + .ToolTipText(PinTooltipText) + .ToolTip(Tooltip) + .Visibility(this, &SFlowGraphNode::IsAddPinButtonVisible) + [ + ButtonContent + ]; AddPinButton->SetCursor(EMouseCursor::Hand); @@ -532,10 +930,407 @@ FReply SFlowGraphNode::OnAddFlowPin(const EEdGraphPinDirection Direction) case EGPD_Output: FlowGraphNode->AddUserOutput(); break; - default: ; + default: + break; } return FReply::Handled(); } -#undef LOCTEXT_NAMESPACE +void SFlowGraphNode::AddSubNode(const TSharedPtr SubNodeWidget) +{ + SubNodes.Add(SubNodeWidget); + + SubNodeBox->AddSlot().AutoHeight() + [ + SubNodeWidget.ToSharedRef() + ]; +} + +FText SFlowGraphNode::GetTitle() const +{ + return GraphNode ? GraphNode->GetNodeTitle(ENodeTitleType::FullTitle) : FText::GetEmpty(); +} + +FText SFlowGraphNode::GetDescription() const +{ + return FlowGraphNode ? FlowGraphNode->GetDescription() : FText::GetEmpty(); +} + +EVisibility SFlowGraphNode::GetDescriptionVisibility() const +{ + // LOD this out once things get too small + const TSharedPtr OwnerPanel = GetOwnerPanel(); + return (!OwnerPanel.IsValid() || OwnerPanel->GetCurrentLOD() > EGraphRenderingLOD::LowDetail) ? EVisibility::Visible : EVisibility::Collapsed; +} + +void SFlowGraphNode::AddPin(const TSharedRef& PinToAdd) +{ + PinToAdd->SetOwner(SharedThis(this)); + + const UEdGraphPin* PinObj = PinToAdd->GetPinObj(); + if (PinObj && PinObj->bAdvancedView) + { + PinToAdd->SetVisibility(TAttribute(PinToAdd, &SGraphPin::IsPinVisibleAsAdvanced)); + } + + if (PinToAdd->GetDirection() == EEdGraphPinDirection::EGPD_Input) + { + LeftNodeBox->AddSlot() + .AutoHeight() + .HAlign(HAlign_Left) + .VAlign(VAlign_Center) + .Padding(Settings->GetInputPinPadding()) + [ + PinToAdd + ]; + InputPins.Add(PinToAdd); + } + else // Direction == EEdGraphPinDirection::EGPD_Output + { + RightNodeBox->AddSlot() + .AutoHeight() + .HAlign(HAlign_Right) + .VAlign(VAlign_Center) + .Padding(Settings->GetOutputPinPadding()) + [ + PinToAdd + ]; + OutputPins.Add(PinToAdd); + } +} + +FReply SFlowGraphNode::OnMouseMove(const FGeometry& SenderGeometry, const FPointerEvent& MouseEvent) +{ + if (MouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton) && !(GEditor->bIsSimulatingInEditor || GEditor->PlayWorld)) + { + // if we are holding mouse over a subnode + if (FlowGraphNode && FlowGraphNode->IsSubNode()) + { + const TSharedRef& Panel = GetOwnerPanel().ToSharedRef(); + const TSharedRef& Node = SharedThis(this); + return FReply::Handled().BeginDragDrop(FDragFlowGraphNode::New(Panel, Node)); + } + } + + if (!MouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton) && bDragMarkerVisible) + { + SetDragMarker(false); + } + + return FReply::Unhandled(); +} + +TSharedRef SFlowGraphNode::GetNodeUnderMouse(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + const TSharedPtr SubNode = GetSubNodeUnderCursor(MyGeometry, MouseEvent); + if (SubNode.IsValid()) + { + return SubNode.ToSharedRef(); + } + else + { + return StaticCastSharedRef(AsShared()); + } +} + +FReply SFlowGraphNode::OnMouseButtonDown(const FGeometry& SenderGeometry, const FPointerEvent& MouseEvent) +{ + if (FlowGraphNode && FlowGraphNode->IsSubNode()) + { + GetOwnerPanel()->SelectionManager.ClickedOnNode(FlowGraphNode, MouseEvent); + return FReply::Handled(); + } + + return FReply::Unhandled(); +} + +TSharedPtr SFlowGraphNode::GetSubNodeUnderCursor(const FGeometry& WidgetGeometry, const FPointerEvent& MouseEvent) +{ + // We just need to find the one WidgetToFind among our descendants. + TSet< TSharedRef > SubWidgetsSet; + for (int32 i = 0; i < SubNodes.Num(); i++) + { + SubWidgetsSet.Add(SubNodes[i].ToSharedRef()); + } + + TMap, FArrangedWidget> Result; + FindChildGeometries(WidgetGeometry, SubWidgetsSet, Result); + + TSharedPtr ResultNode; + + if (Result.Num() <= 0) + { + return ResultNode; + } + + FArrangedChildren ArrangedChildren(EVisibility::Visible); + Result.GenerateValueArray(ArrangedChildren.GetInternalArray()); + + const int32 HoveredIndex = SWidget::FindChildUnderMouse(ArrangedChildren, MouseEvent); + if (HoveredIndex == INDEX_NONE) + { + return ResultNode; + } + + ResultNode = StaticCastSharedRef(ArrangedChildren[HoveredIndex].Widget); + + // Recurse if the subnode has subnodes + SFlowGraphNode* ResultFlowGraphNode = static_cast(ResultNode.Get()); + const FGeometry& ChildWidgetGeometry = ArrangedChildren[HoveredIndex].Geometry; + if (TSharedPtr ResultFlowGraphNodeSubNode = ResultFlowGraphNode->GetSubNodeUnderCursor(ChildWidgetGeometry, MouseEvent)) + { + return ResultFlowGraphNodeSubNode; + } + + return ResultNode; +} + +void SFlowGraphNode::SetDragMarker(const bool bEnabled) +{ + bDragMarkerVisible = bEnabled; +} + +EVisibility SFlowGraphNode::GetDragOverMarkerVisibility() const +{ + return bDragMarkerVisible ? EVisibility::Visible : EVisibility::Collapsed; +} + +void SFlowGraphNode::OnDragEnter(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) +{ + // Is someone dragging a node? + const TSharedPtr DragConnectionOp = DragDropEvent.GetOperationAs(); + if (DragConnectionOp.IsValid()) + { + // Inform the Drag and Drop operation that we are hovering over this node. + const TSharedPtr SubNode = GetSubNodeUnderCursor(MyGeometry, DragDropEvent); + DragConnectionOp->SetHoveredNode(SubNode.IsValid() ? SubNode : SharedThis(this)); + + if (DragConnectionOp->IsValidOperation() && FlowGraphNode && FlowGraphNode->IsSubNode()) + { + SetDragMarker(true); + } + } + + SGraphNode::OnDragEnter(MyGeometry, DragDropEvent); +} + +FReply SFlowGraphNode::OnDragOver(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) +{ + // Is someone dragging a node? + const TSharedPtr DragConnectionOp = DragDropEvent.GetOperationAs(); + if (DragConnectionOp.IsValid()) + { + // Inform the Drag and Drop operation that we are hovering over this node. + const TSharedPtr SubNode = GetSubNodeUnderCursor(MyGeometry, DragDropEvent); + DragConnectionOp->SetHoveredNode(SubNode.IsValid() ? SubNode : SharedThis(this)); + } + return SGraphNode::OnDragOver(MyGeometry, DragDropEvent); +} + +void SFlowGraphNode::OnDragLeave(const FDragDropEvent& DragDropEvent) +{ + const TSharedPtr DragConnectionOp = DragDropEvent.GetOperationAs(); + if (DragConnectionOp.IsValid()) + { + // Inform the Drag and Drop operation that we are not hovering any pins + DragConnectionOp->SetHoveredNode(TSharedPtr(nullptr)); + } + + SetDragMarker(false); + SGraphNode::OnDragLeave(DragDropEvent); +} + +FReply SFlowGraphNode::OnDrop(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) +{ + SetDragMarker(false); + + const TSharedPtr DragNodeOp = DragDropEvent.GetOperationAs(); + if (!DragNodeOp.IsValid()) + { + return SGraphNode::OnDrop(MyGeometry, DragDropEvent); + } + + if (!DragNodeOp->IsValidOperation()) + { + return FReply::Handled(); + } + + const float DragTime = static_cast(FPlatformTime::Seconds() - DragNodeOp->StartTime); + if (DragTime < 0.5f) + { + return FReply::Handled(); + } + + if (FlowGraphNode == nullptr) + { + return FReply::Unhandled(); + } + + const FScopedTransaction Transaction(LOCTEXT("GraphEd_DragDropNode", "Drag&Drop Node")); + + UFlowGraphNode* DropTargetNode = DragNodeOp->GetDropTargetNode(); + check(DropTargetNode); + + const TArray>& DraggedNodes = DragNodeOp->GetNodes(); + + // Track the set of parents that must be refreshed/reconstructed as a result of this operation + TSet AffectedParents; + + // Remove dragged subnodes from their old parents, and report whether this is purely a reorder. + bool bReorderOperation = true; + + // Inline the parent tracking that RemoveDraggedSubNodes previously couldn't do + for (int32 Idx = 0; Idx < DraggedNodes.Num(); Idx++) + { + UFlowGraphNode* DraggedNode = Cast(DraggedNodes[Idx]->GetNodeObj()); + if (DraggedNode && DraggedNode->GetParentNode()) + { + UFlowGraphNode* OldParent = DraggedNode->GetParentNode(); + AffectedParents.Add(OldParent); + + if (OldParent != GraphNode) + { + bReorderOperation = false; + } + + OldParent->RemoveSubNode(DraggedNode); + } + } + + const bool bShouldDropAsSubNodesOfDropTargetNode = bReorderOperation || ShouldDropDraggedNodesAsSubNodes(DraggedNodes, DropTargetNode); + + // Decide the new parent insertion target (either the hovered node, or its parent for sibling insert) + UFlowGraphNode* DropTargetParentNode = nullptr; + UFlowGraphNode* SiblingInsertBeforeNode = nullptr; + + if (bShouldDropAsSubNodesOfDropTargetNode) + { + DropTargetParentNode = DropTargetNode; + SiblingInsertBeforeNode = nullptr; + } + else + { + DropTargetParentNode = DropTargetNode->GetParentNode(); + SiblingInsertBeforeNode = DropTargetNode; + } + + check(DropTargetParentNode); + AffectedParents.Add(DropTargetParentNode); + + const int32 InsertIndex = DropTargetParentNode->FindSubNodeDropIndex(SiblingInsertBeforeNode); + + // Insert dragged nodes under the new parent + for (int32 Idx = 0; Idx < DraggedNodes.Num(); Idx++) + { + UFlowGraphNode* DraggedFlowNode = Cast(DraggedNodes[Idx]->GetNodeObj()); + if (!IsValid(DraggedFlowNode)) + { + continue; + } + + DraggedFlowNode->Modify(); + DraggedFlowNode->SetParentNodeForSubNode(DropTargetParentNode); + + DropTargetParentNode->Modify(); + DropTargetParentNode->InsertSubNodeAt(DraggedFlowNode, InsertIndex); + DropTargetParentNode->OnSubNodeAdded(DraggedFlowNode); + } + + // One consistent refresh path for both reorder and reparent: + // - NotifyGraphChanged keeps connection harvesting consistent. + // - Queue reconstruct ensures we don't lose reconstruction due to GIsTransacting. + UFlowGraph* MyGraph = DropTargetParentNode->GetFlowGraph(); + if (IsValid(MyGraph)) + { + MyGraph->OnSubNodeDropped(); + + for (UFlowGraphNode* Parent : AffectedParents) + { + if (IsValid(Parent)) + { + MyGraph->EnqueueNodeReconstruct(Parent); + } + } + } + + // UI refresh: reorder wants an immediate widget refresh for the current node tree + if (bReorderOperation) + { + UpdateGraphNode(); + } + + return SGraphNode::OnDrop(MyGeometry, DragDropEvent); +} + +bool SFlowGraphNode::ShouldDropDraggedNodesAsSubNodes(const TArray>& DraggedNodes, const UFlowGraphNode* DropTargetNode) +{ + TSet DraggedFlowGraphNodes; + for (int32 Idx = 0; Idx < DraggedNodes.Num(); Idx++) + { + const UFlowGraphNode* DraggedNode = Cast(DraggedNodes[Idx]->GetNodeObj()); + if (IsValid(DraggedNode)) + { + DraggedFlowGraphNodes.Add(DraggedNode); + } + } + + for (TSet::TConstIterator It(DraggedFlowGraphNodes); It; ++It) + { + const UFlowGraphNode* DraggedNode = Cast(*It); + + // Check if all the dragged nodes can be stopped as a subnode + // (if not ALL, then we cannot drop ANY of them) + const bool bCanDropDraggedNodeAsSubNode = DropTargetNode->CanAcceptSubNodeAsChild(*DraggedNode, DraggedFlowGraphNodes); + + if (!bCanDropDraggedNodeAsSubNode) + { + return false; + } + } + + return true; +} + +void SFlowGraphNode::RemoveDraggedSubNodes(const TArray< TSharedRef >& DraggedNodes, bool& bInOutReorderOperation) const +{ + for (int32 Idx = 0; Idx < DraggedNodes.Num(); Idx++) + { + UFlowGraphNode* DraggedNode = Cast(DraggedNodes[Idx]->GetNodeObj()); + if (DraggedNode && DraggedNode->GetParentNode()) + { + if (DraggedNode->GetParentNode() != GraphNode) + { + bInOutReorderOperation = false; + } + + DraggedNode->GetParentNode()->RemoveSubNode(DraggedNode); + } + } +} + +FText SFlowGraphNode::GetPreviewCornerText() const +{ + return FText::GetEmpty(); +} + +const FSlateBrush* SFlowGraphNode::GetNameIcon() const +{ + return FAppStyle::GetBrush(TEXT("Graph.StateNode.Icon")); +} + +void SFlowGraphNode::SetOwner(const TSharedRef& OwnerPanel) +{ + SGraphNode::SetOwner(OwnerPanel); + + for (auto& ChildWidget : SubNodes) + { + if (ChildWidget.IsValid()) + { + ChildWidget->SetOwner(OwnerPanel); + OwnerPanel->AttachGraphEvents(ChildWidget); + } + } +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode_SubGraph.cpp b/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode_SubGraph.cpp index 7e626f880..7cd94a877 100644 --- a/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode_SubGraph.cpp +++ b/Source/FlowEditor/Private/Graph/Widgets/SFlowGraphNode_SubGraph.cpp @@ -4,25 +4,26 @@ #include "Graph/FlowGraphEditorSettings.h" #include "FlowAsset.h" -#include "Nodes/Route/FlowNode_SubGraph.h" +#include "Nodes/Graph/FlowNode_SubGraph.h" -#include "IDocumentation.h" #include "SGraphPreviewer.h" +#include "Widgets/Layout/SBox.h" #include "Widgets/SToolTip.h" #define LOCTEXT_NAMESPACE "SFlowGraphNode_SubGraph" TSharedPtr SFlowGraphNode_SubGraph::GetComplexTooltip() { - if (UFlowGraphEditorSettings::Get()->bShowSubGraphPreview && FlowGraphNode) + const UFlowGraphEditorSettings* GraphEditorSettings = GetDefault(); + if (GraphEditorSettings->bShowSubGraphPreview && FlowGraphNode) { - if (UFlowNode* FlowNode = FlowGraphNode->GetFlowNode()) + if (UFlowNode* FlowNode = Cast(FlowGraphNode->GetFlowNodeBase())) { const UFlowAsset* AssetToEdit = Cast(FlowNode->GetAssetToEdit()); if (AssetToEdit && AssetToEdit->GetGraph()) { TSharedPtr TitleBarWidget = SNullWidget::NullWidget; - if (UFlowGraphEditorSettings::Get()->bShowSubGraphPath) + if (GraphEditorSettings->bShowSubGraphPath) { FString CleanAssetName = AssetToEdit->GetPathName(nullptr); const int32 SubStringIdx = CleanAssetName.Find(".", ESearchCase::IgnoreCase, ESearchDir::FromEnd); @@ -39,8 +40,8 @@ TSharedPtr SFlowGraphNode_SubGraph::GetComplexTooltip() return SNew(SToolTip) [ SNew(SBox) - .WidthOverride(UFlowGraphEditorSettings::Get()->SubGraphPreviewSize.X) - .HeightOverride(UFlowGraphEditorSettings::Get()->SubGraphPreviewSize.Y) + .WidthOverride(GraphEditorSettings->SubGraphPreviewSize.X) + .HeightOverride(GraphEditorSettings->SubGraphPreviewSize.Y) [ SNew(SOverlay) +SOverlay::Slot() diff --git a/Source/FlowEditor/Private/Graph/Widgets/SFlowPalette.cpp b/Source/FlowEditor/Private/Graph/Widgets/SFlowPalette.cpp index b73374648..278b31bbf 100644 --- a/Source/FlowEditor/Private/Graph/Widgets/SFlowPalette.cpp +++ b/Source/FlowEditor/Private/Graph/Widgets/SFlowPalette.cpp @@ -9,7 +9,6 @@ #include "FlowAsset.h" #include "Nodes/FlowNode.h" -#include "EditorStyleSet.h" #include "Fonts/SlateFontInfo.h" #include "Styling/CoreStyle.h" #include "Styling/SlateBrush.h" @@ -47,7 +46,7 @@ void SFlowPaletteItem::Construct(const FArguments& InArgs, FCreateWidgetForActio } // Find icons - const FSlateBrush* IconBrush = FEditorStyle::GetBrush(TEXT("NoBrush")); + const FSlateBrush* IconBrush = FAppStyle::GetBrush(TEXT("NoBrush")); const FSlateColor IconColor = FSlateColor::UseForeground(); const FText IconToolTip = GraphAction->GetTooltipDescription(); constexpr bool bIsReadOnly = false; @@ -101,16 +100,27 @@ FText SFlowPaletteItem::GetItemTooltip() const void SFlowPalette::Construct(const FArguments& InArgs, TWeakPtr InFlowAssetEditor) { - FlowAssetEditorPtr = InFlowAssetEditor; + FlowAssetEditor = InFlowAssetEditor; UpdateCategoryNames(); UFlowGraphSchema::OnNodeListChanged.AddSP(this, &SFlowPalette::Refresh); + struct LocalUtils + { + static TSharedRef CreateCustomExpanderStatic(const FCustomExpanderData& ActionMenuData, bool bShowFavoriteToggle) + { + TSharedPtr CustomExpander; + // in SBlueprintSubPalette here would be a difference depending on bShowFavoriteToggle + SAssignNew(CustomExpander, SExpanderArrow, ActionMenuData.TableRow); + return CustomExpander.ToSharedRef(); + } + }; + this->ChildSlot [ SNew(SBorder) .Padding(2.0f) - .BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder")) + .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) [ SNew(SVerticalBox) + SVerticalBox::Slot() // Filter UI @@ -135,6 +145,7 @@ void SFlowPalette::Construct(const FArguments& InArgs, TWeakPtr SFlowPalette::OnCreateWidgetForAction(FCreateWidgetForAction void SFlowPalette::CollectAllActions(FGraphActionListBuilderBase& OutAllActions) { - const UClass* AssetClass = UFlowAsset::StaticClass(); - - const TSharedPtr FlowAssetEditor = FlowAssetEditorPtr.Pin(); - if (FlowAssetEditor && FlowAssetEditor->GetFlowAsset()) - { - AssetClass = FlowAssetEditor->GetFlowAsset()->GetClass(); - } + ensureAlways(FlowAssetEditor.Pin() && FlowAssetEditor.Pin()->GetFlowAsset()); + const UFlowAsset* EditedFlowAsset = FlowAssetEditor.Pin()->GetFlowAsset(); FGraphActionMenuBuilder ActionMenuBuilder; - UFlowGraphSchema::GetPaletteActions(ActionMenuBuilder, AssetClass, GetFilterCategoryName()); + UFlowGraphSchema::GetPaletteActions(ActionMenuBuilder, EditedFlowAsset, GetFilterCategoryName()); OutAllActions.Append(ActionMenuBuilder); } @@ -215,11 +221,7 @@ void SFlowPalette::OnActionSelected(const TArray FlowAssetEditor = FlowAssetEditorPtr.Pin(); - if (FlowAssetEditor) - { - FlowAssetEditor->SetUISelectionState(FFlowAssetEditor::PaletteTab); - } + FlowAssetEditor.Pin()->SetUISelectionState(FFlowAssetEditor::PaletteTab); } } diff --git a/Source/FlowEditor/Private/Graph/Widgets/SGraphEditorActionMenuFlow.cpp b/Source/FlowEditor/Private/Graph/Widgets/SGraphEditorActionMenuFlow.cpp new file mode 100644 index 000000000..f6ec37aeb --- /dev/null +++ b/Source/FlowEditor/Private/Graph/Widgets/SGraphEditorActionMenuFlow.cpp @@ -0,0 +1,107 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Graph/Widgets/SGraphEditorActionMenuFlow.h" +#include "Graph/FlowGraphSchema.h" + +#include "EdGraph/EdGraph.h" +#include "Framework/Application/SlateApplication.h" +#include "HAL/Platform.h" +#include "HAL/PlatformCrt.h" +#include "Layout/Margin.h" +#include "Misc/Attribute.h" +#include "Runtime/Launch/Resources/Version.h" +#include "SGraphActionMenu.h" +#include "Styling/AppStyle.h" +#include "Templates/Casts.h" +#include "Types/SlateStructs.h" +#include "Widgets/Layout/SBox.h" + +SGraphEditorActionMenuFlow::~SGraphEditorActionMenuFlow() +{ + OnClosedCallback.ExecuteIfBound(); +} + +void SGraphEditorActionMenuFlow::Construct(const FArguments& InArgs) +{ + this->GraphObj = InArgs._GraphObj; + this->GraphNode = InArgs._GraphNode; + this->DraggedFromPins = InArgs._DraggedFromPins; + this->NewNodePosition = InArgs._NewNodePosition; + this->OnClosedCallback = InArgs._OnClosedCallback; + this->AutoExpandActionMenu = InArgs._AutoExpandActionMenu; + this->SubNodeFlags = InArgs._SubNodeFlags; + + // Build the widget layout + SBorder::Construct(SBorder::FArguments() + .BorderImage(FAppStyle::GetBrush("Menu.Background")) + .Padding(5.f) + [ + // Achieving fixed width by nesting items within a fixed width box. + SNew(SBox) + .WidthOverride(400.f) + [ + SAssignNew(GraphActionMenu, SGraphActionMenu) + .OnActionSelected(this, &SGraphEditorActionMenuFlow::OnActionSelected) + .OnCollectAllActions(this, &SGraphEditorActionMenuFlow::CollectAllActions) + .AutoExpandActionMenu(AutoExpandActionMenu) + ] + ] + ); +} + +void SGraphEditorActionMenuFlow::CollectAllActions(FGraphActionListBuilderBase& OutAllActions) +{ + // Build up the context object + FGraphContextMenuBuilder ContextMenuBuilder(GraphObj); + if (GraphNode != nullptr) + { + ContextMenuBuilder.SelectedObjects.Add(GraphNode); + } + if (DraggedFromPins.Num() > 0) + { + ContextMenuBuilder.FromPin = DraggedFromPins[0]; + } + + // Determine all possible actions + const UFlowGraphSchema* MySchema = Cast(GraphObj->GetSchema()); + if (MySchema) + { + MySchema->GetGraphNodeContextActions(ContextMenuBuilder, SubNodeFlags); + } + + // Copy the added options back to the main list + //@TODO: Avoid this copy + OutAllActions.Append(ContextMenuBuilder); +} + +TSharedRef SGraphEditorActionMenuFlow::GetFilterTextBox() +{ + return GraphActionMenu->GetFilterTextBox(); +} + +void SGraphEditorActionMenuFlow::OnActionSelected(const TArray>& SelectedAction, ESelectInfo::Type InSelectionType) +{ + if (InSelectionType == ESelectInfo::OnMouseClick || InSelectionType == ESelectInfo::OnKeyPress || SelectedAction.Num() == 0) + { + bool bDoDismissMenus = false; + + if (GraphObj) + { + for (int32 ActionIndex = 0; ActionIndex < SelectedAction.Num(); ActionIndex++) + { + TSharedPtr CurrentAction = SelectedAction[ActionIndex]; + + if (CurrentAction.IsValid()) + { + CurrentAction->PerformAction(GraphObj, DraggedFromPins, NewNodePosition); + bDoDismissMenus = true; + } + } + } + + if (bDoDismissMenus) + { + FSlateApplication::Get().DismissAllMenus(); + } + } +} diff --git a/Source/FlowEditor/Private/LevelEditor/SLevelEditorFlow.cpp b/Source/FlowEditor/Private/LevelEditor/SLevelEditorFlow.cpp deleted file mode 100644 index 11c18509e..000000000 --- a/Source/FlowEditor/Private/LevelEditor/SLevelEditorFlow.cpp +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - -#include "LevelEditor/SLevelEditorFlow.h" -#include "FlowAsset.h" -#include "FlowComponent.h" -#include "FlowWorldSettings.h" - -#include "Graph/FlowGraphSettings.h" - -#include "Editor.h" -#include "PropertyCustomizationHelpers.h" - -#define LOCTEXT_NAMESPACE "SLevelEditorFlow" - -void SLevelEditorFlow::Construct(const FArguments& InArgs) -{ - CreateFlowWidget(); - FEditorDelegates::OnMapOpened.AddRaw(this, &SLevelEditorFlow::OnMapOpened); -} - -void SLevelEditorFlow::OnMapOpened(const FString& Filename, bool bAsTemplate) -{ - CreateFlowWidget(); -} - -void SLevelEditorFlow::CreateFlowWidget() -{ - if (UFlowComponent* FlowComponent = FindFlowComponent(); FlowComponent && FlowComponent->RootFlow) - { - FlowPath = FName(*FlowComponent->RootFlow->GetPathName()); - } - else - { - FlowPath = FName(); - } - - ChildSlot - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .AutoWidth() - [ - SNew(SObjectPropertyEntryBox) - .AllowedClass(UFlowGraphSettings::Get()->WorldAssetClass) - .DisplayThumbnail(false) - .OnObjectChanged(this, &SLevelEditorFlow::OnFlowChanged) - .ObjectPath(this, &SLevelEditorFlow::GetFlowPath) - ] - ]; -} - -void SLevelEditorFlow::OnFlowChanged(const FAssetData& NewAsset) -{ - FlowPath = NewAsset.ObjectPath; - - if (UFlowComponent* FlowComponent = FindFlowComponent()) - { - if (UObject* NewObject = NewAsset.GetAsset()) - { - FlowComponent->RootFlow = Cast(NewObject); - } - else - { - FlowComponent->RootFlow = nullptr; - } - - const bool bSuccess = FlowComponent->MarkPackageDirty(); - ensureMsgf(bSuccess, TEXT("World Settings couldn't be marked dirty while changing the assigned Flow Asset.")); - } -} - -FString SLevelEditorFlow::GetFlowPath() const -{ - return FlowPath.IsValid() ? FlowPath.ToString() : FString(); -} - -UFlowComponent* SLevelEditorFlow::FindFlowComponent() const -{ - if (const UWorld* World = GEditor->GetEditorWorldContext().World()) - { - if (const AWorldSettings* WorldSettings = World->GetWorldSettings()) - { - if (UActorComponent* FoundComponent = WorldSettings->GetComponentByClass(UFlowComponent::StaticClass())) - { - return Cast(FoundComponent); - } - } - } - - return nullptr; -} - -#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/MovieScene/FlowSection.cpp b/Source/FlowEditor/Private/MovieScene/FlowSection.cpp index 5a67120b4..db37fa34e 100644 --- a/Source/FlowEditor/Private/MovieScene/FlowSection.cpp +++ b/Source/FlowEditor/Private/MovieScene/FlowSection.cpp @@ -1,41 +1,41 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors -#include "FlowSection.h" +#include "MovieScene/FlowSection.h" #include "MovieScene/MovieSceneFlowRepeaterSection.h" #include "MovieScene/MovieSceneFlowTriggerSection.h" -#include "CommonMovieSceneTools.h" #include "Fonts/FontMeasure.h" #include "Framework/Application/SlateApplication.h" -#include "MovieSceneEventUtils.h" #include "MovieSceneTrack.h" #include "Rendering/DrawElements.h" +#include "Runtime/Launch/Resources/Version.h" #include "Sections/MovieSceneEventSection.h" #include "SequencerSectionPainter.h" #include "SequencerTimeSliderController.h" +#include "TimeToPixel.h" #define LOCTEXT_NAMESPACE "FlowSection" bool FFlowSectionBase::IsSectionSelected() const { - TSharedPtr SequencerPtr = Sequencer.Pin(); + const TSharedPtr SequencerPtr = Sequencer.Pin(); TArray SelectedTracks; SequencerPtr->GetSelectedTracks(SelectedTracks); - UMovieSceneSection* Section = WeakSection.Get(); + const UMovieSceneSection* Section = WeakSection.Get(); UMovieSceneTrack* Track = Section ? CastChecked(Section->GetOuter()) : nullptr; return Track && SelectedTracks.Contains(Track); } void FFlowSectionBase::PaintEventName(FSequencerSectionPainter& Painter, int32 LayerId, const FString& InEventString, float PixelPos, bool bIsEventValid) const { - static const float BoxOffsetPx = 10.f; + static constexpr float BoxOffsetPx = 10.f; static const TCHAR* WarningString = TEXT("\xf071"); - const FSlateFontInfo FontAwesomeFont = FEditorStyle::Get().GetFontStyle("FontAwesome.10"); + const FSlateFontInfo FontAwesomeFont = FAppStyle::Get().GetFontStyle("FontAwesome.10"); const FSlateFontInfo SmallLayoutFont = FCoreStyle::GetDefaultFontStyle("Bold", 10); - const FLinearColor DrawColor = FEditorStyle::GetSlateColor("SelectionColor").GetColor(FWidgetStyle()); + const FLinearColor DrawColor = FAppStyle::GetSlateColor("SelectionColor").GetColor(FWidgetStyle()); TSharedRef FontMeasureService = FSlateApplication::Get().GetRenderer()->GetFontMeasureService(); @@ -65,8 +65,8 @@ void FFlowSectionBase::PaintEventName(FSequencerSectionPainter& Painter, int32 L FSlateDrawElement::MakeBox( Painter.DrawElements, LayerId + 1, - Painter.SectionGeometry.ToPaintGeometry(BoxOffset, BoxSize), - FEditorStyle::GetBrush("WhiteBrush"), + Painter.SectionGeometry.ToPaintGeometry(BoxSize, FSlateLayoutTransform(1.0f, TransformPoint(1.0f, UE::Slate::CastToVector2f(BoxOffset)))), + FAppStyle::GetBrush("WhiteBrush"), ESlateDrawEffect::None, FLinearColor::Black.CopyWithNewOpacity(0.5f) ); @@ -77,18 +77,18 @@ void FFlowSectionBase::PaintEventName(FSequencerSectionPainter& Painter, int32 L FSlateDrawElement::MakeText( Painter.DrawElements, LayerId + 2, - Painter.SectionGeometry.ToPaintGeometry(BoxOffset + IconOffset, IconSize), + Painter.SectionGeometry.ToPaintGeometry(IconSize, FSlateLayoutTransform(1.0f, TransformPoint(1.0f, UE::Slate::CastToVector2f(BoxOffset + IconOffset)))), WarningString, FontAwesomeFont, Painter.bParentEnabled ? ESlateDrawEffect::None : ESlateDrawEffect::DisabledEffect, - FEditorStyle::GetWidgetStyle("Log.Warning").ColorAndOpacity.GetSpecifiedColor() + FAppStyle::GetWidgetStyle("Log.Warning").ColorAndOpacity.GetSpecifiedColor() ); } FSlateDrawElement::MakeText( Painter.DrawElements, LayerId + 2, - Painter.SectionGeometry.ToPaintGeometry(BoxOffset + TextOffset, TextSize), + Painter.SectionGeometry.ToPaintGeometry(TextSize, FSlateLayoutTransform(1.0f, TransformPoint(1.0f, UE::Slate::CastToVector2f(BoxOffset + TextOffset)))), InEventString, SmallLayoutFont, Painter.bParentEnabled ? ESlateDrawEffect::None : ESlateDrawEffect::DisabledEffect, @@ -99,7 +99,7 @@ void FFlowSectionBase::PaintEventName(FSequencerSectionPainter& Painter, int32 L int32 FFlowSection::OnPaintSection(FSequencerSectionPainter& Painter) const { const int32 LayerId = Painter.PaintSectionBackground(); - UMovieSceneEventSection* EventSection = Cast(WeakSection.Get()); + const UMovieSceneEventSection* EventSection = Cast(WeakSection.Get()); if (!EventSection || !IsSectionSelected()) { return LayerId; @@ -160,7 +160,7 @@ int32 FFlowRepeaterSection::OnPaintSection(FSequencerSectionPainter& Painter) co { const int32 LayerId = Painter.PaintSectionBackground(); - UMovieSceneFlowRepeaterSection* EventRepeaterSection = Cast(WeakSection.Get()); + const UMovieSceneFlowRepeaterSection* EventRepeaterSection = Cast(WeakSection.Get()); if (!EventRepeaterSection) { return LayerId; diff --git a/Source/FlowEditor/Private/MovieScene/FlowTrackEditor.cpp b/Source/FlowEditor/Private/MovieScene/FlowTrackEditor.cpp index 51dd0d55d..5adc92d7b 100644 --- a/Source/FlowEditor/Private/MovieScene/FlowTrackEditor.cpp +++ b/Source/FlowEditor/Private/MovieScene/FlowTrackEditor.cpp @@ -1,20 +1,19 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors -#include "FlowTrackEditor.h" -#include "FlowSection.h" +#include "MovieScene/FlowTrackEditor.h" +#include "MovieScene/FlowSection.h" #include "MovieScene/MovieSceneFlowRepeaterSection.h" #include "MovieScene/MovieSceneFlowTrack.h" #include "MovieScene/MovieSceneFlowTriggerSection.h" +#include "Components/HorizontalBox.h" #include "Framework/MultiBox/MultiBoxBuilder.h" -#include "UObject/Package.h" #include "ISequencerSection.h" -#include "DetailLayoutBuilder.h" -#include "DetailCategoryBuilder.h" +#include "LevelSequence.h" +#include "MovieSceneSequenceEditor.h" #include "Sections/MovieSceneEventSection.h" #include "SequencerUtilities.h" -#include "MovieSceneSequenceEditor.h" #define LOCTEXT_NAMESPACE "FFlowTrackEditor" @@ -67,7 +66,7 @@ void FFlowTrackEditor::AddFlowSubMenu(FMenuBuilder& MenuBuilder) void FFlowTrackEditor::BuildAddTrackMenu(FMenuBuilder& MenuBuilder) { UMovieSceneSequence* RootMovieSceneSequence = GetSequencer()->GetRootMovieSceneSequence(); - FMovieSceneSequenceEditor* SequenceEditor = FMovieSceneSequenceEditor::Find(RootMovieSceneSequence); + const FMovieSceneSequenceEditor* SequenceEditor = FMovieSceneSequenceEditor::Find(RootMovieSceneSequence); if (SequenceEditor && SequenceEditor->SupportsEvents(RootMovieSceneSequence)) { @@ -76,7 +75,7 @@ void FFlowTrackEditor::BuildAddTrackMenu(FMenuBuilder& MenuBuilder) LOCTEXT("AddTooltip", "Adds a new flow track that can trigger events in the Flow graph."), FNewMenuDelegate::CreateRaw(this, &FFlowTrackEditor::AddFlowSubMenu), false, - FSlateIcon(FEditorStyle::GetStyleSetName(), "Sequencer.Tracks.Event") + FSlateIcon(FAppStyle::GetAppStyleSetName(), "Sequencer.Tracks.Event") ); } } @@ -138,16 +137,15 @@ bool FFlowTrackEditor::SupportsType(TSubclassOf Type) const bool FFlowTrackEditor::SupportsSequence(UMovieSceneSequence* InSequence) const { - static UClass* LevelSequenceClass = FindObject(ANY_PACKAGE, TEXT("LevelSequence"), true); - return InSequence && LevelSequenceClass && InSequence->GetClass()->IsChildOf(LevelSequenceClass); + return InSequence && InSequence->GetClass()->IsChildOf(ULevelSequence::StaticClass()); } const FSlateBrush* FFlowTrackEditor::GetIconBrush() const { - return FEditorStyle::GetBrush("Sequencer.Tracks.Event"); + return FAppStyle::GetBrush("Sequencer.Tracks.Event"); } -void FFlowTrackEditor::HandleAddFlowTrackMenuEntryExecute(UClass* SectionType) +void FFlowTrackEditor::HandleAddFlowTrackMenuEntryExecute(UClass* SectionType) const { UMovieScene* FocusedMovieScene = GetFocusedMovieScene(); @@ -165,9 +163,9 @@ void FFlowTrackEditor::HandleAddFlowTrackMenuEntryExecute(UClass* SectionType) FocusedMovieScene->Modify(); TArray NewTracks; - - UMovieSceneFlowTrack* NewMasterTrack = FocusedMovieScene->AddMasterTrack(); + UMovieSceneFlowTrack* NewMasterTrack = FocusedMovieScene->AddTrack(); NewTracks.Add(NewMasterTrack); + if (GetSequencer().IsValid()) { GetSequencer()->OnAddTrack(NewMasterTrack, FGuid()); @@ -182,12 +180,12 @@ void FFlowTrackEditor::HandleAddFlowTrackMenuEntryExecute(UClass* SectionType) } } -void FFlowTrackEditor::CreateNewSection(UMovieSceneTrack* Track, int32 RowIndex, UClass* SectionType, bool bSelect) const +void FFlowTrackEditor::CreateNewSection(UMovieSceneTrack* Track, const int32 RowIndex, UClass* SectionType, const bool bSelect) const { - TSharedPtr SequencerPtr = GetSequencer(); + const TSharedPtr SequencerPtr = GetSequencer(); if (SequencerPtr.IsValid()) { - UMovieScene* FocusedMovieScene = GetFocusedMovieScene(); + const UMovieScene* FocusedMovieScene = GetFocusedMovieScene(); const FQualifiedFrameTime CurrentTime = SequencerPtr->GetLocalTime(); FScopedTransaction Transaction(LOCTEXT("CreateNewFlowSectionTransactionText", "Add Flow Section")); @@ -221,7 +219,7 @@ void FFlowTrackEditor::CreateNewSection(UMovieSceneTrack* Track, int32 RowIndex, } else { - const float DefaultLengthInSeconds = 5.f; + constexpr float DefaultLengthInSeconds = 5.f; NewSectionRange = TRange(CurrentTime.Time.FrameNumber, CurrentTime.Time.FrameNumber + (DefaultLengthInSeconds * SequencerPtr->GetFocusedTickResolution()).FloorToFrame()); } diff --git a/Source/FlowEditor/Private/Nodes/AssetTypeActions_FlowNodeAddOnBlueprint.cpp b/Source/FlowEditor/Private/Nodes/AssetTypeActions_FlowNodeAddOnBlueprint.cpp new file mode 100644 index 000000000..4d5775d1e --- /dev/null +++ b/Source/FlowEditor/Private/Nodes/AssetTypeActions_FlowNodeAddOnBlueprint.cpp @@ -0,0 +1,34 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Nodes/AssetTypeActions_FlowNodeAddOnBlueprint.h" +#include "Nodes/FlowNodeBlueprintFactory.h" +#include "Nodes/FlowNodeAddOnBlueprint.h" +#include "AddOns/FlowNodeAddOn.h" +#include "Graph/FlowGraphSettings.h" +#include "FlowEditorModule.h" + +#define LOCTEXT_NAMESPACE "AssetTypeActions_FlowNodeAddOnBlueprint" + +FText FAssetTypeActions_FlowNodeAddOnBlueprint::GetName() const +{ + return LOCTEXT("AssetTypeActions_FlowNodeBlueprint", "Flow Node AddOn Blueprint"); +} + +uint32 FAssetTypeActions_FlowNodeAddOnBlueprint::GetCategories() +{ + return GetDefault()->bExposeFlowNodeCreation ? FFlowEditorModule::FlowAssetCategory : 0; +} + +UClass* FAssetTypeActions_FlowNodeAddOnBlueprint::GetSupportedClass() const +{ + return UFlowNodeAddOnBlueprint::StaticClass(); +} + +UFactory* FAssetTypeActions_FlowNodeAddOnBlueprint::GetFactoryForBlueprintType(UBlueprint* InBlueprint) const +{ + UFlowNodeAddOnBlueprintFactory* FlowNodeAddOnBlueprintFactory = NewObject(); + FlowNodeAddOnBlueprintFactory->ParentClass = TSubclassOf(*InBlueprint->GeneratedClass); + return FlowNodeAddOnBlueprintFactory; +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Nodes/AssetTypeActions_FlowNodeBlueprint.cpp b/Source/FlowEditor/Private/Nodes/AssetTypeActions_FlowNodeBlueprint.cpp index a7b1b6afe..06560acf8 100644 --- a/Source/FlowEditor/Private/Nodes/AssetTypeActions_FlowNodeBlueprint.cpp +++ b/Source/FlowEditor/Private/Nodes/AssetTypeActions_FlowNodeBlueprint.cpp @@ -2,10 +2,11 @@ #include "Nodes/AssetTypeActions_FlowNodeBlueprint.h" #include "Nodes/FlowNodeBlueprintFactory.h" -#include "FlowEditorModule.h" +#include "Nodes/FlowNodeBlueprint.h" +#include "Nodes/FlowNode.h" #include "Graph/FlowGraphSettings.h" +#include "FlowEditorModule.h" -#include "Nodes/FlowNodeBlueprint.h" #define LOCTEXT_NAMESPACE "AssetTypeActions_FlowNodeBlueprint" @@ -16,7 +17,7 @@ FText FAssetTypeActions_FlowNodeBlueprint::GetName() const uint32 FAssetTypeActions_FlowNodeBlueprint::GetCategories() { - return UFlowGraphSettings::Get()->bExposeFlowNodeCreation ? FFlowEditorModule::FlowAssetCategory : 0; + return GetDefault()->bExposeFlowNodeCreation ? FFlowEditorModule::FlowAssetCategory : 0; } UClass* FAssetTypeActions_FlowNodeBlueprint::GetSupportedClass() const diff --git a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_CustomInputDetails.cpp b/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_CustomInputDetails.cpp deleted file mode 100644 index 4892202b3..000000000 --- a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_CustomInputDetails.cpp +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - -#include "FlowNode_CustomInputDetails.h" -#include "FlowAsset.h" -#include "Nodes/Route/FlowNode_CustomInput.h" - -#include "DetailCategoryBuilder.h" -#include "DetailWidgetRow.h" -#include "PropertyEditing.h" -#include "UnrealEd.h" -#include "Widgets/Input/SComboBox.h" -#include "Widgets/Text/STextBlock.h" - -#define LOCTEXT_NAMESPACE "FlowNode_CustomInputDetails" - -void FFlowNode_CustomInputDetails::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) -{ - DetailLayout.GetObjectsBeingCustomized(ObjectsBeingEdited); - GetEventNames(); - - IDetailCategoryBuilder& Category = DetailLayout.EditCategory("CustomInput", LOCTEXT("CustomInputCategory", "Custom Event")); - Category.AddCustomRow(LOCTEXT("CustomRowName", "Event Name")) - .NameContent() - [ - SNew(STextBlock) - .Text(LOCTEXT("EventName", "Event Name")) - ] - .ValueContent() - .HAlign(HAlign_Fill) - [ - SNew(SComboBox>) - .OptionsSource(&EventNames) - .OnGenerateWidget(this, &FFlowNode_CustomInputDetails::GenerateEventWidget) - .OnSelectionChanged(this, &FFlowNode_CustomInputDetails::PinSelectionChanged) - [ - SNew(STextBlock) - .Text(this, &FFlowNode_CustomInputDetails::GetSelectedEventText) - ] - ]; -} - -void FFlowNode_CustomInputDetails::GetEventNames() -{ - EventNames.Empty(); - EventNames.Emplace(MakeShareable(new FName(NAME_None))); - - if (ObjectsBeingEdited[0].IsValid() && ObjectsBeingEdited[0].Get()->GetOuter()) - { - const UFlowAsset* FlowAsset = Cast(ObjectsBeingEdited[0].Get()->GetOuter()); - TArray SortedNames = FlowAsset->GetCustomInputs(); - - for (const TPair& Node : FlowAsset->GetNodes()) - { - if (Node.Value->GetClass()->IsChildOf(UFlowNode_CustomInput::StaticClass())) - { - SortedNames.Remove(Cast(Node.Value)->EventName); - } - } - - SortedNames.Sort([](const FName& A, const FName& B) - { - return A.LexicalLess(B); - }); - - for (const FName& EventName : SortedNames) - { - if (!EventName.IsNone()) - { - EventNames.Emplace(MakeShareable(new FName(EventName))); - } - } - } -} - -TSharedRef FFlowNode_CustomInputDetails::GenerateEventWidget(const TSharedPtr Item) const -{ - return SNew(STextBlock).Text(FText::FromName(*Item.Get())); -} - -FText FFlowNode_CustomInputDetails::GetSelectedEventText() const -{ - FText PropertyValue; - - ensure(ObjectsBeingEdited[0].IsValid()); - if (const UFlowNode_CustomInput* Node = Cast(ObjectsBeingEdited[0].Get())) - { - PropertyValue = FText::FromName(Node->EventName); - } - - return PropertyValue; -} - -void FFlowNode_CustomInputDetails::PinSelectionChanged(const TSharedPtr Item, ESelectInfo::Type SelectInfo) const -{ - ensure(ObjectsBeingEdited[0].IsValid()); - if (UFlowNode_CustomInput* Node = Cast(ObjectsBeingEdited[0].Get())) - { - Node->EventName = *Item.Get(); - } -} - -#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_CustomInputDetails.h b/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_CustomInputDetails.h deleted file mode 100644 index 2fe9b995a..000000000 --- a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_CustomInputDetails.h +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - -#pragma once - -#include "IDetailCustomization.h" -#include "Templates/SharedPointer.h" -#include "Widgets/SWidget.h" - -class FFlowNode_CustomInputDetails final : public IDetailCustomization -{ -public: - static TSharedRef MakeInstance() - { - return MakeShareable(new FFlowNode_CustomInputDetails()); - } - - // IDetailCustomization - virtual void CustomizeDetails(IDetailLayoutBuilder& DetailLayout) override; - // -- - -private: - void GetEventNames(); - TSharedRef GenerateEventWidget(TSharedPtr Item) const; - FText GetSelectedEventText() const; - void PinSelectionChanged(TSharedPtr Item, ESelectInfo::Type SelectInfo) const; - - TArray> ObjectsBeingEdited; - TArray> EventNames; -}; diff --git a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_CustomOutputDetails.cpp b/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_CustomOutputDetails.cpp deleted file mode 100644 index bc7923bb2..000000000 --- a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_CustomOutputDetails.cpp +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - -#include "FlowNode_CustomOutputDetails.h" -#include "FlowAsset.h" -#include "Nodes/Route/FlowNode_CustomOutput.h" - -#include "DetailCategoryBuilder.h" -#include "DetailWidgetRow.h" -#include "PropertyEditing.h" -#include "UnrealEd.h" -#include "Widgets/Input/SComboBox.h" -#include "Widgets/Text/STextBlock.h" - -#define LOCTEXT_NAMESPACE "FlowNode_CustomOutputDetails" - -void FFlowNode_CustomOutputDetails::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) -{ - DetailLayout.GetObjectsBeingCustomized(ObjectsBeingEdited); - GetEventNames(); - - IDetailCategoryBuilder& Category = DetailLayout.EditCategory("CustomOutput", LOCTEXT("CustomEventsCategory", "Custom Output")); - Category.AddCustomRow(LOCTEXT("CustomRowName", "Event Name")) - .NameContent() - [ - SNew(STextBlock) - .Text(LOCTEXT("EventName", "Event Name")) - ] - .ValueContent() - .HAlign(HAlign_Fill) - [ - SNew(SComboBox>) - .OptionsSource(&EventNames) - .OnGenerateWidget(this, &FFlowNode_CustomOutputDetails::GenerateEventWidget) - .OnSelectionChanged(this, &FFlowNode_CustomOutputDetails::PinSelectionChanged) - [ - SNew(STextBlock) - .Text(this, &FFlowNode_CustomOutputDetails::GetSelectedEventText) - ] - ]; -} - -void FFlowNode_CustomOutputDetails::GetEventNames() -{ - EventNames.Empty(); - EventNames.Emplace(MakeShareable(new FName(NAME_None))); - - if (ObjectsBeingEdited[0].IsValid() && ObjectsBeingEdited[0].Get()->GetOuter()) - { - const UFlowAsset* FlowAsset = Cast(ObjectsBeingEdited[0].Get()->GetOuter()); - TArray SortedNames = FlowAsset->GetCustomOutputs(); - - SortedNames.Sort([](const FName& A, const FName& B) - { - return A.LexicalLess(B); - }); - - for (const FName& EventName : SortedNames) - { - if (!EventName.IsNone()) - { - EventNames.Emplace(MakeShareable(new FName(EventName))); - } - } - } -} - -TSharedRef FFlowNode_CustomOutputDetails::GenerateEventWidget(const TSharedPtr Item) const -{ - return SNew(STextBlock).Text(FText::FromName(*Item.Get())); -} - -FText FFlowNode_CustomOutputDetails::GetSelectedEventText() const -{ - FText PropertyValue; - - ensure(ObjectsBeingEdited[0].IsValid()); - if (const UFlowNode_CustomOutput* Node = Cast(ObjectsBeingEdited[0].Get())) - { - PropertyValue = FText::FromName(Node->EventName); - } - - return PropertyValue; -} - -void FFlowNode_CustomOutputDetails::PinSelectionChanged(const TSharedPtr Item, ESelectInfo::Type SelectInfo) const -{ - ensure(ObjectsBeingEdited[0].IsValid()); - if (UFlowNode_CustomOutput* Node = Cast(ObjectsBeingEdited[0].Get())) - { - Node->EventName = *Item.Get(); - } -} - -#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_CustomOutputDetails.h b/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_CustomOutputDetails.h deleted file mode 100644 index dba0de2c2..000000000 --- a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_CustomOutputDetails.h +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - -#pragma once - -#include "IDetailCustomization.h" -#include "Templates/SharedPointer.h" -#include "Widgets/SWidget.h" - -class FFlowNode_CustomOutputDetails final : public IDetailCustomization -{ -public: - static TSharedRef MakeInstance() - { - return MakeShareable(new FFlowNode_CustomOutputDetails()); - } - - // IDetailCustomization - virtual void CustomizeDetails(IDetailLayoutBuilder& DetailLayout) override; - // -- - -private: - void GetEventNames(); - TSharedRef GenerateEventWidget(TSharedPtr Item) const; - FText GetSelectedEventText() const; - void PinSelectionChanged(TSharedPtr Item, ESelectInfo::Type SelectInfo) const; - - TArray> ObjectsBeingEdited; - TArray> EventNames; -}; diff --git a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_Details.cpp b/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_Details.cpp deleted file mode 100644 index ae9ddd072..000000000 --- a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_Details.cpp +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - -#include "FlowNode_Details.h" -#include "PropertyEditing.h" - -void FFlowNode_Details::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) -{ - // hide class properties while editing node instance placed in the graph - if (DetailLayout.HasClassDefaultObject() == false) - { - DetailLayout.HideCategory(TEXT("FlowNode")); - } -} diff --git a/Source/FlowEditor/Private/Nodes/FlowNodeBlueprintFactory.cpp b/Source/FlowEditor/Private/Nodes/FlowNodeBlueprintFactory.cpp index 06209c356..ff62898b8 100644 --- a/Source/FlowEditor/Private/Nodes/FlowNodeBlueprintFactory.cpp +++ b/Source/FlowEditor/Private/Nodes/FlowNodeBlueprintFactory.cpp @@ -4,16 +4,18 @@ #include "Nodes/FlowNode.h" #include "Nodes/FlowNodeBlueprint.h" +#include "Nodes/FlowNodeAddOnBlueprint.h" +#include "AddOns/FlowNodeAddOn.h" #include "BlueprintEditorSettings.h" #include "ClassViewerFilter.h" #include "ClassViewerModule.h" #include "Editor.h" -#include "EditorStyleSet.h" #include "Kismet2/KismetEditorUtilities.h" #include "Misc/MessageDialog.h" #include "Modules/ModuleManager.h" #include "SlateOptMacros.h" +#include "Templates/SubclassOf.h" #include "Widgets/Input/SButton.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SBox.h" @@ -24,6 +26,9 @@ #define LOCTEXT_NAMESPACE "FlowNodeBlueprintFactory" +// Forward Declarations +class UFlowNodeBase; + // ------------------------------------------------------------------------------ // Dialog to configure creation properties // ------------------------------------------------------------------------------ @@ -33,19 +38,20 @@ class SFlowNodeBlueprintCreateDialog final : public SCompoundWidget { public: SLATE_BEGIN_ARGS(SFlowNodeBlueprintCreateDialog) {} + SLATE_ARGUMENT(TSubclassOf, ParentClass) SLATE_END_ARGS() /** Constructs this widget with InArgs */ void Construct(const FArguments& InArgs) { bOkClicked = false; - ParentClass = UFlowNode::StaticClass(); + ParentClass = InArgs._ParentClass.Get(); ChildSlot [ SNew(SBorder) .Visibility(EVisibility::Visible) - .BorderImage(FEditorStyle::GetBrush("Menu.Background")) + .BorderImage(FAppStyle::GetBrush("Menu.Background")) [ SNew(SBox) .Visibility(EVisibility::Visible) @@ -56,7 +62,7 @@ class SFlowNodeBlueprintCreateDialog final : public SCompoundWidget .FillHeight(1) [ SNew(SBorder) - .BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder")) + .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) .Content() [ SAssignNew(ParentClassContainer, SVerticalBox) @@ -69,14 +75,14 @@ class SFlowNodeBlueprintCreateDialog final : public SCompoundWidget .Padding(8) [ SNew(SUniformGridPanel) - .SlotPadding(FEditorStyle::GetMargin("StandardDialog.SlotPadding")) - .MinDesiredSlotWidth(FEditorStyle::GetFloat("StandardDialog.MinDesiredSlotWidth")) - .MinDesiredSlotHeight(FEditorStyle::GetFloat("StandardDialog.MinDesiredSlotHeight")) + .SlotPadding(FAppStyle::GetMargin("StandardDialog.SlotPadding")) + .MinDesiredSlotWidth(FAppStyle::GetFloat("StandardDialog.MinDesiredSlotWidth")) + .MinDesiredSlotHeight(FAppStyle::GetFloat("StandardDialog.MinDesiredSlotHeight")) + SUniformGridPanel::Slot(0, 0) [ SNew(SButton) .HAlign(HAlign_Center) - .ContentPadding(FEditorStyle::GetMargin("StandardDialog.ContentPadding")) + .ContentPadding(FAppStyle::GetMargin("StandardDialog.ContentPadding")) .OnClicked(this, &SFlowNodeBlueprintCreateDialog::OkClicked) .Text(LOCTEXT("CreateFlowNodeBlueprintOk", "OK")) ] @@ -84,7 +90,7 @@ class SFlowNodeBlueprintCreateDialog final : public SCompoundWidget [ SNew(SButton) .HAlign(HAlign_Center) - .ContentPadding(FEditorStyle::GetMargin("StandardDialog.ContentPadding")) + .ContentPadding(FAppStyle::GetMargin("StandardDialog.ContentPadding")) .OnClicked(this, &SFlowNodeBlueprintCreateDialog::CancelClicked) .Text(LOCTEXT("CreateFlowNodeBlueprintCancel", "Cancel")) ] @@ -97,7 +103,7 @@ class SFlowNodeBlueprintCreateDialog final : public SCompoundWidget } /** Sets properties for the supplied FlowNodeBlueprintFactory */ - bool ConfigureProperties(const TWeakObjectPtr InFlowNodeBlueprintFactory) + bool ConfigureProperties(const TWeakObjectPtr InFlowNodeBlueprintFactory) { FlowNodeBlueprintFactory = InFlowNodeBlueprintFactory; @@ -151,8 +157,11 @@ class SFlowNodeBlueprintCreateDialog final : public SCompoundWidget const TSharedPtr Filter = MakeShareable(new FFlowNodeBlueprintParentFilter); - // All child child classes of UFlowNode are valid - Filter->AllowedChildrenOfClasses.Add(UFlowNode::StaticClass()); + // All child classes of ParentClass are valid + if (UClass* ParentClassObject = ParentClass.Get()) + { + Filter->AllowedChildrenOfClasses.Add(ParentClassObject); + } Options.ClassFilters = {Filter.ToSharedRef()}; ParentClassContainer->ClearChildren(); @@ -165,7 +174,10 @@ class SFlowNodeBlueprintCreateDialog final : public SCompoundWidget /** Handler for when a parent class is selected */ void OnClassPicked(UClass* ChosenClass) { - ParentClass = ChosenClass; + if (ChosenClass) + { + ParentClass = ChosenClass; + } } /** Handler for when ok is clicked */ @@ -209,7 +221,7 @@ class SFlowNodeBlueprintCreateDialog final : public SCompoundWidget private: /** The factory for which we are setting up properties */ - TWeakObjectPtr FlowNodeBlueprintFactory; + TWeakObjectPtr FlowNodeBlueprintFactory; /** A pointer to the window that is asking the user to select a parent class */ TWeakPtr PickerWindow; @@ -227,56 +239,92 @@ class SFlowNodeBlueprintCreateDialog final : public SCompoundWidget END_SLATE_FUNCTION_BUILD_OPTIMIZATION /*------------------------------------------------------------------------------ - UFlowNodeBlueprintFactory implementation + UFlowNodeBaseBlueprintFactory implementation ------------------------------------------------------------------------------*/ -UFlowNodeBlueprintFactory::UFlowNodeBlueprintFactory(const FObjectInitializer& ObjectInitializer) +UFlowNodeBaseBlueprintFactory::UFlowNodeBaseBlueprintFactory(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { - SupportedClass = UFlowNodeBlueprint::StaticClass(); - ParentClass = UFlowNode::StaticClass(); + SupportedClass = nullptr; + + DefaultParentClass = UFlowNodeBase::StaticClass(); + ParentClass = DefaultParentClass; bCreateNew = true; bEditAfterNew = true; } -bool UFlowNodeBlueprintFactory::ConfigureProperties() +bool UFlowNodeBaseBlueprintFactory::ConfigureProperties() { - const TSharedRef Dialog = SNew(SFlowNodeBlueprintCreateDialog); + const TSharedRef Dialog = + SNew(SFlowNodeBlueprintCreateDialog) + .ParentClass(ParentClass); + return Dialog->ConfigureProperties(this); } -UObject* UFlowNodeBlueprintFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn, FName CallingContext) +UObject* UFlowNodeBaseBlueprintFactory::FactoryCreateNew(UClass* BlueprintClass, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn, FName CallingContext) { - check(Class->IsChildOf(UFlowNodeBlueprint::StaticClass())); + check(BlueprintClass->IsChildOf(SupportedClass)); - if (ParentClass == nullptr || !FKismetEditorUtilities::CanCreateBlueprintOfClass(ParentClass) || !ParentClass->IsChildOf(UFlowNode::StaticClass())) + if (ParentClass == nullptr || !FKismetEditorUtilities::CanCreateBlueprintOfClass(ParentClass) || !ParentClass->IsChildOf(DefaultParentClass)) { - FFormatNamedArguments Args; - Args.Add(TEXT("ClassName"), ParentClass ? FText::FromString(ParentClass->GetName()) : LOCTEXT("Null", "(null)")); - FMessageDialog::Open(EAppMsgType::Ok, FText::Format(LOCTEXT("CannotCreateFlowNodeBlueprint", "Cannot create a Flow Node Blueprint based on the class '{ClassName}'."), Args)); + ShowCannotCreateBlueprintDialog(); + return nullptr; } - UFlowNodeBlueprint* NewBP = CastChecked(FKismetEditorUtilities::CreateBlueprint(ParentClass, InParent, Name, BPTYPE_Normal, UFlowNodeBlueprint::StaticClass(), UBlueprintGeneratedClass::StaticClass(), CallingContext)); + UBlueprint* NewBP = FKismetEditorUtilities::CreateBlueprint(ParentClass, InParent, Name, BPTYPE_Normal, BlueprintClass, UBlueprintGeneratedClass::StaticClass(), CallingContext); if (NewBP && NewBP->UbergraphPages.Num() > 0) { - UBlueprintEditorSettings* Settings = GetMutableDefault(); - if(Settings && Settings->bSpawnDefaultBlueprintNodes) + const UBlueprintEditorSettings* Settings = GetMutableDefault(); + if (Settings && Settings->bSpawnDefaultBlueprintNodes) { int32 NodePositionY = 0; - FKismetEditorUtilities::AddDefaultEventNode(NewBP, NewBP->UbergraphPages[0], FName(TEXT("K2_ExecuteInput")), UFlowNode::StaticClass(), NodePositionY); - FKismetEditorUtilities::AddDefaultEventNode(NewBP, NewBP->UbergraphPages[0], FName(TEXT("K2_Cleanup")), UFlowNode::StaticClass(), NodePositionY); + FKismetEditorUtilities::AddDefaultEventNode(NewBP, NewBP->UbergraphPages[0], FName(TEXT("K2_ExecuteInput")), DefaultParentClass, NodePositionY); + FKismetEditorUtilities::AddDefaultEventNode(NewBP, NewBP->UbergraphPages[0], FName(TEXT("K2_Cleanup")), DefaultParentClass, NodePositionY); } } - + return NewBP; } -UObject* UFlowNodeBlueprintFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +UObject* UFlowNodeBaseBlueprintFactory::FactoryCreateNew(UClass* BlueprintClass, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + return FactoryCreateNew(BlueprintClass, InParent, Name, Flags, Context, Warn, NAME_None); +} + +void UFlowNodeBaseBlueprintFactory::ShowCannotCreateBlueprintDialog() +{ + FFormatNamedArguments Args; + Args.Add(TEXT("DefaultClassName"), DefaultParentClass ? FText::FromString(DefaultParentClass->GetName()) : LOCTEXT("Null", "(null)")); + Args.Add(TEXT("ClassName"), ParentClass ? FText::FromString(ParentClass->GetName()) : LOCTEXT("Null", "(null)")); + FMessageDialog::Open(EAppMsgType::Ok, FText::Format(LOCTEXT("CannotCreateFlowNodeBlueprint", "Cannot create a {DefaultClassName} Blueprint based on the class '{ClassName}'."), Args)); +} + +/*------------------------------------------------------------------------------ + UFlowNodeBlueprintFactory implementation +------------------------------------------------------------------------------*/ + +UFlowNodeBlueprintFactory::UFlowNodeBlueprintFactory(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + SupportedClass = UFlowNodeBlueprint::StaticClass(); + DefaultParentClass = UFlowNode::StaticClass(); + ParentClass = DefaultParentClass; +} + +/*------------------------------------------------------------------------------ + UFlowNodeAddOnBlueprintFactory implementation +------------------------------------------------------------------------------*/ + +UFlowNodeAddOnBlueprintFactory::UFlowNodeAddOnBlueprintFactory(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) { - return FactoryCreateNew(Class, InParent, Name, Flags, Context, Warn, NAME_None); + SupportedClass = UFlowNodeAddOnBlueprint::StaticClass(); + DefaultParentClass = UFlowNodeAddOn::StaticClass(); + ParentClass = DefaultParentClass; } #undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Private/Pins/SFlowInputPinHandle.cpp b/Source/FlowEditor/Private/Pins/SFlowInputPinHandle.cpp index c8e41ae44..93f0fd7d1 100644 --- a/Source/FlowEditor/Private/Pins/SFlowInputPinHandle.cpp +++ b/Source/FlowEditor/Private/Pins/SFlowInputPinHandle.cpp @@ -2,6 +2,9 @@ #include "Pins/SFlowInputPinHandle.h" #include "Nodes/FlowNode.h" +#include "Pins/SFlowPinHandle.h" +#include "EdGraphSchema_K2.h" +#include "Engine/Blueprint.h" void SFlowInputPinHandle::RefreshNameList() { diff --git a/Source/FlowEditor/Private/Pins/SFlowOutputPinHandle.cpp b/Source/FlowEditor/Private/Pins/SFlowOutputPinHandle.cpp index 5fca65021..24538b20f 100644 --- a/Source/FlowEditor/Private/Pins/SFlowOutputPinHandle.cpp +++ b/Source/FlowEditor/Private/Pins/SFlowOutputPinHandle.cpp @@ -2,12 +2,16 @@ #include "Pins/SFlowOutputPinHandle.h" #include "Nodes/FlowNode.h" +#include "Pins/SFlowPinHandle.h" + +#include "EdGraphSchema_K2.h" +#include "Engine/Blueprint.h" void SFlowOutputPinHandle::RefreshNameList() { PinNames.Empty(); - if (Blueprint && Blueprint->GeneratedClass) + if (Blueprint && Blueprint->GeneratedClass && Blueprint->GeneratedClass->IsChildOf()) { if (UFlowNode* FlowNode = Blueprint->GeneratedClass->GetDefaultObject()) { diff --git a/Source/FlowEditor/Private/Pins/SFlowPinHandle.cpp b/Source/FlowEditor/Private/Pins/SFlowPinHandle.cpp index 1a85544c9..eb45360b9 100644 --- a/Source/FlowEditor/Private/Pins/SFlowPinHandle.cpp +++ b/Source/FlowEditor/Private/Pins/SFlowPinHandle.cpp @@ -2,6 +2,8 @@ #include "Pins/SFlowPinHandle.h" +#include "ScopedTransaction.h" + #define LOCTEXT_NAMESPACE "SFlowPinHandle" SFlowPinHandle::SFlowPinHandle() diff --git a/Source/FlowEditor/Private/UnrealExtensions/IFlowCuratedNamePropertyCustomization.cpp b/Source/FlowEditor/Private/UnrealExtensions/IFlowCuratedNamePropertyCustomization.cpp new file mode 100644 index 000000000..77e64d428 --- /dev/null +++ b/Source/FlowEditor/Private/UnrealExtensions/IFlowCuratedNamePropertyCustomization.cpp @@ -0,0 +1,300 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +// NOTE (gtaylor) This class is planned for submission to Epic to include in baseline UE. +// If/when that happens, we will want to remove this version and update to the latest one in the PropertyModule + +#include "UnrealExtensions/IFlowCuratedNamePropertyCustomization.h" + +#include "DetailLayoutBuilder.h" +#include "DetailWidgetRow.h" +#include "EditorClassUtils.h" +#include "IDetailPropertyRow.h" +#include "IDetailChildrenBuilder.h" +#include "Internationalization/Text.h" +#include "IPropertyUtilities.h" +#include "PropertyHandle.h" +#include "Widgets/Input/SComboBox.h" +#include "Widgets/Text/STextBlock.h" + +TSharedPtr IFlowCuratedNamePropertyCustomization::NoneAsText = nullptr; + +void IFlowCuratedNamePropertyCustomization::Initialize() +{ + // Cache off "None" as a sharable FText, for use later + if (!NoneAsText.IsValid()) + { + NoneAsText = MakeShared(FText::FromName(NAME_None)); + } + + // Cache the Name property handle + check(StructPropertyHandle.IsValid()); + CachedNameHandle = GetCuratedNamePropertyHandle(); + check(CachedNameHandle->IsValidHandle()); + + // Initial setup the CachedTextSelected and CachedTextList + // (via SetCuratedNameWithSideEffects) + check(!CachedTextSelected.IsValid()); + check(CachedTextList.IsEmpty()); + + FName CuratedName; + if (TryGetCuratedName(CuratedName)) + { + const bool bChangedValue = TrySetCuratedNameWithSideEffects(CuratedName); + + check(bChangedValue); + check(CachedTextSelected.IsValid()); + check(CachedTextList.Num() == 1); + } +} + +void IFlowCuratedNamePropertyCustomization::CreateHeaderRowWidget(FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ + // Do one-time setup first + Initialize(); + + CachedPropertyUtils = StructCustomizationUtils.GetPropertyUtilities(); + + // Replace the default HeaderRow widget with one of our own + HeaderRow + .NameContent() + [ + SAssignNew(HeaderTextBlock, STextBlock) + .Text(BuildHeaderText()) + ] + .ValueContent() + .MaxDesiredWidth(250.0f) + [ + SAssignNew(TextListWidget, SComboBox>) + .OptionsSource(&CachedTextList) + .OnGenerateWidget_Static(&IFlowCuratedNamePropertyCustomization::GenerateTextListWidget) + .OnComboBoxOpening(this, &IFlowCuratedNamePropertyCustomization::OnTextListComboBoxOpening) + .OnSelectionChanged(this, &IFlowCuratedNamePropertyCustomization::OnTextSelected) + [ + SNew(STextBlock) + .Text(this, &IFlowCuratedNamePropertyCustomization::GetCachedText) + .Font(IDetailLayoutBuilder::GetDetailFont()) + .ToolTipText(this, &IFlowCuratedNamePropertyCustomization::GetCachedText) + ] + ]; + + // Hook-up the ResetToDefault overrides + FIsResetToDefaultVisible IsResetVisible = + FIsResetToDefaultVisible::CreateSP( + this, + &IFlowCuratedNamePropertyCustomization::CustomIsResetToDefaultVisible); + FResetToDefaultHandler ResetHandler = + FResetToDefaultHandler::CreateSP( + this, + &IFlowCuratedNamePropertyCustomization::CustomResetToDefault); + FResetToDefaultOverride ResetOverride = FResetToDefaultOverride::Create(IsResetVisible, ResetHandler); + + HeaderRow.OverrideResetToDefault(ResetOverride); + + // Replacement IsEnabled Attribute + TAttribute IsEnabledAttribute = TAttribute::CreateSP(this, &IFlowCuratedNamePropertyCustomization::CustomIsEnabled); + HeaderRow.IsEnabled(IsEnabledAttribute); +} + +bool IFlowCuratedNamePropertyCustomization::CustomIsResetToDefaultVisibleImpl(TSharedPtr Property) const +{ + FName CuratedName; + if (!TryGetCuratedName(CuratedName)) + { + return false; + } + + return !CuratedName.IsNone(); +} + +void IFlowCuratedNamePropertyCustomization::CustomResetToDefaultImpl(TSharedPtr Property) +{ + if (TrySetCuratedNameWithSideEffects(NAME_None)) + { + RepaintTextListWidget(); + } +} + +bool IFlowCuratedNamePropertyCustomization::CustomIsEnabledImpl() const +{ + if (StructPropertyHandle->IsEditConst()) + { + return false; + } + + if (CachedPropertyUtils && !CachedPropertyUtils->IsPropertyEditingEnabled()) + { + return false; + } + + return true; +} + +bool IFlowCuratedNamePropertyCustomization::TrySetCuratedNameWithSideEffects(const FName& NewName) +{ + FName ExistingName; + (void)TryGetCuratedName(ExistingName); + + if (ExistingName != NewName) + { + // Set the new name on the actual struct first + SetCuratedName(NewName); + } + + // Ensure the FText representations are up to date + + TSharedPtr NewText = FindCachedOrCreateText(NewName); + const bool bIsChanged = (NewText != CachedTextSelected); + + CachedTextSelected = NewText; + + InsertAtHeadOfCachedTextList(CachedTextSelected); + + // Set the Name property to the new value + check(CachedNameHandle.IsValid()); + CachedNameHandle->SetValue(NewName); + + return bIsChanged; +} + +FText IFlowCuratedNamePropertyCustomization::GetCachedText() const +{ + if (CachedTextSelected.IsValid()) + { + return *CachedTextSelected.Get(); + } + else + { + return FText(); + } +} + +TSharedRef IFlowCuratedNamePropertyCustomization::GenerateTextListWidget(TSharedPtr InItem) +{ + return + SNew(STextBlock) + .Text(*InItem) + .ColorAndOpacity(FSlateColor::UseForeground()) + .Font(IDetailLayoutBuilder::GetDetailFont()); +} + +void IFlowCuratedNamePropertyCustomization::OnTextListComboBoxOpening() +{ + if (!CachedTextSelected.IsValid()) + { + return; + } + + // Create a dictionary of Names to their shared FTexts + // (to preserve the shared FText objects, if they already exist) + TMap> MapNameToText; + + FName CurrentName; + if (TryGetCuratedName(CurrentName)) + { + MapNameToText.Add(CurrentName, CachedTextSelected); + } + + for (TSharedPtr& Text : CachedTextList) + { + (void)MapNameToText.FindOrAdd(FName(Text.Get()->ToString()), Text); + } + + TArray CuratedNameOptions = GetCuratedNameOptions(); + + // (+2 to reserve space for the Selected and None entry) + CachedTextList.Empty(CuratedNameOptions.Num() + 2); + + // Populate the current selection at the top of the list + if (CuratedNameOptions.Contains(CurrentName) || CurrentName.IsNone()) + { + CachedTextList.Add(CachedTextSelected); + } + + // Populate the other curated name options + for (const FName& NameOption : CuratedNameOptions) + { + if (!NameOption.IsNone() && NameOption != CurrentName) + { + AddToCachedTextList(FindCachedOrCreateText(NameOption)); + } + } + + // Ensure "None" is in the list (if CurrentName is not None) + if (!CurrentName.IsNone() && (CuratedNameOptions.IsEmpty() || AllowNameNoneIfOtherOptionsExist())) + { + check(!CachedTextList.Contains(NoneAsText)); + + CachedTextList.Add(NoneAsText); + } +} + +void IFlowCuratedNamePropertyCustomization::OnTextSelected(TSharedPtr NewSelection, ESelectInfo::Type SelectInfo) +{ + // Called when the combo box has selected a new element + + // Process NewSelection and derive the matching Name + // (NewSelection can be null) + + FName NewName; + + if (NewSelection.IsValid()) + { + // Ensure NewSelection is in the CachedTextList + AddToCachedTextList(NewSelection); + + NewName = FName(NewSelection->ToString()); + } + else + { + NewName = NAME_None; + } + + if (TrySetCuratedNameWithSideEffects(NewName)) + { + RepaintTextListWidget(); + } +} + +TSharedPtr IFlowCuratedNamePropertyCustomization::FindCachedOrCreateText(const FName& NewName) +{ + if (NewName.IsNone()) + { + return NoneAsText; + } + + const FText NewText = FText::FromName(NewName); + + for (int32 Index = 0; Index < CachedTextList.Num(); ++Index) + { + const TSharedPtr& TextCur = CachedTextList[Index]; + + if (TextCur->EqualTo(NewText, ETextComparisonLevel::Default)) + { + return TextCur; + } + } + + TSharedPtr Result = MakeShareable(new FText(NewText)); + return Result; +} + +void IFlowCuratedNamePropertyCustomization::InsertAtHeadOfCachedTextList(TSharedPtr Text) +{ + CachedTextList.Remove(Text); + + CachedTextList.Insert(Text, 0); +} + +void IFlowCuratedNamePropertyCustomization::AddToCachedTextList(TSharedPtr Text) +{ + CachedTextList.AddUnique(Text); +} + +void IFlowCuratedNamePropertyCustomization::RepaintTextListWidget() const +{ + if (TextListWidget.IsValid()) + { + // Prod UDE to refresh the widget to show the new change + TextListWidget->Invalidate(EInvalidateWidgetReason::Paint); + } +} diff --git a/Source/FlowEditor/Private/UnrealExtensions/IFlowExtendedPropertyTypeCustomization.cpp b/Source/FlowEditor/Private/UnrealExtensions/IFlowExtendedPropertyTypeCustomization.cpp new file mode 100644 index 000000000..96d42a729 --- /dev/null +++ b/Source/FlowEditor/Private/UnrealExtensions/IFlowExtendedPropertyTypeCustomization.cpp @@ -0,0 +1,79 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +// NOTE (gtaylor) This class is planned for submission to Epic to include in baseline UE. +// If/when that happens, we will want to remove this version and update to the latest one in the PropertyModule + +#include "UnrealExtensions/IFlowExtendedPropertyTypeCustomization.h" + +#include "DetailWidgetRow.h" +#include "IDetailChildrenBuilder.h" +#include "IDetailPropertyRow.h" +#include "Widgets/Text/STextBlock.h" + +void IFlowExtendedPropertyTypeCustomization::CustomizeHeader(TSharedRef InStructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ + StructPropertyHandle = InStructPropertyHandle; + + // Connect our property callback to any of the children properties changing + uint32 NumChildren; + StructPropertyHandle->GetNumChildren(NumChildren); + + for (uint32 ChildIndex = 0; ChildIndex < NumChildren; ++ChildIndex) + { + const TSharedRef ChildHandle = StructPropertyHandle->GetChildHandle(ChildIndex).ToSharedRef(); + + ChildHandle->SetOnPropertyValueChanged( + FSimpleDelegate::CreateSP(this, &IFlowExtendedPropertyTypeCustomization::OnAnyChildPropertyChanged)); + } + + CreateHeaderRowWidget(HeaderRow, StructCustomizationUtils); +} + +void IFlowExtendedPropertyTypeCustomization::CustomizeChildrenDefaultImpl(TSharedRef PropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ + // A 'default' implementation of CustomizeChildren + + uint32 NumChildren = 0; + PropertyHandle->GetNumChildren(NumChildren); + + for (uint32 ChildNum = 0; ChildNum < NumChildren; ++ChildNum) + { + StructBuilder.AddProperty(PropertyHandle->GetChildHandle(ChildNum).ToSharedRef()); + } +} + +void IFlowExtendedPropertyTypeCustomization::CreateHeaderRowWidget(FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) +{ + // Build the Slate widget for the header row + HeaderRow + .NameContent() + [ + SAssignNew(HeaderTextBlock, STextBlock) + .Text(BuildHeaderText()) + ]; +} + +void IFlowExtendedPropertyTypeCustomization::OnAnyChildPropertyChanged() const +{ + RefreshHeader(); +} + +void IFlowExtendedPropertyTypeCustomization::RefreshHeader() const +{ + if (HeaderTextBlock.IsValid() && StructPropertyHandle.IsValid()) + { + HeaderTextBlock->SetText(BuildHeaderText()); + } +} + +FText IFlowExtendedPropertyTypeCustomization::BuildHeaderText() const +{ + if (StructPropertyHandle.IsValid()) + { + return StructPropertyHandle->GetPropertyDisplayName(); + } + else + { + return FText(); + } +} diff --git a/Source/FlowEditor/Private/Utils/SLevelEditorFlow.cpp b/Source/FlowEditor/Private/Utils/SLevelEditorFlow.cpp new file mode 100644 index 000000000..27f04efac --- /dev/null +++ b/Source/FlowEditor/Private/Utils/SLevelEditorFlow.cpp @@ -0,0 +1,99 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Utils/SLevelEditorFlow.h" +#include "FlowAsset.h" +#include "FlowComponent.h" +#include "Graph/FlowGraphSettings.h" + +#include "Editor.h" +#include "GameFramework/WorldSettings.h" +#include "PropertyCustomizationHelpers.h" + +#define LOCTEXT_NAMESPACE "SLevelEditorFlow" + +void SLevelEditorFlow::Construct(const FArguments& InArgs) +{ + OnMapOpenedHandle = FEditorDelegates::OnMapOpened.AddRaw(this, &SLevelEditorFlow::OnMapOpened); +} + +SLevelEditorFlow::~SLevelEditorFlow() +{ + FEditorDelegates::OnMapOpened.Remove(OnMapOpenedHandle); +} + +void SLevelEditorFlow::OnMapOpened(const FString& Filename, bool bAsTemplate) +{ + CreateFlowWidget(); +} + +void SLevelEditorFlow::CreateFlowWidget() +{ + if (const UFlowComponent* FlowComponent = FindFlowComponent(); FlowComponent && FlowComponent->RootFlow) + { + FlowAssetPath = FlowComponent->RootFlow->GetPathName(); + } + else + { + FlowAssetPath = FString(); + } + + ChildSlot + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SObjectPropertyEntryBox) + .AllowedClass(GetDefault()->WorldAssetClass.LoadSynchronous()) + .DisplayThumbnail(false) + .OnObjectChanged(this, &SLevelEditorFlow::OnFlowChanged) + .ObjectPath(this, &SLevelEditorFlow::GetFlowAssetPath) // needs function to automatically refresh view upon data change + ] + ]; +} + +FString SLevelEditorFlow::GetFlowAssetPath() const +{ + return FlowAssetPath; +} + +void SLevelEditorFlow::OnFlowChanged(const FAssetData& NewAsset) +{ + FlowAssetPath = NewAsset.GetObjectPathString(); + + if (UFlowComponent* FlowComponent = FindFlowComponent()) + { + if (UObject* NewObject = NewAsset.GetAsset()) + { + FlowComponent->RootFlow = Cast(NewObject); + } + else + { + FlowComponent->RootFlow = nullptr; + } + + const bool bSuccess = FlowComponent->MarkPackageDirty(); + ensureMsgf(bSuccess, TEXT("World Settings couldn't be marked dirty while changing the assigned Flow Asset.")); + } +} + +UFlowComponent* SLevelEditorFlow::FindFlowComponent() +{ + if (GEditor) + { + if (const UWorld* World = GEditor->GetEditorWorldContext().World()) + { + if (const AWorldSettings* WorldSettings = World->GetWorldSettings()) + { + if (UActorComponent* FoundComponent = WorldSettings->GetComponentByClass(UFlowComponent::StaticClass())) + { + return Cast(FoundComponent); + } + } + } + } + + return nullptr; +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/FlowEditor/Public/Asset/AssetDefinition_FlowAsset.h b/Source/FlowEditor/Public/Asset/AssetDefinition_FlowAsset.h new file mode 100644 index 000000000..5aab41c45 --- /dev/null +++ b/Source/FlowEditor/Public/Asset/AssetDefinition_FlowAsset.h @@ -0,0 +1,24 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "AssetDefinitionDefault.h" +#include "AssetDefinition_FlowAsset.generated.h" + +/** + * + */ +UCLASS() +class FLOWEDITOR_API UAssetDefinition_FlowAsset : public UAssetDefinitionDefault +{ + GENERATED_BODY() + +public: + virtual FText GetAssetDisplayName() const override; + virtual FLinearColor GetAssetColor() const override; + virtual TSoftClassPtr GetAssetClass() const override; + virtual TConstArrayView GetAssetCategories() const override; + virtual FAssetSupportResponse CanLocalize(const FAssetData& InAsset) const override; + + virtual EAssetCommandResult OpenAssets(const FAssetOpenArgs& OpenArgs) const override; + virtual EAssetCommandResult PerformAssetDiff(const FAssetDiffArgs& DiffArgs) const override; +}; diff --git a/Source/FlowEditor/Public/Asset/AssetDefinition_FlowAssetParams.h b/Source/FlowEditor/Public/Asset/AssetDefinition_FlowAssetParams.h new file mode 100644 index 000000000..e39d13847 --- /dev/null +++ b/Source/FlowEditor/Public/Asset/AssetDefinition_FlowAssetParams.h @@ -0,0 +1,23 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "AssetDefinitionDefault.h" +#include "AssetDefinition_FlowAssetParams.generated.h" + +/** + * Asset Definition for Flow Asset Params, providing Content Browser integration. + */ +UCLASS() +class FLOWEDITOR_API UAssetDefinition_FlowAssetParams : public UAssetDefinitionDefault +{ + GENERATED_BODY() + +public: + // UAssetDefinition + virtual FText GetAssetDisplayName() const override; + virtual FLinearColor GetAssetColor() const override; + virtual TSoftClassPtr GetAssetClass() const override; + virtual TConstArrayView GetAssetCategories() const override; + virtual FAssetSupportResponse CanLocalize(const FAssetData& InAsset) const override; + // -- +}; \ No newline at end of file diff --git a/Source/FlowEditor/Public/Asset/AssetTypeActions_FlowAsset.h b/Source/FlowEditor/Public/Asset/AssetTypeActions_FlowAsset.h deleted file mode 100644 index b6f28e659..000000000 --- a/Source/FlowEditor/Public/Asset/AssetTypeActions_FlowAsset.h +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - -#pragma once - -#include "AssetTypeActions_Base.h" -#include "Toolkits/IToolkitHost.h" - -class FAssetTypeActions_FlowAsset : public FAssetTypeActions_Base -{ -public: - virtual FText GetName() const override; - virtual uint32 GetCategories() override; - virtual FColor GetTypeColor() const override { return FColor(255, 196, 128); } - - virtual UClass* GetSupportedClass() const override; - virtual void OpenAssetEditor(const TArray& InObjects, TSharedPtr EditWithinLevelEditor = TSharedPtr()) override; -}; diff --git a/Source/FlowEditor/Public/Asset/FlowAssetEditor.h b/Source/FlowEditor/Public/Asset/FlowAssetEditor.h index cd3bca47e..fc269adc8 100644 --- a/Source/FlowEditor/Public/Asset/FlowAssetEditor.h +++ b/Source/FlowEditor/Public/Asset/FlowAssetEditor.h @@ -1,14 +1,16 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "EditorUndoClient.h" -#include "GraphEditor.h" #include "Misc/NotifyHook.h" #include "Toolkits/AssetEditorToolkit.h" #include "Toolkits/IToolkitHost.h" #include "UObject/GCObject.h" +#include "FlowEditorDefines.h" + +class FFlowMessageLog; +class SFlowGraphEditor; class SFlowPalette; class UFlowAsset; class UFlowGraphNode; @@ -21,36 +23,58 @@ struct FSlateBrush; struct FPropertyChangedEvent; struct Rect; -class FFlowAssetEditor : public FAssetEditorToolkit, public FEditorUndoClient, public FGCObject, public FNotifyHook +/** + * Based class for toolkits used to edit assets built around the Flow Graph. + */ +class FLOWEDITOR_API FFlowAssetEditor : public FAssetEditorToolkit, public FEditorUndoClient, public FGCObject, public FNotifyHook { - /** The FlowAsset asset being inspected */ - UFlowAsset* FlowAsset; +public: + /* The tab ids for all the tabs used. */ + static const FName DetailsTab; + static const FName GraphTab; + static const FName PaletteTab; + static const FName RuntimeLogTab; + static const FName SearchTab; + static const FName ValidationLogTab; + +protected: + /* The Flow Asset being edited. */ + TObjectPtr FlowAsset; TSharedPtr AssetToolbar; - TSharedPtr FlowDebugger; - TSharedPtr FocusedGraphEditor; + TSharedPtr GraphEditor; TSharedPtr DetailsView; TSharedPtr Palette; -public: - /** The tab ids for all the tabs used */ - static const FName DetailsTab; - static const FName GraphTab; - static const FName PaletteTab; +#if ENABLE_SEARCH_IN_ASSET_EDITOR + TSharedPtr SearchBrowser; +#else + TSharedPtr SearchBrowser; +#endif + + /* Runtime message log, with the log listing that it reflects. */ + TSharedPtr RuntimeLog; + TSharedPtr RuntimeLogListing; + + /* Asset Validation message log, with the log listing that it reflects. */ + TSharedPtr ValidationLog; + TSharedPtr ValidationLogListing; private: - /** The current UI selection state of this editor */ + /* The current UI selection state of this editor. */ FName CurrentUISelection; public: FFlowAssetEditor(); virtual ~FFlowAssetEditor() override; - UFlowAsset* GetFlowAsset() const { return FlowAsset; }; + UFlowAsset* GetFlowAsset() const { return FlowAsset; } + TSharedPtr GetFlowGraph() const { return GraphEditor; } // FGCObject virtual void AddReferencedObjects(FReferenceCollector& Collector) override; + virtual FString GetReferencerName() const override { return TEXT("FFlowAssetEditor"); @@ -78,138 +102,67 @@ class FFlowAssetEditor : public FAssetEditorToolkit, public FEditorUndoClient, p virtual void UnregisterTabSpawners(const TSharedRef& TabManager) override; // -- + // FAssetEditorToolkit + virtual void InitToolMenuContext(FToolMenuContext& MenuContext) override; + virtual void PostRegenerateMenusAndToolbars() override; + virtual void SaveAsset_Execute() override; + virtual void SaveAssetAs_Execute() override; + // -- + + bool IsTabFocused(const FTabId& TabId) const; + private: TSharedRef SpawnTab_Details(const FSpawnTabArgs& Args) const; - TSharedRef SpawnTab_GraphCanvas(const FSpawnTabArgs& Args) const; + TSharedRef SpawnTab_Graph(const FSpawnTabArgs& Args) const; TSharedRef SpawnTab_Palette(const FSpawnTabArgs& Args) const; + TSharedRef SpawnTab_RuntimeLog(const FSpawnTabArgs& Args) const; + TSharedRef SpawnTab_Search(const FSpawnTabArgs& Args) const; + TSharedRef SpawnTab_ValidationLog(const FSpawnTabArgs& Args) const; + + void DoPresaveAssetUpdate(); public: - /** Edits the specified FlowAsset object */ - void InitFlowAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr& InitToolkitHost, UObject* ObjectToEdit); + /* Edits the specified FlowAsset object. */ + virtual void InitFlowAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr& InitToolkitHost, UObject* ObjectToEdit); protected: virtual void CreateToolbar(); - virtual void BindToolbarCommands(); + virtual void RefreshAsset(); - virtual void GoToMasterInstance(); - virtual bool CanGoToMasterInstance(); + virtual void RefreshDetails(); - virtual void CreateWidgets(); +private: + void ValidateAsset_Internal(); - virtual TSharedRef CreateGraphWidget(); - virtual FGraphAppearanceInfo GetGraphAppearanceInfo() const; - virtual FText GetCornerText() const; +protected: + virtual void ValidateAsset(FFlowMessageLog& MessageLog); + virtual void SearchInAsset(); - virtual void BindGraphCommands(); + void EditAssetDefaults_Clicked() const; -private: - static void UndoGraphAction(); - static void RedoGraphAction(); + virtual void CreateWidgets(); + virtual void CreateGraphWidget(); - static FReply OnSpawnGraphNodeByShortcut(FInputChord InChord, const FVector2D& InPosition, UEdGraph* InGraph); + static bool CanEdit(); public: - /** Gets the UI selection state of this editor */ - FName GetUISelectionState() const { return CurrentUISelection; } void SetUISelectionState(const FName SelectionOwner); - virtual void ClearSelectionStateFor(const FName SelectionOwner); + FName GetUISelectionState() const; -private: - void OnCreateComment() const; - void OnStraightenConnections() const; - -public: - static bool CanEdit(); - static bool IsPIE(); - static EVisibility GetDebuggerVisibility(); - - TSet GetSelectedFlowNodes() const; - int32 GetNumberOfSelectedNodes() const; - bool GetBoundsForSelectedNodes(class FSlateRect& Rect, float Padding) const; - -protected: virtual void OnSelectedNodesChanged(const TSet& Nodes); -public: - virtual void SelectSingleNode(UEdGraphNode* Node) const; +#if ENABLE_JUMP_TO_INNER_OBJECT + // FAssetEditorToolkit + virtual void JumpToInnerObject(UObject* InnerObject) override; + // -- +#endif protected: - virtual void SelectAllNodes() const; - virtual bool CanSelectAllNodes() const; - - virtual void DeleteSelectedNodes(); - virtual void DeleteSelectedDuplicableNodes(); - virtual bool CanDeleteNodes() const; - - virtual void CopySelectedNodes() const; - virtual bool CanCopyNodes() const; - - virtual void CutSelectedNodes(); - virtual bool CanCutNodes() const; - - virtual void PasteNodes(); + void OnLogTokenClicked(const TSharedRef& Token) const; public: - virtual void PasteNodesHere(const FVector2D& Location); - virtual bool CanPasteNodes() const; - -protected: - virtual void DuplicateNodes(); - virtual bool CanDuplicateNodes() const; - - virtual void OnNodeDoubleClicked(class UEdGraphNode* Node) const; - virtual void OnNodeTitleCommitted(const FText& NewText, ETextCommit::Type CommitInfo, UEdGraphNode* NodeBeingChanged); - - virtual void RefreshContextPins() const; - virtual bool CanRefreshContextPins() const; - -private: - void AddInput() const; - bool CanAddInput() const; - - void AddOutput() const; - bool CanAddOutput() const; - - void RemovePin() const; - bool CanRemovePin() const; - - void OnAddBreakpoint() const; - void OnAddPinBreakpoint() const; - - bool CanAddBreakpoint() const; - bool CanAddPinBreakpoint() const; - - void OnRemoveBreakpoint() const; - void OnRemovePinBreakpoint() const; - - bool CanRemoveBreakpoint() const; - bool CanRemovePinBreakpoint() const; - - void OnEnableBreakpoint() const; - void OnEnablePinBreakpoint() const; - - bool CanEnableBreakpoint() const; - bool CanEnablePinBreakpoint() const; - - void OnDisableBreakpoint() const; - void OnDisablePinBreakpoint() const; - - bool CanDisableBreakpoint() const; - bool CanDisablePinBreakpoint() const; - - void OnToggleBreakpoint() const; - void OnTogglePinBreakpoint() const; - - bool CanToggleBreakpoint() const; - bool CanTogglePinBreakpoint() const; - - void OnForcePinActivation() const; - - void FocusViewport() const; - bool CanFocusViewport() const; - - void JumpToNodeDefinition() const; - bool CanJumpToNodeDefinition() const; + /* Find in flow */ + void JumpToNode(const UEdGraphNode* Node) const; }; diff --git a/Source/FlowEditor/Public/Asset/FlowAssetEditorContext.h b/Source/FlowEditor/Public/Asset/FlowAssetEditorContext.h new file mode 100644 index 000000000..394b09ddf --- /dev/null +++ b/Source/FlowEditor/Public/Asset/FlowAssetEditorContext.h @@ -0,0 +1,21 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/Object.h" + +#include "FlowAssetEditorContext.generated.h" + +class UFlowAsset; +class FFlowAssetEditor; + +UCLASS() +class FLOWEDITOR_API UFlowAssetEditorContext : public UObject +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category="Tool Menus") + UFlowAsset* GetFlowAsset() const; + + TWeakPtr FlowAssetEditor; +}; diff --git a/Source/FlowEditor/Public/Asset/FlowAssetFactory.h b/Source/FlowEditor/Public/Asset/FlowAssetFactory.h index b8a3f93a3..5a6df1739 100644 --- a/Source/FlowEditor/Public/Asset/FlowAssetFactory.h +++ b/Source/FlowEditor/Public/Asset/FlowAssetFactory.h @@ -1,14 +1,21 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Factories/Factory.h" #include "FlowAssetFactory.generated.h" -UCLASS(HideCategories = Object, MinimalAPI) -class UFlowAssetFactory : public UFactory +UCLASS(HideCategories = Object) +class FLOWEDITOR_API UFlowAssetFactory : public UFactory { GENERATED_UCLASS_BODY() + UPROPERTY(EditAnywhere, Category = Asset) + TSubclassOf AssetClass; + + virtual bool ConfigureProperties() override; virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; + +protected: + /* Parameterized guts of ConfigureProperties(). */ + bool ConfigurePropertiesInternal(const FText& TitleText); }; diff --git a/Source/FlowEditor/Public/Asset/FlowAssetIndexer.h b/Source/FlowEditor/Public/Asset/FlowAssetIndexer.h index 859b6c177..8c9310754 100644 --- a/Source/FlowEditor/Public/Asset/FlowAssetIndexer.h +++ b/Source/FlowEditor/Public/Asset/FlowAssetIndexer.h @@ -1,18 +1,16 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once -#include "CoreMinimal.h" #include "IAssetIndexer.h" +#include "UObject/Object.h" class UFlowAsset; class FSearchSerializer; /** * Documentation: https://github.com/MothCocoon/FlowGraph/wiki/Asset-Search - * Uncomment entire class, if you made these changes to the engine: https://github.com/EpicGames/UnrealEngine/pull/9070 */ -/*class FFlowAssetIndexer : public IAssetIndexer +class FLOWEDITOR_API FFlowAssetIndexer : public IAssetIndexer { public: virtual FString GetName() const override { return TEXT("FlowAsset"); } @@ -20,6 +18,6 @@ class FSearchSerializer; virtual void IndexAsset(const UObject* InAssetObject, FSearchSerializer& Serializer) const override; private: - // Variant of FBlueprintIndexer::IndexGraphs + /* Variant of FBlueprintIndexer::IndexGraphs. */ void IndexGraph(const UFlowAsset* InFlowAsset, FSearchSerializer& Serializer) const; -};*/ +}; diff --git a/Source/FlowEditor/Public/Asset/FlowAssetParamsFactory.h b/Source/FlowEditor/Public/Asset/FlowAssetParamsFactory.h new file mode 100644 index 000000000..1f8024533 --- /dev/null +++ b/Source/FlowEditor/Public/Asset/FlowAssetParamsFactory.h @@ -0,0 +1,33 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Factories/Factory.h" +#include "UObject/SoftObjectPtr.h" + +#include "FlowAssetParamsFactory.generated.h" + +class UFlowAssetParams; + +/** + * Factory for creating Flow Asset Params via the Content Browser "Add New" menu. + * This creation path is strictly for creating CHILD params: the user must select a Parent FlowAssetParams. + */ +UCLASS(HideCategories = Object) +class FLOWEDITOR_API UFlowAssetParamsFactory : public UFactory +{ + GENERATED_BODY() + +public: + UFlowAssetParamsFactory(); + + // UFactory + virtual bool ConfigureProperties() override; + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; + // -- + +private: + /* Required selection. */ + TSoftObjectPtr SelectedParentParams; + + bool ShowParentPickerDialog(); +}; diff --git a/Source/FlowEditor/Public/Asset/FlowAssetToolbar.h b/Source/FlowEditor/Public/Asset/FlowAssetToolbar.h index 1b0269ad7..f38ef8513 100644 --- a/Source/FlowEditor/Public/Asset/FlowAssetToolbar.h +++ b/Source/FlowEditor/Public/Asset/FlowAssetToolbar.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Widgets/Input/SComboBox.h" @@ -8,91 +7,133 @@ #include "FlowAsset.h" class FFlowAssetEditor; +class UFlowAssetEditorContext; +class UToolMenu; -////////////////////////////////////////////////////////////////////////// -// Flow Asset Instance List +/** + * Gathers all instances of given Flow Asset per given context. + * Example: all instances for given client in the multiplayer game. + */ +struct FFlowAssetInstanceContext +{ + FText DisplayText; + TArray> AssetInstances; + + FFlowAssetInstanceContext() + { + } + + explicit FFlowAssetInstanceContext(const FText& InDisplayText) + : DisplayText(InDisplayText) + { + } +}; -class SFlowAssetInstanceList final : public SCompoundWidget +/** + * List of all instances of given Flow Asset. + */ +class FLOWEDITOR_API SFlowAssetInstanceList : public SCompoundWidget { public: - SLATE_BEGIN_ARGS(SFlowAssetInstanceList) {} + SLATE_BEGIN_ARGS(SFlowAssetInstanceList) + { + } + SLATE_END_ARGS() void Construct(const FArguments& InArgs, const TWeakObjectPtr InTemplateAsset); virtual ~SFlowAssetInstanceList() override; -private: + static EVisibility GetDebuggerVisibility(); + +protected: void RefreshInstances(); - - TSharedRef OnGenerateWidget(TSharedPtr Item) const; - void OnSelectionChanged(TSharedPtr SelectedItem, ESelectInfo::Type SelectionType); + + EVisibility GetContextVisibility() const; + TSharedRef OnGenerateContextWidget(TSharedPtr Item); + void OnContextSelectionChanged(TSharedPtr SelectedItem, ESelectInfo::Type SelectionType); + FText GetSelectedContextName() const; + + TSharedRef OnGenerateInstanceWidget(TSharedPtr Item) const; + void OnInstanceSelectionChanged(TSharedPtr SelectedItem, ESelectInfo::Type SelectionType); FText GetSelectedInstanceName() const; + FText JoinInstanceAndContextTexts(const FObjectKey& AssetInstance) const; TWeakObjectPtr TemplateAsset; - TSharedPtr>> Dropdown; - TArray> InstanceNames; - TSharedPtr SelectedInstance; + TSharedPtr>> ContextComboBox; + TSharedPtr>> InstanceComboBox; + + TArray> Contexts; + TArray> Instances; + TMap InstancesPerContext; + TSharedPtr NoContext; + TSharedPtr SelectedContext; + TSharedPtr SelectedInstance; + + static FText AllContextsText; static FText NoInstanceSelectedText; }; -////////////////////////////////////////////////////////////////////////// -// Flow Asset Breadcrumb - /** - * The kind of breadcrumbs that Flow Debugger uses + * The kind of breadcrumbs that Flow Debugger uses. */ -struct FFlowBreadcrumb +struct FLOWEDITOR_API FFlowBreadcrumb { - FString AssetPathName; - FName InstanceName; + const TWeakObjectPtr CurrentInstance; + const TWeakObjectPtr ChildInstance; FFlowBreadcrumb() - : AssetPathName(FString()) - , InstanceName(NAME_None) - {} - - FFlowBreadcrumb(const UFlowAsset* FlowAsset) - : AssetPathName(FlowAsset->GetTemplateAsset()->GetPathName()) - , InstanceName(FlowAsset->GetDisplayName()) - {} + : CurrentInstance(nullptr) + , ChildInstance(nullptr) + { + } + + explicit FFlowBreadcrumb(const TWeakObjectPtr InCurrentInstance, const TWeakObjectPtr InChildInstance) + : CurrentInstance(InCurrentInstance) + , ChildInstance(InChildInstance) + { + } }; -class SFlowAssetBreadcrumb final : public SCompoundWidget +/** + * Widget displaying chain of breadcrumbs. + */ +class FLOWEDITOR_API SFlowAssetBreadcrumb : public SCompoundWidget { public: - SLATE_BEGIN_ARGS(SFlowAssetInstanceList) {} + SLATE_BEGIN_ARGS(SFlowAssetInstanceList) + { + } + SLATE_END_ARGS() void Construct(const FArguments& InArgs, const TWeakObjectPtr InTemplateAsset); private: + EVisibility GetBreadcrumbVisibility() const; + void FillBreadcrumb() const; void OnCrumbClicked(const FFlowBreadcrumb& Item) const; - FText GetBreadcrumbText(const TWeakObjectPtr FlowInstance) const; TWeakObjectPtr TemplateAsset; TSharedPtr> BreadcrumbTrail; }; -////////////////////////////////////////////////////////////////////////// -// Flow Asset Toolbar - -class FFlowAssetToolbar final : public TSharedFromThis +/** + * Flow-specific implementation of the asset editor toolbar. + */ +class FLOWEDITOR_API FFlowAssetToolbar : public TSharedFromThis { public: explicit FFlowAssetToolbar(const TSharedPtr InAssetEditor, UToolMenu* ToolbarMenu); private: void BuildAssetToolbar(UToolMenu* ToolbarMenu) const; - void BuildDebuggerToolbar(UToolMenu* ToolbarMenu); + static TSharedRef MakeDiffMenu(const UFlowAssetEditorContext* InContext); -public: - TSharedPtr GetAssetInstanceList() const { return AssetInstanceList; } + void BuildDebuggerToolbar(UToolMenu* ToolbarMenu) const; private: TWeakPtr FlowAssetEditor; - - TSharedPtr AssetInstanceList; - TSharedPtr Breadcrumb; }; diff --git a/Source/FlowEditor/Public/Asset/FlowDebugEditorSubsystem.h b/Source/FlowEditor/Public/Asset/FlowDebugEditorSubsystem.h new file mode 100644 index 000000000..966dad4db --- /dev/null +++ b/Source/FlowEditor/Public/Asset/FlowDebugEditorSubsystem.h @@ -0,0 +1,43 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Logging/TokenizedMessage.h" + +#include "Debugger/FlowDebuggerSubsystem.h" +#include "FlowDebugEditorSubsystem.generated.h" + +class UFlowAsset; +class FFlowMessageLog; + +/** + * Editor-only extension of debugger subsystem. Supports Message Log. + */ +UCLASS() +class FLOWEDITOR_API UFlowDebugEditorSubsystem : public UFlowDebuggerSubsystem +{ + GENERATED_BODY() + +public: + UFlowDebugEditorSubsystem(); + +protected: + TMap, TSharedPtr> RuntimeLogs; + + TWeakObjectPtr HaltedOnFlowAssetInstance; + + virtual void OnInstancedTemplateAdded(UFlowAsset* AssetTemplate) override; + virtual void OnInstancedTemplateRemoved(UFlowAsset* AssetTemplate) override; + + void OnRuntimeMessageAdded(const UFlowAsset* AssetTemplate, const TSharedRef& Message) const; + + virtual void OnBeginPIE(const bool bIsSimulating); + virtual void OnResumePIE(const bool bIsSimulating); + virtual void OnEndPIE(const bool bIsSimulating); + + virtual void PauseSession(UFlowAsset& FlowAssetInstance) override; + virtual void ResumeSession(UFlowAsset& FlowAssetInstance) override; + virtual void StopSession() override; + virtual void OnFlowDebuggerStateChanged(EFlowDebuggerState PrevState, EFlowDebuggerState NextState, UFlowAsset* FlowAssetInstance); + + void OnBreakpointHit(const UFlowNode* FlowNode) const; +}; diff --git a/Source/FlowEditor/Public/Asset/FlowDebugger.h b/Source/FlowEditor/Public/Asset/FlowDebugger.h deleted file mode 100644 index 1487e20c9..000000000 --- a/Source/FlowEditor/Public/Asset/FlowDebugger.h +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - -#pragma once - -#include "CoreMinimal.h" - -/** -** Minimalistic form of breakpoint debugger -** See BehaviorTreeDebugger for a more complex example - */ -class FFlowDebugger -{ -public: - FFlowDebugger(); - ~FFlowDebugger(); - - static void PausePlaySession(); - static bool IsPlaySessionPaused(); -}; diff --git a/Source/FlowEditor/Public/Asset/FlowDiffControl.h b/Source/FlowEditor/Public/Asset/FlowDiffControl.h new file mode 100644 index 000000000..55f1747a8 --- /dev/null +++ b/Source/FlowEditor/Public/Asset/FlowDiffControl.h @@ -0,0 +1,80 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Asset/FlowObjectDiff.h" +#include "DiffResults.h" + +#include "Runtime/Launch/Resources/Version.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 7 +#include "Editor/Kismet/Private/DiffControl.h" +#else +#include "Editor/Kismet/Internal/DiffControl.h" +#endif + +class FBlueprintDifferenceTreeEntry; +class SFlowDiff; +class UEdGraph; +class UEdGraphNode; +class UFlowAsset; +struct FDiffResultItem; +struct FEdGraphEditAction; + +class FLOWEDITOR_API FFlowAssetDiffControl : public FDetailsDiffControl +{ +public: + FFlowAssetDiffControl(const UFlowAsset* InOldFlowAsset, const UFlowAsset* InNewFlowAsset, FOnDiffEntryFocused InSelectionCallback); + + virtual void GenerateTreeEntries(TArray>& OutTreeEntries, TArray>& OutRealDifferences) override; +}; + +/** + * FFlowGraphToDiff: engine's FGraphToDiff customized to Flow Graph. + */ +struct FLOWEDITOR_API FFlowGraphToDiff : public TSharedFromThis, IDiffControl +{ + FFlowGraphToDiff(SFlowDiff* DiffWidget, UEdGraph* GraphOld, UEdGraph* GraphNew, const FRevisionInfo& RevisionOld, const FRevisionInfo& RevisionNew); + virtual ~FFlowGraphToDiff() override; + + /* Add widgets to the differences tree. */ + virtual void GenerateTreeEntries(TArray>& OutTreeEntries, TArray>& OutRealDifferences) override; + + UEdGraph* GetGraphOld() const { return GraphOld; }; + UEdGraph* GetGraphNew() const { return GraphNew; }; + + ENodeDiffType GetNodeDiffType(const UEdGraphNode& Node) const; + + TSharedPtr GetFlowObjectDiff(const FDiffResultItem& DiffResultItem); + + /* Source for list view. */ + TArray> DiffListSource; + TSharedPtr> FoundDiffs; + + /* Index of the first item in RealDifferences that was generated by this graph. */ + int32 RealDifferencesStartIndex = INDEX_NONE; + +private: + FText GetToolTip() const; + TSharedRef GenerateCategoryWidget() const; + + /* Called when the Newer Graph is modified. */ + void OnGraphChanged(const FEdGraphEditAction& Action) const; + + void BuildDiffSourceArray(); + + TSharedPtr GenerateFlowObjectDiff(const TSharedPtr& Differences); + + TSharedPtr FindParentNode(class UFlowGraphNode* Node); + + TMap> FlowObjectDiffsByNodeName; + + SFlowDiff* DiffWidget; + UEdGraph* GraphOld; + UEdGraph* GraphNew; + + /* Description of Old and new graph. */ + FRevisionInfo RevisionOld; + FRevisionInfo RevisionNew; + + FDelegateHandle OnGraphChangedDelegateHandle; + +}; diff --git a/Source/FlowEditor/Public/Asset/FlowImportUtils.h b/Source/FlowEditor/Public/Asset/FlowImportUtils.h new file mode 100644 index 000000000..02316edc4 --- /dev/null +++ b/Source/FlowEditor/Public/Asset/FlowImportUtils.h @@ -0,0 +1,72 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Kismet/BlueprintFunctionLibrary.h" + +#include "FlowAsset.h" +#include "Nodes/FlowPin.h" +#include "FlowImportUtils.generated.h" + +/** + * Helper structure allowing to recreate blueprint graph as Flow Graph. + */ +USTRUCT() +struct FLOWEDITOR_API FImportedGraphNode +{ + GENERATED_USTRUCT_BODY() + + UPROPERTY() + TObjectPtr SourceGraphNode; + + TMultiMap Incoming; + TMultiMap Outgoing; + + FImportedGraphNode() + : SourceGraphNode(nullptr) + { + } +}; + +/** + * Helper structure allowing to copy properties from blueprint function pin to the Flow Node property of different name. + */ +USTRUCT(BlueprintType) +struct FLOWEDITOR_API FBlueprintToFlowPinName +{ + GENERATED_USTRUCT_BODY() + + // Key represents Flow Node property name + // Value represents Input Pin name of blueprint function + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pins") + TMap NodePropertiesToFunctionPins; + + FBlueprintToFlowPinName() + { + } +}; + +/** + * Groundwork for converting blueprint graphs to Flow Graph. + * It's NOT meant to be universal, out-of-box solution as complexity of blueprint graphs conflicts with simplicity of Flow Graph. + * However, it might be useful to provide this basic utility to anyone who would like to batch-convert their custom blueprint-based event system to Flow Graph. + * Pull requests are welcome if you are able to improve this utility w/o with minimal amount of code. + */ +UCLASS(meta = (ScriptName = "FlowImportUtils")) +class FLOWEDITOR_API UFlowImportUtils : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + static TMap> FunctionsToFlowNodes; + static TMap, FBlueprintToFlowPinName> PinMappings; + + UFUNCTION(BlueprintCallable, Category = "FlowImportUtils") + static UFlowAsset* ImportBlueprintGraph(UObject* BlueprintAsset, const TSubclassOf FlowAssetClass, const FString FlowAssetName, + const TMap> InFunctionsToFlowNodes, const TMap, FBlueprintToFlowPinName> InPinMappings, const FName StartEventName = TEXT("BeginPlay")); + + static void ImportBlueprintGraph(UBlueprint* Blueprint, UFlowAsset* FlowAsset, const FName StartEventName = TEXT("BeginPlay")); + static void ImportBlueprintFunction(const UFlowAsset* FlowAsset, const FImportedGraphNode& NodeImport, const TMap& SourceNodes, TMap& TargetNodes); + + static void GetValidInputPins(const UEdGraphNode* GraphNode, TMap& Result); + static const UEdGraphPin* FindPinMatchingToProperty(UClass* FlowNodeClass, const FProperty* Property, const TMapPins); +}; diff --git a/Source/FlowEditor/Public/Asset/FlowMessageLogListing.h b/Source/FlowEditor/Public/Asset/FlowMessageLogListing.h new file mode 100644 index 000000000..8b6bd3559 --- /dev/null +++ b/Source/FlowEditor/Public/Asset/FlowMessageLogListing.h @@ -0,0 +1,36 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "IMessageLogListing.h" + +#include "FlowAsset.h" + +UENUM() +enum class EFlowLogType : uint8 +{ + Runtime, + Validation +}; + +/** + * Scope wrapper for the message log. Ensures we don't leak logs that we don't need (i.e. those that have no messages). + * Replicated after FScopedBlueprintMessageLog. + */ +class FLOWEDITOR_API FFlowMessageLogListing +{ +public: + FFlowMessageLogListing(const UFlowAsset* InFlowAsset, const EFlowLogType Type); + ~FFlowMessageLogListing(); + +public: + TSharedRef Log; + FName LogName; + +private: + static TSharedRef RegisterLogListing(const UFlowAsset* InFlowAsset, const EFlowLogType Type); + static FName GetListingName(const UFlowAsset* InFlowAsset, const EFlowLogType Type); + +public: + static TSharedRef GetLogListing(const UFlowAsset* InFlowAsset, const EFlowLogType Type); + static FString GetLogLabel(const EFlowLogType Type); +}; diff --git a/Source/FlowEditor/Public/Asset/FlowObjectDiff.h b/Source/FlowEditor/Public/Asset/FlowObjectDiff.h new file mode 100644 index 000000000..4e813adc7 --- /dev/null +++ b/Source/FlowEditor/Public/Asset/FlowObjectDiff.h @@ -0,0 +1,69 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Runtime/Launch/Resources/Version.h" +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 7 +#include "Editor/Kismet/Private/DiffControl.h" +#else +#include "Editor/Kismet/Internal/DiffControl.h" +#endif + +class FBlueprintDifferenceTreeEntry; +class FDetailsDiff; +class FFlowObjectDiff; +class UEdGraphNode; +struct FDiffResultItem; +struct FFlowGraphToDiff; + +enum class ENodeDiffType +{ + Old, + New, + Invalid +}; + +/** +* FFlowObjectDiffArgs: Used for FOnDiffEntryFocused arguments. +*/ +struct FLOWEDITOR_API FFlowObjectDiffArgs +{ + FFlowObjectDiffArgs(TWeakPtr InFlowNodeDiff, const FSingleObjectDiffEntry& InPropertyDiff); + + TWeakPtr FlowNodeDiff; + FSingleObjectDiffEntry PropertyDiff; +}; + +/** + * FFlowObjectDiff: represents diff data for a particular node or pin. + */ +class FLOWEDITOR_API FFlowObjectDiff : public TSharedFromThis +{ +public: + FFlowObjectDiff(TSharedPtr InDiffResult, const FFlowGraphToDiff& GraphToDiff); + + void OnSelectDiff(const FSingleObjectDiffEntry& Property) const; + void DiffProperties(TArray& OutPropertyDiffsArray) const; + +private: + void InitializeDetailsDiffFromNode(UEdGraphNode* Node, const UObject* Object, const FFlowGraphToDiff& GraphToDiff); + +public: + /* The tree entry for this diff object, which can be the parent tree node to other changes such as property changes, + * added/removed add-ons, moves or comments. */ + TSharedPtr DiffTreeEntry; + TSharedPtr DiffResult; + + /* Parent of this diff. + * Certain nodes like Add-Ons are displayed inside the DiffTreeEntry for the node they're attached to. */ + TWeakPtr ParentNodeDiff; + + TSharedPtr OldDetailsView; + TSharedPtr NewDetailsView; + + /* Arguments used for FOnDiffEntryFocused. */ + TSharedPtr DiffEntryFocusArg; + TArray> PropertyDiffArgList; + + //* A list for deferring creation of DiffTreeEntries that are lower priority. */ + TArray> LowPriorityChildDiffResult; +}; diff --git a/Source/FlowEditor/Public/Asset/SAssetRevisionMenu.h b/Source/FlowEditor/Public/Asset/SAssetRevisionMenu.h new file mode 100644 index 000000000..bbd6f4350 --- /dev/null +++ b/Source/FlowEditor/Public/Asset/SAssetRevisionMenu.h @@ -0,0 +1,59 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "ISourceControlProvider.h" +#include "Widgets/SCompoundWidget.h" + +class FUpdateStatus; +class SVerticalBox; +struct FRevisionInfo; + +/** + * Forced to make a variant of SBlueprintRevisionMenu, only to replace to UBlueprint* parameter + */ +class FLOWEDITOR_API SAssetRevisionMenu : public SCompoundWidget +{ + DECLARE_DELEGATE_TwoParams(FOnRevisionSelected, FRevisionInfo const& RevisionInfo, const FString& InFilename) + +public: + SLATE_BEGIN_ARGS(SAssetRevisionMenu) + : _bIncludeLocalRevision(false) + { + } + + SLATE_ARGUMENT(bool, bIncludeLocalRevision) + SLATE_EVENT(FOnRevisionSelected, OnRevisionSelected) + SLATE_END_ARGS() + + virtual ~SAssetRevisionMenu() override; + + void Construct(const FArguments& InArgs, const FString& InFilename); + +private: + /* Delegate used to determine the visibility 'in progress' widgets. */ + EVisibility GetInProgressVisibility() const; + + /* Delegate used to determine the visibility of the cancel button. */ + EVisibility GetCancelButtonVisibility() const; + + /* Delegate used to cancel a source control operation in progress. */ + FReply OnCancelButtonClicked() const; + + /* Callback for when the source control operation is complete. */ + void OnSourceControlQueryComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult); + + bool bIncludeLocalRevision = false; + FOnRevisionSelected OnRevisionSelected; + + /* The name of the file we want revision info for. */ + FString Filename; + + /* The box we are using to display our menu. */ + TSharedPtr MenuBox; + + /* The source control operation in progress. */ + TSharedPtr SourceControlQueryOp; + + /* The state of the SCC query. */ + uint32 SourceControlQueryState = 0; +}; diff --git a/Source/FlowEditor/Public/Asset/SFlowDiff.h b/Source/FlowEditor/Public/Asset/SFlowDiff.h new file mode 100644 index 000000000..2c916d51b --- /dev/null +++ b/Source/FlowEditor/Public/Asset/SFlowDiff.h @@ -0,0 +1,234 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "IDetailsView.h" +#include "DiffResults.h" +#include "GraphEditor.h" +#include "SDetailsDiff.h" +#include "Textures/SlateIcon.h" + +struct FFlowGraphToDiff; +struct FFlowObjectDiffArgs; +class IDetailsView; +class SSplitter2x2; +class UFlowAsset; + +enum class EAssetEditorCloseReason : uint8; + +namespace FlowDiffUtils +{ + FLOWEDITOR_API void SelectNextRow(SListView>& ListView, const TArray>& ListViewSource); + FLOWEDITOR_API void SelectPrevRow(SListView>& ListView, const TArray>& ListViewSource); + FLOWEDITOR_API bool HasNextDifference(const SListView>& ListView, const TArray>& ListViewSource); + FLOWEDITOR_API bool HasPrevDifference(const SListView>& ListView, const TArray>& ListViewSource); +} + +/** + * Panel used to display the asset. + */ +struct FLOWEDITOR_API FFlowDiffPanel +{ + FFlowDiffPanel(); + + /* Generate a panel for NewGraph diffed against OldGraph. */ + void GeneratePanel(UEdGraph* NewGraph, UEdGraph* OldGraph); + + /* Generate a panel that displays the Graph and reflects the items in the DiffResults. */ + void GeneratePanel(UEdGraph* Graph, TSharedPtr> DiffResults, TAttribute FocusedDiffResult); + + /* Called when user hits keyboard shortcut to copy nodes. */ + void CopySelectedNodes() const; + + /* Gets whatever nodes are selected in the Graph Editor. */ + FGraphPanelSelectionSet GetSelectedNodes() const; + + /* Can user copy any of the selected nodes?. */ + bool CanCopyNodes() const; + + /* Functions used to focus/find a particular change in a diff result. */ + void FocusDiff(const UEdGraphPin& Pin) const; + void FocusDiff(const UEdGraphNode& Node) const; + + void OnNodeClicked(UObject* ClickedNode ); + + /* The Flow Asset that owns the graph we are showing. */ + const UFlowAsset* FlowAsset; + + /* The box around the graph editor, used to change the content when new graphs are set. */ + TSharedPtr GraphEditorBox; + + /* using SNullWidget::NullNullWidget can only work for a single widget, since widget instances can only be + * used one at a time. PanelDefaultDetailsView is used for displaying an empty details panel instead, as well + * as if the user selects a node in the graph view. */ + TSharedPtr PanelDefaultDetailsView; + + /* The graph editor which does the work of displaying the graph. */ + TWeakPtr GraphEditor; + + /* Revision information for this asset. */ + FRevisionInfo RevisionInfo; + + /* True if we should show a name identifying which asset this panel is displaying. */ + bool bShowAssetName; + + /* The widget that contains the revision info in graph mode. */ + TSharedPtr OverlayGraphRevisionInfo; + + TWeakPtr GraphDiffSplitter = nullptr; + bool bIsOldPanel = false; + +private: + /* Command list for this diff panel. */ + TSharedPtr GraphEditorCommands; +}; + +/* Visual Diff between two Flow Assets. */ +class FLOWEDITOR_API SFlowDiff : public SCompoundWidget +{ +public: + DECLARE_DELEGATE_TwoParams(FOpenInDefaults, const class UFlowAsset*, const class UFlowAsset*); + + SLATE_BEGIN_ARGS(SFlowDiff) + { + } + + SLATE_ARGUMENT(const class UFlowAsset*, OldFlow) + SLATE_ARGUMENT(const class UFlowAsset*, NewFlow) + SLATE_ARGUMENT(struct FRevisionInfo, OldRevision) + SLATE_ARGUMENT(struct FRevisionInfo, NewRevision) + SLATE_ARGUMENT(bool, ShowAssetNames) + SLATE_ARGUMENT(TSharedPtr, ParentWindow) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + virtual ~SFlowDiff() override; + + /* Called when a new Graph is clicked on by user. */ + void OnGraphChanged(const FFlowGraphToDiff* Diff); + + /* Called when user clicks on a new graph list item. */ + void OnGraphSelectionChanged(const TSharedPtr Item, ESelectInfo::Type SelectionType); + + /* Called when user clicks on an entry in the listview of differences. */ + void OnDiffListSelectionChanged(TSharedPtr FlowObjectDiffArgs); + + /* Helper function for generating an empty widget. */ + static TSharedRef DefaultEmptyPanel(); + + /* Helper function to create a window that holds a diff widget. */ + static TSharedPtr CreateDiffWindow(const FText WindowTitle, const UFlowAsset* OldFlow, const UFlowAsset* NewFlow, const struct FRevisionInfo& OldRevision, const struct FRevisionInfo& NewRevision); + +protected: + /* Called when user clicks button to go to next difference. */ + void NextDiff() const; + + /* Called when user clicks button to go to prev difference. */ + void PrevDiff() const; + + /* Called to determine whether we have a list of differences to cycle through. */ + bool HasNextDiff() const; + bool HasPrevDiff() const; + + /* Find the FGraphToDiff that displays the graph with GraphPath relative path. */ + FFlowGraphToDiff* FindGraphToDiffEntry(const FString& GraphPath) const; + + /* Bring these revisions of graph into focus on main display*/ + void FocusOnGraphRevisions(const FFlowGraphToDiff* Diff); + + /* User toggles the option to lock the views between the two assets. */ + void OnToggleLockView(); + + /* User toggles the option to change the split view mode between vertical and horizontal. */ + void OnToggleSplitViewMode(); + + /* Reset the graph editor, called when user switches graphs to display*/ + void ResetGraphEditors() const; + + /* Get the image to show for the toggle lock option*/ + FSlateIcon GetLockViewImage() const; + + /* Get the image to show for the toggle split view mode option*/ + FSlateIcon GetSplitViewModeImage() const; + + /* List of graphs to diff, are added to panel last. */ + TSharedPtr GraphToDiff; + + /* Get Graph editor associated with this Graph. */ + FFlowDiffPanel& GetDiffPanelForNode(const UEdGraphNode& Node); + + /* Event handler that updates the graph view when user selects a new graph. */ + void HandleGraphChanged(const FString& GraphPath); + + /* Function used to generate the list of differences and the widgets needed to calculate that list. */ + void GenerateDifferencesList(); + + /* Called when editor may need to be closed. */ + void OnCloseAssetEditor(UObject* Asset, const EAssetEditorCloseReason CloseReason); + + struct FDiffControl + { + FDiffControl() + : Widget() + , DiffControl(nullptr) + { + } + + TSharedPtr Widget; + TSharedPtr DiffControl; + }; + + FDiffControl GenerateDetailsPanel(); + FDiffControl GenerateGraphPanel(); + + TSharedRef GenerateGraphWidgetForPanel(FFlowDiffPanel& OutDiffPanel) const; + TSharedRef GenerateRevisionInfoWidgetForPanel(TSharedPtr& OutGeneratedWidget, const FText& InRevisionText) const; + + /* Accessor and event handler for toggling between diff view modes (defaults, components, graph view, interface, macro). */ + void SetCurrentMode(FName NewMode); + FName GetCurrentMode() const { return CurrentMode; } + void OnModeChanged(const FName& InNewViewMode) const; + + void UpdateTopSectionVisibility(const FName& InNewViewMode) const; + + FName CurrentMode; + + /* The two panels used to show the old & new revision. */ + FFlowDiffPanel PanelOld, PanelNew; + + /* If the two views should be locked. */ + bool bLockViews; + + /* If the view on Graph Mode should be divided vertically. */ + bool bVerticalSplitGraphMode = true; + + /* Contents widget that we swap when mode changes (defaults, components, etc). */ + TSharedPtr ModeContents; + + TSharedPtr TopRevisionInfoWidget; + TSharedPtr DiffGraphSplitter; + TSharedPtr GraphToolBarWidget; + + friend struct FListItemGraphToDiff; + + /* We can't use the global tab manager because we need to instance the diff control, so we have our own tab manager. */ + TSharedPtr TabManager; + + /* Tree of differences collected across all panels. */ + TArray> PrimaryDifferencesList; + + /* List of all differences, cached so that we can iterate only the differences and not labels, etc. */ + TArray> RealDifferences; + + /* Tree view that displays the differences, cached for the buttons that iterate the differences. */ + TSharedPtr>> DifferencesTreeView; + + /* Stored references to widgets used to display various parts of asset, from the mode name. */ + TMap ModePanels; + + /* A pointer to the window holding this. */ + TWeakPtr WeakParentWindow; + + TSharedPtr GraphDiffSplitter = nullptr; + + FDelegateHandle AssetEditorCloseDelegate; +}; diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowActorOwnerComponentRefCustomization.h b/Source/FlowEditor/Public/DetailCustomizations/FlowActorOwnerComponentRefCustomization.h new file mode 100644 index 000000000..4e1acb4d9 --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowActorOwnerComponentRefCustomization.h @@ -0,0 +1,44 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UnrealExtensions/IFlowCuratedNamePropertyCustomization.h" +#include "Types/FlowActorOwnerComponentRef.h" + +class UFlowAsset; +class UFlowNode; +class UObject; +class UClass; + +/** + * Details customization for FFlowActorOwnerComponentRef. + */ +class FFlowActorOwnerComponentRefCustomization : public IFlowCuratedNamePropertyCustomization +{ +private: + typedef IFlowCuratedNamePropertyCustomization Super; + +public: + static TSharedRef MakeInstance() { return MakeShareable(new FFlowActorOwnerComponentRefCustomization()); } + +protected: + + // IPropertyTypeCustomization + virtual void CustomizeChildren(TSharedRef InStructPropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + // -- + + // ICuratedNamePropertyCustomization + virtual TSharedPtr GetCuratedNamePropertyHandle() const override; + virtual void SetCuratedName(const FName& NewName) override; + virtual bool TryGetCuratedName(FName& OutName) const override; + virtual TArray GetCuratedNameOptions() const override; + // -- + + /* Accessor to return the actual struct being edited. */ + FORCEINLINE FFlowActorOwnerComponentRef* GetFlowActorOwnerComponentRef() const + { return IFlowExtendedPropertyTypeCustomization::TryGetTypedStructValue(StructPropertyHandle); } + + UClass* TryGetExpectedOwnerClass() const; + UFlowNode* TryGetFlowNodeOuter() const; + + TArray GetFlowActorOwnerComponents(TSubclassOf ExpectedActorOwnerClass) const; +}; diff --git a/Source/FlowEditor/Private/Asset/FlowAssetDetails.h b/Source/FlowEditor/Public/DetailCustomizations/FlowAssetDetails.h similarity index 69% rename from Source/FlowEditor/Private/Asset/FlowAssetDetails.h rename to Source/FlowEditor/Public/DetailCustomizations/FlowAssetDetails.h index 016151dd1..092d7d9fc 100644 --- a/Source/FlowEditor/Private/Asset/FlowAssetDetails.h +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowAssetDetails.h @@ -1,11 +1,12 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "IDetailCustomization.h" #include "Templates/SharedPointer.h" #include "Types/SlateEnums.h" +class UFlowNode_CustomEventBase; +class UFlowAsset; class IDetailChildrenBuilder; class IDetailLayoutBuilder; class IPropertyHandle; @@ -28,4 +29,14 @@ class FFlowAssetDetails final : public IDetailCustomization FText GetCustomPinText(TSharedRef PropertyHandle) const; static void OnCustomPinTextCommitted(const FText& InText, ETextCommit::Type InCommitType, TSharedRef PropertyHandle); static bool VerifyNewCustomPinText(const FText& InNewText, FText& OutErrorMessage); + + void OnBrowseClicked(TSharedRef PropertyHandle); + bool IsBrowseEnabled(TSharedRef PropertyHandle) const; + UFlowNode_CustomEventBase* GetCustomEventNode(TSharedRef PropertyHandle) const; + + TArray> ObjectsBeingEdited; + + TSharedPtr CustomInputsHandle; + TSharedPtr CustomOutputsHandle; + }; diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowAssetParamsPtrCustomization.h b/Source/FlowEditor/Public/DetailCustomizations/FlowAssetParamsPtrCustomization.h new file mode 100644 index 000000000..fb22fbe26 --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowAssetParamsPtrCustomization.h @@ -0,0 +1,25 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "IPropertyTypeCustomization.h" +#include "PropertyHandle.h" + +class IPropertyHandle; + +/** + * Customizes the FFlowAssetParamsPtr property in the Details panel. + */ +class FFlowAssetParamsPtrCustomization : public IPropertyTypeCustomization +{ +public: + static TSharedRef MakeInstance(); + + virtual void CustomizeHeader(TSharedRef PropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils) override; + virtual void CustomizeChildren(TSharedRef PropertyHandle, IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& CustomizationUtils) override; + +private: + TSharedPtr StructPropertyHandle; + + void HandleCreateNew(); + bool ShouldFilterAsset(const FAssetData& AssetData) const; +}; \ No newline at end of file diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowDataPinValueCustomization.h b/Source/FlowEditor/Public/DetailCustomizations/FlowDataPinValueCustomization.h new file mode 100644 index 000000000..0753fde6e --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowDataPinValueCustomization.h @@ -0,0 +1,137 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Types/FlowPinType.h" +#include "Types/FlowDataPinValue.h" +#include "Types/FlowPinEnums.h" +#include "UnrealExtensions/IFlowExtendedPropertyTypeCustomization.h" +#include "IPropertyTypeCustomization.h" +#include "Widgets/Input/SComboBox.h" +#include "Templates/Function.h" + +class IFlowDataPinValueOwnerInterface; + +/* +* Flow Data Pin Value Customization +* +* Responsibilities: +* - Header with MultiType selector + Input Pin checkbox. +* - Child rows for single vs array modes (built conditionally now). +* - Base for specialized enum/class/object customizations. +* - Exposes array validation helpers. +* +* Dynamic Mode Switching: +* - After MultiType changes, invokes OwnerInterface->RequestFlowDataPinValuesDetailsRebuild() +* (implemented via owner-level detail customization) to force a full rebuild. +*/ +class FLOWEDITOR_API FFlowDataPinValueCustomization : public IFlowExtendedPropertyTypeCustomization +{ + using Super = IFlowExtendedPropertyTypeCustomization; + +protected: + // Property handles + TSharedPtr MultiTypeHandle; + TSharedPtr ValuesHandle; + TSharedPtr IsInputPinHandle; + TSharedPtr PropertyUtilities; + + // Cached context + const FFlowPinType* DataPinType = nullptr; + IPropertyTypeCustomizationUtils* CustomizationUtils = nullptr; + IFlowDataPinValueOwnerInterface* OwnerInterface = nullptr; + + // MultiType UI state (enum values) + TArray> MultiTypeOptions; + TSharedPtr SelectedMultiType; + TSharedPtr>> MultiTypeComboBox; + + /* Cached flag whether this pin type supports Array mode. */ + bool bArraySupported = true; + +public: + FFlowDataPinValueCustomization() = default; + static TSharedRef MakeInstance(); + + // Non-copyable / non-movable + FFlowDataPinValueCustomization(const FFlowDataPinValueCustomization&) = delete; + FFlowDataPinValueCustomization& operator=(const FFlowDataPinValueCustomization&) = delete; + FFlowDataPinValueCustomization(FFlowDataPinValueCustomization&&) = delete; + FFlowDataPinValueCustomization& operator=(FFlowDataPinValueCustomization&&) = delete; + + // IPropertyTypeCustomization Interface + virtual void CustomizeHeader(TSharedRef InStructPropertyHandle, + FDetailWidgetRow& HeaderRow, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + + virtual void CustomizeChildren(TSharedRef InStructPropertyHandle, + IDetailChildrenBuilder& StructBuilder, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + +protected: + // Build flow + virtual void BuildValueRows(TSharedRef InStructPropertyHandle, + IDetailChildrenBuilder& StructBuilder, + IPropertyTypeCustomizationUtils& StructCustomizationUtils); + + virtual void BuildSingleBranch(IDetailChildrenBuilder& StructBuilder); + virtual void BuildArrayBranch(IDetailChildrenBuilder& StructBuilder); // Skips if !bArraySupported + + void EnsureSingleElementExists(); + void RequestRefresh(); + + // Mode / State + EFlowDataMultiType GetCurrentMultiType() const; + EVisibility GetSingleModeVisibility() const; + EVisibility GetArrayModeVisibility() const; + void TrimArrayToSingle(); + + // Appearance + FFlowDataPinValue* GetFlowDataPinValueBeingEdited() const + { + return IFlowExtendedPropertyTypeCustomization::TryGetTypedStructValue(StructPropertyHandle); + } + + // Input Pin + ECheckBoxState GetCurrentIsInputPin() const; + EVisibility GetInputPinCheckboxVisibility() const; + bool GetInputPinCheckboxEnabled() const; + + // MultiType UI Helpers + TSharedRef GenerateMultiTypeWidget(TSharedPtr Item) const; + FText GetSelectedMultiTypeText() const; + + // Change Handlers + void OnMultiTypeChanged(TSharedPtr NewSelection, ESelectInfo::Type SelectInfo); + void OnInputPinChanged(ECheckBoxState NewState); + + // Caching + void CacheHandles(const TSharedRef& PropertyHandle, + IPropertyTypeCustomizationUtils& StructCustomizationUtils); + void CacheOwnerInterface(); + void CacheArraySupported(); + + // Shared Helpers ------------------------------------------------------ + void BuildVisibilityAwareArray(IDetailChildrenBuilder& StructBuilder, + TSharedPtr ArrayHandle, + TFunction, int32, IDetailChildrenBuilder&, const TAttribute&)> Generator, + TAttribute VisibilityAttribute); + + void ValidateArrayElements(TSharedPtr ArrayHandle, + TFunction)> IsValidPredicate, + TFunction)> InvalidateAction); + + // Tooltips (centralized) + static FText GetMultiTypeTooltip(); + static FText GetInputPinTooltip(); +}; + +/* Template customization for simple scalar value structs. */ +template +class TFlowDataPinValueCustomization : public FFlowDataPinValueCustomization +{ +public: + static TSharedRef MakeInstance() + { + return MakeShareable(new TFlowDataPinValueCustomization()); + } +}; \ No newline at end of file diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowDataPinValueCustomization_Class.h b/Source/FlowEditor/Public/DetailCustomizations/FlowDataPinValueCustomization_Class.h new file mode 100644 index 000000000..e8ffabc29 --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowDataPinValueCustomization_Class.h @@ -0,0 +1,80 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "DetailCustomizations/FlowDataPinValueCustomization.h" + +class SClassPropertyEntryBox; + +/* +* Class value customization: +* - Conditionally shows ClassFilter (OwnerInterface->ShowFlowDataPinValueClassFilter). +* - If MetaClass metadata present: show row but disabled. +* - Enabled state otherwise: OwnerInterface->CanEditFlowDataPinValueClassFilter. +* - Validates stored FSoftClassPath values against effective filter. +*/ +class FLOWEDITOR_API FFlowDataPinValueCustomization_Class : public FFlowDataPinValueCustomization +{ + using Super = FFlowDataPinValueCustomization; + +public: + static TSharedRef MakeInstance() + { + return MakeShareable(new FFlowDataPinValueCustomization_Class()); + } + +protected: + virtual void BuildValueRows(TSharedRef InStructPropertyHandle, + IDetailChildrenBuilder& StructBuilder, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + +private: + // Property handles + TSharedPtr ClassFilterHandle; + + // Metadata-derived flags + const UClass* RequiredInterface = nullptr; + bool bAllowAbstract = true; + bool bIsBlueprintBaseOnly = false; + bool bAllowNone = true; + bool bShowTreeView = false; + bool bHideViewOptions = false; + bool bShowDisplayNames = false; + bool bHasMetaClass = false; + + // Effective filter + TWeakObjectPtr CachedEffectiveFilter; + + // Helpers + void ExtractMetadata(); + void TrySetClassFilterFromMetaData() const; + UClass* DeriveBestClassFilter() const; + void RefreshEffectiveFilter(); + + // UI + void BuildClassFilterRow(IDetailChildrenBuilder& StructBuilder, bool bSourceEditable) const; + void BuildSingleBranch(IDetailChildrenBuilder& StructBuilder); + void BuildArrayBranch(IDetailChildrenBuilder& StructBuilder); + + // Delegates / validation + void BindDelegates(); + void OnClassFilterChanged(); + void OnValuesChanged(); + + void ValidateAllElements(); + bool IsElementValid(TSharedPtr ElementHandle) const; + + // Access / modification + static const UClass* GetSelectedClassForHandle(TSharedPtr ElementHandle); + void OnSetClassForHandle(const UClass* NewClass, TSharedPtr ElementHandle) const; + + static bool GetElementPathString(const TSharedPtr& ElementHandle, FString& OutPath); + static bool IsNoneString(const FString& Str); + + // Permissions (inline owner queries) + bool ShouldShowSourceRow() const; + bool IsSourceEditable() const; + bool AreValuesEditable() const { return true; } + + // Value struct access + struct FFlowDataPinValue_Class* GetValueStruct() const; +}; \ No newline at end of file diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowDataPinValueCustomization_Enum.h b/Source/FlowEditor/Public/DetailCustomizations/FlowDataPinValueCustomization_Enum.h new file mode 100644 index 000000000..fc283b809 --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowDataPinValueCustomization_Enum.h @@ -0,0 +1,78 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "DetailCustomizations/FlowDataPinValueCustomization.h" + +class UEnum; + +/* +* Enum customization: +* - Conditionally shows EnumClass / EnumName (OwnerInterface->ShowFlowDataPinValueClassFilter). +* - Enabled if OwnerInterface->CanEditFlowDataPinValueClassFilter (MetaClass concept not applied here). +* - Enumerator selection via combo boxes (single / array). +* - Validates stored names. +* - Uses base single/array visibility helpers. +*/ +class FLOWEDITOR_API FFlowDataPinValueCustomization_Enum : public FFlowDataPinValueCustomization +{ + using Super = FFlowDataPinValueCustomization; + +public: + static TSharedRef MakeInstance() + { + return MakeShareable(new FFlowDataPinValueCustomization_Enum()); + } + +protected: + virtual void BuildValueRows(TSharedRef InStructPropertyHandle, + IDetailChildrenBuilder& StructBuilder, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + +private: + // Source handles + TSharedPtr EnumClassHandle; + TSharedPtr EnumNameHandle; + + // Enumerator state + TArray> EnumeratorOptions; + bool bEnumResolved = false; + bool bMultiTypeDelegateBound = false; + + // Build helpers + void BuildSingle(IDetailChildrenBuilder& StructBuilder); + void BuildArray(IDetailChildrenBuilder& StructBuilder); + + // Enum resolution + void CacheEnumHandles(const TSharedRef& StructHandle); + void OnEnumSourceChanged(); + void RebuildEnumData(); + UEnum* ResolveEnum() const; + void CollectEnumerators(UEnum& EnumObj); + + // Validation + void ValidateStoredValues(); + bool IsValueValid(const FName& Candidate) const; + TSharedPtr FindEnumeratorMatch(const FName& Current) const; + + // Multi-type reaction + void OnMultiTypeChanged(); + + // Widgets + TSharedRef GenerateEnumeratorWidget(TSharedPtr Item) const; + static FText GetEnumeratorDisplayText(const FName& Value); + FText GetEnumSourceTooltip() const; + + // Selection handlers + static void OnSingleValueChanged(TSharedPtr NewSelection, ESelectInfo::Type SelectInfo, TSharedPtr ElementHandle); + static void OnArrayElementChanged(TSharedPtr NewSelection, ESelectInfo::Type SelectInfo, TSharedPtr ElementHandle); + + // Convenience + struct FFlowDataPinValue_Enum* GetEnumValueStruct() const; + bool HasEnumeratorOptions() const { return bEnumResolved && EnumeratorOptions.Num() > 0; } + bool IsValueEditingEnabled() const { return HasEnumeratorOptions(); } + + // Permissions (inline owner queries) + bool ShouldShowSourceRow() const; + bool IsSourceEditable() const; + bool AreValuesEditable() const { return true; } +}; diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowDataPinValueCustomization_Object.h b/Source/FlowEditor/Public/DetailCustomizations/FlowDataPinValueCustomization_Object.h new file mode 100644 index 000000000..e18422740 --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowDataPinValueCustomization_Object.h @@ -0,0 +1,66 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "DetailCustomizations/FlowDataPinValueCustomization.h" + +/* +* Object value customization: +* - Conditionally shows ClassFilter (OwnerInterface->ShowFlowDataPinValueClassFilter). +* - MetaClass metadata forces filter (row shown but disabled). +* - Validates object references against effective filter. +*/ +class FLOWEDITOR_API FFlowDataPinValueCustomization_Object : public FFlowDataPinValueCustomization +{ + using Super = FFlowDataPinValueCustomization; + +public: + static TSharedRef MakeInstance() + { + return MakeShareable(new FFlowDataPinValueCustomization_Object()); + } + +protected: + virtual void BuildValueRows(TSharedRef InStructPropertyHandle, + IDetailChildrenBuilder& StructBuilder, + IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + +private: + // Property handles + TSharedPtr ClassFilterHandle; + + // MetaClass state + bool bMetaClassForced = false; + TWeakObjectPtr EffectiveFilterClass; + + // UI building + void BuildClassFilterRow(IDetailChildrenBuilder& StructBuilder, bool bSourceEditable) const; + virtual void BuildSingleBranch(IDetailChildrenBuilder& StructBuilder) override; + virtual void BuildArrayBranch(IDetailChildrenBuilder& StructBuilder) override; + + // Metadata / filter + void TryApplyMetaClass(); + void ResolveEffectiveFilter(); + + // Delegates & validation + void BindDelegates(); + void OnClassFilterChanged(); + void OnValuesChanged(); + void ValidateAll(); + bool IsElementValid(TSharedPtr ElementHandle) const; + static void InvalidateElement(TSharedPtr ElementHandle); + + // Value access + static UObject* GetObjectValue(TSharedPtr ElementHandle); + static void SetObjectValue(TSharedPtr ElementHandle, UObject* NewObj); + + // Permissions (inline owner queries) + bool ShouldShowSourceRow() const; + bool IsSourceEditable() const; + static bool AreValuesEditable() { return true; } + + // Value struct accessor + struct FFlowDataPinValue_Object* GetValueStruct() const; + + // Widget + TSharedRef BuildObjectValueWidgetForElement(TSharedPtr ElementHandle) const; +}; \ No newline at end of file diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowDataPinValueOwnerCustomization.h b/Source/FlowEditor/Public/DetailCustomizations/FlowDataPinValueOwnerCustomization.h new file mode 100644 index 000000000..c783ac60a --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowDataPinValueOwnerCustomization.h @@ -0,0 +1,59 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "IDetailCustomization.h" +#include "DetailLayoutBuilder.h" +#include "Delegates/Delegate.h" +#include "Templates/SharedPointer.h" + +#include "Interfaces/FlowDataPinValueOwnerInterface.h" + +class IFlowDataPinValueOwnerInterface; + +/* +* Template customization for owner types implementing IFlowDataPinValueOwnerInterface. +* Captures the layout builder pointer and installs a rebuild delegate. +*/ +template +class TFlowDataPinValueOwnerCustomization : public IDetailCustomization +{ +public: + static TSharedRef MakeInstance() + { + return MakeShareable(new TFlowDataPinValueOwnerCustomization()); + } + + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override + { + CachedBuilder = &DetailBuilder; + + TArray> Objects; + DetailBuilder.GetObjectsBeingCustomized(Objects); + + for (TWeakObjectPtr& Obj : Objects) + { + OwnerT* TypedOwner = Cast(Obj.Get()); + if (!TypedOwner) + { + continue; + } + + if (IFlowDataPinValueOwnerInterface* InterfacePtr = Cast(TypedOwner)) + { + InterfacePtr->SetFlowDataPinValuesRebuildDelegate( + FSimpleDelegate::CreateSP(this, &TFlowDataPinValueOwnerCustomization::RequestRebuild)); + } + } + } + +private: + IDetailLayoutBuilder* CachedBuilder = nullptr; + + void RequestRebuild() + { + if (CachedBuilder) + { + CachedBuilder->ForceRefreshDetails(); // Full rebuild; will recreate this customization instance + } + } +}; diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowDataPinValueOwnerCustomizations.h b/Source/FlowEditor/Public/DetailCustomizations/FlowDataPinValueOwnerCustomizations.h new file mode 100644 index 000000000..437de9a59 --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowDataPinValueOwnerCustomizations.h @@ -0,0 +1,12 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "FlowDataPinValueOwnerCustomization.h" + +#include "Asset/FlowAssetParams.h" +#include "Nodes/FlowNodeBase.h" +#include "FlowExecutableActorComponent.h" + +using FFlowAssetParamsCustomization = TFlowDataPinValueOwnerCustomization; +using FFlowNodeBaseCustomization = TFlowDataPinValueOwnerCustomization; +using FFlowExecutableActorComponentCustomization = TFlowDataPinValueOwnerCustomization; diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowDataPinValueStandardCustomizations.h b/Source/FlowEditor/Public/DetailCustomizations/FlowDataPinValueStandardCustomizations.h new file mode 100644 index 000000000..e920827b2 --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowDataPinValueStandardCustomizations.h @@ -0,0 +1,26 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "FlowDataPinValueCustomization.h" +#include "Types/FlowDataPinValuesStandard.h" + +// Specialized customizations +#include "DetailCustomizations/FlowDataPinValueCustomization_Enum.h" +#include "DetailCustomizations/FlowDataPinValueCustomization_Class.h" +#include "DetailCustomizations/FlowDataPinValueCustomization_Object.h" + +// Scalar / simple using aliases +using FFlowDataPinValueCustomization_Bool = TFlowDataPinValueCustomization; +using FFlowDataPinValueCustomization_Int = TFlowDataPinValueCustomization; +using FFlowDataPinValueCustomization_Int64 = TFlowDataPinValueCustomization; +using FFlowDataPinValueCustomization_Float = TFlowDataPinValueCustomization; +using FFlowDataPinValueCustomization_Double = TFlowDataPinValueCustomization; +using FFlowDataPinValueCustomization_Name = TFlowDataPinValueCustomization; +using FFlowDataPinValueCustomization_String = TFlowDataPinValueCustomization; +using FFlowDataPinValueCustomization_Text = TFlowDataPinValueCustomization; +using FFlowDataPinValueCustomization_Vector = TFlowDataPinValueCustomization; +using FFlowDataPinValueCustomization_Rotator = TFlowDataPinValueCustomization; +using FFlowDataPinValueCustomization_Transform = TFlowDataPinValueCustomization; +using FFlowDataPinValueCustomization_GameplayTag = TFlowDataPinValueCustomization; +using FFlowDataPinValueCustomization_GameplayTagContainer = TFlowDataPinValueCustomization; +using FFlowDataPinValueCustomization_InstancedStruct = TFlowDataPinValueCustomization; diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowDetailsAddOnUI.h b/Source/FlowEditor/Public/DetailCustomizations/FlowDetailsAddOnUI.h new file mode 100644 index 000000000..799f8942b --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowDetailsAddOnUI.h @@ -0,0 +1,30 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UObject/Object.h" + +class SWidget; +class UEdGraph; +class UFlowGraphNode; + +/** +* Shared UI helpers for "Attach AddOn..." in details panels. +*/ +class FLOWEDITOR_API FFlowDetailsAddOnUI +{ +public: + /** Try to resolve the edited UObject (node or addon instance) to its UFlowGraphNode wrapper. */ + static UFlowGraphNode* FindGraphNodeForEditedObject(UObject* EditedObject); + + /** Return the owning UEdGraph for the graph node. */ + static UEdGraph* GetOwningEdGraph(UFlowGraphNode* GraphNode); + + /** Returns true if we can open/build the Attach AddOn menu for the edited object. */ + static bool CanAttachAddOn(UObject* EditedObject); + + /** Builds the menu widget content for attaching an addon (the same selector UI used by the context menu). */ + static TSharedRef BuildAttachAddOnMenuContent(UObject* EditedObject); + + /** Lower-level overload if caller already resolved graph + node. */ + static TSharedRef BuildAttachAddOnMenuContent(UEdGraph* Graph, UFlowGraphNode* GraphNode); +}; \ No newline at end of file diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowNamedDataPinPropertyCustomization.h b/Source/FlowEditor/Public/DetailCustomizations/FlowNamedDataPinPropertyCustomization.h new file mode 100644 index 000000000..41ee4914b --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowNamedDataPinPropertyCustomization.h @@ -0,0 +1,19 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UnrealExtensions/IFlowExtendedPropertyTypeCustomization.h" + +/** + * Details customization for FFlowPin. + */ +class FFlowNamedDataPinPropertyCustomization : public IFlowExtendedPropertyTypeCustomization +{ + typedef IFlowExtendedPropertyTypeCustomization Super; + +public: + static TSharedRef MakeInstance() { return MakeShareable(new FFlowNamedDataPinPropertyCustomization()); } + +protected: + + virtual FText BuildHeaderText() const override; +}; diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowNodeAddOn_Details.h b/Source/FlowEditor/Public/DetailCustomizations/FlowNodeAddOn_Details.h new file mode 100644 index 000000000..4f980bc8c --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowNodeAddOn_Details.h @@ -0,0 +1,24 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "FlowDataPinValueOwnerCustomization.h" +#include "IDetailCustomization.h" +#include "Templates/SharedPointer.h" + +class UFlowNodeAddOn; + +class FFlowNodeAddOn_Details final : public TFlowDataPinValueOwnerCustomization +{ +public: + static TSharedRef MakeInstance() + { + return MakeShareable(new FFlowNodeAddOn_Details()); + } + + // IDetailCustomization + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailLayout) override; + // -- + +private: + TWeakObjectPtr EditedAddOn = nullptr; +}; \ No newline at end of file diff --git a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_ComponentObserverDetails.h b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_ComponentObserverDetails.h similarity index 99% rename from Source/FlowEditor/Private/Nodes/Customizations/FlowNode_ComponentObserverDetails.h rename to Source/FlowEditor/Public/DetailCustomizations/FlowNode_ComponentObserverDetails.h index c3450294f..c56ec54ee 100644 --- a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_ComponentObserverDetails.h +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_ComponentObserverDetails.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "IDetailCustomization.h" diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowNode_CustomEventBaseDetails.h b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_CustomEventBaseDetails.h new file mode 100644 index 000000000..ddda1485b --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_CustomEventBaseDetails.h @@ -0,0 +1,37 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "IDetailCustomization.h" +#include "Templates/SharedPointer.h" +#include "Types/SlateEnums.h" +#include "Widgets/Input/SComboBox.h" + +class IDetailCategoryBuilder; +class UFlowAsset; + +class FFlowNode_CustomEventBaseDetails : public IDetailCustomization +{ +public: + // IDetailCustomization + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailLayout) override; + // -- + +protected: + void CustomizeDetailsInternal(IDetailLayoutBuilder& DetailLayout, const FText& CustomRowNameText, const FText& EventNameText); + + virtual IDetailCategoryBuilder& CreateDetailCategory(IDetailLayoutBuilder& DetailLayout) const = 0; + virtual TArray BuildEventNames(const UFlowAsset& FlowAsset) const = 0; + + void OnComboBoxOpening(); + void RebuildEventNames(); + TSharedRef GenerateEventWidget(TSharedPtr Item) const; + FText GetSelectedEventText() const; + void PinSelectionChanged(TSharedPtr Item, ESelectInfo::Type SelectInfo); + bool IsInEventNames(const FName& EventName) const; + + TArray> ObjectsBeingEdited; + TArray> EventNames; + TSharedPtr CachedEventNameSelected; + TSharedPtr>> EventTextListWidget; + bool bExcludeReferencedEvents = false; +}; diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowNode_CustomInputDetails.h b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_CustomInputDetails.h new file mode 100644 index 000000000..ad948c5fb --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_CustomInputDetails.h @@ -0,0 +1,24 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "FlowNode_CustomEventBaseDetails.h" +#include "Templates/SharedPointer.h" + +class FFlowNode_CustomInputDetails final : public FFlowNode_CustomEventBaseDetails +{ +public: + FFlowNode_CustomInputDetails(); + + static TSharedRef MakeInstance() + { + return MakeShareable(new FFlowNode_CustomInputDetails()); + } + + // IDetailCustomization + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailLayout) override; + // -- + +protected: + virtual IDetailCategoryBuilder& CreateDetailCategory(IDetailLayoutBuilder& DetailLayout) const override; + virtual TArray BuildEventNames(const UFlowAsset& FlowAsset) const override; +}; diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowNode_CustomOutputDetails.h b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_CustomOutputDetails.h new file mode 100644 index 000000000..9640d013c --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_CustomOutputDetails.h @@ -0,0 +1,24 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "FlowNode_CustomEventBaseDetails.h" +#include "Templates/SharedPointer.h" + +class FFlowNode_CustomOutputDetails final : public FFlowNode_CustomEventBaseDetails +{ +public: + FFlowNode_CustomOutputDetails(); + + static TSharedRef MakeInstance() + { + return MakeShareable(new FFlowNode_CustomOutputDetails()); + } + + // IDetailCustomization + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailLayout) override; + // -- + +protected: + virtual IDetailCategoryBuilder& CreateDetailCategory(IDetailLayoutBuilder& DetailLayout) const override; + virtual TArray BuildEventNames(const UFlowAsset& FlowAsset) const override; +}; diff --git a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_Details.h b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_Details.h similarity index 58% rename from Source/FlowEditor/Private/Nodes/Customizations/FlowNode_Details.h rename to Source/FlowEditor/Public/DetailCustomizations/FlowNode_Details.h index ce85a66d6..2b2b92051 100644 --- a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_Details.h +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_Details.h @@ -1,10 +1,13 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once +#include "FlowDataPinValueOwnerCustomization.h" #include "IDetailCustomization.h" +#include "Templates/SharedPointer.h" + +class UFlowNode; -class FFlowNode_Details final : public IDetailCustomization +class FFlowNode_Details final : public TFlowDataPinValueOwnerCustomization { public: static TSharedRef MakeInstance() @@ -15,4 +18,7 @@ class FFlowNode_Details final : public IDetailCustomization // IDetailCustomization virtual void CustomizeDetails(IDetailLayoutBuilder& DetailLayout) override; // -- -}; + +private: + TWeakObjectPtr EditedNode = nullptr; +}; \ No newline at end of file diff --git a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_PlayLevelSequenceDetails.h b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_PlayLevelSequenceDetails.h similarity index 99% rename from Source/FlowEditor/Private/Nodes/Customizations/FlowNode_PlayLevelSequenceDetails.h rename to Source/FlowEditor/Public/DetailCustomizations/FlowNode_PlayLevelSequenceDetails.h index efb935389..e5fe2f25c 100644 --- a/Source/FlowEditor/Private/Nodes/Customizations/FlowNode_PlayLevelSequenceDetails.h +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_PlayLevelSequenceDetails.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "IDetailCustomization.h" diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowNode_SubGraphDetails.h b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_SubGraphDetails.h new file mode 100644 index 000000000..4195316d7 --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_SubGraphDetails.h @@ -0,0 +1,15 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "IDetailCustomization.h" + +class FFlowNode_SubGraphDetails final : public IDetailCustomization +{ +public: + static TSharedRef MakeInstance() + { + return MakeShareable(new FFlowNode_SubGraphDetails); + } + + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailLayout) override; +}; diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowPinCustomization.h b/Source/FlowEditor/Public/DetailCustomizations/FlowPinCustomization.h new file mode 100644 index 000000000..7810ad279 --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowPinCustomization.h @@ -0,0 +1,30 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "UnrealExtensions/IFlowExtendedPropertyTypeCustomization.h" + +struct FFlowPin; + +/** + * Details customization for FFlowPin. + */ +class FFlowPinCustomization : public IFlowExtendedPropertyTypeCustomization +{ + typedef IFlowExtendedPropertyTypeCustomization Super; + +public: + static TSharedRef MakeInstance() { return MakeShareable(new FFlowPinCustomization()); } + + virtual void CustomizeChildren(TSharedRef InStructPropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + +protected: + /* Accessor to return the actual struct being edited. */ + FORCEINLINE FFlowPin* GetFlowPin() const + { + return IFlowExtendedPropertyTypeCustomization::TryGetTypedStructValue(StructPropertyHandle); + } + + void OnChildPropertyValueChanged(); + + virtual FText BuildHeaderText() const override; +}; diff --git a/Source/FlowEditor/Public/Find/FindInFlow.h b/Source/FlowEditor/Public/Find/FindInFlow.h new file mode 100644 index 000000000..2c0357024 --- /dev/null +++ b/Source/FlowEditor/Public/Find/FindInFlow.h @@ -0,0 +1,239 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Containers/Array.h" +#include "Containers/BitArray.h" +#include "Containers/Set.h" +#include "Containers/SparseArray.h" +#include "Containers/UnrealString.h" +#include "Delegates/Delegate.h" +#include "HAL/PlatformCrt.h" +#include "Input/Reply.h" +#include "Internationalization/Text.h" +#include "Misc/Optional.h" +#include "Templates/SharedPointer.h" +#include "Templates/TypeHash.h" +#include "Templates/UnrealTemplate.h" +#include "Types/SlateEnums.h" +#include "UObject/WeakObjectPtr.h" +#include "UObject/WeakObjectPtrTemplates.h" +#include "Widgets/DeclarativeSyntaxSupport.h" +#include "Widgets/SCompoundWidget.h" +#include "Widgets/Input/SSpinBox.h" +#include "Widgets/Views/STableViewBase.h" +#include "Widgets/Views/STreeView.h" + +#include "FindInFlowEnums.h" + +class ITableRow; +class SWidget; +class UFlowGraphNode; +class UEdGraphNode; +class UFlowAsset; +class UFlowNodeBase; + +/** + * Item that matched the search results. + */ +class FFindInFlowResult +{ +public: + /*Create a root (or only text) result. */ + FFindInFlowResult(const FString& InValue, UFlowAsset* InOwningFlowAsset = nullptr); + + /* Create a flow node result. */ + FFindInFlowResult(const FString& InValue, TSharedPtr InParent, UEdGraphNode* InNode, bool bInIsSubGraphNode = false, UFlowAsset* InOwningFlowAsset = nullptr); + + /* Called when user clicks on the search item. */ + FReply OnClick(TWeakPtr FlowAssetEditorPtr); + + /* Called when user double clicks on the search item. */ + FReply OnDoubleClick() const; + + /* Create an icon to represent the result. */ + TSharedRef CreateIcon() const; + + /* Gets the description on flow node if any. */ + FString GetDescriptionText() const; + + /* Gets the comment on this node if any. */ + FString GetCommentText() const; + + /* Gets the node type. */ + FString GetNodeTypeText() const; + + /* Gets the node tool tip. */ + FText GetToolTipText() const; + + /* Returns a snippet of the matched property/value for tooltip. */ + FText GetMatchedSnippet() const; + + /* Human-readable list of categories this result matched in. */ + FText GetMatchedCategoriesText() const; + + /* Any children listed under this flow node (decorators, services, addons, subnodes). */ + TArray< TSharedPtr > Children; + + /* The string value for this result. */ + FString Value; + + /* Stores a snippet of the matched property/value (e.g. "Damage:50"). */ + FString MatchedPropertySnippet; + + /* Which search categories actually produced a hit for this item. */ + EFlowSearchFlags MatchedFlags = EFlowSearchFlags::None; + + /* The graph node that this search result refers to. */ + TWeakObjectPtr GraphNode; + + /* The owning flow asset for this result. */ + TWeakObjectPtr OwningFlowAsset; + + /* Search result parent. */ + TWeakPtr Parent; + + /* Whether this item is a subgraph node. */ + bool bIsSubGraphNode = false; +}; + +struct FFindInFlowCache +{ + /* Removes all cached data for the changed flow asset. */ + static void OnFlowAssetChanged(UFlowAsset& ChangedFlowAsset); + + /* Cache searchable strings per node (for repeat searches). */ + static TMap, TMap>> CategoryStringCache; +}; + +struct FFindInFlowAllResults +{ + typedef TSharedPtr FSearchResult; + + /* we need to keep a handle on the root result, because it won't show up in the tree. */ + FSearchResult RootSearchResult; + + /* This buffer stores the currently displayed results. */ + TArray ItemsFound; + + /* Visited assets to prevent cycles in subgraph recursion. */ + TSet VisitedAssets; + + void Setup() + { + RootSearchResult = MakeShareable(new FFindInFlowResult(TEXT("Root"))); + } + + void Reset() + { + ItemsFound.Empty(); + RootSearchResult->Children.Empty(); + VisitedAssets.Empty(); + } +}; + +/** + * Widget for searching for (Flow nodes) across focused FlowNodes. + */ +class SFindInFlow : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SFindInFlow) {} + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs, TSharedPtr InFlowAssetEditor); + + /* Focuses this widget's search box. */ + void FocusForUse() const; + +protected: + + typedef TSharedPtr FSearchResult; + typedef STreeView STreeViewType; + + /* Called when user changes the text they are searching for. */ + void OnSearchTextChanged(const FText& Text); + + /* Called when user commits text. */ + void OnSearchTextCommitted(const FText& Text, ETextCommit::Type CommitType); + + /* Called when search button is clicked. */ + FReply OnSearchButtonClicked(); + + /* Get the children of a row. */ + void OnGetChildren(FSearchResult InItem, TArray& OutChildren); + + /* Called when user clicks on a new result. */ + void OnTreeSelectionChanged(FSearchResult Item, ESelectInfo::Type SelectInfo); + + /* Called when user double clicks on a new result. */ + void OnTreeSelectionDoubleClicked(FSearchResult Item); + + /* Called when scope selection changed. */ + void OnScopeChanged(TSharedPtr NewSelection, ESelectInfo::Type SelectInfo); + + /* Called when max depth changed. */ + void OnMaxDepthChanged(int32 NewDepth); + + /* Called when a new row is being generated. */ + TSharedRef OnGenerateRow(FSearchResult InItem, const TSharedRef& OwnerTable); + + /* Begins the search based on the SearchValue. */ + void InitiateSearch(); + + /* Build searchable string from node and its FlowNodeBase + AddOns. */ + const TMap>* BuildCategoryStrings(UEdGraphNode* Node, int32 Depth) const; + + /* Determines if a string matches the search tokens. */ + static bool StringMatchesSearchTokens(const TArray& Tokens, const FString& ComparisonString); + static bool StringSetMatchesSearchTokens(const TArray& Tokens, const TSet& StringSet); + + /* Generate widget for scope combo. */ + TSharedRef GenerateScopeWidget(TSharedPtr Item) const; + + /* Get current scope display text. */ + FText GetCurrentScopeText() const; + + bool ProcessAsset(UFlowAsset* Asset, FSearchResult ParentResult, const TArray& Tokens, int32 Depth); + + bool RecurseIntoSubgraphsIfEnabled(UEdGraphNode* EdNode, FSearchResult ParentResult, const TArray& Tokens, int32 Depth); + + void UpdateSearchFlagToStringMapForEdGraphNode(const UEdGraphNode& EdGraphNode, TMap>& SearchFlagToStringMap, int32 Depth) const; + void UpdateSearchFlagToStringMapForFlowNodeBase(const UFlowNodeBase& FlowNodeBase, TMap>& SearchFlagToStringMap, int32 Depth) const; + void AppendPropertyValues(const void* Container, const UStruct* Struct, const UObject* ParentObject, TMap>& SearchFlagToStringMap, int32 Depth) const; + +protected: + /* Pointer back to the flow editor that owns us. */ + TWeakPtr FlowAssetEditorPtr; + + /* The tree view displays the results. */ + TSharedPtr TreeView; + + /* The search text box. */ + TSharedPtr SearchTextField; + + /* The search button. */ + TSharedPtr SearchButton; + + /* Struct with all of the search results. */ + FFindInFlowAllResults SearchResults; + + /* Repeat Search Caching. */ + FFindInFlowCache SearchCache; + + /* The string to highlight in the results. */ + FText HighlightText; + + /* The string to search for. */ + FString SearchValue; + + /* Search configuration. */ + EFlowSearchFlags SearchFlags = EFlowSearchFlags::DefaultSearchFlags; + + TSharedPtr> MaxDepthSpinBox; + int32 MaxSearchDepth = 3; + + /* Scope selection. */ + TArray> ScopeOptionList; + TSharedPtr SelectedScopeOption; + EFlowSearchScope SearchScope = EFlowSearchScope::ThisAssetOnly; +}; \ No newline at end of file diff --git a/Source/FlowEditor/Public/Find/FindInFlowEnums.h b/Source/FlowEditor/Public/Find/FindInFlowEnums.h new file mode 100644 index 000000000..27d88dda8 --- /dev/null +++ b/Source/FlowEditor/Public/Find/FindInFlowEnums.h @@ -0,0 +1,50 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Types/FlowEnumUtils.h" +#include "FindInFlowEnums.generated.h" + +/** + * Bitflags controlling what parts of a Flow node are included in search. + */ +UENUM(Meta = (Bitflags, UseEnumValuesAsMaskValuesInEditor = "true")) +enum class EFlowSearchFlags : uint32 +{ + None = 0 UMETA(Hidden), + + Titles = 1 << 0 UMETA(DisplayName = "Titles"), + Tooltips = 1 << 1 UMETA(DisplayName = "Tooltips"), + Classes = 1 << 2 UMETA(DisplayName = "Classes"), + Comments = 1 << 3 UMETA(DisplayName = "Comments"), + Descriptions = 1 << 4 UMETA(DisplayName = "Descriptions"), + ConfigText = 1 << 5 UMETA(DisplayName = "Config Text"), + PropertyNames = 1 << 6 UMETA(DisplayName = "Property Names"), + PropertyValues = 1 << 7 UMETA(DisplayName = "Property Values"), + AddOns = 1 << 8 UMETA(DisplayName = "Add-Ons"), + Subgraphs = 1 << 9 UMETA(DisplayName = "Subgraphs"), + + All = + Titles | Tooltips | Classes | Comments | Descriptions | ConfigText | + PropertyNames | PropertyValues | AddOns | Subgraphs UMETA(Hidden), + + // Default mask — used at startup and for "reset" + DefaultSearchFlags = All UMETA(Hidden), + PropertiesFlags = PropertyNames | PropertyValues | Tooltips UMETA(Hidden), +}; +ENUM_CLASS_FLAGS(EFlowSearchFlags); + +/** + * Search scope — intentionally minimal. + */ +UENUM() +enum class EFlowSearchScope : uint8 +{ + ThisAssetOnly UMETA(DisplayName = "This Asset", ToolTip = "Search only the currently open Flow Asset"), + AllOfThisType UMETA(DisplayName = "All Flow Assets of This Type",ToolTip = "Search all Flow Assets of this type (or subclasses)"), + AllFlowAssets UMETA(DisplayName = "All Flow Assets", ToolTip = "Search every Flow Asset in the project"), + + Max UMETA(Hidden), + Invalid UMETA(Hidden), + Min = 0 UMETA(Hidden), +}; +FLOW_ENUM_RANGE_VALUES(EFlowSearchScope); \ No newline at end of file diff --git a/Source/FlowEditor/Public/Find/SFindInFlowFilterPopup.h b/Source/FlowEditor/Public/Find/SFindInFlowFilterPopup.h new file mode 100644 index 000000000..819661686 --- /dev/null +++ b/Source/FlowEditor/Public/Find/SFindInFlowFilterPopup.h @@ -0,0 +1,41 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Widgets/DeclarativeSyntaxSupport.h" +#include "Widgets/SCompoundWidget.h" +#include "Widgets/SBoxPanel.h" + +#include "FindInFlowEnums.h" + +DECLARE_DELEGATE_OneParam(FFindInFlowApplyDelegate, EFlowSearchFlags); + +class SFindInFlowFilterPopup : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SFindInFlowFilterPopup) {} + SLATE_ARGUMENT(FFindInFlowApplyDelegate, OnApply) + SLATE_ARGUMENT(FFindInFlowApplyDelegate, OnSaveAsDefault) + SLATE_ARGUMENT(EFlowSearchFlags, InitialFlags) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + +protected: + + EFlowSearchFlags ProposedFlags = EFlowSearchFlags::DefaultSearchFlags; + + FFindInFlowApplyDelegate OnApplyDelegate; + FFindInFlowApplyDelegate OnSaveAsDefaultDelegate; + + TSharedPtr CheckBoxContainer; + + ECheckBoxState GetCheckState(EFlowSearchFlags Flag) const + { + return EnumHasAnyFlags(ProposedFlags, Flag) ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; + } + + FReply OnApplyClicked(); + FReply OnCancelClicked(); + FReply OnToggleAllClicked(); + FReply OnSaveAsDefaultClicked(); +}; \ No newline at end of file diff --git a/Source/FlowEditor/Public/FlowEditorCommands.h b/Source/FlowEditor/Public/FlowEditorCommands.h index 6f9defc3e..5a530e44e 100644 --- a/Source/FlowEditor/Public/FlowEditorCommands.h +++ b/Source/FlowEditor/Public/FlowEditorCommands.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "EdGraph/EdGraphSchema.h" @@ -7,58 +6,73 @@ #include "Framework/Commands/UICommandInfo.h" #include "Templates/SharedPointer.h" -class FFlowToolbarCommands final : public TCommands +class FLOWEDITOR_API FFlowToolbarCommands : public TCommands { public: FFlowToolbarCommands(); TSharedPtr RefreshAsset; - TSharedPtr GoToMasterInstance; + TSharedPtr ValidateAsset; + + TSharedPtr SearchInAsset; + TSharedPtr EditAssetDefaults; virtual void RegisterCommands() override; }; -/** Generic graph commands for the flow graph */ -class FFlowGraphCommands final : public TCommands +/** + * Generic graph commands for the flow graph. + */ +class FLOWEDITOR_API FFlowGraphCommands : public TCommands { public: FFlowGraphCommands(); - /** Context Pins */ - TSharedPtr RefreshContextPins; + // Context Pins + TSharedPtr ReconstructNode; - /** Pins */ + // Pins TSharedPtr AddInput; TSharedPtr AddOutput; TSharedPtr RemovePin; - /** Breakpoints */ + // Pin Breakpoints TSharedPtr AddPinBreakpoint; TSharedPtr RemovePinBreakpoint; TSharedPtr EnablePinBreakpoint; TSharedPtr DisablePinBreakpoint; TSharedPtr TogglePinBreakpoint; - /** Execution Override */ + // Breakpoints + TSharedPtr EnableAllBreakpoints; + TSharedPtr DisableAllBreakpoints; + TSharedPtr RemoveAllBreakpoints; + + // Execution Override + TSharedPtr EnableNode; + TSharedPtr DisableNode; + TSharedPtr SetPassThrough; TSharedPtr ForcePinActivation; - /** Jumps */ + // Jumps TSharedPtr FocusViewport; TSharedPtr JumpToNodeDefinition; virtual void RegisterCommands() override; }; -/** Handles spawning nodes by keyboard shortcut */ -class FFlowSpawnNodeCommands : public TCommands +/** + * Handles spawning nodes by keyboard shortcut. + */ +class FLOWEDITOR_API FFlowSpawnNodeCommands : public TCommands { public: FFlowSpawnNodeCommands(); virtual void RegisterCommands() override; - TSharedPtr GetChordByClass(UClass* NodeClass) const; - TSharedPtr GetActionByChord(FInputChord& InChord) const; + TSharedPtr GetChordByClass(const UClass* NodeClass) const; + TSharedPtr GetActionByChord(const FInputChord& InChord) const; private: TSharedPtr GetActionByClass(UClass* NodeClass) const; diff --git a/Source/FlowEditor/Public/FlowEditorDefines.h b/Source/FlowEditor/Public/FlowEditorDefines.h new file mode 100644 index 000000000..182ce8da5 --- /dev/null +++ b/Source/FlowEditor/Public/FlowEditorDefines.h @@ -0,0 +1,21 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +/** + * Documentation: https://github.com/MothCocoon/FlowGraph/wiki/Asset-Search + * Set macro value to 1, if you made these changes to the engine: https://github.com/EpicGames/UnrealEngine/pull/9882 + */ +#define ENABLE_JUMP_TO_INNER_OBJECT 0 + +/** + * When false, use custom SWidget to complete the search logic (cf.SFindInFlow) + * Documentation: https://github.com/MothCocoon/FlowGraph/wiki/Asset-Search + * Set macro value to 1, if you made these changes to the engine: https://github.com/EpicGames/UnrealEngine/pull/9943 + */ +#define ENABLE_SEARCH_IN_ASSET_EDITOR 0 + +/** + * Documentation: https://github.com/MothCocoon/FlowGraph/wiki/Import-Utils + * Set macro value to 1, if you made these changes to the engine: https://github.com/EpicGames/UnrealEngine/pull/10004 + */ +#define ENABLE_ASYNC_NODES_IMPORT 0 diff --git a/Source/FlowEditor/Public/FlowEditorLogChannels.h b/Source/FlowEditor/Public/FlowEditorLogChannels.h new file mode 100644 index 000000000..104256f12 --- /dev/null +++ b/Source/FlowEditor/Public/FlowEditorLogChannels.h @@ -0,0 +1,6 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Logging/LogMacros.h" + +FLOWEDITOR_API DECLARE_LOG_CATEGORY_EXTERN(LogFlowEditor, Log, All); diff --git a/Source/FlowEditor/Public/FlowEditorModule.h b/Source/FlowEditor/Public/FlowEditorModule.h index 0672e916d..a239f07b8 100644 --- a/Source/FlowEditor/Public/FlowEditorModule.h +++ b/Source/FlowEditor/Public/FlowEditorModule.h @@ -1,18 +1,24 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "AssetTypeCategories.h" #include "IAssetTypeActions.h" -#include "Modules/ModuleInterface.h" +#include "Modules/ModuleManager.h" +#include "PropertyEditorDelegates.h" +#include "Toolkits/AssetEditorToolkit.h" +#include "Toolkits/IToolkit.h" class FSlateStyleSet; +class FToolBarBuilder; struct FGraphPanelPinConnectionFactory; class FFlowAssetEditor; class UFlowAsset; -DECLARE_LOG_CATEGORY_EXTERN(LogFlowEditor, Log, All) +struct FLOWEDITOR_API FFlowAssetCategoryPaths : EAssetCategoryPaths +{ + static FAssetCategoryPath Flow; +}; class FLOWEDITOR_API FFlowEditorModule : public IModuleInterface { @@ -22,28 +28,39 @@ class FLOWEDITOR_API FFlowEditorModule : public IModuleInterface private: TArray> RegisteredAssetActions; TSet CustomClassLayouts; + TSet CustomStructLayouts; + + bool bIsRegisteredForAssetChanges = false; public: virtual void StartupModule() override; virtual void ShutdownModule() override; + void RegisterForAssetChanges(); + private: + void TrySetFlowNodeDisplayStyleDefaults() const; + void RegisterAssets(); void UnregisterAssets(); - void RegisterPropertyCustomizations() const; + void RegisterDetailCustomizations(); + void UnregisterDetailCustomizations(); + void RegisterCustomClassLayout(const TSubclassOf Class, const FOnGetDetailCustomizationInstance DetailLayout); + void RegisterCustomStructLayout(const UScriptStruct& Struct, const FOnGetPropertyTypeCustomizationInstance DetailLayout); public: FDelegateHandle FlowTrackCreateEditorHandle; FDelegateHandle ModulesChangedHandle; private: - void ModulesChangesCallback(FName ModuleName, EModuleChangeReason ReasonForChange); - void RegisterAssetIndexers() const; - - void CreateFlowToolbar(FToolBarBuilder& ToolbarBuilder) const; + static void ModulesChangesCallback(FName ModuleName, EModuleChangeReason ReasonForChange); + static void RegisterAssetIndexers(); public: static TSharedRef CreateFlowAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr& InitToolkitHost, UFlowAsset* FlowAsset); -}; + + static void OnAssetUpdated(const FAssetData& AssetData); + static void OnAssetRenamed(const FAssetData& AssetData, const FString& OldObjectPath); +}; \ No newline at end of file diff --git a/Source/FlowEditor/Public/FlowEditorStyle.h b/Source/FlowEditor/Public/FlowEditorStyle.h index 0aa06b843..b38efc9b4 100644 --- a/Source/FlowEditor/Public/FlowEditorStyle.h +++ b/Source/FlowEditor/Public/FlowEditorStyle.h @@ -1,10 +1,9 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Styling/SlateStyle.h" -class FFlowEditorStyle +class FLOWEDITOR_API FFlowEditorStyle { public: static TSharedPtr Get() { return StyleSet; } @@ -13,7 +12,7 @@ class FFlowEditorStyle static void Initialize(); static void Shutdown(); - static const FSlateBrush* GetBrush(FName PropertyName, const ANSICHAR* Specifier = nullptr) + static const FSlateBrush* GetBrush(const FName PropertyName, const ANSICHAR* Specifier = nullptr) { return Get()->GetBrush(PropertyName, Specifier); } diff --git a/Source/FlowEditor/Public/Graph/FlowGraph.h b/Source/FlowEditor/Public/Graph/FlowGraph.h index 9ca219976..32bdc9db4 100644 --- a/Source/FlowEditor/Public/Graph/FlowGraph.h +++ b/Source/FlowEditor/Public/Graph/FlowGraph.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "EdGraph/EdGraph.h" @@ -7,26 +6,112 @@ #include "FlowAsset.h" #include "FlowGraph.generated.h" -class FLOWEDITOR_API FFlowGraphInterface final : public IFlowGraphInterface -{ -public: - virtual ~FFlowGraphInterface() {} - - virtual void OnInputTriggered(UEdGraphNode* GraphNode, const int32 Index) const override; - virtual void OnOutputTriggered(UEdGraphNode* GraphNode, const int32 Index) const override; -}; +class SFlowGraphEditor; +class UFlowGraphNode; +class UFlowGraphNode_Reroute; +class UFlowGraphSchema; +/** + * Flow-specific implementation of engine's EdGraph. + */ UCLASS() class FLOWEDITOR_API UFlowGraph : public UEdGraph { GENERATED_UCLASS_BODY() - static UEdGraph* CreateGraph(UFlowAsset* InFlowAsset); +protected: + /* Graph version number. */ + UPROPERTY() + int32 GraphVersion; + + static constexpr int32 CurrentGraphVersion = 2; + + /* If set, graph modifications won't cause updates in internal tree structure. + * Flag allows freezing update during heavy changes like pasting new nodes. */ + uint32 bLockUpdates : 1; + + /* Is currently loading the Flow Graph (used to suppress some work during load)? */ + uint32 bIsLoadingGraph : 1; + + bool bIsSavingGraph = false; + + /* Reroute nodes that requested a post-unlock type fixup (avoids reconstruct storms on paste). */ + UPROPERTY(Transient) + TSet> PendingRerouteTypeFixups; + + /* Nodes that requested a post-unlock reconstruct (avoids reconstruct storms + avoids reconstruct while transacting). */ + UPROPERTY(Transient) + TSet> PendingNodeReconstructs; + +public: + static void CreateGraph(UFlowAsset* InFlowAsset); + static void CreateGraph(UFlowAsset* InFlowAsset, TSubclassOf FlowSchema); + void RefreshGraph(); + +protected: + void UpgradeAllFlowNodePins(); + + void RecursivelyRefreshAddOns(UFlowGraphNode& FromFlowGraphNode); + static void RecursivelySetupAllFlowGraphNodesForEditing(UFlowGraphNode& FromFlowGraphNode); + + /* Run deferred reroute retyping after unlocking updates. */ + void ProcessPendingRerouteTypeFixups(); + /* Run deferred node reconstructs after unlocking updates. */ + void ProcessPendingNodeReconstructs(); + +public: + /* Called by reroute nodes when graph is locked and type changes should be deferred. */ + void EnqueueRerouteTypeFixup(UFlowGraphNode_Reroute* RerouteNode); + + /* Called by nodes when a reconstruct should occur, but must be deferred (transaction/lock). */ + void EnqueueNodeReconstruct(UFlowGraphNode* Node); + +public: // UEdGraph virtual void NotifyGraphChanged() override; // -- - /** Returns the FlowAsset that contains this graph */ UFlowAsset* GetFlowAsset() const; -}; + void ValidateAsset(FFlowMessageLog& MessageLog); + + // UObject + virtual void Serialize(FArchive& Ar) override; + // -- + +public: + virtual void OnCreated(); + virtual void OnLoaded(); + virtual void OnSave(); + + virtual void Initialize(); + virtual void UpdateVersion(); + virtual void MarkVersion(); + + void UpdateClassData(); + virtual void UpdateAsset(const int32 UpdateFlags = 0); + bool UpdateUnknownNodeClasses(); + void UpdateDeprecatedClasses(); + +protected: + static void UpdateFlowGraphNodeErrorMessage(UFlowGraphNode& Node); + static FString GetDeprecationMessage(const UClass* Class); + +public: + virtual void OnSubNodeDropped(); + virtual void OnNodesPasted(const FString& ImportStr) {} + + void RemoveOrphanedNodes(); + virtual void CollectAllNodeInstances(TSet& NodeInstances); + virtual bool CanRemoveNestedObject(UObject* TestObject) const; + virtual void OnNodeInstanceRemoved(UObject* NodeInstance) {} + + static UEdGraphPin* FindGraphNodePin(UEdGraphNode* Node, const EEdGraphPinDirection Direction); + + bool IsLocked() const; + void LockUpdates(); + void UnlockUpdates(); + + bool IsLoadingGraph() const { return bIsLoadingGraph; } + bool IsSavingGraph() const { return bIsSavingGraph; } +}; \ No newline at end of file diff --git a/Source/FlowEditor/Public/Graph/FlowGraphConnectionDrawingPolicy.h b/Source/FlowEditor/Public/Graph/FlowGraphConnectionDrawingPolicy.h index 1cf9ea48c..fca7f1422 100644 --- a/Source/FlowEditor/Public/Graph/FlowGraphConnectionDrawingPolicy.h +++ b/Source/FlowEditor/Public/Graph/FlowGraphConnectionDrawingPolicy.h @@ -1,10 +1,12 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "ConnectionDrawingPolicy.h" #include "EdGraphUtilities.h" +class FSlateWindowElementList; +class UEdGraph; + UENUM() enum class EFlowConnectionDrawType : uint8 { @@ -12,20 +14,19 @@ enum class EFlowConnectionDrawType : uint8 Circuit }; -struct FFlowGraphConnectionDrawingPolicyFactory : public FGraphPanelPinConnectionFactory +struct FLOWEDITOR_API FFlowGraphConnectionDrawingPolicyFactory : public FGraphPanelPinConnectionFactory { - virtual ~FFlowGraphConnectionDrawingPolicyFactory() + virtual ~FFlowGraphConnectionDrawingPolicyFactory() override { } virtual class FConnectionDrawingPolicy* CreateConnectionPolicy(const class UEdGraphSchema* Schema, int32 InBackLayerID, int32 InFrontLayerID, float ZoomFactor, const class FSlateRect& InClippingRect, class FSlateWindowElementList& InDrawElements, class UEdGraph* InGraphObj) const override; }; -class FSlateWindowElementList; -class UEdGraph; - -// This class draws the connections between nodes -class FFlowGraphConnectionDrawingPolicy : public FConnectionDrawingPolicy +/** + * This class draws the connections between nodes. + */ +class FLOWEDITOR_API FFlowGraphConnectionDrawingPolicy : public FConnectionDrawingPolicy { float RecentWireDuration; @@ -39,25 +40,32 @@ class FFlowGraphConnectionDrawingPolicy : public FConnectionDrawingPolicy float RecordedWireThickness; float SelectedWireThickness; - // runtime values + // Runtime values UEdGraph* GraphObj; TMap RecentPaths; TMap RecordedPaths; TMap SelectedPaths; + /* Used to help reversing pins on nodes that go backwards. */ + TMap RerouteToReversedDirectionMap; + public: FFlowGraphConnectionDrawingPolicy(int32 InBackLayerID, int32 InFrontLayerID, float ZoomFactor, const FSlateRect& InClippingRect, FSlateWindowElementList& InDrawElements, UEdGraph* InGraphObj); void BuildPaths(); - // FConnectionDrawingPolicy interface - virtual void DrawConnection(int32 LayerId, const FVector2D& Start, const FVector2D& End, const FConnectionParams& Params) override; + // FConnectionDrawingPolicy + virtual void DrawConnection(int32 LayerId, const FVector2f& Start, const FVector2f& End, const FConnectionParams& Params); virtual void DetermineWiringStyle(UEdGraphPin* OutputPin, UEdGraphPin* InputPin, FConnectionParams& Params) override; virtual void Draw(TMap, FArrangedWidget>& PinGeometries, FArrangedChildren& ArrangedNodes) override; - // End of FConnectionDrawingPolicy interface + // -- protected: - void DrawCircuitSpline(const int32& LayerId, const FVector2D& Start, const FVector2D& End, const FConnectionParams& Params) const; - void DrawCircuitConnection(const int32& LayerId, const FVector2D& Start, const FVector2D& StartDirection, const FVector2D& End, const FVector2D& EndDirection, const FConnectionParams& Params) const; - static FVector2D GetControlPoint(const FVector2D& Source, const FVector2D& Target); + void DrawCircuitSpline(const int32& LayerId, const FVector2f& Start, const FVector2f& End, const FConnectionParams& Params) const; + void DrawCircuitConnection(const int32& LayerId, const FVector2f& Start, const FVector2f& StartDirection, const FVector2f& End, const FVector2f& EndDirection, const FConnectionParams& Params) const; + static FVector2f GetControlPoint(const FVector2f& Source, const FVector2f& Target); + + bool ShouldChangeTangentForReroute(class UFlowGraphNode_Reroute* Reroute); + bool FindPinCenter(const UEdGraphPin* Pin, FVector2D& OutCenter) const; + bool GetAverageConnectedPosition(class UFlowGraphNode_Reroute* Reroute, EEdGraphPinDirection Direction, FVector2D& OutPos) const; }; diff --git a/Source/FlowEditor/Public/Graph/FlowGraphEditor.h b/Source/FlowEditor/Public/Graph/FlowGraphEditor.h new file mode 100644 index 000000000..6c40e052b --- /dev/null +++ b/Source/FlowEditor/Public/Graph/FlowGraphEditor.h @@ -0,0 +1,173 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "GraphEditor.h" +#include "Widgets/DeclarativeSyntaxSupport.h" + +#include "FlowGraph.h" + +class FFlowAssetEditor; +class IDetailsView; +class UEdGraphPin; +class UFlowDebuggerSubsystem; +struct FFlowBreakpoint; + +/** + * Flow-specific implementation of engine's Graph Editor. + */ +class FLOWEDITOR_API SFlowGraphEditor : public SGraphEditor +{ +public: + SLATE_BEGIN_ARGS(SFlowGraphEditor) + { + } + + SLATE_ARGUMENT(FGraphEditorEvents, GraphEvents) + SLATE_ARGUMENT(TSharedPtr, DetailsView) + SLATE_END_ARGS() + +protected: + TWeakObjectPtr FlowAsset; + + TWeakPtr FlowAssetEditor; + TSharedPtr DetailsView; + TSharedPtr CommandList; + + TWeakObjectPtr DebuggerSubsystem; + +public: + void Construct(const FArguments& InArgs, const TSharedPtr InAssetEditor); + + virtual void CreateDebugMenu(); + virtual void BindGraphCommands(); + + virtual FGraphAppearanceInfo GetGraphAppearanceInfo() const; + virtual FText GetCornerText() const; + virtual FText GetPIENotifyText() const; + +private: + static void UndoGraphAction(); + static void RedoGraphAction(); + + static FReply OnSpawnGraphNodeByShortcut(FInputChord InChord, const FVector2f& InPosition, UEdGraph* InGraph); + + void OnCreateComment() const; + +public: + virtual bool IsTabFocused() const; + + static bool CanEdit(); + static bool IsPIE(); + static bool IsPlaySessionPaused(); + + virtual void SelectSingleNode(UEdGraphNode* Node); + +protected: + virtual void OnSelectedNodesChanged(const TSet& Nodes); + +public: + FOnSelectionChanged OnSelectionChangedEvent; + + TSet GetSelectedFlowNodes() const; + +protected: + virtual bool CanSelectAllNodes() const { return true; } + + static void ReconnectExecPins(const UFlowGraphNode* Node); + virtual void DeleteSelectedNodes(); + virtual void DeleteSelectedDuplicableNodes(); + virtual bool CanDeleteNodes() const; + + virtual void CopySelectedNodes() const; + static void PrepareFlowGraphNodeForCopy(UFlowGraphNode& FlowGraphNode, const int32 ParentEdNodeIndex, FGraphPanelSelectionSet& NewSelectedNodes); + virtual bool CanCopyNodes() const; + + virtual void CutSelectedNodes(); + virtual bool CanCutNodes() const; + + virtual void PasteNodes(); + + static bool CanPasteNodesAsSubNodes(const TSet& NodesToPaste, const UFlowGraphNode& PasteTargetNode); + static TSet ImportNodesToPasteFromClipboard(UFlowGraph& FlowGraph, FString& OutTextToImport); + TArray DerivePasteTargetNodesFromSelectedNodes() const; + +public: + virtual void PasteNodesHere(const FVector2D& Location); + virtual bool CanPasteNodes() const; + +protected: + virtual void DuplicateNodes(); + virtual bool CanDuplicateNodes() const; + + virtual void OnNodeDoubleClicked(class UEdGraphNode* Node) const; + virtual void OnNodeTitleCommitted(const FText& NewText, ETextCommit::Type CommitInfo, UEdGraphNode* NodeBeingChanged); + + virtual void ReconstructNode() const; + virtual bool CanReconstructNode() const; + + // ---- Pin breakpoint helpers ---- + static bool GetValidExecBreakpointPinContext(const UEdGraphPin* Pin, FGuid& OutNodeGuid, FName& OutPinName); + static const FFlowBreakpoint* FindPinBreakpoint(UFlowDebuggerSubsystem* InDebuggerSubsystem, const UEdGraphPin* Pin); + static bool HasPinBreakpoint(UFlowDebuggerSubsystem* InDebuggerSubsystem, const UEdGraphPin* Pin); + static bool HasEnabledPinBreakpoint(UFlowDebuggerSubsystem* InDebuggerSubsystem, const UEdGraphPin* Pin); + +private: + void AddInput() const; + bool CanAddInput() const; + + void AddOutput() const; + bool CanAddOutput() const; + + void RemovePin(); + bool CanRemovePin(); + + void OnAddBreakpoint() const; + void OnAddPinBreakpoint(); + + bool CanAddBreakpoint() const; + bool CanAddPinBreakpoint(); + + void OnRemoveBreakpoint() const; + void OnRemovePinBreakpoint(); + + bool CanRemoveBreakpoint() const; + bool CanRemovePinBreakpoint(); + + void OnEnableBreakpoint() const; + void OnEnablePinBreakpoint(); + + bool CanEnableBreakpoint() const; + bool CanEnablePinBreakpoint(); + + void OnDisableBreakpoint() const; + void OnDisablePinBreakpoint(); + + bool CanDisableBreakpoint() const; + bool CanDisablePinBreakpoint(); + + void OnToggleBreakpoint() const; + void OnTogglePinBreakpoint(); + + bool CanToggleBreakpoint() const; + bool CanTogglePinBreakpoint(); + + void EnableAllBreakpoints() const; + bool HasAnyDisabledBreakpoints() const; + + void DisableAllBreakpoints() const; + bool HasAnyEnabledBreakpoints() const; + + void RemoveAllBreakpoints() const; + bool HasAnyBreakpoints() const; + + void SetSignalMode(const EFlowSignalMode Mode) const; + bool CanSetSignalMode(const EFlowSignalMode Mode) const; + + void OnForcePinActivation(); + + void FocusViewport() const; + bool CanFocusViewport() const; + + void JumpToNodeDefinition() const; + bool CanJumpToNodeDefinition() const; +}; \ No newline at end of file diff --git a/Source/FlowEditor/Public/Graph/FlowGraphEditorSettings.h b/Source/FlowEditor/Public/Graph/FlowGraphEditorSettings.h index 11ceb0619..e9d6b7871 100644 --- a/Source/FlowEditor/Public/Graph/FlowGraphEditorSettings.h +++ b/Source/FlowEditor/Public/Graph/FlowGraphEditorSettings.h @@ -1,36 +1,55 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Engine/DeveloperSettings.h" +#include "Find/FindInFlowEnums.h" + #include "FlowGraphEditorSettings.generated.h" UENUM() enum class EFlowNodeDoubleClickTarget : uint8 { - NodeDefinition UMETA(Tooltip = "Open node class: either blueprint or C++ class"), - PrimaryAsset UMETA(Tooltip = "Open asset defined as primary asset, i.e. Dialogue asset for PlayDialogue node") + NodeDefinition UMETA(Tooltip = "Open node class: either blueprint or C++ class"), + PrimaryAsset UMETA(Tooltip = "Open asset defined as primary asset, i.e. Dialogue asset for PlayDialogue node"), + PrimaryAssetOrNodeDefinition UMETA(Tooltip = "First try opening the asset then if there is none, open the node class") }; /** * */ UCLASS(Config = EditorPerProjectUserSettings, meta = (DisplayName = "Flow Graph")) -class UFlowGraphEditorSettings final : public UDeveloperSettings +class FLOWEDITOR_API UFlowGraphEditorSettings : public UDeveloperSettings { - GENERATED_UCLASS_BODY() + GENERATED_BODY() - static UFlowGraphEditorSettings* Get() { return StaticClass()->GetDefaultObject(); } +public: + UFlowGraphEditorSettings(); - // Double-clicking a Flow Node might open relevant asset/code editor +#if WITH_EDITOR + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; +#endif + + /* Double-clicking a Flow Node might open relevant asset/code editor. */ UPROPERTY(config, EditAnywhere, Category = "Nodes") EFlowNodeDoubleClickTarget NodeDoubleClickTarget; - // Displays information on the graph node, either C++ class name or path to blueprint asset + /* Displays information on the graph node, either C++ class name or path to blueprint asset. */ UPROPERTY(config, EditAnywhere, Category = "Nodes") bool bShowNodeClass; - // Renders preview of entire graph while hovering over + /* Shows the node description when you play in editor. */ + UPROPERTY(config, EditAnywhere, Category = "Nodes") + bool bShowNodeDescriptionWhilePlaying; + + /* Display descriptions from attached addons in node descriptions. */ + UPROPERTY(EditAnywhere, config, Category = "Nodes") + bool bShowAddonDescriptions; + + /* Pin names will be displayed in a format that is easier to read, even if PinFriendlyName wasn't set. */ + UPROPERTY(EditAnywhere, config, Category = "Nodes") + bool bEnforceFriendlyPinNames; + + /* Renders preview of entire graph while hovering over. */ UPROPERTY(config, EditAnywhere, Category = "Nodes") bool bShowSubGraphPreview; @@ -45,4 +64,16 @@ class UFlowGraphEditorSettings final : public UDeveloperSettings UPROPERTY(EditAnywhere, config, Category = "Wires") bool bHighlightOutputWiresOfSelectedNodes; + + /* Default search filter flags for the Flow Editor. */ + UPROPERTY(VisibleAnywhere, config, Category = "Search", meta = (Bitmask, BitmaskEnum = "/Script/Flow.EFlowSearchFlags")) + uint32 DefaultSearchFlags = uint32(EFlowSearchFlags::DefaultSearchFlags); + + /* Max search depth for inline objects in the Flow Editor. */ + UPROPERTY(EditAnywhere, config, Category = "Search", meta = (ClampMin = 1)) + int32 DefaultMaxSearchDepth = 1; + +public: + virtual FName GetCategoryName() const override { return FName("Flow Graph"); } + virtual FText GetSectionText() const override { return INVTEXT("User Settings"); } }; diff --git a/Source/FlowEditor/Public/Graph/FlowGraphNodesPolicy.h b/Source/FlowEditor/Public/Graph/FlowGraphNodesPolicy.h new file mode 100644 index 000000000..5684006fc --- /dev/null +++ b/Source/FlowEditor/Public/Graph/FlowGraphNodesPolicy.h @@ -0,0 +1,77 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Types/FlowEnumUtils.h" + +#include "FlowGraphNodesPolicy.generated.h" + +class UFlowNodeBase; + +UENUM() +enum class EFlowGraphPolicyResult : int8 +{ + // Forbidden by the policy unless a more specific rule applies + TentativeForbidden, + + // Allowed by the policy unless a more specific rule applies + TentativeAllowed, + + // Strictly forbidden by the policy + Forbidden, + + // Strictly allowed by the policy + Allowed, + + Max UMETA(Hidden), + Invalid = -1 UMETA(Hidden), + Min = 0 UMETA(Hidden), + + // Subrange for strict results + StrictFirst = Forbidden UMETA(Hidden), + StrictLast = Allowed UMETA(Hidden), + + // Subrange for tentative results + TentativeFirst = TentativeForbidden UMETA(Hidden), + TentativeLast = TentativeAllowed UMETA(Hidden), +}; +FLOW_ENUM_RANGE_VALUES(EFlowGraphPolicyResult); + +namespace EFlowGraphPolicyResult_Classifiers +{ + FORCEINLINE bool IsStrictPolicyResult(const EFlowGraphPolicyResult Result) { return FLOW_IS_ENUM_IN_SUBRANGE(Result, EFlowGraphPolicyResult::Strict); } + FORCEINLINE bool IsTentativePolicyResult(const EFlowGraphPolicyResult Result) { return FLOW_IS_ENUM_IN_SUBRANGE(Result, EFlowGraphPolicyResult::Tentative); } + FORCEINLINE bool IsAnyAllowedPolicyResult(const EFlowGraphPolicyResult Result) { return Result == EFlowGraphPolicyResult::Allowed || Result == EFlowGraphPolicyResult::TentativeAllowed; } + FORCEINLINE bool IsAnyForbiddenPolicyResult(const EFlowGraphPolicyResult Result) { return Result == EFlowGraphPolicyResult::Forbidden || Result == EFlowGraphPolicyResult::TentativeForbidden; } + + FORCEINLINE EFlowGraphPolicyResult MergePolicyResult(const EFlowGraphPolicyResult Result0, const EFlowGraphPolicyResult Result1) + { + checkf(!(IsStrictPolicyResult(Result0) && IsStrictPolicyResult(Result1)), TEXT("Should not be deciding between two strict results")); + + // Numerically prefer: Allowed or Forbidden > TentativeAllowed > TentativeForbidden > Invalid + return static_cast(FMath::Max(FlowEnum::ToInt(Result0), FlowEnum::ToInt(Result1))); + } +} + +USTRUCT() +struct FFlowGraphNodesPolicy +{ + GENERATED_BODY(); + +public: +#if WITH_EDITORONLY_DATA + UPROPERTY(Config, EditAnywhere, Category = "Nodes") + TArray AllowedCategories; + + UPROPERTY(Config, EditAnywhere, Category = "Nodes") + TArray DisallowedCategories; +#endif + +#if WITH_EDITOR +public: + EFlowGraphPolicyResult IsNodeAllowedByPolicy(const UFlowNodeBase* FlowNodeBase) const; + +protected: + static bool IsAnySubcategory(const FString& CheckCategory, const TArray& Categories); +#endif +}; + diff --git a/Source/FlowEditor/Public/Graph/FlowGraphPinFactory.h b/Source/FlowEditor/Public/Graph/FlowGraphPinFactory.h new file mode 100644 index 000000000..cac23e1b0 --- /dev/null +++ b/Source/FlowEditor/Public/Graph/FlowGraphPinFactory.h @@ -0,0 +1,17 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "EdGraphSchema_K2.h" +#include "EdGraphUtilities.h" + +struct FFlowPin; + +class FFlowGraphPinFactory : public FGraphPanelPinFactory +{ +public: + // FGraphPanelPinFactory + virtual TSharedPtr CreatePin(class UEdGraphPin* InPin) const override; + // -- + + static int32 GatherValidPinsCount(const TArray& Pins); +}; diff --git a/Source/FlowEditor/Public/Graph/FlowGraphSchema.h b/Source/FlowEditor/Public/Graph/FlowGraphSchema.h index ed4110b81..93c82bfc8 100644 --- a/Source/FlowEditor/Public/Graph/FlowGraphSchema.h +++ b/Source/FlowEditor/Public/Graph/FlowGraphSchema.h @@ -1,67 +1,165 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "EdGraph/EdGraphSchema.h" +#include "Templates/SubclassOf.h" + +#include "Policies/FlowPinTypeMatchPolicy.h" #include "FlowGraphSchema.generated.h" class UFlowAsset; +class UFlowNode; +class UFlowNodeAddOn; +class UFlowNodeBase; +class UFlowGraphNode; +struct FFlowPinType; +class UFlowGraphNode_Reroute; DECLARE_MULTICAST_DELEGATE(FFlowGraphSchemaRefresh); +/** + * Flow-specific implementation of engine's Graph Schema. + */ UCLASS() class FLOWEDITOR_API UFlowGraphSchema : public UEdGraphSchema { GENERATED_UCLASS_BODY() + friend class UFlowGraph; + private: + static bool bInitialGatherPerformed; static TArray NativeFlowNodes; + static TArray NativeFlowNodeAddOns; static TMap BlueprintFlowNodes; - static TMap AssignedGraphNodeClasses; + static TMap BlueprintFlowNodeAddOns; + static TMap, TSubclassOf> GraphNodesByFlowNodes; static bool bBlueprintCompilationPending; public: static void SubscribeToAssetChanges(); - static void GetPaletteActions(FGraphActionMenuBuilder& ActionMenuBuilder, const UClass* AssetClass, const FString& CategoryName); + static void GetPaletteActions(FGraphActionMenuBuilder& ActionMenuBuilder, const UFlowAsset* EditedFlowAsset, const FString& CategoryName); // EdGraphSchema virtual void GetGraphContextActions(FGraphContextMenuBuilder& ContextMenuBuilder) const override; virtual void CreateDefaultNodesForGraph(UEdGraph& Graph) const override; virtual const FPinConnectionResponse CanCreateConnection(const UEdGraphPin* A, const UEdGraphPin* B) const override; + virtual const FPinConnectionResponse CanMergeNodes(const UEdGraphNode* NodeA, const UEdGraphNode* NodeB) const override; virtual bool TryCreateConnection(UEdGraphPin* A, UEdGraphPin* B) const override; virtual bool ShouldHidePinDefaultValue(UEdGraphPin* Pin) const override; virtual FLinearColor GetPinTypeColor(const FEdGraphPinType& PinType) const override; + virtual FText GetPinDisplayName(const UEdGraphPin* Pin) const override; virtual void BreakNodeLinks(UEdGraphNode& TargetNode) const override; virtual void BreakPinLinks(UEdGraphPin& TargetPin, bool bSendsNodeNotification) const override; virtual int32 GetNodeSelectionCount(const UEdGraph* Graph) const override; virtual TSharedPtr GetCreateCommentAction() const override; - virtual void OnPinConnectionDoubleCicked(UEdGraphPin* PinA, UEdGraphPin* PinB, const FVector2D& GraphPosition) const override; + + virtual void OnPinConnectionDoubleCicked(UEdGraphPin* PinA, UEdGraphPin* PinB, const FVector2f& GraphPosition) const override; + + virtual bool IsCacheVisualizationOutOfDate(int32 InVisualizationCacheID) const override; + virtual int32 GetCurrentVisualizationCacheID() const override; + virtual void ForceVisualizationCacheClear() const override; + virtual bool ArePinsCompatible(const UEdGraphPin* PinA, const UEdGraphPin* PinB, const UClass* CallingContext = nullptr, bool bIgnoreArray = false) const override; + virtual void ConstructBasicPinTooltip(const UEdGraphPin& Pin, const FText& PinDescription, FString& TooltipOut) const override; + virtual bool IsTitleBarPin(const UEdGraphPin& Pin) const override; + virtual bool CanShowDataTooltipForPin(const UEdGraphPin& Pin) const override; + // -- + + static const FFlowPinType* LookupDataPinTypeForPinCategory(const FName& PinCategory); + + bool ArePinSubCategoryObjectsCompatible( + const UStruct* OutputStruct, + const UStruct* InputStruct, + const FFlowPinTypeMatchPolicy& PinTypeMatchPolicy, + FPinConnectionResponse& OutConnectionResponse) const; + + /** + * Returns true if the two pin types are schema compatible. Handles outputting a more derived + * type to an input pin expecting a less derived type. + * + * @param Output The output type. + * @param Input The input type. + * @param CallingContext (optional) The calling context (required to properly evaluate pins of type Self) + * @param bIgnoreArray (optional) Whether or not to ignore differences between array and non-array types + * + * @return true if the pin types are compatible. + */ + 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 + * types with a compatible direction. InputPin and OutputPin are PinA and PinB or vis versa, indicating their direction. + * + * @param PinA The pin a. + * @param PinB The pin b. + * @param InputPin Either PinA or PinB, depending on which one is the input. + * @param OutputPin Either PinA or PinB, depending on which one is the output. + * + * @return The message and action to take on trying to make this connection. + */ + virtual const FPinConnectionResponse DetermineConnectionResponseOfCompatibleTypedPins(const UEdGraphPin* PinA, const UEdGraphPin* PinB, const UEdGraphPin* InputPin, const UEdGraphPin* OutputPin) const; + + virtual void GetGraphNodeContextActions(FGraphContextMenuBuilder& ContextMenuBuilder, int32 SubNodeFlags) const; + + virtual bool ShouldAlwaysPurgeOnModification() const override { return false; } + + static bool IsAddOnAllowedForSelectedObjects(const TArray& SelectedObjects, const UFlowNodeAddOn* AddOnTemplate); + // -- + static void UpdateGeneratedDisplayNames(); + static void UpdateGeneratedDisplayName(UClass* NodeClass, bool bBatch = false); + static TArray> GetFlowNodeCategories(); - static UClass* GetAssignedGraphNodeClass(const UClass* FlowNodeClass); + static TSubclassOf GetAssignedGraphNodeClass(const TSubclassOf& FlowNodeClass); + + static bool IsPIESimulating(); + + static const UFlowNodeBase* GetFlowNodeBaseForPin(const UEdGraphPin& EdGraphPin); + static const UFlowAsset* GetFlowAssetForPin(const UEdGraphPin& EdGraphPin); + +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, const UEdGraphPin& TypeFromPin) const; + + /* Handles post-connection notifications for affected nodes. */ + void NotifyNodesChanged(UFlowGraphNode* NodeA, UFlowGraphNode* NodeB, UEdGraph* Graph) const; private: - static bool IsClassContained(const TArray> Classes, const UClass* Class); - static void GetFlowNodeActions(FGraphActionMenuBuilder& ActionMenuBuilder, const UFlowAsset* AssetClassDefaults, const FString& CategoryName); + static void ApplyNodeOrAddOnFilter(const UFlowAsset* EditedFlowAsset, const UClass* FlowNodeClass, TArray& FilteredNodes); + static void GetFlowNodeActions(FGraphActionMenuBuilder& ActionMenuBuilder, const UFlowAsset* EditedFlowAsset, const FString& CategoryName); + static TArray GetFilteredPlaceableNodesOrAddOns(const UFlowAsset* EditedFlowAsset, const TArray& InNativeNodesOrAddOns, const TMap& InBlueprintNodesOrAddOns); + static void GetCommentAction(FGraphActionMenuBuilder& ActionMenuBuilder, const UEdGraph* CurrentGraph = nullptr); - static bool IsFlowNodePlaceable(const UClass* Class); + static bool IsFlowNodeOrAddOnPlaceable(const UClass* Class); static void OnBlueprintPreCompile(UBlueprint* Blueprint); static void OnBlueprintCompiled(); - - static void GatherFlowNodes(); static void OnHotReload(EReloadCompleteReason ReloadCompleteReason); + static void GatherNativeNodesOrAddOns(const TSubclassOf& FlowNodeBaseClass, TArray& InOutNodesOrAddOnsArray); + static void GatherNodes(); + static void OnAssetAdded(const FAssetData& AssetData); static void AddAsset(const FAssetData& AssetData, const bool bBatch); + static bool ShouldAddToBlueprintFlowNodesMap(const FAssetData& AssetData, const TSubclassOf& BlueprintClass, const TSubclassOf& FlowNodeBaseClass); + static void OnAssetRemoved(const FAssetData& AssetData); + static void OnAssetRenamed(const FAssetData& AssetData, const FString& OldObjectPath); public: static FFlowGraphSchemaRefresh OnNodeListChanged; - static UBlueprint* GetPlaceableNodeBlueprint(const FAssetData& AssetData); + static UBlueprint* GetPlaceableNodeOrAddOnBlueprint(const FAssetData& AssetData); - static const UFlowAsset* GetAssetClassDefaults(const UEdGraph* Graph); + static const UFlowAsset* GetEditedAssetOrClassDefault(const UEdGraph* Graph); + +private: + /* ID for checking dirty status of node titles against. */ + static int32 CurrentCacheRefreshID; }; diff --git a/Source/FlowEditor/Public/Graph/FlowGraphSchema_Actions.h b/Source/FlowEditor/Public/Graph/FlowGraphSchema_Actions.h index d882bd41d..d3d106fa7 100644 --- a/Source/FlowEditor/Public/Graph/FlowGraphSchema_Actions.h +++ b/Source/FlowEditor/Public/Graph/FlowGraphSchema_Actions.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "EdGraph/EdGraphSchema.h" @@ -8,14 +7,18 @@ #include "Nodes/FlowNode.h" #include "FlowGraphSchema_Actions.generated.h" -/** Action to add a node to the graph */ +class UFlowGraphSettings; + +/** + * Action to add a node to the graph. + */ USTRUCT() struct FLOWEDITOR_API FFlowGraphSchemaAction_NewNode : public FEdGraphSchemaAction { GENERATED_USTRUCT_BODY() UPROPERTY() - class UClass* NodeClass; + TObjectPtr NodeClass; static FName StaticGetTypeId() { @@ -26,31 +29,77 @@ struct FLOWEDITOR_API FFlowGraphSchemaAction_NewNode : public FEdGraphSchemaActi virtual FName GetTypeId() const override { return StaticGetTypeId(); } FFlowGraphSchemaAction_NewNode() - : FEdGraphSchemaAction() - , NodeClass(nullptr) + : NodeClass(nullptr) { } - FFlowGraphSchemaAction_NewNode(UClass* Node) - : FEdGraphSchemaAction() - , NodeClass(Node) + explicit FFlowGraphSchemaAction_NewNode(UClass* InNodeClass) + : NodeClass(InNodeClass) { } - FFlowGraphSchemaAction_NewNode(const UFlowNode* Node) - : FEdGraphSchemaAction(FText::FromString(Node->GetNodeCategory()), Node->GetNodeTitle(), Node->GetNodeToolTip(), 0, FText::FromString(Node->GetClass()->GetMetaData("Keywords"))) + explicit FFlowGraphSchemaAction_NewNode(const UFlowNodeBase* Node, const UFlowGraphSettings& GraphSettings) + : FEdGraphSchemaAction(GetNodeCategory(Node, GraphSettings), Node->GetNodeTitle(), Node->GetNodeToolTip(), 0, FText::FromString(Node->GetClass()->GetMetaData("Keywords"))) , NodeClass(Node->GetClass()) { } + // FEdGraphSchemaAction + virtual UEdGraphNode* PerformAction(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, const FVector2f& Location, bool bSelectNewNode = true) override; + // -- + + static UFlowGraphNode* CreateNode(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, const UClass* NodeClass, const FVector2f Location, const bool bSelectNewNode = true); + static UFlowGraphNode* RecreateNode(UEdGraph* ParentGraph, UEdGraphNode* OldInstance, UFlowNode* FlowNode); + static UFlowGraphNode* ImportNode(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, const UClass* NodeClass, const FGuid& NodeGuid, const FVector2D Location); + +private: + static FText GetNodeCategory(const UFlowNodeBase* Node, const UFlowGraphSettings& GraphSettings); +}; + +/** + * Action to add a subnode to the selected node. + */ +USTRUCT() +struct FLOWEDITOR_API FFlowSchemaAction_NewSubNode : public FEdGraphSchemaAction +{ + GENERATED_USTRUCT_BODY(); + + /* Template of node we want to create. */ + UPROPERTY() + TObjectPtr NodeTemplate; + + /* Parent node. */ + UPROPERTY() + TObjectPtr ParentNode; + + FFlowSchemaAction_NewSubNode() + : FEdGraphSchemaAction() + , NodeTemplate(nullptr) + , ParentNode(nullptr) + { + } + + FFlowSchemaAction_NewSubNode(FText InNodeCategory, FText InMenuDesc, FText InToolTip, const int32 InGrouping) + : FEdGraphSchemaAction(MoveTemp(InNodeCategory), MoveTemp(InMenuDesc), MoveTemp(InToolTip), InGrouping) + , NodeTemplate(nullptr) + , ParentNode(nullptr) + { + } + // FEdGraphSchemaAction virtual UEdGraphNode* PerformAction(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, const FVector2D Location, bool bSelectNewNode = true) override; + virtual UEdGraphNode* PerformAction(class UEdGraph* ParentGraph, TArray& FromPins, const FVector2D Location, bool bSelectNewNode = true) override; + virtual void AddReferencedObjects(FReferenceCollector& Collector) override; // -- - static UFlowGraphNode* CreateNode(class UEdGraph* ParentGraph, UEdGraphPin* FromPin, UClass* NodeClass, const FVector2D Location, const bool bSelectNewNode = true); + static UFlowGraphNode* RecreateNode(UEdGraph* ParentGraph, UEdGraphNode* OldInstance, UFlowGraphNode* ParentFlowGraphNode, UFlowNodeAddOn* FlowNodeAddOn); + + static TSharedPtr AddNewSubNodeAction(FGraphActionListBuilderBase& ContextMenuBuilder, const FText& Category, const FText& MenuDesc, const FText& Tooltip); }; -/** Action to paste clipboard contents into the graph */ +/** + * Action to paste clipboard contents into the graph. + */ USTRUCT() struct FLOWEDITOR_API FFlowGraphSchemaAction_Paste : public FEdGraphSchemaAction { @@ -71,13 +120,14 @@ struct FLOWEDITOR_API FFlowGraphSchemaAction_Paste : public FEdGraphSchemaAction // -- }; -/** Action to create new comment */ +/** + * Action to create new comment. + */ USTRUCT() struct FLOWEDITOR_API FFlowGraphSchemaAction_NewComment : public FEdGraphSchemaAction { GENERATED_USTRUCT_BODY() - // Simple type info static FName StaticGetTypeId() { static FName Type("FFlowGraphSchemaAction_NewComment"); diff --git a/Source/FlowEditor/Public/Graph/FlowGraphSettings.h b/Source/FlowEditor/Public/Graph/FlowGraphSettings.h index b82092d3c..1c2642840 100644 --- a/Source/FlowEditor/Public/Graph/FlowGraphSettings.h +++ b/Source/FlowEditor/Public/Graph/FlowGraphSettings.h @@ -1,59 +1,137 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "FlowGraphConnectionDrawingPolicy.h" #include "Engine/DeveloperSettings.h" +#include "GameplayTagContainer.h" #include "FlowTypes.h" +#include "Graph/FlowGraphNodesPolicy.h" #include "FlowGraphSettings.generated.h" +class UFlowNodeBase; + +USTRUCT() +struct FFlowNodeDisplayStyleConfig +{ + GENERATED_BODY() + +public: + FFlowNodeDisplayStyleConfig() + : TitleColor(FLinearColor::White) + { + } + + FFlowNodeDisplayStyleConfig(const FGameplayTag& InTag, const FLinearColor& InNodeColor) + : Tag(InTag) + , TitleColor(InNodeColor) + { + } + + FORCEINLINE bool operator ==(const FFlowNodeDisplayStyleConfig& Other) const + { + return Tag == Other.Tag; + } + + FORCEINLINE bool operator !=(const FFlowNodeDisplayStyleConfig& Other) const + { + return Tag != Other.Tag; + } + + FORCEINLINE bool operator <(const FFlowNodeDisplayStyleConfig& Other) const + { + return Tag < Other.Tag; + } + +public: + UPROPERTY(Config, EditAnywhere, Category = "Nodes", meta = (Categories = "Flow.NodeStyle")) + FGameplayTag Tag; + + UPROPERTY(Config, EditAnywhere, Category = "Nodes") + FLinearColor TitleColor; +}; + /** - * + * Editor-only graph settings. */ UCLASS(Config = Editor, defaultconfig, meta = (DisplayName = "Flow Graph")) -class UFlowGraphSettings final : public UDeveloperSettings +class FLOWEDITOR_API UFlowGraphSettings : public UDeveloperSettings { GENERATED_UCLASS_BODY() - static UFlowGraphSettings* Get() { return StaticClass()->GetDefaultObject(); } + virtual void PostInitProperties() override; - /** Show Flow Asset in Flow category of "Create Asset" menu? - * Requires restart after making a change. */ - UPROPERTY(EditAnywhere, config, Category = "Default UI") +#if WITH_EDITOR + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; +#endif + + /* Show Flow Asset in Flow category of "Create Asset" menu? + * Requires restart after making a change. */ + UPROPERTY(EditAnywhere, config, Category = "Default UI", meta = (ConfigRestartRequired = true)) bool bExposeFlowAssetCreation; - /** Show Flow Node blueprint in Flow category of "Create Asset" menu? - * Requires restart after making a change. */ - UPROPERTY(EditAnywhere, config, Category = "Default UI") + /* Show Flow Node blueprint in Flow category of "Create Asset" menu? + * Requires restart after making a change. */ + UPROPERTY(EditAnywhere, config, Category = "Default UI", meta = (ConfigRestartRequired = true)) bool bExposeFlowNodeCreation; - - /** Show Flow Asset toolbar? - * Requires restart after making a change. */ - UPROPERTY(EditAnywhere, config, Category = "Default UI") + + /* Show Flow Asset toolbar? + * Requires restart after making a change. */ + UPROPERTY(EditAnywhere, config, Category = "Default UI", meta = (ConfigRestartRequired = true)) bool bShowAssetToolbarAboveLevelEditor; - UPROPERTY(EditAnywhere, config, Category = "Default UI") + UPROPERTY(EditAnywhere, config, Category = "Default UI", meta = (ConfigRestartRequired = true)) FText FlowAssetCategoryName; - /** Flow Asset class allowed to be assigned via Level Editor toolbar*/ + /* Use this class to create new assets. Class picker will show up if None. */ + UPROPERTY(EditAnywhere, config, Category = "Default UI") + TSoftClassPtr DefaultFlowAssetClass; + + /* Flow Asset class allowed to be assigned via Level Editor toolbar. */ UPROPERTY(EditAnywhere, config, Category = "Default UI", meta = (EditCondition = "bShowAssetToolbarAboveLevelEditor")) - TSubclassOf WorldAssetClass; - - /** Hide specific nodes from the Flow Palette without changing the source code. - * Requires restart after making a change. */ + TSoftClassPtr WorldAssetClass; + + /* Hide specific nodes from the Flow Palette without changing the source code. + * Requires restart after making a change. */ + UPROPERTY(EditAnywhere, config, Category = "Nodes", meta = (ConfigRestartRequired = true)) + TArray> NodesHiddenFromPalette; + + /* Configurable map of FlowAsset subclasses to the FlowAssetNodePolicy for that subclass. */ + UPROPERTY(EditAnywhere, Config, Category = "Nodes", meta = (ConfigRestartRequired = true, AllowedClasses = "/Script/Flow.FlowAsset")) + TMap PerAssetSubclassFlowNodePolicies; + + /* Allows anyone to override Flow Palette category for specific nodes without modifying source code. */ UPROPERTY(EditAnywhere, config, Category = "Nodes") - TArray> NodesHiddenFromPalette; + TMap, FString> OverridenNodeCategories; - /** Hide default pin names on simple nodes, reduces UI clutter */ + /* Hide default pin names on simple nodes, reduces UI clutter. */ UPROPERTY(EditAnywhere, config, Category = "Nodes") bool bShowDefaultPinNames; + /* List of prefixes to hide on node titles and palette without need to add custom DisplayName. + * If node class has meta = (DisplayName = ... ) or BlueprintDisplayName, those texts will be displayed. */ UPROPERTY(EditAnywhere, config, Category = "Nodes") + TArray NodePrefixesToRemove; + + /* Display Styles for nodes, keyed by Gameplay Tag. */ + UPROPERTY(EditAnywhere, config, Category = "Nodes", meta = (TitleProperty = "{Tag}")) + TArray NodeDisplayStyles; + +#if WITH_EDITORONLY_DATA + /* Tags in the NodeDisplayStylesMap, used to detect when the map needs updating. */ + UPROPERTY(Transient) + FGameplayTagContainer NodeDisplayStylesAuthoredTags; + + /* Cached map of the data in NodeDisplayStyles for GameplayTag-keyed lookup. */ + UPROPERTY(Transient) + TMap NodeDisplayStylesMap; +#endif + + UPROPERTY(EditAnywhere, config, Category = "Nodes", meta = (Deprecated)) TMap NodeTitleColors; UPROPERTY(Config, EditAnywhere, Category = "Nodes") - TMap, FLinearColor> NodeSpecificColors; + TMap, FLinearColor> NodeSpecificColors; UPROPERTY(EditAnywhere, config, Category = "Nodes") FLinearColor ExecPinColorModifier; @@ -75,7 +153,7 @@ class UFlowGraphSettings final : public UDeveloperSettings UPROPERTY(config, EditAnywhere, Category = "Wires", meta = (EditCondition = "ConnectionDrawType == EFlowConnectionDrawType::Circuit")) FVector2D CircuitConnectionSpacing; - + UPROPERTY(EditAnywhere, config, Category = "Wires") FLinearColor InactiveWireColor; @@ -85,7 +163,7 @@ class UFlowGraphSettings final : public UDeveloperSettings UPROPERTY(EditAnywhere, config, Category = "Wires", meta = (ClampMin = 1.0f)) float RecentWireDuration; - /** The color to display execution wires that were just executed */ + /* The color to display execution wires that were just executed. */ UPROPERTY(EditAnywhere, config, Category = "Wires") FLinearColor RecentWireColor; @@ -103,4 +181,17 @@ class UFlowGraphSettings final : public UDeveloperSettings UPROPERTY(EditAnywhere, config, Category = "Wires", meta = (ClampMin = 0.0f)) float SelectedWireThickness; + +public: + virtual FName GetCategoryName() const override { return FName("Flow Graph"); } + virtual FText GetSectionText() const override { return INVTEXT("Graph Settings"); } + + /* Override-safe category query for Flow Node. */ + static FString GetNodeCategoryForNode(const UFlowNodeBase& FlowNodeBase); + +#if WITH_EDITOR + const TMap& EnsureNodeDisplayStylesMap(); + void TryAddDefaultNodeDisplayStyle(const FFlowNodeDisplayStyleConfig& StyleConfig); + const FLinearColor* LookupNodeTitleColorForNode(const UFlowNodeBase& FlowNodeBase); +#endif }; diff --git a/Source/FlowEditor/Public/Graph/FlowGraphUtils.h b/Source/FlowEditor/Public/Graph/FlowGraphUtils.h index c9d8f7f8d..b643d41d0 100644 --- a/Source/FlowEditor/Public/Graph/FlowGraphUtils.h +++ b/Source/FlowEditor/Public/Graph/FlowGraphUtils.h @@ -1,15 +1,22 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "CoreMinimal.h" +#include "Templates/SharedPointer.h" +class UFlowAsset; class FFlowAssetEditor; +class SFlowGraphEditor; +class UEdGraph; class FLOWEDITOR_API FFlowGraphUtils { public: FFlowGraphUtils() {} - static TSharedPtr GetFlowAssetEditor(const UObject* ObjectToFocusOn); + static TSharedPtr GetFlowAssetEditor(const UEdGraph* Graph); + static TSharedPtr GetFlowAssetEditor(const UFlowAsset* FlowAsset); + static TSharedPtr GetFlowGraphEditor(const UEdGraph* Graph); + + static FString RemovePrefixFromNodeText(const FText& Source); }; diff --git a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode.h b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode.h index 45a37adbb..484c71128 100644 --- a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode.h +++ b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode.h @@ -1,9 +1,9 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "EdGraph/EdGraphNode.h" #include "EdGraph/EdGraphPin.h" +#include "SearchSerializer.h" #include "Templates/SubclassOf.h" #include "FlowTypes.h" @@ -11,42 +11,16 @@ #include "FlowGraphNode.generated.h" class UEdGraphSchema; - +class UFlowGraph; +class UFlowNodeBase; class UFlowNode; +class UFlowAsset; +class FFlowMessageLog; -USTRUCT() -struct FFlowBreakpoint -{ - GENERATED_USTRUCT_BODY() - - UPROPERTY() - bool bHasBreakpoint; - - bool bBreakpointEnabled; - bool bBreakpointHit; - - FFlowBreakpoint() - { - bHasBreakpoint = false; - bBreakpointEnabled = false; - bBreakpointHit = false; - }; - - void AddBreakpoint(); - void RemoveBreakpoint(); - bool HasBreakpoint() const; - - void EnableBreakpoint(); - bool CanEnableBreakpoint() const; - - void DisableBreakpoint(); - bool IsBreakpointEnabled() const; - - void ToggleBreakpoint(); -}; +DECLARE_DELEGATE(FFlowGraphNodeEvent); /** - * Graph representation of the Flow Node + * Graph representation of the Flow Node. */ UCLASS() class FLOWEDITOR_API UFlowGraphNode : public UEdGraphNode @@ -56,22 +30,27 @@ class FLOWEDITOR_API UFlowGraphNode : public UEdGraphNode ////////////////////////////////////////////////////////////////////////// // Flow node -private: +protected: + /* The FlowNode or FlowNodeAddOn runtime instance that is being edited by this UFlowGraphNode. */ UPROPERTY(Instanced) - UFlowNode* FlowNode; + TObjectPtr NodeInstance; bool bBlueprintCompilationPending; + bool bIsReconstructingNode; + bool bIsDestroyingNode; bool bNeedsFullReconstruction; static bool bFlowAssetsLoaded; public: - // It would be intuitive to assign a custom Graph Node class in Flow Node class - // However, we shouldn't assign class from editor module to runtime module class + /* It would be intuitive to assign a custom Graph Node class in Flow Node class. + * However, we shouldn't assign class from editor module to runtime module class. */ UPROPERTY() - TArray> AssignedNodeClasses; + TArray> AssignedNodeClasses; - void SetFlowNode(UFlowNode* InFlowNode); - UFlowNode* GetFlowNode() const; + void SetNodeTemplate(UFlowNodeBase* InNodeInstance); + const UFlowNodeBase* GetNodeTemplate() const; + + UFlowNodeBase* GetFlowNodeBase() const; // UObject virtual void PostLoad() override; @@ -82,25 +61,23 @@ class FLOWEDITOR_API UFlowGraphNode : public UEdGraphNode // UEdGraphNode virtual void PostPlacedNewNode() override; virtual void PrepareForCopying() override; + virtual void PostPasteNode() override; // -- void PostCopyNode(); private: void SubscribeToExternalChanges(); - - void OnBlueprintPreCompile(UBlueprint* Blueprint); - void OnBlueprintCompiled(); - void OnExternalChange(); +public: + virtual void OnGraphRefresh(); + virtual bool CanPlaceBreakpoints() const; + ////////////////////////////////////////////////////////////////////////// // Graph node public: - UPROPERTY() - FFlowBreakpoint NodeBreakpoint; - // UEdGraphNode virtual bool CanCreateUnderSpecifiedSchema(const UEdGraphSchema* Schema) const override; virtual void AutowireNewNode(UEdGraphPin* FromPin) override; @@ -115,6 +92,8 @@ class FLOWEDITOR_API UFlowGraphNode : public UEdGraphNode */ void InsertNewNode(UEdGraphPin* FromPin, UEdGraphPin* NewLinkPin, TSet& OutNodeList); + void MarkNeedsFullReconstruction() { bNeedsFullReconstruction = true; } + // UEdGraphNode virtual void ReconstructNode() override; virtual void AllocateDefaultPins() override; @@ -122,13 +101,14 @@ class FLOWEDITOR_API UFlowGraphNode : public UEdGraphNode // variants of K2Node methods void RewireOldPinsToNewPins(TArray& InOldPins); - void ReconstructSinglePin(UEdGraphPin* NewPin, UEdGraphPin* OldPin); + static void ReconstructSinglePin(UEdGraphPin* NewPin, UEdGraphPin* OldPin); // -- // UEdGraphNode virtual void GetNodeContextMenuActions(class UToolMenu* Menu, class UGraphNodeContextMenuContext* Context) const override; virtual bool CanUserDeleteNode() const override; virtual bool CanDuplicateNode() const override; + virtual bool CanPasteHere( const UEdGraph* TargetGraph ) const override; virtual TSharedPtr CreateVisualWidget() override; virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override; virtual FLinearColor GetNodeTitleColor() const override; @@ -137,33 +117,60 @@ class FLOWEDITOR_API UFlowGraphNode : public UEdGraphNode virtual FText GetTooltipText() const override; // -- + void CreateAttachAddOnSubMenu(UToolMenu* Menu, UEdGraph* Graph) const; + bool CanAcceptSubNodeAsChild(const UFlowGraphNode& OtherSubNode, const TSet& AllRootSubNodesToPaste, FString* OutReasonString = nullptr) const; + bool IsAncestorNode(const UFlowGraphNode& OtherNode) const; + +protected: + void RebuildPinArraysOnLoad(); + ////////////////////////////////////////////////////////////////////////// // Utils public: - // Short summary of node's content + /* Short summary of node's content. */ FString GetNodeDescription() const; - // Get flow node for the inspected asset instance + /* Get flow node for the inspected asset instance. */ UFlowNode* GetInspectedNodeInstance() const; - // Used for highlighting active nodes of the inspected asset instance + UFlowAsset* GetFlowAsset() const; + + /* Used for highlighting active nodes of the inspected asset instance. */ EFlowNodeState GetActivationState() const; - // Information displayed while node is active + /* Information displayed while node is active. */ FString GetStatusString() const; FLinearColor GetStatusBackgroundColor() const; - // Check this to display information while node is preloaded + /* Check this to display information while node is preloaded. */ bool IsContentPreloaded() const; bool CanFocusViewport() const; + /* Index properties that are not indexed by default. */ + virtual void AdditionalNodeIndexing(FSearchSerializer& Serializer) const {} + // UEdGraphNode virtual bool CanJumpToDefinition() const override; virtual void JumpToDefinition() const override; + virtual bool SupportsCommentBubble() const override; // -- + virtual void OnNodeDoubleClicked() const; + virtual void OnNodeDoubleClickedInPIE() const {} + + /* Check if node has any errors, used for assigning colors on graph. */ + virtual bool HasErrors() const; + + void ValidateGraphNode(FFlowMessageLog& MessageLog) const; + +protected: + bool CanReconstructNode() const; + + bool TryUpdateNodePins() const; + bool CheckGraphPinsMatchNodePins() const; + ////////////////////////////////////////////////////////////////////////// // Pins @@ -171,9 +178,6 @@ class FLOWEDITOR_API UFlowGraphNode : public UEdGraphNode TArray InputPins; TArray OutputPins; - UPROPERTY() - TMap PinBreakpoints; - void CreateInputPin(const FFlowPin& FlowPin, const int32 Index = INDEX_NONE); void CreateOutputPin(const FFlowPin& FlowPin, const int32 Index = INDEX_NONE); @@ -190,37 +194,126 @@ class FLOWEDITOR_API UFlowGraphNode : public UEdGraphNode void AddUserInput(); void AddUserOutput(); - // Add pin only on this instance of node, under default pins - void AddInstancePin(const EEdGraphPinDirection Direction, const FName& PinName); + /* Add pin only on this instance of node, under default pins. */ + void AddInstancePin(const EEdGraphPinDirection Direction, const uint8 NumberedPinsAmount); - // Call node and graph updates manually, if using bBatchRemoval + /* Call node and graph updates manually, if using bBatchRemoval. */ void RemoveInstancePin(UEdGraphPin* Pin); - // Create pins from the context asset, i.e. Sequencer events - void RefreshContextPins(const bool bReconstructNode); - +public: // UEdGraphNode virtual void GetPinHoverText(const UEdGraphPin& Pin, FString& HoverTextOut) const override; // -- + /* Returns true, if pins cannot be connected due to node's inner logic, put message for user in OutReason. */ + virtual bool IsConnectionDisallowed(const UEdGraphPin* MyPin, const UEdGraphPin* OtherPin, FString& OutReason) const { return false; } + ////////////////////////////////////////////////////////////////////////// -// Breakpoints +// Execution Override public: - void OnInputTriggered(const int32 Index); - void OnOutputTriggered(const int32 Index); + FFlowGraphNodeEvent OnSignalModeChanged; + FFlowGraphNodeEvent OnReconstructNodeCompleted; + + /* Pin activation forced by user during PIE. */ + virtual void ForcePinActivation(const FEdGraphPinReference PinReference) const; -private: - void TryPausingSession(bool bPauseSession); + /* Pass-through forced by designer, set per node instance. */ + virtual void SetSignalMode(const EFlowSignalMode Mode); - void OnResumePIE(const bool bIsSimulating); - void OnEndPIE(const bool bIsSimulating); - void ResetBreakpoints(); + virtual EFlowSignalMode GetSignalMode() const; + virtual bool CanSetSignalMode(const EFlowSignalMode Mode) const; ////////////////////////////////////////////////////////////////////////// -// Execution Override +// SubNode Support + + // UEdGraphNode + UFlowGraph* GetFlowGraph() const; + virtual void DestroyNode() override; + virtual void NodeConnectionListChanged() override; + virtual void FindDiffs(class UEdGraphNode* OtherNode, struct FDiffResults& Results) override; + virtual FString GetPropertyNameAndValueForDiff(const FProperty* Prop, const uint8* PropertyAddr) const override; + // -- + + void SetParentNodeForSubNode(UFlowGraphNode* InParentNode); + UFlowGraphNode* GetParentNode() const { return ParentNode; } + + /** Returns the top-most ancestor (root) node in the subnode tree (could be this). */ + UFlowGraphNode* GetRootFlowGraphNode() const; + + /** + * Request a reconstruction on the owning root FlowNode. + * Needed for nested AddOn trees (AddOn inside AddOn) where removing/reparenting a subnode + * should cause the root FlowNode to refresh its context/data pins and update visuals. + */ + void RequestReconstructOnRootFlowNode() const; + + void RebuildRuntimeAddOnsFromEditorSubNodes(bool bForceReconstructNode = true); + + static void DiffSubNodes(const FText& NodeTypeDisplayName, const TArray& LhsSubNodes, const TArray& RhsSubNodes, FDiffResults& Results); + + // UObject +#if WITH_EDITOR + virtual void PostEditUndo() override; +#endif + // -- + + virtual UEdGraphPin* GetInputPin(int32 InputIndex = 0) const; + virtual UEdGraphPin* GetOutputPin(int32 InputIndex = 0) const; + virtual UEdGraph* GetBoundGraph() const { return nullptr; } + + virtual FText GetDescription() const; + + void AddSubNode(UFlowGraphNode* SubNode, class UEdGraph* ParentGraph); + void RemoveSubNode(UFlowGraphNode* SubNode); + virtual void RemoveAllSubNodes(); + virtual void OnSubNodeRemoved(UFlowGraphNode* SubNode); + virtual void OnSubNodeAdded(UFlowGraphNode* SubNode); + + virtual int32 FindSubNodeDropIndex(UFlowGraphNode* SubNode) const; + virtual void InsertSubNodeAt(UFlowGraphNode* SubNode, const int32 DropIndex); + + virtual bool IsSubNode() const; + + virtual void InitializeInstance(); + virtual bool RefreshNodeClass(); + virtual void UpdateNodeClassData(); + + /* Check if node instance uses blueprint for its implementation. */ + bool UsesBlueprint() const; + +protected: + virtual void ResetNodeOwner(); + + void LogError(const FString& MessageToLog, const UFlowNodeBase* FlowNodeBase) const; public: - // Pin activation forced by user during PIE - void ForcePinActivation(const FEdGraphPinReference PinReference) const; -}; + UPROPERTY() + TSoftClassPtr NodeInstanceClass; + + /* SubNodes that are owned by this UFlowGraphNode. */ + UPROPERTY() + TArray> SubNodes; + + /* Subnode's parent index assigned during copy operation to connect nodes again on paste. */ + UPROPERTY() + int32 CopySubNodeParentIndex = INDEX_NONE; + + /* Subnode index assigned during copy operation to connect nodes again on paste. */ + UPROPERTY() + int32 CopySubNodeIndex = INDEX_NONE; + + /* If set, this node will always be considered as subnode. */ + UPROPERTY() + bool bIsSubNode = false; + + UPROPERTY() + FString ErrorMessage; + +private: + /* Parent UFlowGraphNode for this node + * Note: this is not saved, and is restored in when the graph is opened in the editor via + * UFlowGraph::RecursivelySetParentNodeForAllSubNodes. */ + UPROPERTY(Transient) + TObjectPtr ParentNode; +}; \ No newline at end of file diff --git a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Branch.h b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Branch.h new file mode 100644 index 000000000..48d441a76 --- /dev/null +++ b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Branch.h @@ -0,0 +1,15 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Graph/Nodes/FlowGraphNode.h" +#include "FlowGraphNode_Branch.generated.h" + +UCLASS() +class FLOWEDITOR_API UFlowGraphNode_Branch : public UFlowGraphNode +{ + GENERATED_UCLASS_BODY() + + // UEdGraphNode + virtual FSlateIcon GetIconAndTint(FLinearColor& OutColor) const override; + // -- +}; diff --git a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_ExecutionSequence.h b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_ExecutionSequence.h index d7a4e1b9f..8a523a82f 100644 --- a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_ExecutionSequence.h +++ b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_ExecutionSequence.h @@ -1,12 +1,11 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Graph/Nodes/FlowGraphNode.h" #include "FlowGraphNode_ExecutionSequence.generated.h" UCLASS() -class UFlowGraphNode_ExecutionSequence final : public UFlowGraphNode +class FLOWEDITOR_API UFlowGraphNode_ExecutionSequence : public UFlowGraphNode { GENERATED_UCLASS_BODY() diff --git a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Finish.h b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Finish.h index c8e2ac632..9fafb852d 100644 --- a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Finish.h +++ b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Finish.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Graph/Nodes/FlowGraphNode.h" diff --git a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Reroute.h b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Reroute.h index 41364c825..45070acdf 100644 --- a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Reroute.h +++ b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Reroute.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Graph/Nodes/FlowGraphNode.h" @@ -14,4 +13,20 @@ class FLOWEDITOR_API UFlowGraphNode_Reroute : public UFlowGraphNode virtual TSharedPtr CreateVisualWidget() override; virtual bool ShouldDrawNodeAsControlPointOnly(int32& OutInputPinIndex, int32& OutOutputPinIndex) const override; // -- -}; + + virtual bool CanPlaceBreakpoints() const override; + + void ConfigureRerouteNodeFromPinConnections(UEdGraphPin& InPin, UEdGraphPin &OutPin); + + virtual void NodeConnectionListChanged() override; + + /* Re-type this reroute based on a pin it is connected to (or is being connected to). + * This is the single place that should: + * - update reroute graph pin types + * - update reroute template (UFlowNode_Reroute) pin types + * - refresh visuals without forcing a reconstruct storm */ + void ApplyTypeFromConnectedPin(const UEdGraphPin& OtherPin); + +private: + void ReconfigureFromConnections(); +}; \ No newline at end of file diff --git a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Start.h b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Start.h index c69e23433..9f929c8be 100644 --- a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Start.h +++ b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_Start.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Graph/Nodes/FlowGraphNode.h" diff --git a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_SubGraph.h b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_SubGraph.h index ae9f1c682..d39744d3f 100644 --- a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_SubGraph.h +++ b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode_SubGraph.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Graph/Nodes/FlowGraphNode.h" @@ -13,4 +12,8 @@ class FLOWEDITOR_API UFlowGraphNode_SubGraph : public UFlowGraphNode // UEdGraphNode virtual TSharedPtr CreateVisualWidget() override; // -- + + // UFlowGraphNode + virtual void OnNodeDoubleClickedInPIE() const override; + // -- }; diff --git a/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode.h b/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode.h index a4fceb3ed..160843947 100644 --- a/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode.h +++ b/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "SGraphNode.h" @@ -7,7 +6,9 @@ #include "Graph/Nodes/FlowGraphNode.h" -class SFlowGraphPinExec final : public SGraphPinExec +class UFlowDebuggerSubsystem; + +class FLOWEDITOR_API SFlowGraphPinExec : public SGraphPinExec { public: SFlowGraphPinExec(); @@ -18,7 +19,7 @@ class SFlowGraphPinExec final : public SGraphPinExec void Construct(const FArguments& InArgs, UEdGraphPin* InPin); }; -class SFlowGraphNode : public SGraphNode +class FLOWEDITOR_API SFlowGraphNode : public SGraphNode { public: SLATE_BEGIN_ARGS(SFlowGraphNode) {} @@ -26,34 +27,116 @@ class SFlowGraphNode : public SGraphNode void Construct(const FArguments& InArgs, UFlowGraphNode* InNode); + virtual ~SFlowGraphNode() override; + protected: // SNodePanel::SNode virtual void GetNodeInfoPopups(FNodeInfoContext* Context, TArray& Popups) const override; virtual const FSlateBrush* GetShadowBrush(bool bSelected) const override; - virtual void GetOverlayBrushes(bool bSelected, const FVector2D WidgetSize, TArray& Brushes) const override; + virtual void GetOverlayBrushes(bool bSelected, const FVector2f& WidgetSize, TArray& Brushes) const override; // -- - virtual void GetPinBrush(const bool bLeftSide, const float WidgetWidth, const int32 PinIndex, const FFlowBreakpoint& Breakpoint, TArray& Brushes) const; - + const FSlateBrush* GetSlateBrush(const FName BrushName, const FName StyleSetName) const; + // SGraphNode + virtual void GetPinBrush(const bool bLeftSide, const float WidgetWidth, const int32 PinIndex, const struct FFlowBreakpoint* Breakpoint, TArray& Brushes) const; + + virtual FText GetTitle() const; + virtual FText GetDescription() const; + virtual EVisibility GetDescriptionVisibility() const; + + virtual FText GetPreviewCornerText() const; + virtual const FSlateBrush* GetNameIcon() const; + + virtual FSlateColor GetBorderBackgroundColor() const; + virtual FSlateColor GetConfigBoxBackgroundColor() const; + + virtual void AddSubNode(TSharedPtr SubNodeWidget); + // -- + + // SGraphNode Interface + virtual TSharedPtr GetComplexTooltip() override; + virtual void OnDragEnter(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) override; + virtual FReply OnDragOver(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) override; + virtual FReply OnDrop(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) override; + + virtual void OnDragLeave(const FDragDropEvent& DragDropEvent) override; + virtual FReply OnMouseMove(const FGeometry& SenderGeometry, const FPointerEvent& MouseEvent) override; + virtual TSharedRef GetNodeUnderMouse(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + virtual void SetOwner(const TSharedRef& OwnerPanel) override; + virtual void AddPin(const TSharedRef& PinToAdd) override; + virtual void UpdateGraphNode() override; virtual void UpdateErrorInfo() override; + + virtual TSharedRef CreateTitleWidget(TSharedPtr NodeTitle) override; virtual TSharedRef CreateNodeContentArea() override; + virtual void CreateBelowPinControls(TSharedPtr MainBox) override; virtual const FSlateBrush* GetNodeBodyBrush() const override; - virtual void CreateStandardPinWidget(UEdGraphPin* Pin) override; - virtual TSharedPtr GetComplexTooltip() override; - virtual void CreateInputSideAddButton(TSharedPtr OutputBox) override; virtual void CreateOutputSideAddButton(TSharedPtr OutputBox) override; // -- - - // Variant of SGraphNode::AddPinButtonContent + + // SWidget + virtual FReply OnMouseButtonDown(const FGeometry& SenderGeometry, const FPointerEvent& MouseEvent) override; + // -- + + FSlateColor GetNodeTitleColor() const; + FSlateColor GetNodeBodyColor() const; + FSlateColor GetNodeTitleIconColor() const; + FLinearColor GetNodeTitleTextColor() const; + TSharedPtr GetEnabledStateWidget() const; + + /* Variant of SGraphNode::AddPinButtonContent. */ virtual void AddPinButton(TSharedPtr OutputBox, TSharedRef ButtonContent, const EEdGraphPinDirection Direction, FString DocumentationExcerpt = FString(), TSharedPtr CustomTooltip = nullptr); - // Variant of SGraphNode::OnAddPin + /* Variant of SGraphNode::OnAddPin. */ virtual FReply OnAddFlowPin(const EEdGraphPinDirection Direction); protected: + /* Adds a sub node widget inside current node. */ + void AddSubNodeWidget(const TSharedPtr& NewSubNodeWidget); + + /* Removes dragged subnodes from the current node, + * bInOutReorderOperation reports if this is a simple "reorder" internally within the node or + * if one or more of the removed SubNodes will be removed from the node completely. */ + void RemoveDraggedSubNodes(const TArray< TSharedRef >& DraggedNodes, bool& bInOutReorderOperation) const; + + static bool ShouldDropDraggedNodesAsSubNodes(const TArray>& DraggedNodes, const UFlowGraphNode* DropTargetNode); + + /* Gets decorator or service node if one is found under mouse cursor. */ + TSharedPtr GetSubNodeUnderCursor(const FGeometry& WidgetGeometry, const FPointerEvent& MouseEvent); + + /* Gets drag over marker visibility. */ + EVisibility GetDragOverMarkerVisibility() const; + + /* Sets drag marker visible or collapsed on this node. */ + void SetDragMarker(bool bEnabled); + + FMargin ComputeSubNodeChildIndentPaddingMargin() const; + + void CreateConfigText(const TSharedPtr& MainBox); + FText GetNodeConfigText() const; + EVisibility GetNodeConfigTextVisibility() const; + + void CreateOrRebuildSubNodeBox(const TSharedPtr& MainBox); + + bool IsFlowGraphNodeSelected(UFlowGraphNode* Node) const; + +protected: + /* The graph node this slate widget is representing. */ UFlowGraphNode* FlowGraphNode = nullptr; + + /* Subsystem pointer cached to avoid retrieving it every frame. */ + TWeakObjectPtr DebuggerSubsystem; + + bool bDragMarkerVisible = false; + TArray> SubNodes; + TSharedPtr SubNodeBox; + TSharedPtr ConfigTextBlock; + +public: + static const FLinearColor UnselectedNodeTint; + static const FLinearColor ConfigBoxColor; }; diff --git a/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode_Finish.h b/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode_Finish.h index f68137550..3265d6336 100644 --- a/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode_Finish.h +++ b/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode_Finish.h @@ -1,9 +1,8 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Graph/Widgets/SFlowGraphNode.h" -class SFlowGraphNode_Finish : public SFlowGraphNode +class FLOWEDITOR_API SFlowGraphNode_Finish : public SFlowGraphNode { }; diff --git a/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode_Start.h b/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode_Start.h index 80c7946e5..cd7b47d64 100644 --- a/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode_Start.h +++ b/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode_Start.h @@ -1,9 +1,8 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Graph/Widgets/SFlowGraphNode.h" -class SFlowGraphNode_Start : public SFlowGraphNode +class FLOWEDITOR_API SFlowGraphNode_Start : public SFlowGraphNode { }; diff --git a/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode_SubGraph.h b/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode_SubGraph.h index a88348351..99bdf0ed8 100644 --- a/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode_SubGraph.h +++ b/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode_SubGraph.h @@ -1,10 +1,9 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Graph/Widgets/SFlowGraphNode.h" -class SFlowGraphNode_SubGraph : public SFlowGraphNode +class FLOWEDITOR_API SFlowGraphNode_SubGraph : public SFlowGraphNode { protected: // SGraphNode diff --git a/Source/FlowEditor/Public/Graph/Widgets/SFlowPalette.h b/Source/FlowEditor/Public/Graph/Widgets/SFlowPalette.h index d11873ef3..ac7d786f5 100644 --- a/Source/FlowEditor/Public/Graph/Widgets/SFlowPalette.h +++ b/Source/FlowEditor/Public/Graph/Widgets/SFlowPalette.h @@ -1,13 +1,14 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "SGraphPalette.h" class FFlowAssetEditor; -/** Widget displaying a single item */ -class SFlowPaletteItem : public SGraphPaletteItem +/** + * Widget displaying a single Palette item. + */ +class FLOWEDITOR_API SFlowPaletteItem : public SGraphPaletteItem { public: SLATE_BEGIN_ARGS(SFlowPaletteItem) {} @@ -20,8 +21,10 @@ class SFlowPaletteItem : public SGraphPaletteItem virtual FText GetItemTooltip() const override; }; -/** Flow Palette */ -class SFlowPalette : public SGraphPalette +/** + * Flow-specific implementation of engine's Graph Palette, a list of nodes to place in the graph. + */ +class FLOWEDITOR_API SFlowPalette : public SGraphPalette { public: SLATE_BEGIN_ARGS(SFlowPalette) {} @@ -48,7 +51,7 @@ class SFlowPalette : public SGraphPalette void ClearGraphActionMenuSelection() const; protected: - TWeakPtr FlowAssetEditorPtr; + TWeakPtr FlowAssetEditor; TArray> CategoryNames; TSharedPtr CategoryComboBox; }; diff --git a/Source/FlowEditor/Public/Graph/Widgets/SGraphEditorActionMenuFlow.h b/Source/FlowEditor/Public/Graph/Widgets/SGraphEditorActionMenuFlow.h new file mode 100644 index 000000000..d47d16316 --- /dev/null +++ b/Source/FlowEditor/Public/Graph/Widgets/SGraphEditorActionMenuFlow.h @@ -0,0 +1,68 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "Containers/Array.h" +#include "CoreMinimal.h" +#include "EdGraph/EdGraphSchema.h" +#include "GraphEditor.h" +#include "HAL/PlatformMath.h" +#include "Math/Vector2D.h" +#include "Templates/SharedPointer.h" +#include "Types/SlateEnums.h" +#include "Widgets/DeclarativeSyntaxSupport.h" +#include "Widgets/Layout/SBorder.h" + +class SEditableTextBox; +class SGraphActionMenu; +class UEdGraphNode; +class UEdGraph; +class UEdGraphPin; +struct FEdGraphSchemaAction; +struct FGraphActionListBuilderBase; + +/** + * Adapted from SGraphEditorActionMenuAI, changing UAIGraphNode to UEdGraphNode, and using UFlowGraphSchema. + */ +class FLOWEDITOR_API SGraphEditorActionMenuFlow : public SBorder +{ +public: + SLATE_BEGIN_ARGS(SGraphEditorActionMenuFlow) + : _GraphObj(static_cast(nullptr)) + , _GraphNode(nullptr) + , _NewNodePosition(FVector2f::ZeroVector) + , _AutoExpandActionMenu(false) + , _SubNodeFlags(0) + { + } + + SLATE_ARGUMENT(UEdGraph*, GraphObj) + SLATE_ARGUMENT(UEdGraphNode*, GraphNode) + SLATE_ARGUMENT(FVector2f, NewNodePosition) + SLATE_ARGUMENT(TArray, DraggedFromPins) + SLATE_ARGUMENT(SGraphEditor::FActionMenuClosed, OnClosedCallback) + SLATE_ARGUMENT(bool, AutoExpandActionMenu) + SLATE_ARGUMENT(int32, SubNodeFlags) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + + virtual ~SGraphEditorActionMenuFlow() override; + + TSharedRef GetFilterTextBox(); + +protected: + UEdGraph* GraphObj = nullptr; + UEdGraphNode* GraphNode = nullptr; + TArray DraggedFromPins; + FVector2f NewNodePosition; + bool AutoExpandActionMenu = false; + int32 SubNodeFlags = 0; + + SGraphEditor::FActionMenuClosed OnClosedCallback; + TSharedPtr GraphActionMenu; + + void OnActionSelected(const TArray>& SelectedAction, ESelectInfo::Type InSelectionType); + + /* Callback used to populate all actions list in SGraphActionMenu. */ + void CollectAllActions(FGraphActionListBuilderBase& OutAllActions); +}; diff --git a/Source/FlowEditor/Private/MovieScene/FlowSection.h b/Source/FlowEditor/Public/MovieScene/FlowSection.h similarity index 60% rename from Source/FlowEditor/Private/MovieScene/FlowSection.h rename to Source/FlowEditor/Public/MovieScene/FlowSection.h index e1202d61e..f805b323f 100644 --- a/Source/FlowEditor/Private/MovieScene/FlowSection.h +++ b/Source/FlowEditor/Public/MovieScene/FlowSection.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "ISequencerSection.h" @@ -7,10 +6,10 @@ class FSequencerSectionPainter; -class FFlowSectionBase : public FSequencerSection +class FLOWEDITOR_API FFlowSectionBase : public FSequencerSection { public: - FFlowSectionBase(UMovieSceneSection& InSectionObject, TWeakPtr InSequencer) + FFlowSectionBase(UMovieSceneSection& InSectionObject, const TWeakPtr& InSequencer) : FSequencerSection(InSectionObject) , Sequencer(InSequencer) { @@ -26,10 +25,10 @@ class FFlowSectionBase : public FSequencerSection /** * An implementation of flow sections. */ -class FFlowSection final : public FFlowSectionBase +class FLOWEDITOR_API FFlowSection : public FFlowSectionBase { public: - FFlowSection(UMovieSceneSection& InSectionObject, TWeakPtr InSequencer) + FFlowSection(UMovieSceneSection& InSectionObject, const TWeakPtr& InSequencer) : FFlowSectionBase(InSectionObject, InSequencer) { } @@ -37,10 +36,10 @@ class FFlowSection final : public FFlowSectionBase virtual int32 OnPaintSection(FSequencerSectionPainter& Painter) const override; }; -class FFlowTriggerSection : public FFlowSectionBase +class FLOWEDITOR_API FFlowTriggerSection : public FFlowSectionBase { public: - FFlowTriggerSection(UMovieSceneSection& InSectionObject, TWeakPtr InSequencer) + FFlowTriggerSection(UMovieSceneSection& InSectionObject, const TWeakPtr& InSequencer) : FFlowSectionBase(InSectionObject, InSequencer) { } @@ -48,10 +47,10 @@ class FFlowTriggerSection : public FFlowSectionBase virtual int32 OnPaintSection(FSequencerSectionPainter& Painter) const override; }; -class FFlowRepeaterSection : public FFlowSectionBase +class FLOWEDITOR_API FFlowRepeaterSection : public FFlowSectionBase { public: - FFlowRepeaterSection(UMovieSceneSection& InSectionObject, TWeakPtr InSequencer) + FFlowRepeaterSection(UMovieSceneSection& InSectionObject, const TWeakPtr& InSequencer) : FFlowSectionBase(InSectionObject, InSequencer) { } diff --git a/Source/FlowEditor/Private/MovieScene/FlowTrackEditor.h b/Source/FlowEditor/Public/MovieScene/FlowTrackEditor.h similarity index 79% rename from Source/FlowEditor/Private/MovieScene/FlowTrackEditor.h rename to Source/FlowEditor/Public/MovieScene/FlowTrackEditor.h index 6e7fa7ed0..1724fa84e 100644 --- a/Source/FlowEditor/Private/MovieScene/FlowTrackEditor.h +++ b/Source/FlowEditor/Public/MovieScene/FlowTrackEditor.h @@ -1,9 +1,7 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once -#include "CoreMinimal.h" -#include "Sequencer/Public/ISequencer.h" +#include "Editor/Sequencer/Public/ISequencer.h" #include "MovieSceneTrack.h" #include "ISequencerSection.h" #include "ISequencerTrackEditor.h" @@ -14,7 +12,7 @@ class FMenuBuilder; /** * A property track editor for named events. */ -class FFlowTrackEditor final : public FMovieSceneTrackEditor +class FLOWEDITOR_API FFlowTrackEditor : public FMovieSceneTrackEditor { public: /** @@ -30,24 +28,25 @@ class FFlowTrackEditor final : public FMovieSceneTrackEditor * * @param InSequencer The sequencer instance to be used by this tool. */ - FFlowTrackEditor(TSharedRef InSequencer); - - // ISequencerTrackEditor interface + explicit FFlowTrackEditor(TSharedRef InSequencer); + // ISequencerTrackEditor virtual void BuildAddTrackMenu(FMenuBuilder& MenuBuilder) override; virtual bool SupportsType(TSubclassOf Type) const override; virtual bool SupportsSequence(UMovieSceneSequence* InSequence) const override; virtual const FSlateBrush* GetIconBrush() const override; virtual TSharedPtr BuildOutlinerEditWidget(const FGuid& ObjectBinding, UMovieSceneTrack* Track, const FBuildEditWidgetParams& Params) override; + // -- - //~ FPropertyTrackEditor interface + // FPropertyTrackEditor virtual TSharedRef MakeSectionInterface(UMovieSceneSection& SectionObject, UMovieSceneTrack& Track, FGuid ObjectBinding) override; + // -- private: void AddFlowSubMenu(FMenuBuilder& MenuBuilder); - /** Callback for executing the "Add Event Track" menu entry. */ - void HandleAddFlowTrackMenuEntryExecute(UClass* SectionType); + /* Callback for executing the "Add Event Track" menu entry. */ + void HandleAddFlowTrackMenuEntryExecute(UClass* SectionType) const; void CreateNewSection(UMovieSceneTrack* Track, int32 RowIndex, UClass* SectionType, bool bSelect) const; }; diff --git a/Source/FlowEditor/Public/Nodes/AssetTypeActions_FlowNodeAddOnBlueprint.h b/Source/FlowEditor/Public/Nodes/AssetTypeActions_FlowNodeAddOnBlueprint.h new file mode 100644 index 000000000..a22f4a8fd --- /dev/null +++ b/Source/FlowEditor/Public/Nodes/AssetTypeActions_FlowNodeAddOnBlueprint.h @@ -0,0 +1,20 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "AssetTypeActions/AssetTypeActions_Blueprint.h" + +class FLOWEDITOR_API FAssetTypeActions_FlowNodeAddOnBlueprint : public FAssetTypeActions_Blueprint +{ +public: + virtual FText GetName() const override; + virtual uint32 GetCategories() override; + virtual FColor GetTypeColor() const override { return FColor(255, 196, 128); } + + virtual UClass* GetSupportedClass() const override; + +protected: + // FAssetTypeActions_Blueprint + virtual bool CanCreateNewDerivedBlueprint() const override { return false; } + virtual UFactory* GetFactoryForBlueprintType(UBlueprint* InBlueprint) const override; + // -- +}; diff --git a/Source/FlowEditor/Public/Nodes/AssetTypeActions_FlowNodeBlueprint.h b/Source/FlowEditor/Public/Nodes/AssetTypeActions_FlowNodeBlueprint.h index 201124de5..a9b3a2316 100644 --- a/Source/FlowEditor/Public/Nodes/AssetTypeActions_FlowNodeBlueprint.h +++ b/Source/FlowEditor/Public/Nodes/AssetTypeActions_FlowNodeBlueprint.h @@ -1,10 +1,9 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "AssetTypeActions/AssetTypeActions_Blueprint.h" -class FAssetTypeActions_FlowNodeBlueprint final : public FAssetTypeActions_Blueprint +class FLOWEDITOR_API FAssetTypeActions_FlowNodeBlueprint : public FAssetTypeActions_Blueprint { public: virtual FText GetName() const override; diff --git a/Source/FlowEditor/Public/Nodes/FlowNodeBlueprintFactory.h b/Source/FlowEditor/Public/Nodes/FlowNodeBlueprintFactory.h index cec3d3a25..ba96b29fd 100644 --- a/Source/FlowEditor/Public/Nodes/FlowNodeBlueprintFactory.h +++ b/Source/FlowEditor/Public/Nodes/FlowNodeBlueprintFactory.h @@ -1,22 +1,46 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "Factories/Factory.h" #include "FlowNodeBlueprintFactory.generated.h" -UCLASS(hidecategories=Object, MinimalAPI) -class UFlowNodeBlueprintFactory : public UFactory +UCLASS(Abstract, hidecategories=Object) +class UFlowNodeBaseBlueprintFactory : public UFactory { GENERATED_UCLASS_BODY() - // The parent class of the created blueprint + /* The Default parent class of the created blueprint (set by subclasses). */ + UPROPERTY() + TSubclassOf DefaultParentClass; + + /* The parent class of the created blueprint. */ UPROPERTY(EditAnywhere, Category = "FlowNodeBlueprintFactory") - TSubclassOf ParentClass; + TSubclassOf ParentClass; // UFactory - virtual bool ConfigureProperties() override; - virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn, FName CallingContext) override; - virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; + FLOWEDITOR_API virtual bool ConfigureProperties() override; + FLOWEDITOR_API virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn, FName CallingContext) override; + FLOWEDITOR_API virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; + + void ShowCannotCreateBlueprintDialog(); // -- }; + +/** + * Specialization of UFlowNodeBaseBlueprintFactory for UFlowNode blueprints. + */ +UCLASS(hidecategories = Object) +class FLOWEDITOR_API UFlowNodeBlueprintFactory : public UFlowNodeBaseBlueprintFactory +{ + GENERATED_UCLASS_BODY() +}; + +/** + * Specialization of UFlowNodeBaseBlueprintFactory for UFlowNodeAddOn blueprints. + */ +UCLASS(hidecategories = Object) +class FLOWEDITOR_API UFlowNodeAddOnBlueprintFactory : public UFlowNodeBaseBlueprintFactory +{ + GENERATED_UCLASS_BODY() + +}; diff --git a/Source/FlowEditor/Public/Pins/SFlowInputPinHandle.h b/Source/FlowEditor/Public/Pins/SFlowInputPinHandle.h index 5355265bb..d42e75fdb 100644 --- a/Source/FlowEditor/Public/Pins/SFlowInputPinHandle.h +++ b/Source/FlowEditor/Public/Pins/SFlowInputPinHandle.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "EdGraphUtilities.h" diff --git a/Source/FlowEditor/Public/Pins/SFlowOutputPinHandle.h b/Source/FlowEditor/Public/Pins/SFlowOutputPinHandle.h index a291ac83c..0dfe78f05 100644 --- a/Source/FlowEditor/Public/Pins/SFlowOutputPinHandle.h +++ b/Source/FlowEditor/Public/Pins/SFlowOutputPinHandle.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "EdGraphUtilities.h" diff --git a/Source/FlowEditor/Public/Pins/SFlowPinHandle.h b/Source/FlowEditor/Public/Pins/SFlowPinHandle.h index 141ba3c1a..e82d9d143 100644 --- a/Source/FlowEditor/Public/Pins/SFlowPinHandle.h +++ b/Source/FlowEditor/Public/Pins/SFlowPinHandle.h @@ -1,5 +1,4 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once #include "SGraphPin.h" diff --git a/Source/FlowEditor/Public/UnrealExtensions/IFlowCuratedNamePropertyCustomization.h b/Source/FlowEditor/Public/UnrealExtensions/IFlowCuratedNamePropertyCustomization.h new file mode 100644 index 000000000..84da03609 --- /dev/null +++ b/Source/FlowEditor/Public/UnrealExtensions/IFlowCuratedNamePropertyCustomization.h @@ -0,0 +1,66 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "IFlowExtendedPropertyTypeCustomization.h" +#include "Widgets/Input/SComboBox.h" + +/** + * NOTE (gtaylor) This class is planned for submission to Epic to include in baseline UE. + * If/when that happens, we will want to remove this version and update to the latest one in the PropertyModule. + */ + +/** + * A base-class to do property Customization for a struct that presents a curated list of FNames for selection. + */ +class FLOWEDITOR_API IFlowCuratedNamePropertyCustomization : public IFlowExtendedPropertyTypeCustomization +{ +protected: + // IFlowExtendedPropertyTypeCustomization + virtual void CreateHeaderRowWidget(FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + // --- + + void Initialize(); + + /* Helper function to set the property to a specified value, and handle all the side effects. */ + bool TrySetCuratedNameWithSideEffects(const FName& NewName); + + // Callbacks for the TextListWidget (see CreateHeaderRowWidget) + FText GetCachedText() const; + static TSharedRef GenerateTextListWidget(const TSharedPtr InItem); + void OnTextListComboBoxOpening(); + void OnTextSelected(const TSharedPtr NewSelection, ESelectInfo::Type SelectInfo); + + void RepaintTextListWidget() const; + + TSharedPtr FindCachedOrCreateText(const FName& NewName); + void AddToCachedTextList(const TSharedPtr Text); + void InsertAtHeadOfCachedTextList(const TSharedPtr Text); + + bool CustomIsResetToDefaultVisible(TSharedPtr Property) const { return CustomIsResetToDefaultVisibleImpl(Property); } + void CustomResetToDefault(TSharedPtr Property) { CustomResetToDefaultImpl(Property); } + bool CustomIsEnabled() const { return CustomIsEnabledImpl(); } + + // IFlowCuratedNamePropertyCustomization + virtual TSharedPtr GetCuratedNamePropertyHandle() const = 0; + virtual void SetCuratedName(const FName& NewName) = 0; + virtual bool TryGetCuratedName(FName& OutName) const = 0; + virtual TArray GetCuratedNameOptions() const = 0; + virtual bool AllowNameNoneIfOtherOptionsExist() const { return true; } + virtual bool CustomIsResetToDefaultVisibleImpl(TSharedPtr Property) const; + virtual void CustomResetToDefaultImpl(TSharedPtr Property); + virtual bool CustomIsEnabledImpl() const; + // --- + +public: + TSharedPtr CachedNameHandle; + TSharedPtr CachedPropertyUtils; + + /* Cache FTexts for the ComboBox dropdown & current selected. */ + TArray> CachedTextList; + TSharedPtr CachedTextSelected; + + static TSharedPtr NoneAsText; + + /* Combo Box widget for displaying the curated list of Names */ + TSharedPtr>> TextListWidget; +}; diff --git a/Source/FlowEditor/Public/UnrealExtensions/IFlowExtendedPropertyTypeCustomization.h b/Source/FlowEditor/Public/UnrealExtensions/IFlowExtendedPropertyTypeCustomization.h new file mode 100644 index 000000000..4451656b9 --- /dev/null +++ b/Source/FlowEditor/Public/UnrealExtensions/IFlowExtendedPropertyTypeCustomization.h @@ -0,0 +1,74 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "IPropertyTypeCustomization.h" +#include "PropertyHandle.h" +#include "Templates/SharedPointer.h" + +class STextBlock; +class FDetailWidgetRow; +class IDetailChildrenBuilder; +class IPropertyTypeCustomizationUtils; +class IDetailPropertyRow; +class IPropertyHandle; + +/** + * NOTE (gtaylor) This class is planned for submission to Epic to include in baseline UE. + * If/when that happens, we will want to remove this version and update to the latest one in the PropertyModule. + */ + +/** + * An extension of IPropertyTypeCustomization which adds some quality-of-life improvements for subclasses. + */ +class FLOWEDITOR_API IFlowExtendedPropertyTypeCustomization : public IPropertyTypeCustomization +{ +public: + // IPropertyTypeCustomization + virtual void CustomizeHeader(TSharedRef InStructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; + + virtual void CustomizeChildren(TSharedRef InStructPropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override + { + CustomizeChildrenDefaultImpl(InStructPropertyHandle, StructBuilder, StructCustomizationUtils); + } + // --- + + static void CustomizeChildrenDefaultImpl(TSharedRef StructPropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils); + + template + static StructT* TryGetTypedStructValue(const TSharedPtr& StructPropertyHandle); + +protected: + void RefreshHeader() const; + + virtual void CreateHeaderRowWidget(FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils); + virtual FText BuildHeaderText() const; + + /* Callbacks for property editor delegates. */ + void OnAnyChildPropertyChanged() const; + +protected: + /* Cached struct property. */ + TSharedPtr StructPropertyHandle; + + /* Header property text block, (re-)built in RefreshHeader. */ + TSharedPtr HeaderTextBlock; +}; + +template +StructT* IFlowExtendedPropertyTypeCustomization::TryGetTypedStructValue(const TSharedPtr& StructPropertyHandle) +{ + if (StructPropertyHandle.IsValid()) + { + // Get the actual struct data from the handle and cast it to the correct type + TArray RawData; + StructPropertyHandle->AccessRawData(RawData); + + // (must be exactly one, multi-select is not supported for this feature) + if (RawData.Num() == 1) + { + return reinterpret_cast(RawData[0]); + } + } + + return nullptr; +} diff --git a/Source/FlowEditor/Public/UnrealExtensions/VisibilityArrayBuilder.h b/Source/FlowEditor/Public/UnrealExtensions/VisibilityArrayBuilder.h new file mode 100644 index 000000000..f73e536b1 --- /dev/null +++ b/Source/FlowEditor/Public/UnrealExtensions/VisibilityArrayBuilder.h @@ -0,0 +1,278 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "IDetailCustomNodeBuilder.h" +#include "IDetailChildrenBuilder.h" +#include "DetailWidgetRow.h" +#include "PropertyHandle.h" + +#include "Widgets/DeclarativeSyntaxSupport.h" +#include "Widgets/SBoxPanel.h" +#include "Widgets/Text/STextBlock.h" +#include "PropertyCustomizationHelpers.h" +#include "SResetToDefaultMenu.h" + +DECLARE_DELEGATE_FourParams(FOnGenerateArrayElementWidgetVisible, + TSharedRef, // ElementHandle + int32, // Index + IDetailChildrenBuilder&, // ChildrenBuilder + const TAttribute&); // RowVisibility + +/* +* FVisibilityArrayBuilder +* +* A fork of UE's FDetailArrayBuilder that: +* - Adds a live visibility getter via SetVisibilityGetter(TFunction) +* - Uses a 4‑parameter element generation delegate: +* (ElementHandle, Index, ChildrenBuilder, RowVisibility) +* - Works around engine behavior where setting NodeRow.Visibility() on a custom node +* header does not always live‑update by wrapping the header name & value widgets +* in SBoxes whose Visibility attributes are dynamic. +* +* Usage: +* TSharedRef ArrayBuilder = +* MakeShareable(new FVisibilityArrayBuilder(ValuesHandle.ToSharedRef(), true, true, true)); +* +* ArrayBuilder->SetVisibilityGetter([this]() +* { +* uint8 Raw = 0; +* if (MultiTypeHandle.IsValid() && +* MultiTypeHandle->GetValue(Raw) == FPropertyAccess::Success && +* (EFlowDataMultiType)Raw == EFlowDataMultiType::Array) +* { +* return EVisibility::Visible; +* } +* return EVisibility::Collapsed; +* }); +* +* ArrayBuilder->OnGenerateArrayElementWidget( +* FOnGenerateArrayElementWidgetVisible::CreateSP( +* this, &FFlowDataPinValueCustomization_Enum::GenerateArrayElementVisible)); +* +* StructBuilder.AddCustomBuilder(ArrayBuilder); +* +* Notes: +* - Structural changes (Add / Remove / Reorder) still require RequestRefresh() to rebuild rows. +* - The dynamic visibility lambda MUST read the property handle each time (no cached enum mode). +* - If you want to supply your own header (e.g. to insert custom buttons), construct with +* bInGenerateHeader = false +* and add a separate AddCustomRow() above the builder with its own .Visibility binding. +*/ + +class FVisibilityArrayBuilder : public IDetailCustomNodeBuilder +{ +public: + FVisibilityArrayBuilder(TSharedRef InBaseProperty, + bool bInGenerateHeader = true, + bool bInDisplayResetToDefault = true, + bool bInDisplayElementNum = true) + : ArrayProperty(InBaseProperty->AsArray()) + , BaseProperty(InBaseProperty) + , bGenerateHeader(bInGenerateHeader) + , bDisplayResetToDefault(bInDisplayResetToDefault) + , bDisplayElementNum(bInDisplayElementNum) + { + check(ArrayProperty.IsValid()); + + FSimpleDelegate OnNumChildrenChanged = + FSimpleDelegate::CreateRaw(this, &FVisibilityArrayBuilder::OnNumChildrenChanged); + OnNumElementsChangedHandle = ArrayProperty->SetOnNumElementsChanged(OnNumChildrenChanged); + + // Hide original property presentation so only our custom builder is shown. + BaseProperty->MarkHiddenByCustomization(); + } + + ~FVisibilityArrayBuilder() + { + if (ArrayProperty.IsValid()) + { + ArrayProperty->UnregisterOnNumElementsChanged(OnNumElementsChangedHandle); + } + } + + // Non-copyable / non-movable: avoid duplicate delegate registrations + FVisibilityArrayBuilder(const FVisibilityArrayBuilder&) = delete; + FVisibilityArrayBuilder& operator=(const FVisibilityArrayBuilder&) = delete; + FVisibilityArrayBuilder(FVisibilityArrayBuilder&&) = delete; + FVisibilityArrayBuilder& operator=(FVisibilityArrayBuilder&&) = delete; + + // Assign a visibility callback (evaluated whenever Slate queries the attribute). + FVisibilityArrayBuilder& SetVisibilityGetter(TFunction&& InGetter) + { + VisibilityGetter = MoveTemp(InGetter); + return *this; + } + + void SetDisplayName(const FText& InDisplayName) + { + DisplayName = InDisplayName; + } + + void OnGenerateArrayElementWidget(FOnGenerateArrayElementWidgetVisible InDelegate) + { + OnGenerateArrayElementWidgetDelegate = InDelegate; + } + + // IDetailCustomNodeBuilder interface + virtual bool RequiresTick() const override { return false; } + virtual void Tick(float /*DeltaTime*/) override {} + + virtual FName GetName() const override + { + return BaseProperty->GetProperty()->GetFName(); + } + + virtual bool InitiallyCollapsed() const override { return false; } + + virtual void GenerateHeaderRowContent(FDetailWidgetRow& NodeRow) override + { + if (!bGenerateHeader) + { + return; + } + + // Dynamic visibility attribute for header contents (NOT on NodeRow itself). + TAttribute DynamicVis = MakeDynamicVisibilityAttribute(); + + // Horizontal box for value content (mirrors stock array builder's layout). + TSharedPtr ContentHorizontalBox; + SAssignNew(ContentHorizontalBox, SHorizontalBox); + + if (bDisplayElementNum) + { + ContentHorizontalBox->AddSlot() + [ + BaseProperty->CreatePropertyValueWidget() + ]; + } + + FUIAction CopyAction; + FUIAction PasteAction; + BaseProperty->CreateDefaultPropertyCopyPasteActions(CopyAction, PasteAction); + + NodeRow + // Leave NodeRow itself always present; hide internal widgets via SBox visibility. + .FilterString(!DisplayName.IsEmpty() ? DisplayName : BaseProperty->GetPropertyDisplayName()) + .NameContent() + [ + SNew(SBox) + .Visibility(DynamicVis) + [ + BaseProperty->CreatePropertyNameWidget(DisplayName, FText::GetEmpty()) + ] + ] + .ValueContent() + [ + SNew(SBox) + .Visibility(DynamicVis) + [ + ContentHorizontalBox.ToSharedRef() + ] + ] + .CopyAction(CopyAction) + .PasteAction(PasteAction); + + if (bDisplayResetToDefault) + { + TSharedPtr ResetToDefaultMenu; + ContentHorizontalBox->AddSlot() + .AutoWidth() + .Padding(FMargin(2.f, 0.f, 0.f, 0.f)) + [ + SAssignNew(ResetToDefaultMenu, SResetToDefaultMenu) + ]; + ResetToDefaultMenu->AddProperty(BaseProperty); + } + } + + virtual void GenerateChildContent(IDetailChildrenBuilder& ChildrenBuilder) override + { + uint32 NumChildren = 0; + ArrayProperty->GetNumElements(NumChildren); + + TAttribute DynamicVis = MakeDynamicVisibilityAttribute(); + + for (uint32 ChildIndex = 0; ChildIndex < NumChildren; ++ChildIndex) + { + TSharedRef ElementHandle = ArrayProperty->GetElement(ChildIndex); + + if (OnGenerateArrayElementWidgetDelegate.IsBound()) + { + OnGenerateArrayElementWidgetDelegate.Execute( + ElementHandle, + static_cast(ChildIndex), + ChildrenBuilder, + DynamicVis); + } + else + { + IDetailPropertyRow& Row = ChildrenBuilder.AddProperty(ElementHandle); + Row.Visibility(DynamicVis); + } + } + } + + // Manual refresh (not virtual in some engine versions) + void RefreshChildren() + { + OnRebuildChildren.ExecuteIfBound(); + } + + virtual TSharedPtr GetPropertyHandle() const + { + return BaseProperty; + } + +protected: + virtual void SetOnRebuildChildren(FSimpleDelegate InOnRebuildChildren) override + { + OnRebuildChildren = InOnRebuildChildren; + } + + void OnNumChildrenChanged() + { + OnRebuildChildren.ExecuteIfBound(); + } + +private: + TAttribute MakeDynamicVisibilityAttribute() const + { + // Optimization: if no custom getter, return a simple constant attribute (no lambda capture) + if (!VisibilityGetter) + { + return TAttribute(EVisibility::Visible); + } + + // Capture 'this' only when needed + return TAttribute::CreateLambda([this]() + { + return VisibilityGetter(); + }); + } + +private: + // Display name override + FText DisplayName; + + // Element generator + FOnGenerateArrayElementWidgetVisible OnGenerateArrayElementWidgetDelegate; + + // Array + base property handles + TSharedPtr ArrayProperty; + TSharedRef BaseProperty; + + // Rebuild delegate + FSimpleDelegate OnRebuildChildren; + + // Visibility getter (live) + TFunction VisibilityGetter; + + // Config + bool bGenerateHeader; + bool bDisplayResetToDefault; + bool bDisplayElementNum; + + // Delegate handle for array size changes + FDelegateHandle OnNumElementsChangedHandle; +}; \ No newline at end of file diff --git a/Source/FlowEditor/Public/LevelEditor/SLevelEditorFlow.h b/Source/FlowEditor/Public/Utils/SLevelEditorFlow.h similarity index 64% rename from Source/FlowEditor/Public/LevelEditor/SLevelEditorFlow.h rename to Source/FlowEditor/Public/Utils/SLevelEditorFlow.h index 5d4be19b0..f609ff1c4 100644 --- a/Source/FlowEditor/Public/LevelEditor/SLevelEditorFlow.h +++ b/Source/FlowEditor/Public/Utils/SLevelEditorFlow.h @@ -1,14 +1,12 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors - #pragma once -#include "CoreMinimal.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/SCompoundWidget.h" struct FAssetData; -class SLevelEditorFlow : public SCompoundWidget +class FLOWEDITOR_API SLevelEditorFlow : public SCompoundWidget { public: SLATE_BEGIN_ARGS(SLevelEditorFlow) {} @@ -16,14 +14,17 @@ class SLevelEditorFlow : public SCompoundWidget void Construct(const FArguments& InArgs); -private: + ~SLevelEditorFlow(); + +protected: void OnMapOpened(const FString& Filename, bool bAsTemplate); void CreateFlowWidget(); + FString GetFlowAssetPath() const; void OnFlowChanged(const FAssetData& NewAsset); - FString GetFlowPath() const; - class UFlowComponent* FindFlowComponent() const; + static class UFlowComponent* FindFlowComponent(); - FName FlowPath; + FString FlowAssetPath; + FDelegateHandle OnMapOpenedHandle; }; diff --git a/docs/.gitattributes b/docs/.gitattributes new file mode 100644 index 000000000..d18bf85f0 --- /dev/null +++ b/docs/.gitattributes @@ -0,0 +1,2 @@ +# Enforce LF line endings for all text files +* text=auto eol=lf diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 000000000..0d010b157 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,4 @@ +_site/ +.jekyll-cache/ +.jekyll-metadata +Gemfile.lock diff --git a/docs/Features/AssetSearch.md b/docs/Features/AssetSearch.md new file mode 100644 index 000000000..54239e779 --- /dev/null +++ b/docs/Features/AssetSearch.md @@ -0,0 +1,22 @@ +--- +title: Asset Search +--- + +Feature based on the engine plugin added in UE 4.26. The plugin is marked as beta, probably because of the search performance. + +## Using the feature +- Make sure the `Asset Search` plugin is enabled in your project. +- Open the `Tools/Search` tab. +- If you never used this Search yet, check the status of "missing indexed assets" in the bottom right corner. Click it, and it gonna index all assets supported by the Asset Search. It can take many minutes, depending on your project size. +- Wait until indexing finishes and the status in the bottom left corner is `Ready` again. + +## Limiting Search by asset type +If you'd merge the provided engine modification, you can exclude specific asset types from Asset Search indexing. On the project level (via Project Settings) or user level (Editor Preferences). +* [Added Search Roles to Asset Search settings as a huge time-saver](https://github.com/EpicGames/UnrealEngine/pull/9332) + +Here's the list of asset types supported out of the box, hardcoded in the engine's code. (as of UE 5.0) +![image](https://user-images.githubusercontent.com/5065057/175774889-bf4b3ed4-ba6b-47e2-af17-10320e3da8c1.png) + +## Useful engine modifications +* [Jump from Asset Search result to the node in any graph editor!](https://github.com/EpicGames/UnrealEngine/pull/9882) - set the `ENABLE_JUMP_TO_INNER_OBJECT` value to 1 after integrating this change. +* [Asset Search: added option to run search on single asset, with Search Browser opened as asset editor tab](https://github.com/EpicGames/UnrealEngine/pull/9943) \ No newline at end of file diff --git a/docs/Features/ForcePinActivation.md b/docs/Features/ForcePinActivation.md new file mode 100644 index 000000000..4298ba415 --- /dev/null +++ b/docs/Features/ForcePinActivation.md @@ -0,0 +1,11 @@ +--- +title: Force Pin Activation +--- + +* It's a debugging feature available from Pin's context menu during PIE. +* Allows pushing the graph execution in case of blockers, i.e. specific node doesn't work for whatever reason and we want to continue playtesting. +* It works both on Input pins and Output pins. You can even trigger unconnected Input pins this way. + +Here's a link to [**a short video presenting how this works**](https://user-images.githubusercontent.com/5065057/198881114-1bdb5a2b-3d08-4e99-b5b4-ad1d226f1a8a.mp4). + +![firefox_bbitKtMkwt](https://user-images.githubusercontent.com/5065057/198881818-87c558c0-fe46-45a2-a540-9b1541cb9505.png) diff --git a/docs/Features/GenericGameplayTagEvents.md b/docs/Features/GenericGameplayTagEvents.md new file mode 100644 index 000000000..3a149f6b6 --- /dev/null +++ b/docs/Features/GenericGameplayTagEvents.md @@ -0,0 +1,65 @@ +--- +title: Generic Gameplay Tag Events +--- + +Nodes available in the Flow plugin support finding actors and communicating with them by Gameplay Tags. +- This can have many advantages over using soft references to actor instances, i.e. changing actor instance name won't break reference. +- This also allows communicating with actors spawned in runtime, i.e. all characters. +- You can call multiple actors identified by the same Gameplay Tag, which may be useful in many cases. + +Obviously, you can use soft references to actors in your project-specific Flow nodes. Although the Flow plugin comes with a mechanism to call events in a generic way, without the need to know the actor's class and functions. + +## Adding Flow Component to Actor + +* Add `Flow Component` to the actor. +* Assign a unique gameplay tag on the `Identity Tags` list. + +![image](https://user-images.githubusercontent.com/5065057/176913188-077dc82a-2c7d-4af1-ac30-42d7fba869d5.png) + +## Calling Actor events from the Flow Graph + +* Add the `Receive Notify` event from the Flow Component. + +![image](https://user-images.githubusercontent.com/5065057/176913685-180f9f6c-a17e-4097-8e8f-2984a89efaae.png) +* Place the `Notify Actor` node in your Flow Graph. + * `Identity Tags` should include the Identity Tag assigned in the Flow Component. + * `Notify Tag` is an optional tag that would be sent through the `Receive Notify` event to your actor. It allows you to call different events in the same actor. + +![image](https://user-images.githubusercontent.com/5065057/176920012-98c34c6c-fa7c-43b7-a5ce-be928eae24a3.png) + +## Calling Flow Graph event from the Actor + +* Call `Notify Graph` from the Flow Component. + +![image](https://user-images.githubusercontent.com/5065057/176914462-d91f43a3-722d-4c77-bc83-48ee69d0525b.png) +* Place the `On Notify From Actor` node in your Flow Graph. + * `Identity Tags` should include the Identity Tag assigned in the Flow Component. + * `Notify Tag` is optional. If added, it needs to match the tag selected on Notify Graph in the step above. Otherwise, the `Success` output won't be triggered. + * If `Notify Tag` will be empty, the Notify node won't check what tag has been sent from the actor. The `Success` output will always be triggered. + +![image](https://user-images.githubusercontent.com/5065057/176920480-9341ac92-b96d-448d-976a-5590e14ae20b.png) + +## Calling events between Actors + +* Source Actor: call `Notify Actor` from the Flow Component. + * `Notify Tag` is optional. It allows you to call different events in the target actor. + +![image](https://user-images.githubusercontent.com/5065057/176920761-d7f10c83-e11f-4955-85d5-3caf70f20c49.png) +* Target Actor: add the `Receive Notify` event from the Flow Component. + +![image](https://user-images.githubusercontent.com/5065057/176921316-bc021dcc-a024-4a56-923e-c2a19889c58e.png) + +## Calling Flow Graph events from Sequencer + +* Add the `Flow Events` track to the Level Sequence. + * Simply place a key on the section and give it a name. + +![image](https://user-images.githubusercontent.com/5065057/176922388-60e42447-a6aa-4cd8-a67d-5e2ac1be5280.png) + +* Include this Level Sequence on any `Play Level Sequence` node. + * Either use the `Refresh Asset` button on the toolbar or the `Refresh Context Pins` option on the node. You need to use it after every change of event names, adding or removing events from the timeline. + +![image](https://user-images.githubusercontent.com/5065057/176922906-a0a8b327-8227-4ba0-a088-0e1d33370d2d.png) + +![image](https://user-images.githubusercontent.com/5065057/176923030-1d906f00-44fa-4812-8e04-2b8550828aff.png) + diff --git a/docs/Features/SaveGameSupport.md b/docs/Features/SaveGameSupport.md new file mode 100644 index 000000000..33cf621cb --- /dev/null +++ b/docs/Features/SaveGameSupport.md @@ -0,0 +1,27 @@ +--- +title: Save Game Support +layout: default +--- + +Flow Graph plugs into Unreal's `SaveGame` system. If you haven't used it yet, read [Saving and Loading Your Game](https://dev.epicgames.com/documentation/en-us/unreal-engine/saving-and-loading-your-game-in-unreal-engine). + +You control which properties are included in SaveGame by marking C++ properties with the `SaveGame` specifier. Or by ticking the `SaveGame` checkbox in the blueprint editor. + +## What to look for? +* `FlowSave.h`. Active graphs are serialized to the `UFlowSaveGame` object, which extends the engine's `USaveGame`. That allows you to integrate Flow Graph into your SaveGame setup. +* `UFlowSubsystem` keeps a registry of all active Flow Graphs at the given moment. That's why it also contains methods providing SaveGame support. To support Flow Graph in your save system, you need to call `OnGameSaved` and `OnGameLoaded` methods. These are accessible from blueprints. +* `UFlowNode` class provides overridable events `OnSave` and `OnLoad`, so you can add custom SaveGame logic to any node, like restoring Timer with "RemainingTime" value read from SaveGame. Check the `UFlowNode_Timer` class for reference. +* `UFlowNode_Checkpoint` node is a built-in example of implementing autosave called from quests. +* `UFlowAsset` and `UFlowComponent` expose similar `OnSave` and `OnLoad` events, so you should be able to customize SaveGame logic in every plugin's class that's involved in SaveGame operations. + +[Signal Modes](SignalModes.html) features provide a solution for modifying Flow Graphs post-launch, once players already have SaveGames with serialized graph state. + +## Quick sample +You can find a quick example of integrating Flow into your SaveGame setup in the `FlowGame` project. Here are [simple C++ classes related to this](https://github.com/MothCocoon/FlowGame/blob/5.7/Source/FlowGame/). + +Note: You might need to call `UFlowSubsystem::LoadRootFlow` manually on your Root Flow owners if you're loading a game while the world is already active. Flow Component automatically calls `LoadRootFlow` only on BeginPlay! Supporting in-game loading is up to you. + +## Support for graphs not instantiated from the Flow Component +It's possible to create Root Flow for any UObject owner, like Player Controller or a subsystem. If these objects don't include the Flow Component, supporting Save/Load logic requires a bit more work. +* You need to call `UFlowSubsystem::LoadRootFlow` on this custom owner after deserializing the SaveGame with `UFlowSubsystem::OnGameLoaded`. Look at the sample code linked above. You need to iterate on owners if they don't include the Flow Component's logic. +* If your Root Flow is created on an UObject owner that doesn't belong to the world (Game Instance or its subsystem), you need to set the `bWorldBound` property on your Flow Asset to False. \ No newline at end of file diff --git a/docs/Features/SignalModes.md b/docs/Features/SignalModes.md new file mode 100644 index 000000000..c8d954190 --- /dev/null +++ b/docs/Features/SignalModes.md @@ -0,0 +1,24 @@ +--- +title: Signal Modes +layout: default +--- + +The Signal Modes concept was born from the need to support patching already released games. Flow Graphs serialize the state of their active nodes to a SaveGame. This works well, but there's one inherent limitation. If a player has a SaveGame with a given Flow Node active and serialized to a SaveGame, we cannot remove this node from the graph post-launch. Hence, we shouldn't remove any node post-launch to be perfectly safe. + +## Basics + +Yet we sometimes need to change the logic of our game in patches. Signal Modes provide a solution for that. It allows designers to mark nodes as: +* **Enabled** - Default state, node is fully executed. +* **Disabled** - No logic executed, any Input Pin activation is ignored. Node instantly enters a deactivated state. +* **PassThrough** - Internal node logic not executed. All connected outputs are triggered, node finishes its work. + +This way, we can deactivate nodes without removing them from the graph, so we can properly continue graph execution from a legacy SaveGame. Pin connections aren't serialized to a SaveGame, so we can safely change connections on pass-through nodes anyway we need. + +![image](https://user-images.githubusercontent.com/5065057/201344208-2020cdad-ae0d-4df6-bd06-25dddc1c88aa.png) + +## Allowed Signal Modes + +The node author can limit a list of available signal nodes for a given Flow Node class. +Some nodes are already acting like pass-through nodes by design, like Reroute or Sequence. It would be redundant or confusing to mark them as PassThrough, so we removed that from the list of available signal modes. + +`AllowedSignalModes = {EFlowSignalMode::Enabled, EFlowSignalMode::Disabled};` \ No newline at end of file diff --git a/docs/FlowBased/Games.md b/docs/FlowBased/Games.md new file mode 100644 index 000000000..7c756c8a1 --- /dev/null +++ b/docs/FlowBased/Games.md @@ -0,0 +1,21 @@ +--- +title: Games +--- + +This page collects games built with the Flow Graph! + +Feel free to add information on project. Every game entry should provide these few elements, so this list wouldn't look boring. +* Title and short description of the game. +* At least a single embedded image, or a link to the game video. +* (Optional) Short info on how Flow Graph helped you with the development of this game. + +Only games publicly available in game stores should be added here. +

+ +## The Thaumaturge +The Thaumaturge is an RPG set in the tumultuous times of 20th-century Warsaw, developed by Fool's Theory and published by 11 bit studios. Esoteric creatures known as salutors roam the streets. You play as Wiktor, a talented thaumaturge, who captures and commands those spirits. + +* We switched our quest system from blueprint-based solution in mid-production, and we were very happy with the decision. We managed to automatically convert most of quest graphs. +* We built cinematic dialogue system on top of Flow Graph. It gave us easy-to-use graph for creating non-linear dialogues. Having a single Flow Node for dialogue section allowed us to greatly simplify code responsible for generating Level Sequence from raw data of dialogue lines (text and parameters filled by written, narrative designers and cinematic artitsts). + +The Thaumaturge | Example of non-linear cinematic dialogue \ No newline at end of file diff --git a/docs/FlowBased/Plugins.md b/docs/FlowBased/Plugins.md new file mode 100644 index 000000000..d372b3289 --- /dev/null +++ b/docs/FlowBased/Plugins.md @@ -0,0 +1,10 @@ +--- +title: Plugins +--- + +This page collects notable extensions that build upon Flow Graph as the base plugin. + +## AI Flow Graph +AI Flow plug-in is an extension to the Flow plug-in that adds support for commonly-used AI features like the Environment Query System (EQS) and Blackboards. + +[GitHub: AI Flow Graph](https://github.com/LindyHopperGT/AIFlowGraph) diff --git a/docs/Forks/NotablePullRequests.md b/docs/Forks/NotablePullRequests.md new file mode 100644 index 000000000..3096fb294 --- /dev/null +++ b/docs/Forks/NotablePullRequests.md @@ -0,0 +1,12 @@ +--- +title: Notable Pull Requests +--- + +This document lists worthwile pull requests that haven't been integrated to mainline Flow Graph repository. + +That usually happens for one of following reasons. +* Change is useful to some users but it shouldn't part of the core Flow system. Users might integrate given change to their project code, or turn into another plugin. +* Change might useful to some users but contradicts the Flow Graph design. + +## Pull Requests +* [Added NamedReroute Nodes](https://github.com/MothCocoon/FlowGraph/pull/335) diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 000000000..8ca91477a --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,5 @@ +source "https://rubygems.org" + +gem "jekyll", "~> 4.3" +gem "jekyll-theme-midnight" +gem 'wdm', '>= 0.1.0' diff --git a/docs/Guides/AddingNodeSpawnShortcut.md b/docs/Guides/AddingNodeSpawnShortcut.md new file mode 100644 index 000000000..a51108a19 --- /dev/null +++ b/docs/Guides/AddingNodeSpawnShortcut.md @@ -0,0 +1,26 @@ +--- +title: Adding Node Spawn Shortcut +--- + +1. Create `DefaultEditorPerProjectUserSettings.ini` in your project's Config folder, if you don't have this .ini yet. +2. Add this section to the .ini +``` +[FlowSpawnNodes] +; Flow ++Node=(Class=FlowNode_OnNotifyFromActor Key=A Shift=false Ctrl=false Alt=false) ++Node=(Class=FlowNode_Finish Key=F Shift=false Ctrl=false Alt=false) ++Node=(Class=FlowNode_SubGraph Key=G Shift=false Ctrl=false Alt=false) ++Node=(Class=FlowNode_CustomInput Key=I Shift=false Ctrl=false Alt=false) ++Node=(Class=FlowNode_Log Key=L Shift=false Ctrl=false Alt=false) ++Node=(Class=FlowNode_ExecutionMultiGate Key=M Shift=false Ctrl=false Alt=false) ++Node=(Class=FlowNode_NotifyActor Key=N Shift=false Ctrl=false Alt=false) ++Node=(Class=FlowNode_CustomOutput Key=O Shift=false Ctrl=false Alt=false) ++Node=(Class=FlowNode_Reroute Key=R Shift=false Ctrl=false Alt=false) ++Node=(Class=FlowNode_ExecutionSequence Key=S Shift=false Ctrl=false Alt=false) ++Node=(Class=FlowNode_Timer Key=T Shift=false Ctrl=false Alt=false) +; Comment ++Node=(Name=Comment Key=C Shift=false Ctrl=false Alt=false) +``` +3. Restart the editor. You should see the assigned shortcut in the Palette. + +![BKCt3pDj9s](https://user-images.githubusercontent.com/5065057/114264581-062ee400-99ec-11eb-9ffc-ba3e6d901b3d.png) diff --git a/docs/Guides/ComparisonToGAS.md b/docs/Guides/ComparisonToGAS.md new file mode 100644 index 000000000..9de898c69 --- /dev/null +++ b/docs/Guides/ComparisonToGAS.md @@ -0,0 +1,28 @@ +--- +title: Comparison to GAS +--- + +Some people asked how the Flow system differs from systems like Gameplay Ability System (GAS)? Naturally, this might be unclear if someone used to implement gameplay mechanics, but not event systems like quests. + +Flow and GAS are designed for different groups of designers, working on separate layers of gameplay. + +### Ability System is designed to script second-to-second gameplay. A core gameplay loop. +* It's typically used by gameplay-heavy games, where designers need to implement a lot of different skills, attacks/spells, buffs/debuffs. All different kinds of gameplay abilities and effects. GAS provides an abstract framework for this kind of gameplay. +* **GAS is a tool for gameplay designers.** Developers that script game mechanics in blueprints. If we'd have a scripting language in the engine, they might spend most of their time implementing features in such scripting language (instead of Blueprint or C++). +* Execution of abilities depends heavily on the player's actions and AI behaviors. Abilities are conceptually related to Pawns and Player/AI Controllers. Usually, the player character and enemy character would use abilities against each other. +* It's player-driven and AI-driven. Players decide what skill, weapon, or attack use at the given moment. GAS doesn't use any kind of global graph defining what should happen in the game. It provides methods to manage complex interactions between actions, i.e. casting spell X on an enemy would interrupt his attack Z (which is another ability). +* GAS allows for scripting combat/action mechanics in blueprints but in a very systemic and organized way. It's probably the only kind of blueprint which supports efficient network replication (i.e. optimal replication of arrays which is normally only available in C++). + +### The Flow system is designed to easily script pre-designed minute-to-minute gameplay. Event-heavy games. +* It's used for directing what exactly happens in the given place, in the specific moments of the storyline. Events happen in specifically designed order, in predictable cause-effect chains. +* **Flow is a tool for content designers.** People who usually don't focus on scripting gameplay systems. Content designers craft world and story. Things that the player explores while repeating the core gameplay loop. The flow-based system should be easy to use by the least technical designers, as writers. +* In the perfect world, a Flow-based graph for the quest would resemble a whiteboard graph drawn while the quest was designed. It should be easier to translate story/event design documents to the Flow Graph than any other visual scripting available for the Unreal Engine. +* It's important to have easy-to-maintain graphs telling the entire story of the game. It's your way to script a "walkthrough of the game". +* If there's at least one gameplay programmer in your team, he might implement all the systems and Flow Nodes in C++. It wouldn't take away anything from content designers if we assume they only want to build a game from reusable "event blocks". + +### Example: implementing the Starcraft campaign. +* You might use Ability System to do script skills of all units. Also how much resources they consume. What units can be produced in a given building, etc. +* You might use Flow to script specific missions. For instance, the first objective is to destroy 3 buildings on the map. Once player units enter trigger X for the first time, we play a video message from the command center. After destroying these 3 buildings, we give players another objective like "destroy all enemy constructions". +* You might use Flow graphs to script tutorials activated under specific conditions. "If the Coal resource is depleted for the first time, start tutorial explaining how to build a coal mine". + +Obviously, Flow Graph can be used to script many different things. It's basically a generalized way for triggering any kind of actions and reacting to dispatched events (brodcasted delegates). It might be used to implement gameplay mechanics, especially if the gameplay is mixed with the narrative (like puzzles). diff --git a/docs/Overview/Concept.md b/docs/Overview/Concept.md new file mode 100644 index 000000000..52b045483 --- /dev/null +++ b/docs/Overview/Concept.md @@ -0,0 +1,72 @@ +--- +title: Concept +--- + +Flow plug-in for Unreal Engine provides a graph editor tailored for scripting flow of events in virtual worlds. It's based on a decade of experience with designing and implementing narrative in video games. All we need here is simplicity. + +It's a design-agnostic event node editor. + +![Flow101](https://user-images.githubusercontent.com/5065057/103543817-6d924080-4e9f-11eb-87d9-15ab092c3875.png) + +* A single node in this graph is a simple UObject, not a function like in blueprints. This allows you to encapsulate the entire gameplay element (logic with its data) within a single Flow Node. The idea is to write a reusable "event script" only once for the entire game! +* Unlike blueprints, Flow Node is async/latent by design. Active nodes usually subscribe to delegates, so they can react to an event by triggering the output pin (or whatever you choose). +* Every node defines its own set of input/output pins. It's dead simple to design the flow of the game - connect nodes representing features. +* Developers creating a Flow Node can call the execution of pins in any way they need. API is extremely simple. +* Editor supports conveniently displaying debug information on nodes and wires while playing a game. You provide what kind of message would be displayed over active Flow Nodes. You can't have that with blueprint functions. +* Works well with the World Partition introduced with UE 5.0. In thic case, there are no sublevels and no level blueprints. + +## Base for your own systems and tools +* It's up to you to add game-specific functionalities by writing your nodes and editor customizations. It's not like a marketplace providing the very specific implementation of systems. It's a convenient base for building systems tailored to fit your needs. +* Quickly build your own Quest system, Dialogue system, or any other custom system that would control the flow of events in the game. +* Expand it, build Articy:Draft equivalent right in the Unreal Engine. + +## In-depth video presentation +This 24-minute presentation breaks down the concept of the Flow Graph. It goes through everything written here, but in greater detail. + +Introducing Flow Graph for Unreal Engine + +## Simplicity is a key +* It's all about simplifying the cooperation between gameplay programmers and content designers by providing a clean interface between "code of systems" and "using systems". +* The code of gameplay mechanics wouldn't ever be mixed. Usually, system X shouldn't even know about the existence of system Y. Flow Graph is a place to combine features by connecting nodes. +* Every mechanic is exposed to content designers once, in one way only - as the Flow Node. It greatly reduces the number of bugs. Refactoring mechanics is easy since you don't have to update dozens of level blueprints by directly calling system functions. +* Systems based on such an editor are simple to use for the least technical team members, like narrative designers, writers, QA. Every time I ask designers why they love working with such a system, they usually reply: "It's so simple to understand and make a game with it". +* Even a complex game might end up with a few dozen Flow Nodes. It's easy to manage the game's complexity - a lot of mechanics, mission scripting, narrative events. It makes it very efficient to develop lengthy campaigns and multiplayer games. + +## Flexibility of the system design +Flow Graph assets aren't part of the world. Every Flow Node can communicate with world actors any way you see fit. +* Nodes can obtain actors in the world by using [Gameplay Tags](https://dev.epicgames.com/documentation/en-us/unreal-engine/using-gameplay-tags-in-unreal-engine). No direct references to actors are used in this variant of scripting. That brings a lot of new possibilities. + * Simply add a Flow Component to every "event actor", and assign Gameplay Tags identifying this actor. Flow Component registers itself with the Flow Subsystem (or any derived system) when it appears in the world. It's easy to find any event actor this way, just ask the Flow Subsystem for actors registered with a given Gameplay Tag. + * This is the best way of identifying on runtime-spawner actors, especially NPCs. + * In some cases actor with a given Gameplay Tag doesn't even have to exist when starting a related action! Example: On Trigger Enter in the image above would pick up the required trigger after loading a sublevel with this trigger. +* Nodes can obtain actors in other way like Soft Object Reference or Guid. This is often the preferred way of referencing unique triggers or spawn points. + +Thanks to this flexibility, it's possible to place actors used by the single Flow Graph in different sublevels or even worlds. This removes one of the workflow limitations related to the level design. + +Flow Graph could live as long as the game session, not even bound to a specific world. You can have a meta Flow Graph waiting for events happening anywhere during the game. + +## Healthy architecture +* Flow Graph is meant to entirely replace the need to use Level Blueprints in production maps. The flow of the game - the connection between consecutive events and actors - should be scripted by using Flow Graphs only. Otherwise, you end up creating a mess, using multiple tools for the same job. +* This graph also entirely replaces another way of doing things: referencing different actors directly, i.e. hooking up Spawner actor directly to the Trigger actor. This seemingly works fine, but it's impossible to read the designed flow of events scripted this way. Debugging can be very cumbersome and time-consuming. +* Actor blueprints are supposed to be used only to script the inner logic of actors, not connections between actors belonging to different systems. +* Flow Nodes can send and receive blueprint events via the Flow Component. This recommended way of communicating between Flow Graphs and blueprints. +* Technically, it's always possible to call custom blueprint events directly from a blueprint Flow Node, but this would require creating a new Flow Node for every custom blueprint actor. Effectively, you would throw the simplicity of Flow Graph out of the window. + +## C++ vs Blueprints +* A programmer writing a new gameplay feature can quickly expose it to content creators by creating a new Flow Node. A given C++ feature doesn't have to be exposed to blueprints at all. +* Flow Nodes can be created in blueprints as well. Personally, I would recommend using blueprint nodes mostly for prototyping and rarely used custom actions, if you have a gameplay programmer in your team. If not, sure, you can implement your systems in blueprints entirely. + +## Performance +* Performance loss in blueprint graphs comes from executing a large network of nodes, processing pins and connections between them. Moving away from overcomplicated level blueprints and messy "system blueprints" to simple Flow Graphs might improve framerate and memory management. +* As Flow Nodes are designed to be event-based, executing graph connections might happen only a few times per minute or so. (heavily depends on your logic and event mechanics). Finally, Flow Graph has its own execution logic, doesn't utilize blueprint VM. +* Flow-based event systems are generally more performant than blueprint counterparts. Especially if frequently used nodes are implemented in C++. + +## Related resources +* [Introduction to Gameplay Tags](https://dev.epicgames.com/documentation/en-us/unreal-engine/using-gameplay-tags-in-unreal-engine) +* [Behind the Scenes of the Cinematic Dialogues in The Witcher 3: Wild Hunt](https://www.youtube.com/watch?v=chf3REzAjgI) +* [Story of Choices: the quest system in Dying Light 2](https://www.youtube.com/watch?v=DPcz_-m3SwQ) +* [Sinking City - story scripting for the open-world game](https://youtu.be/W_yiopwoXt0?t=929) as part of their talk on Sinking City development. +* [Large worlds in UE5: A whole new (open) world](https://www.youtube.com/watch?v=ZxJ5DG8Ytog) - describes World Partition and related features. +* [Blueprints In-depth - Part 1](https://youtu.be/j6mskTgL7kU?t=1048) - great talk on blueprint system, the timestamp at the Performance part. +* [Blueprints In-depth - Part 2](https://www.youtube.com/watch?v=0YMS2wnykbc) +* [The Visual Logger: For All Your Gameplay Needs!](https://www.youtube.com/watch?v=hWpbco3F4L4) +* [Gamedec exemplifies how to incorporate complex branching pathways using Unreal Engine](https://www.unrealengine.com/en-US/tech-blog/gamedec-exemplifies-how-to-incorporate-complex-branching-pathways-using-unreal-engine) - example of how the integration of Articy:Draft with Unreal Engine looks like. diff --git a/docs/Overview/FAQ.md b/docs/Overview/FAQ.md new file mode 100644 index 000000000..0bcf33029 --- /dev/null +++ b/docs/Overview/FAQ.md @@ -0,0 +1,18 @@ +--- +title: FAQ +--- + +## How to separate the logic between many graphs? +The basic and recommended way is to use the `Sub Graph` node to start graph B from graph A. This way we could have very complex logic, i.e. separate graphs for every quest or event. + +![image](https://user-images.githubusercontent.com/5065057/204156931-8f21eb1a-d438-4430-8e8a-f1365e4528ea.png) + +## Is it possible to have a Flow Graph independent from the world? +Yes, it's an everyday use case, i.e. if you want to have a global "meta-quest" (controlling the game's story) or graph that is tracking achievements. You can create a Flow Asset instance from anywhere in the game by calling `UFlowSubsystem::StartRootFlow`. + +![image](https://user-images.githubusercontent.com/5065057/150850841-3827e785-dd71-4c08-9ab7-aa1a94631052.png) + +## My Flow Node blueprint isn't visible in the Palette? +This happens if a given blueprint was created with a standard blueprint creation menu. You have to create blueprint nodes via a dedicated Content Browser menu item. (Similar inconvenience applies to creating Editor Blutilites, which won't run if created via the standard blueprint menu). + +![image](https://user-images.githubusercontent.com/5065057/204156900-87fe32cd-daa9-4bd4-9e7f-a42ead0d0443.png) diff --git a/docs/Overview/GettingStarted.md b/docs/Overview/GettingStarted.md new file mode 100644 index 000000000..9331fb66b --- /dev/null +++ b/docs/Overview/GettingStarted.md @@ -0,0 +1,25 @@ +--- +title: Getting Started +--- + +## Sample projects +There's a separate repository including [a sample project called FlowGame](https://github.com/MothCocoon/FlowSolo), so you can easily check how this plug-in works. It includes +* Flow plugin +* Additional C++ Flow nodes, as `FlowQuest` plugin. +* Simple map with a few Flow Graphs. + +## Adding Flow Graph to your project +1. Unpack the plugin to the Plugins folder in your project folder. If you don't have such a folder yet, simply create it. +2. First of all, open Project Settings in the editor. Change `World Settings` to the `Flow World Settings` class. Restart the editor. That class starts the Flow assigned to the map, just when starting the game. So it serves as a replacement for BeginPlay in the level blueprint. +3. You can assign a Flow Asset to the map via the Flow toolbar above the main viewport. Assigning it directly via the World Settings editor window also works. + +It's crucial to perform Step 3 after Step 2. Otherwise, a reference to the Flow Asset might be improperly saved in the map and wouldn't work. If that happens, just try to revert your map. And assign the Flow Asset again. + +## Including GitHub repository in your repository +You can include this plugin repository as a dependency. It can be done by using Git submodules. +``` +git submodule add -b 5.0 https://github.com/MothCocoon/FlowGraph.git Plugins/Flow +``` + +## Useful tools +* [Subsystem Browser plugin](https://github.com/aquanox/SubsystemBrowserPlugin) works really well with Flow Graph, as it displays runtime data of subsystems like Flow Subsystem. \ No newline at end of file diff --git a/docs/Overview/HowToContribute.md b/docs/Overview/HowToContribute.md new file mode 100644 index 000000000..46e9a881c --- /dev/null +++ b/docs/Overview/HowToContribute.md @@ -0,0 +1,29 @@ +--- +title: How To Contribute +--- + +### Contribute to plugin's code +We are here very open to accept plugin improvements via pull requests. At the moment of writing this words, we got 60 contributors with accepted pull requests. Over 200 pull requests have been resolved. + +* Don't be shy to ask on our Discord channel "hey, would this kind of code change would be accepted?". This might indeed save you some time, especially if Flow Graph is a new thing for you and your team. +* Feel free to open new pull request, even if you are quite unsure if given change would be accepted. It's often easier to discuss technicalities while proposed code change can be accessed by community. + +[Pull requests](https://github.com/MothCocoon/FlowGraph/pulls) + +### Contribute to documentation +The entire plugin documentation has been moved from the GitHub wiki to GitHub Pages. The most important advantage is that now anybody can contribute to improving documentation via pull requests. + +Contributing to docs is quite easy. There are a few things to know. +* Every documentation page is a separate Markdown file: a text file with .md extension. Example: this page is written as HowToContribute.md. + * Markdown is a common markup language used to write wikis and documentation. You will find plenty of guides, cheet sheets for Markdown on the web. + * All pages live in `FlowGraph/docs` folder, organized in a few folders like `Overview`, `Features`, `Guides`. You might already notices that folder names match the structure of sidebar links. +* If you'd like add to a new page. + * Please remember to add new pages in one of existing folders. And let's talk on Discord, if you think we need a new folder (a new page category). + * A link to a new page must be added manually by editing `_data/navigation.yml`. It's one of files expected by Jekyll-based site (the thing powering GitHub Pages). + * The only element required in a new page is to include "title" section at the top of Markdown file. Please check how existing files have it defined. +* If you'd like embed image or other file. Files can be uploaded to GitHub servers, but it is quite unintuitive. + * Go to GitHub repository on GitHub.com. Go to Issues, Pull Requests or Wiki. + * Create draft of a new issue, or start writing comment on existing issue, or comment on exisint pull request. Don't worry, you don't need to send it. + * Just drag your image into text editing area. GitHub will automatically upload the image. Copy-paste the generated link to the Markdown file you're editing locally. + * Discard draft of whatever text you were editing on GitHub.com. + * It's cumbersome way, but this way we avoid bloating the disk size of the Flow Graph repository. Plugin itself consumes 2MB of disk size. Adding images to the repository would quickly consume hundreds of MBs. diff --git a/docs/Releases/Version10.md b/docs/Releases/Version10.md new file mode 100644 index 000000000..ff064f220 --- /dev/null +++ b/docs/Releases/Version10.md @@ -0,0 +1,35 @@ +--- +title: Flow 1.0 +--- + +March 8, 2021. The first publicly released version of the plugin! + +* [Flow 4.26 + example content](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.0-example) +* [Flow 4.26](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.0-4.26) +* [Flow 4.25](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.0-4.25) +* [Flow 4.24](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.0-4.24) +* [Flow 4.23](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.0-4.23) +* [Flow 4.22](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.0-4.22) + +## Flow Graph +* `Flow Graph` is a custom visual scripting system in which a single node (`FlowNode`) represents UObject, while a blueprint node (K2Node) represents UFunction. +* Unlike blueprints, Flow Node is async/latent by design. Active nodes usually subscribe to delegates, so they can react to events by triggering output pins (or whatever you choose to). +* Its concept is described in this [24-minute in-depth presentation](https://www.youtube.com/watch?v=BAqhccgKx_k). + +## Flow Subsystem +* `Flow Subsystem` is a runtime [programming subsystem](https://dev.epicgames.com/documentation/en-us/unreal-engine/programming-subsystems-in-unreal-engine). +* It manages the lifetime of Flow Assets that contain Flow Graph (and its Flow Nodes). In other words, the Flow Subsystem creates instances of Flow Assets. +* `Flow World Settings` provides a method to automatically instantiate Flow Asset assigned to the persistent level. The new toolbar in the main editor window is used to assign this asset to the Flow World Settings. You have to set Flow World Settings as your World Settings class in Project Settings. +* Flow Subsystem allows instantiating Flow Assets from any script in the game by calling StartRootFlow method. Conceptually, it's like you could instantiate your own Level Blueprint from anywhere - a graph controlling events in the level, but not part of any actor or component. + +## Flow Component Registry +* Instantiated Flow Asset isn't part of the world, and it doesn't allow to reference level actors like Level Blueprints. How do we access actors then? Every actor you want to access in Flow Graph needs to have a simple `Flow Component` (part of the plugin). Every Flow Component in the world is registered on `BeginPlay` to the Flow Subsystem. +* This what I call the `Flow Component Registry`. This registry is a map of all Flow Components existing in loaded levels and spawned actors. +* It exposes methods to quickly obtain references to Flow Components existing in the world, identified by `Identity Tags` - a Gameplay Tag container included in Flow Component. Since you can get components, you can easily get an actor that owns a given component. +* It's possible to access Flow Component Registry in Flow Subsystem from any script. It's a cheap way of getting actor references because it filters out optimized TMap instead of iterating all actors in the world every time (like GetAllActorsOfClass method). + +## Things not included +There are few things I described in that 24-minute presentation, but they're not part of the plugin. +* Variables support. Adding variables to assets and passing values to nodes. This is a core blueprint feature that I'd like to implement in some way in Flow Asset. It's not a trivial thing to do. I don't know when this would be done. +* You won't find any game-specific systems in the Flow plugin, like quest system or dialogue system. The fundamental idea of this plugin is to provide you an excellent framework for building your project-specific system. I might only provide some examples in the future. This would be added to the separate [FlowGame](https://github.com/MothCocoon/FlowGame) repository. +* FactsDB isn't implemented, it might be in the future. \ No newline at end of file diff --git a/docs/Releases/Version11.md b/docs/Releases/Version11.md new file mode 100644 index 000000000..61098146f --- /dev/null +++ b/docs/Releases/Version11.md @@ -0,0 +1,124 @@ +--- +title: Flow 1.1 +--- + +January 23, 2022. + +This release addresses the first wave of feedback I'm receiving from early adopters. It includes pull requests from the community: BorMor (Sergey Vikhirev), IceRiverr, Mahoukyou, Mephiztopheles (Markus Ahrweiler), moadib (Marat Yakupov), ryanjon2040, Solessfir (Constantine Romakhov), sturcotte06. + +This is the first release for UE 4.27 and UE5 Early Access. + +This is also the last release for UE 4.22, 4.23, 4.24. Already a few improvements in v1.1 couldn't be backported. + +* [Flow 5.0 EA](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.1-5.0EA) +* [Flow 4.27](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.1-4.27) +* [Flow 4.26](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.1-4.26) +* [Flow 4.25](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.1-4.25) +* [Flow 4.24](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.1-4.24) +* [Flow 4.23](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.1-4.23) +* [Flow 4.22](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.1-4.22) + +## New: SaveGame support +* Added initial support for loading/saving instanced Flow Graphs. + +## Specific Flow Nodes +* New node: `MultiGate` node which replicates the blueprint node of the same. (submitted by BorMor) +* New nodes: `OnActorRegistered` and `OnActorUnregistered` waiting until actor would appear in the world, or disappear. +* Nodes based on `UFlowNode_ComponentObserver` class can be set to work indefinitely, i.e. On Trigger Enter node would continue to work after the player entered trigger once. The node would trigger the `Success` output pin every time player entered the trigger. Obviously, a node can be stopped anytime by calling the `Stop` input pin. If you have any custom Component Observer nodes, you just need to change a single function call in order to use new options. Replace `TriggerOutput(TEXT("Success"), true)` with `OnEventReceived()`. +* A few nodes now use a Gameplay Tag Container instead of a single Gameplay Tag. So now we have an `IdentityTags` container instead of a single `IdentityTag`. And `NotifyTags` container instead of a single `NotifyTag`. Changed nodes are: `UFlowNode_ComponentObserver`, `UFlowNode_NotifyActor`, `UFlowNode_NotifyFromActor`. Your code might need to be updated if you have a class inheriting after any of these node classes. (partially implemented by moadib) +* Added `IdentityMatchType` to `UFlowNode_ComponentObserver`. Now you have better control over which actors should be accepted on the specific nodes, i.e. you can specify to accept actor that has all Identity Tags listed on the node. +* `CustomEvent` node has been renamed to the `CustomInput`. It should fix the confusion caused by the previous name. Now we have pair of nodes working with custom pins on the `SubGraph` node: `CustomInput` and `CustomOutput`. +* Exposed `FMovieSceneSequencePlaybackSettings` on `UFlowNode_PlayLevelSequence` node. +* (4.24+) Exposed `FLevelSequenceCameraSettings` on `UFlowNode_PlayLevelSequence` node. +* Exposed log verbosity on the `UFlowNode_Log`. (submitted by ryanjon2040) + +## Blueprint-specific Flow Node changes +* Exposed `CanUserAddInput` and `CanUserAddOutput` to the blueprint Flow Node. If a method is returning True, the user will be able to add numbered pins on a node in the graph. See Sequence node for reference. +* Flow Asset editor now reads `Blueprint Display Name` property. If set, this overrides asset name as... you guessed... displayed name of the Flow Node in the Flow Graph. +* Flow Asset editor now displays the description for Flow Node blueprints. Simply type text into the `Blueprint Description` property in Class Details. +* API change. `Blueprint Category` property of Flow Node blueprints now overrides the `Category` property from the C++ Flow Node class. This is the way of setting categories for blueprint nodes from now on. +* Exposed methods to get active Flows and Flow Nodes. It allows for creating a custom debug UI. +* Fixed: changing inputs/outputs of the Flow Node blueprint isn't reflected in opened Flow Graphs. +* Fixed: optimized obtaining Flow Node blueprints by editor's Palette. +* Fixed: Details panel of Flow Asset editor doesn't display the non-public properties of blueprint Flow Nodes anymore. + +## Flow Node +* API change. Refactored Flow Node pins, now defined as `FFlowPin` struct. Pins now can have tooltips. Blueprint nodes and Flow Assets are automagically updated. C++ nodes need a manual update as defining pins has changed a bit. C++ pins are now defined via methods like `AddInputPins({TEXT("Spawn"), TEXT("Despawn")});` or simply constructing struct with tooltip like `InputPins.Add(FFlowPin(TEXT("Reset"), TEXT("MyTooltip")));` +* Flow Pin constructor accepts many types as PinName: FString, FName, FText, TChar*, int32, uint8. +* Added a few C++ flavors of `TriggerOutput()` methods, accepting FString, FText, TCHAR*. +* API change. Renamed methods for adding numbered pins to `SetNumberedInputPins()` and `SetNumberedOutputPins()`. +* Added `IsOutputConnected` function to Flow Node. +* Added `InitializeInstance` function to Flow Node. It's called just after creating the node instance while initializing the Flow Asset instance. This happens before executing the graph, only called during gameplay. (proposed by sturcotte06) +* Implemented `IsDataValid()` method in the UFlowAsset, so it would return the validation result of Flow Nodes. (submitted by sturcotte06) +* Added `NodeSpecificColors` to `UFlowGraphSettings` (Flow Graph in Project Settings), so it's possible to override the color of any node. (original code submitted by Mephiztopheles) +* Added `GetDynamicTitleColor()` method to the `UFlowNode`, so every node instance can have a different color based on the runtime logic. Node color could even change in runtime! +* Added ability to hide specific nodes from the Flow Palette without changing the source code. Check `NodesHiddenFromPalette` in `UFlowGraphSettings`. +* `LogError` method is now marked as `const`. +* (4.26+) Utilized Viewport Stats Subsystem to display error messages (added by `UFlowNode::LogError`) permanently on the screen. If you'd like to use revert to the old behavior, call `LogError` with parameter `OnScreenMessageType` equal to `Temporary`. + +## Flow Asset +* Added `AllowedNodeClasses` list to the `UFlowAsset`. This lets you limit what nodes can be added to a given Flow Asset type. + * By default, all Flow Node classes are accepted. + * You can simply change to accept only custom nodes (i.e. dialogue nodes) by adding setting your base Dialogue Node class as the only element of `AllowedNodeClasses` array. + * You can add multiple node classes, assign a few specific nodes to the given asset type. + +## Flow Component +* Flow Components marked as blueprintable, so people could extend it freely. +* Added methods for adding/removing `IdentityTags` (in Flow Component) during gameplay. These methods update the Flow Component Registry in Flow Subsystem. +* Exposed set of Flow Subsystem functions and delegates to blueprints. This exposes obtaining references to actors in the world containing Flow Component with valid Identity Tag. +* `GetComponents()` method in Flow Subsystem now returns TSet instead of TArray. Actually, there was no advantage of using TArray here, since we can't rely on the order of elements returned from the component registry. +* Two delegates in Flow Subystem became dynamic (exposed to blueprint): `OnComponentRegistered()` and `OnComponentUnregistered()`. You might need to add UFUNCTION() above your methods bound to these delegates in C++. + +## Generic multiplayer support - entirely new! +This set of changes attempts to provide generic support for networked projects, in order to minimize the need to modify the plugin's code. +* By default, Flow Subsystem is created on clients. This allows to access the `Flow Component Registry` - an optimized way to find "event actors" in the world. It's also possible to instantiate client-only Flow Graphs. You can disable Flow Subsystem on clients by changing `bCreateFlowSubsystemOnClients` flag in Flow Settings (Flow section in Project Settings). +* By default, the Flow asset assigned to the world (via Flow Toolbar above the main viewport) isn't instantiated on clients. This kind of Flow usually contains gameplay-critical logic (i.e. quests), which should exist only on the server. If you need to call some code there on the client, I would recommend handling it inside a specific Flow Node. However, you can change it by selecting another `RootFlowMode" on the Flow Component in World Settings. +* Added network replication of `IdentityTags` in Flow Component. +* Added network replication of `NotifyTags` in Flow Component. This way designers can script custom multiplayer logic in their actors. + +## Root Flow +Root Flow is a Flow Asset instanced by an object from outside of the Flow System, i.e. World Settings or Player Controller. This Root Flow can create child Flow Assets in runtime by using the `SubGraph` node. This release adds more flexibility in managing Root Flows. +* Added `StartRootFlow()` and `FinishRootFlow()` methods to the Flow Component, so it's possible to reduce boilerplate code in actor classes. +* You can now specify on Flow Component, should it start RootFlow on `BeginPlay`? If not, you can freely script when it would be started. +* You can now specify on Flow Component, under which Net Mode would be allowed to start RootFlow? Single-player only (Standalone), Server-only, Authority (both single-player and server), Client-only, Any Mode. Change it by selecting the appropriate `RootFlowMode`. +* Flow Toolbar (available above the main editor viewport) isn't tied to AFlowWorldSettings class anymore. It works with the Flow Component which needs to be a part of your `AWorldSettings` class. +* Added comments in code to clarify what is the `Owner` of the Root Flow instance. It's any object passed as parameter of `UFlowSubsystem::StartRootFlow()`. Typically it would be an object that called this method, i.e. Flow Component. +* `Owner` of the Root Flow instance is now passed to all dependent Flow Asset instances, for easier access. (submitted by sturcotte06) +* Flow Asset is now a blueprint type, so it's possible to create blueprint variables of this type. +* Removed `FlowAsset` input argument from `UFlowSubsystem::FinishRootFlow()` method, it was redundant. +* It's now possible to set Allowed Class for Flow Asset set in Level Editor Toolbar. It's exposed in Flow Graph Settings (project settings). +* Fixed issue with starting Root Flow assigned on different `AFlowWorldSettings` than currently chosen in Project Settings. It might happen if you subclassed `AFlowWorldSettings`. Unfortunately, the engine would instantiate a few World Settings instances, both for your class and parent `AFlowWorldSettings` class. + +# Editor +* You can now create Reroute node by double-clicking on the wire. (contributed by ryanjon2040) +* Added circuit connection drawing. (contributed by ryanjon2040) +* Added "graph tooltip", a preview of the entire nested graph while hovering over SubGraph node. (contributed by ryanjon2040) +* Added optional displaying asset path on the Flow Node in the graph (contributed by ryanjon2040) +* Added option to hide Asset Toolbar above Level Editor. Some projects might not use it or replace it with a custom toolbar. +* Added option hide base `UFlowAsset` class from Flow asset category in "Create Asset" menu, by changing `bExposeFlowAssetCreation` flag in `UFlowGraphEditorSettings`. It's useful if you only wanna use your custom Flow Asset classes in the project. +* Added option hide base `UFlowNode` class from Flow asset category in "Create Asset" menu, by changing `bExposeFlowNodeCreation` flag in `UFlowGraphEditorSettings`. It's useful if you only wanna use your custom Flow Node classes in the project. +* Exposed `FlowAssetCategoryName` to `UFlowGraphEditorSettings`, so you can override the default name of the Flow asset category. +* Extracted some plugin-specific Project Settings to a new plugin-specific Editor Preferences (a new class `UFlowGraphEditorSettings`). +* Created `SFlowGraphNode_SubGraph` widget to allow customizations specific to the SubGraph node. +* Marked many methods in the FlowAssetEditor as virtual, for your convenience. +* Added option to override Flow Asset category name, so you group more assets together. + +## Fixes +* Changed LoadingPhase of runtime Flow module to `PreDefault`. This fixes rare issues with the plugin not being loaded on time while blueprint actors already try to load the Flow Component. +* Fixed a few editor issues with blueprint Flow Nodes. Editor code was loading blueprint data too aggressively, causing performance issues. +* Added missing editor logic: auto-wiring a newly placed node. (submitted by IceRiverr) +* Fixed node not being registered with the asset after undoing its deletion. (submitted by Mahoukyou) +* Clear previously selected nodes before new node creation. (submitted by BorMor) +* Fixed crash on saving Flow Asset after force-deleting blueprint node used in that asset. +* Fixed case where Root Flow wouldn't be properly finished and cleaned up. (reported and fixed by BorMor) +* Fixed Clang compilation. Fixed compilation with unity disabled. (submitted by sturcotte06) +* Fixed a few compilation issues. (submitted by moadib) +* Solved filename conflicts with Dungeon Architect plugin which caused compilation errors. Make sure you updated the Dungeon Architect plugin to the latest version. +* Added permanent invite link to the Flow discord server in plugin's SupportURL and ReadMe. +* (UE5) Fixed Engine parsing Plugin version error while cooking (submitted by Solessfir) + +## API changes +* If you exposed any Flow Node properties (to be edited by users in the Flow Asset) by `EditDefaultsOnly`, you need to change to correct `EditAnywhere` specifier. Otherwise, the property won't be visible in the Details panel of the Flow Asset. +* If you changed anything in the Flow Editor settings, you need to perform a manual fixup. This class has been renamed to the FlowGraphSettings (as part of fixing filename conflict with Dungeon Architect). You need to edit your DefaultEditor.ini and change the old section name `[/Script/FlowEditor.FlowEditorSettings]` to `[/Script/FlowEditor.FlowGraphSettings]`. +* `TemplateAsset` property in FlowAsset is now private. Use `GetTemplateAsset()` method, if you need to access it. +* (4.25+) Merged code from the `FlowDebuggerToolbar` class/file into `FlowAssetToolbar`. Simplified code around creating it, should be less confusing to extend it. Although you might need to update your code if you've done any changes around the toolbar. diff --git a/docs/Releases/Version12.md b/docs/Releases/Version12.md new file mode 100644 index 000000000..d92a1c263 --- /dev/null +++ b/docs/Releases/Version12.md @@ -0,0 +1,36 @@ +--- +title: Flow 1.2 +--- + +April 5, 2022. + +This release includes pull requests from the community: Drakynfly (Guy Lundvall), kathelon (Erdem Acar), LunaRyuko (Luna Ryuko Zaremba), Mephiztopheles (Markus Ahrweiler), ryanjon2040, seimei0083 (Joseph). + +This is the first release for UE 5.0, and the last one for UE 4.25. + +* [Flow 5.0](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.2-5.0) +* [Flow 4.27](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.2-4.27) +* [Flow 4.26](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.2-4.26) +* [Flow 4.25](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.2-4.25) + +Flow Solo (sample project) releases +* [Flow Solo 5.0](https://github.com/MothCocoon/FlowSolo/releases/tag/v1.2-5.0) +* [Flow Solo 4.27](https://github.com/MothCocoon/FlowSolo/releases/tag/v1.2-4.27) +* [Flow Solo 4.26](https://github.com/MothCocoon/FlowSolo/releases/tag/v1.2-4.26) + +## Flow Node +* It's now possible to deprecate Flow Nodes. Simply set the `bNodeDeprecated` flag to True. You can provide a replacement class by choosing another Flow Node as `ReplacedBy` class. Nodes deprecated this way continue to work normally. +* It's now possible to mark a Flow Node class as `bCanDelete` and `bCanDuplicate` without creating the `UFlowGraphNode` class just for that. (submitted by ryanjon2040) +* Added DeniedClasses as option to blacklist FlowNodes. (submitted by Mephiztopheles) +* `UFlowNodeBlueprint` isn't marked as final anymore, it's now possible to extend it (submitted by LunaRyuko) +* Added `IsInputConnected` method. +* Fixed typo in `JumpToNodeDefinition` label (submitted by Drakynfly) + +## Specific Flow Nodes +* Added Restart input pin to the `UFlowNode_Timer` node. (submitted by kathelon) +* `UFlowNode_PlayLevelSequence` now takes into account `CustomTimeDilation` of actor owning Root Flow (actor with a Flow Component creating that Flow Graph instance). (submitted by seimei0083) + +## Fixes +* Clearing `UFlowSaveGame` in `UFlowSubsystem::OnGameSaved` method, before serializing the current data. This prevents the critical issue if game-specific code passes reused SaveGame object here. We only remove data from the current world and global Flow Graph instances (not bound to any world). We keep data from all other worlds. +* Loading SaveGame no longer calls `Start` node on all loaded Sub Graphs. +* Fixed some localization key collisions (submitted by Drakynfly). \ No newline at end of file diff --git a/docs/Releases/Version13.md b/docs/Releases/Version13.md new file mode 100644 index 000000000..3e07268e4 --- /dev/null +++ b/docs/Releases/Version13.md @@ -0,0 +1,84 @@ +--- +title: Flow 1.3 +--- + +November 15, 2022. + +This release includes pull requests from the community: ArseniyZvezda (Arseniy Zvezda), Bargestt, Cchnn, dnault1, DoubleDeez (Dylan Dumesnil), iknowDavenMC, jhartikainen (Jani Hartikainen), Mephiztopheles (Markus Ahrweiler), ryanjon2040, seimei0083, sturcotte06. + +This is the first release for UE 5.1, and the last one for UE 4.26, 4.27. + +* [Flow 5.1](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.3-5.1) +* [Flow 5.0](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.3-5.0) +* [Flow 4.27](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.3-4.27) +* [Flow 4.26](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.3-4.26) + +Flow Solo (sample project) releases +* [Flow Solo 5.1](https://github.com/MothCocoon/FlowSolo/releases/tag/v1.3-5.1) +* [Flow Solo 5.0](https://github.com/MothCocoon/FlowSolo/releases/tag/v1.3-5.0) + +## SaveGame support +* [Signal Modes](../Features/SignalModes.html) features provides a solution for modifying Flow Graphs post-launch, once players already have SaveGames with serialized graph state. + +## New: Force Pin Activation +* It's a debugging feature available from Pin's context menu during PIE. +* Allows pushing the graph execution in case of blockers, i.e. specific node doesn't work for whatever reason and we want to continue playtesting. +* It works both on Input pins and Output pins. You can even trigger unconnected Input pins this way. + +## New: Asset Search +* (UE 5.1+) Implemented Asset Search for Flow Assets. It's now possible to use properties of Flow Assets and Flow Nodes. See this [Asset Search](../Features/AssetSearch.html) documentation. + +## New: Visual Diff +* (UE 5.1+) Implemented Visual Diff for Flow Assets. + +## Flow Node +* Added `AllowedAssetClasses` & `DeniedAssetClasses` to the Flow Node class. Flow Node class now can be defined in which Flow Asset class can be placed. So it's another way of defining it - as the Flow Asset class has had a similar list of Flow Node classes for a long time. +* Added `OnActivate` event, called while activating the node (executing any input for the first time). (contributed by Cchnn). +* Refactored numbered pins support, so it would fully safe to add regular pins. +* Added `CanFinishGraph` method, so the logic of finishing the graph from the node isn't hardcoded to the `Finish` node anymore. + +## Flow Node blueprint +* Added blueprint `TriggerOutputPin` method with a convenient dropdown for Pin Name selection. + +## Specific Flow Nodes +* Now it's possible to disable infinite execution of the OR node. Now OR node executes output only once, by default. Added logic to enable/disable the `Logical OR` node. This can be a breaking change if you somewhere relied on an infinitely working OR node. You can fix this by changing the `Execution Limit` value to 0. (based on Mephiztopheles's pull request) +* Added option to replicate Level Sequence over a network via `PlayLevelSequence` node. (contributed by ArseniyZvezda) +* Added out-of-box support for applying Transform Origin via `PlayLevelSequence` node. It can be enabled by `bUseGraphOwnerAsTransformOrigin` flag on the node. Read official docs on [Transform Origin feature](https://dev.epicgames.com/documentation/en-us/unreal-engine/creating-level-sequences-with-dynamic-transforms-in-unreal-engine). (code changes based seimei0083's pull request) +* Fixed one place in `UFlowNode_ComponentObserver` where `IdentityMatchType` property wasn't respected. +* Fixed a rare case in `UFlowNode_ComponentObserver` where the loop in `StartObserving` method would keep on iterating after the node finished its work. +* Removed limitation of executing only the first found `CustomInput` node with the given Event Name. It was actually more confusing than useful. +* Fixed highlighting of activate wire coming out of `CustomInput` node. + +## Known Issues +* In UE 5.1 version some playback settings from the `PlayLevelSequence` node aren't passed to the Level Sequence because of the engine change. This [community commit fixes the issue](https://github.com/MothCocoon/FlowGraph/commit/3c46b7ddaf2aafb9c3e9a0f7c293a701cae05189). The next release will include this fix. + +## Flow Subsystem +* Compilation-breaking change for C++ users. `UFlowSubsystem::GetActors` methods now return only actors of a given type. The previous implementation moved to `UFlowSubsystem::GetActorsAndComponents`. +* Added `bExactMatch` bool parameter to all blueprints and C++ methods returning actors/components registered to the Flow Component Registry. If this new option is set to False, the search will accept Flow Components with Identity Tags only partially matching provided tag. Be careful, this new option might be very costly as all registered tags needs are iterated in the loop. Const of using the original option (`bExactMatch` set to True) is constant. (implementation inspired by iknowDavenMC's pull request) +* Implemented support for launching multiple DIFFERENT flows for a single owner, i.e. World Settings, Player Controller, etc. Now when starting/finishing/obtaining a Root Flow instance, you need to provide a Template Asset as the parameter. +* Fixed Flow Asset instance names to be truly unique. (contributed by sturcotte06) + +## Graph +* Added `PinFriendlyName` to the `Flow Pin` definition. You can use it to override PinName without the need to update graph connections. +* Added `GetStatusBackgroundColor` method to Flow Node, available in blueprint nodes. +* Added `NodeDoubleClickTarget` setting to Flow Graph editor settings. Now users can choose if double-clicking the node opens "Node Definition" (node's blueprint editor or C++ class in IDE) or the "Primary Asset" (i.e. Dialogue asset for PlayDialogue node). (inspired by ryanjon2040's pull request) +* Added `AssetGuid` property to `UFlowAsset`. It might be helpful in advanced cases of save systems, where the game might need to save per-asset progress, i.e. used dialogue choices. A programmer might save the asset name to SaveGame, but... renaming/moving an asset post-launch would break saves. +* Added `bStartNodePlacedAsGhostNode` flag to `UFlowAsset` class. If set to True, the `Start` node in the newly created asset will be displayed as a ghost node. (based on ryanjon2040's pull request) +* Support keyword search for node search in the graph. (contributed by ryanjon2040) +* Improve drawing for Reroute nodes that go backward. (contributed by jhartikainen) +* Fixed issue where clicking the `Add Pin` button on the node would add the Input pin instead of the Output pin. It was happening if adding both Input and Output pins have been enabled on the given node class. +* Prevented crash if `SubGraph` node would try to instantiate the same Flow Asset as the asset containing this `SubGraph` node. Such an option is still disabled by default, so we could avoid triggering an accidental infinite loop. Users can remove this limitation by checking the `bCanInstanceIdenticalAsset` flag on the specific `SubGraph` nodes. + +## SaveGame support +* Fixed case where restoring Root Flow from the save might not work, if we were loading the game without changing or reloading the world. +* Added throwing a permanent onscreen error if Flow Component starting Root Flow has no Identity Tag assigned. This is important, as it would break loading SaveGame for this component. +* Exposed `bWorldBound` bool to eliminate the need to subclass UFlowAsset for simple world-independent Root Flows. + +## Asset creation +* Added logic allowing to add `FlowAsset` and `FlowNode` blueprints to already existing asset categories, i.e. "Gameplay". (contributed by Bargestt) +* From now if you'd clear the `Default Flow Asset Class` value when creating a new Flow Asset you'd get a "Pick Flow Asset Class" dialog. This might come in handy if have multiple Flow-based systems in the project. (contributed by Bargestt) +* Added `ConfigRestartRequired` to a few Flow Graph Settings properties. (contributed by Bargestt) + +# Editor +* `Go To Master` button renamed to `Go To Parent`. The button is now only visible when it can be used, previously it was only disabled. +* Renamed uses of `FEditorStyle` to `FAppStyle` as it's been deprecated in 5.1. (contributed by DoubleDeez) diff --git a/docs/Releases/Version14.md b/docs/Releases/Version14.md new file mode 100644 index 000000000..bf0b0892c --- /dev/null +++ b/docs/Releases/Version14.md @@ -0,0 +1,98 @@ +--- +title: Flow 1.4 +--- + +May 21, 2023. + +This release includes pull requests from the community: ArseniyZvezda (Arseniy Zvezda), bohdon (Bohdon Sayre), LindyHopperGT (Greg Taylor), lfowles (Landon Fowles), Mephiztopheles (Markus Ahrweiler), michalmocarskiintermarum (Michał Mocarski), ryanjon2040 (Satheesh), twevs, Vi-So (Alex van Mansom). + +This is the first release for UE 5.2. + +* [Flow 5.2](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.4-5.2) +* [Flow 5.1](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.4-5.1) +* [Flow 5.0](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.4-5.0) + +Flow Solo (sample project) releases +* [Flow Solo 5.2](https://github.com/MothCocoon/FlowSolo/releases/tag/v1.4-5.2) +* [Flow Solo 5.1](https://github.com/MothCocoon/FlowSolo/releases/tag/v1.4-5.1) +* [Flow Solo 5.0](https://github.com/MothCocoon/FlowSolo/releases/tag/v1.4-5.0) + +## Upgrade notes +Do not update directly from version 1.0 to this version. Important fixups of deprecated properties have been removed in this version. + +## New: Message Log support +* Added `Validate` toolbar action calling data validation on the entire asset, checking all Flow Nodes. If any issues are found, these are reported in the new `Validation Log` window. +* Node validation uses the new `UFlowNode::ValidateNode` method which is available in C++ only. +* Added `Runtime Log` windows displaying runtime message set by Flow Nodes, using methods `LogError`, `LogWarning`, and `LogNote`. +* Editor throws a notification on the PIE end if there are any warnings or errors reported. +* Refactored plain debugger struct into Flow Debugger Subsystem which gathers Runtime Logs. +* Fixed: `UFlowNode::LogError` doesn't execute its logic in Shipping builds anymore. Also, prevented calling runtime log methods on templates of the Flow Asset. + +![2UFTrHOoGP](https://user-images.githubusercontent.com/5065057/226177111-a987759b-43b9-45bc-befc-88069367aae5.png) + +## Editor misc +* Compilation-breaking change for C++. Context Pins now are constructed directly as `FFlowPin` instead of `FName`. It allows setting display names and tooltips on such pins. +* `Refresh` command now also recreates `UFlowGraphNode` instances if a given `UFlowNode` now has a different `UFlowGraphNode` assigned. Only applicable to C++ classes, as blueprint nodes cannot have custom `UFlowGraphNode`. (reworked pull request by ArseniyZvezda) +* Improved filtering of given Flow Node subclasses in `Flow Graph Schema`. +* Extending the `GetAssignedGraphNodeClass()` function to support returning the correct `EdGraphNode` for grandchild classes of `UFlowNode`. (contributed by Vi-So) +* The FlowGraphSchema will now create default nodes for any CustomInputs that exist when the asset is first created. (contributed by LindyHopperGT) +* Show a pretty readable pin name even if a friendly name is not provided. This is optional behavior that might be enabled by the `bEnforceFriendlyPinNames` flag in editor settings. (based on ryanjon2040 contribution) +* Extracted graph-related code from the `FFlowAssetEditor` to the new `SFlowGraphEditor` class. +* Added `SFlowGraphEditor::IsTabFocused` method to prevent delete/paste/copy nodes if the graph tab isn't focused. +* Exposed all remaining classes out of the module, allowing you to extend whatever you need. +* Moved `MovieScene` headers to the Public folder. Moved the `SLevelEditorFlow` class to the Utils folder. +* Moved all Details Customizations classes to a single `DetailsCustomizations` folder. + +## Asset Search +* Jump from the Asset Search result to the Flow Node in any graph editor. To unlock this feature + * Integrate this pull request to your engine: [Jump from Asset Search result to the node in any graph editor!](https://github.com/EpicGames/UnrealEngine/pull/9882). + * Set the `ENABLE_JUMP_TO_INNER_OBJECT` value to 1. +* Added option to run a search on a single asset, with Search Browser opened as an asset editor tab. To unlock this feature + * Integrate this pull request to your engine: [Asset Search: added option to run a search on a single asset, with Search Browser opened as asset editor tab](https://github.com/EpicGames/UnrealEngine/pull/9943) + * Set the `ENABLE_SEARCH_IN_ASSET_EDITOR` value to 1. + +## Flow Node +* Added `Node Color` property directly to the Flow Node class. (contributed by ryanjon2040) +* Added log message while node executes in `Disabled` or `PassThrough` mode. +* Added options to disable printing Signal Mode messages to the Output Log. (contributed by ryanjon2040) +* Added editor user setting `bShowNodeDescriptionWhilePlaying` allowing to hide of static node descriptions while PIE/SIE is active. (contributed by ryanjon2040) +* Moved the `TryGetRootFlowActorOwner` method from the `PlayLevelSequence` node to the base class. Also provided a component version of the same code. (contributed by LindyHopperGT) +* Exposed direct access to `Flow Node` connections in C++. +* Fixed: correctly updating user-add pins on nodes like `Sequence` after removing one of pins. +* Fixed: rare crash if node would trigger output on exiting game. +* Compilation-breaking change: Removed template method `LoadAsset` as it was redundant. +* Optimized building debug-only status strings, when using methods like `GetProgressAsString`. + +## Specific Flow Nodes +* Introduced a superclass to `UFlowNode_CustomInput` and `UFlowNode_CustomOutput` so they can share more code. (contributed by LindyHopperGT) +* Exposed CustomInput Add/Remove functions on UFlowNode to allow subclasses to modify the `CustomInputs` array. (contributed by LindyHopperGT) +* Added a UseAdaptiveNodeTitles option to optionally make CustomInputs integrate their EventName into the title for the node. This defaults to false (to preserve previous behavior by default). (contributed by LindyHopperGT) +* Added the `exact match` option to the `Notify Actor` node. (contributed by bohdon) +* Timer node complete in next tick if a value is closer to 0. (contributed by ryanjon2040) +* Added search keywords to nodes: `AND`, `Checkpoint`, `Log`, `Multi Gate`, `NotifyActor`, `OnActorRegistered`, `OnActorUnregistered`, `OR`, `Timer`. (contributed by ryanjon2040) +* (UE 5.1+) Fixed applying `FMovieSceneSequencePlaybackSettings` while creating `UFlowLevelSequencePlayer`. (contributed by michalmocarskiintermarum) +* Fixed: "Pause at End" does nothing in the `Play Level Sequence` node. (contributed by twevs) +* Fixed: Component Observer may continue triggering outputs if the last component triggered a finish during `UFlowNode_ComponentObserver::StartObserving`. (fix proposed by lfowles) + +## Flow Component +* Fixed bug in `UFlowComponent::GetRootInstances()` where the Owner parameter wasn't being used. (contributed by LindyHopperGT) + +## SaveGame support +* Exposed methods to blueprints: `LoadRootFlow`, `LoadSubFlow`. (contributed by Mephiztopheles) +* Fixed rare issue with loading saves: prevent triggering input on a not-yet-loaded node. Now when restoring the graph state, we iterate the graph "from the end", backward to the execution order. This prevents an issue when the preceding node would instantly fire output to a not-yet-loaded node. + +## Runtime misc +* Flow Asset no longers requires `Start Node` to be always a default entry node. It's still a default behavior, but you can change it by overriding `UFlowAsset::GetDefaultEntryNode()`. +* Corrected templated version of `UFlowAsset::GetOwner()`. (contributed by ryanjon2040) +* Added null `UFlowAsset` check when calling `UFlowSubsystem::StartRootFlow`. (contributed by bohdon) +* Changed some functions to return const &'s rather than doing a full deep copy of a member container. (contributed by LindyHopperGT) +* Added includes and forward declaration fixing some compiler errors. (contributed by LindyHopperGT) + +## New: Import from blueprint graph +* Added `UFlowImportUtils` blueprint library to create Flow Graph asset in the same folder as the selected blueprint. +* Includes simple utility method that recreates Flow Graph with nodes matching blueprint function nodes (`UK2Node_CallFunctions`). +* Utility automatically transfers blueprint input pin values to Flow Node properties if the pin name matches the Flow Node property name. It's also possible to map mismatched names (blueprint pin to Flow Node property) as the utility input parameter. +* Added `UFlowNode::PostImport()` allowing to update a newly created Flow Node just after it got created. + +NOTE. It's NOT meant to be the universal, out-of-box solution as the complexity of blueprint graphs conflicts with the simplicity of the Flow Graph. +However, it might be useful to provide this basic utility to anyone who would like to batch-convert their custom blueprint-based event system to the Flow Graph. diff --git a/docs/Releases/Version15.md b/docs/Releases/Version15.md new file mode 100644 index 000000000..2b03123db --- /dev/null +++ b/docs/Releases/Version15.md @@ -0,0 +1,57 @@ +--- +title: Flow 1.5 +--- + +September 8, 2023. + +This release includes pull requests from the community: Deams51, Drakynfly (Guy Lundvall), eddieataberk (Eddie Ataberk), growlitheharpo (James Keats), jhartikainen (Jani Hartikainen), LindyHopperGT (Greg Taylor), ryanjon2040 (Satheesh), soraphis, twevs. + +This is the first release for UE 5.3, and the last for UE 5.0. + +* [Flow 5.3](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.5-5.3) +* [Flow 5.2](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.5-5.2) +* [Flow 5.1](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.5-5.x) +* [Flow 5.0](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.5-5.0) + +Flow Solo (sample project) releases +* [Flow Solo 5.3](https://github.com/MothCocoon/FlowSolo/releases/tag/v1.5-5.3) +* [Flow Solo 5.2](https://github.com/MothCocoon/FlowSolo/releases/tag/v1.5-5.2) +* [Flow Solo 5.1](https://github.com/MothCocoon/FlowSolo/releases/tag/v1.5-5.1) +* [Flow Solo 5.0](https://github.com/MothCocoon/FlowSolo/releases/tag/v1.5-5.0) + +## New: Call Owner Function +This allows us to call blueprint functions on owning objects from the Flow Graph. This is quite a significant feature added by contributed by LindyHopperGT. + +## Flow Asset +* Calling `CustomOutput` on the Root Flow instance now attempts to call an event on the owning Flow Component. Event it's called `OnTriggerRootFlowOutputEvent`, and it's available in the blueprint version. (contributed by LindyHopperGT) +* Improved `UFlowAsset::GetDefaultEntryNode` returns an unconnected Start Node if a connected one hasn't been found. (contributed by LindyHopperGT) +* Added `HasStartedFlow` method returning True, if any node recorded a pin activation. (contributed by LindyHopperGT) +* Exposed the `GetNodesInExecutionOrder` method to blueprints. Added to it the `UFlowNode* FirstIteratedNode` parameter, allowing to start iteration from any node. +* Removed the default implementation of `UFlowAsset::PreloadNodes`, it was a forever prototype. You can still implement this method on your own. + +## Flow Node +* Added the `DeinitializeInstance` method called on all Flow Nodes in the graph from `UFlowAsset::DeinitializeInstance`. (contributed by LindyHopperGT) +* Exposed the `GetConnectedNodes` method to blueprints. (contributed by eddieataberk) +* Removed `RecursiveFindNodesByClass` method, superseded by `UFlowAsset::GetNodesInExecutionOrder`. + +## Specific Flow Nodes +* Allow the `UFlowNode_ExecutionSequence` node to execute new connections. (contributed by jhartikainen) +* `Play Level Sequence` + * Added Pause and Resume input pins. (contributed by twevs) + * Moved declaration of `FStreamableManager` from `UFlowNode` as no other nodes in the plugin use it. + +## SaveGame support +* `UFlowSubsystem::AbortActiveFlows` is now a blueprintable function. (contributed by soraphis) + +## Editor +* Added the `Asset Defaults` button to the Flow Asset toolbar. (contributed by LindyHopperGT) +* Added hyperlink allowing to open class of the given Flow Asset. Works the same as with blueprints. It's helpful when subclassing `UFlowAsset`. (contributed by Drakynfly) +* Add validation error on `UFlowAsset` for disallowed node classes. (contributed by Deams51) +* Add the ability for FlowAsset child classes to define if they are allowed in subgraph nodes. (contributed by Deams51) +* Fixed `JumpToInnerObject` not working when SearchTree provides the GraphNode itself. (contributed by growlitheharpo) +* Specified categories and setting names for the plugin's UDeveloper class, so it's now easier to find them. (contributed by ryanjon2040) + +## Misc +* Reworked FFlowBreakpoint into FFlowPinTrait - inviting to use for things other than breakpoints. +* Removed the use of `FStreamableManager` in the `UFlowSubsystem`. +* Updated code to build in 5.1 and 5.2 using engine version checks instead of separate code on different plugin branches. (contributed by LindyHopperGT) \ No newline at end of file diff --git a/docs/Releases/Version16.md b/docs/Releases/Version16.md new file mode 100644 index 000000000..4abe77581 --- /dev/null +++ b/docs/Releases/Version16.md @@ -0,0 +1,68 @@ +--- +title: Flow 1.6 +--- + +April 27, 2024. + +This release includes pull requests from the community: DoubleDeez, IllusiveS (Patryk Wysocki), InfiniteAutomaton (Mark Price), jnucc, LindyHopperGT, Maksym Kapelianovych, sergeypdev, Soraphis, VintageDeveloper. + +This is the first release for UE 5.4. It's the last for UE 5.1 and UE 5.2. + +* [Flow 5.4](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.6-5.4) +* [Flow 5.3](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.6-5.3) +* [Flow 5.2](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.6-5.2) +* [Flow 5.1](https://github.com/MothCocoon/FlowGraph/releases/tag/v1.6-5.1) + +Flow Solo (sample project) releases +* [Flow Solo 5.4](https://github.com/MothCocoon/FlowSolo/releases/tag/v1.6-5.4) +* [Flow Solo 5.3](https://github.com/MothCocoon/FlowSolo/releases/tag/v1.6-5.3) +* [Flow Solo 5.2](https://github.com/MothCocoon/FlowSolo/releases/tag/v1.6-5.2) +* [Flow Solo 5.1](https://github.com/MothCocoon/FlowSolo/releases/tag/v1.6-5.1) + + +## Flow Asset +* Added Graph Alignment feature known from blueprints. (submitted by VintageDeveloper) +* Add graph action to delete node and reconnect pins if possible. (submitted by MaksymKapelianovych) +* Change the C++ protection level of remaining runtime methods from private to protected. +* Fixed crash when doing a diff of a Flow Graph vs Perforce. (submitted by InfiniteAutomaton) +* Fixed: Nodes with Context Pins, loose ALL pins on CTRL+Z / can lead to a crash. (submitted by MaksymKapelianovych) +* Fixed incorrect `MustImplement` paths. (submitted by DoubleDeez) +* Fixed Palette does not indent headers for sub-categories. (submitted by Soraphis) +* Fixed: Reroute node does not format correctly when going in reverse. (submitted by VintageDeveloper) +* Fixed: Improve drawing for reroute nodes that go backwards when they are selected, connection is executing ot executed during PIE. (submitted by MaksymKapelianovych) +* `bHighlightInputWiresOfSelectedNodes` set to True by default in the Flow Graph Editor Settings. (submitted by VintageDeveloper) +* Removed unconditional dirtying asset which might occur on constructing graph editor. + +## Flow Node +* Exposed the `CanFinishGraph` and `Finish` methods as public C++ methods. +* Exposed `SetGuid` and `GetGuid` methods to blueprints. +* Add `EFlowNodeDoubleClickTarget::PrimaryAssetOrNodeDefinition` to support opening the Asset and falling back to opening the Node Class if the node doesn't have an asset. (submitted by DoubleDeez) +* Exposing gameplay tag `Match Type` on the `NotifyActor` node. (submitted by IllusiveS) +* Allow optional hot reload for native Flow Nodes. (submitted by sergeypdev) +* Fixed UFlowNode_SubGraph in cooked builds where Asset member could dereference to nullptr, when trying to get the PathName after converting to UObject*. Just try to get the Path Name from the SoftObjectPtr itself instead. (submitted by jnucc) +* Improvements for working with Input/Output pins in BP FlowNodes. (submitted by MaksymKapelianovych) + * Before this change, FlowGraph reflected changes in BP FlowNode only after pressing Compile. If the user wanted to revert changes, Ctrl+Z was reverting only changes in BP FlowNode, but not in the FlowGraph. To show changes in the Graph, Compile needed to be pressed again. Now every change inside Input/Output pins is immediately reflected in opened FlowGraphs and pressing Ctrl+Z will revert changes both in BP FlowNode and FlowGraph + * UFlowNode now also executes OnReconstructionRequested if pin info has been changed inside the pin array (PinName, PinFriendlyName, PinToolTip). + * Removed OnBlueprintPreCompile() and OnBlueprintCompiled() from UFlowGraphNode, because all possible node reconstructions, handled by these functions, now are triggered from UFlowNode::PostEditChangeProperty() + * SFlowGraphNode now hides pin name only if there is one valid pin in the array, not just Pins.Num() == 1. +* Fixed a bug in `CallOwnerFunction` where it would not bind correctly. (submitted by LindyHopperGT) +* Fixed a bug where flow nodes were in the palette, even in inaccessible plugins (now the Flow Node palette respects plugin access rules). (submitted by LindyHopperGT) +* Added expanded `CustomOutput` access. (submitted by LindyHopperGT) +* Clarified editor-accessible vs. runtime-accessible information for custom in/outputs. (submitted by LindyHopperGT) +* `CallOwnerFunction` improvements: Display node description above a node (as for triggers, timers etc). (submitted by MaksymKapelianovych) +* `CallOwnerFunction` improvements: Support for context pins (as for PlayLevelSequence). Now if Params type is changed in referenced function or pins inside Params are renamed/added/removed, CallOwnerFuntion node will not be refreshed. Pins will appear only if user changes referenced function to some other function and then returns back, which is not very convenient. After my changes, Refresh action will update node pins as well. Also, deleted some now excessive function to unify logic for pins refreshing. (submitted by MaksymKapelianovych) +* Fix crash in `UFlowNode_CallOwnerFunction`. (submitted by MaksymKapelianovych) + +## Flow Component +* Split out some functionality for `Flow Subsystem` registration to be called separately (to support our object pooling model). (submitted by LindyHopperGT) + +## Reducing compilation times +* Moved definitions of the log channels to separate headers: `FlowLogChannels` and `FlowEditorLogChannels`. +* Added `#include UE_INLINE_GENERATED_CPP_BY_NAME` to every possible .cpp. It's a new thing in UE 5.1 and it's meant to "improve compile times because less header parsing is required." +* Wrapped editor-only includes with the `WITH_EDITOR`. +* Removed some of the redundant includes indicated by Resharper. +* Moved a few includes to .cpp files. Added forward declaration where needed. +* Non-unity mode build fixes. (submitted by InfiniteAutomaton) + +Misc +* Removed `AFlowWorldSettings::IsValidInstance()` needed in pre-UE5 era. \ No newline at end of file diff --git a/docs/Releases/Version20.md b/docs/Releases/Version20.md new file mode 100644 index 000000000..771489652 --- /dev/null +++ b/docs/Releases/Version20.md @@ -0,0 +1,84 @@ +--- +title: Flow 2.0 +--- + +December 15, 2024. + +This release number is bumped to 2.0 thanks to 2 huge features introduced by Riot Games. They did awesome work this year and chose to contribute to our humble open-source project. + +This release includes pull requests from the community: Benxidosz, Bigotry0, DoubleDeez (Dylan Dumesnil), dyanikoglu (Doğa Can Yanıkoğlu), dzxmxd (Wang Xudong), Joschuka, LindyHopperGT, MaksymKapelianovych, NachoAbril, slobodin (Nikolay Slobodin), Soraphis, SPontadit, Unbansheee. + +This is the first release for UE 5.5, and the last for UE 5.3. + +* [Flow 5.5](https://github.com/MothCocoon/FlowGraph/releases/tag/v2.0-5.5) +* [Flow 5.4](https://github.com/MothCocoon/FlowGraph/releases/tag/v2.0-5.4) +* [Flow 5.3](https://github.com/MothCocoon/FlowGraph/releases/tag/v2.0-5.3) + +Flow Solo (sample project) releases +* [Flow Solo 5.5](https://github.com/MothCocoon/FlowSolo/releases/tag/v2.0-5.5) +* [Flow Solo 5.4](https://github.com/MothCocoon/FlowSolo/releases/tag/v2.0-5.4) +* [Flow Solo 5.3](https://github.com/MothCocoon/FlowSolo/releases/tag/v2.0-5.3) + +## Flow AddOns +AddOns lets us create modular Flow Nodes. This solves the problem of big monolithic Flow Nodes with overly complex code trying to support all use cases of given mechanics in the game. +(contributed by LindyHopperGT, Riot Games) + +Related work +* Restored SFlowGraphNode pin alignment to pre-FlowAddon settings. (contributed by Unbansheee) +* This feature replaces the need to use `Call Blueprint Owner Function`. Please remove any usage of that old feature, it will be removed in the next plugin release. + +## Data Pins +This is initial support for passing property values between Flow Nodes! This implementation is based on Struct Utils introduced with UE5. It doesn't utilize blueprint-specific code. +(contributed by LindyHopperGT, Riot Games) + +Related work +* Initialize Data Pin Struct Values in order to remove errors. (contributed by Benxidosz) + +## Flow Asset +* POTENTIALLY BREAKING CHANGE: Replaced usage of old `FAssetTypeActions` with new `UAssetDefinition`. Added `UAssetDefinition_FlowAsset` implementing a new way of defining editor-only asset properties like asset category. This is a direct replacement for `FAssetTypeActions_FlowAsset`. If you had a custom asset class extending `FAssetTypeActions_FlowAsset`, you must convert it to a class inheriting after `UAssetDefinition_FlowAsset`. + * This is quite a straightforward process. You don't need to register new class in the editor module class. + * Here's a [commit](https://github.com/MothCocoon/FlowGraph/commit/5ef45f328809213024c99e9aba883ace7a104c58) introducing that change. +* Added a search functionality that does not rely on engine modifications. (contributed by dzxmxd) +* Added the `AdditionalNodeIndexing` method to Flow Asset Indexer. This allows developers to add custom logic to Asset Search indexing. (contributed by SPontadit) +* Improvements to Flow Diff. (contributed by Riot Games) + * Fix FlowGraph details diffing by assigning Splitter to the `FDiffControl.Widget` in `SFlowDiff::GenerateDetailsPanel()` + * Add nesting to tree entry display to more easily read the diffs. + * Add individual property diffing with highlights. + * Nest Add-On nodes inside their parent flow nodes. + * Added FFlowObjectDiff to track all the data needed to display an individual tree entry's diff. +* Fixed Diff menu not using the FlowAsset of the current editor when several flow asset editors are opened. (contributed by SPontadit) +* Added `OnDetailsRefreshRequested` delegate. It allows developers to refresh the Asset or Node details panel without adding a boilerplate to projects. +* Made it possible to pass InstanceName to CreateRootFlow. (contributed by dyanikoglu) +* Finish root flow instances without calling finish root flows externally. (contributed by Joschuka) +* Call NotifyGraphChanged when validating an asset to refresh nodes without making the asset dirty. (contributed by SPontadit) + +## Flow Node +* The display style of nodes (the body color) is now controlled by the gameplay tag from the `Flow.NodeStyle` category instead of the `ENodeStyle` enum. (refractor contributed by LindyHopperGT) + * All existing enum options have been recreated as native tags declared in the `FlowNodeStyle` namespace. Pre-existing settings of nodes and color schemes are automatically converted to the new format. + * It also means, you only need a minor code change to use new tags directly. Simply change your C++ code from `EFlowNodeStyle::InOut` to `FlowNodeStyle::InOut. +* Improvements to generating Node Title. (contributed by MaksymKapelianovych) + * Editing `UFlowGraphEditorSettings::bShowNodeClass` now immediately updates node titles in the graph. + * Editing `UFlowSettings::bUseAdaptiveNodeTitles` now immediately updates node titles in the graph. + * `UFlowGraphNode::GetTooltipText()` now calls `UFlowNode::GetNodeToolTip()`. + * Refresh the graph only once, when the node asset is renamed. + * User can now specify node prefixes that will be automatically removed, instead of manually writing custom `meta = (DisplayName = ...)`. Now `UFlowGraphSettings` holds the `NodePrefixesToRemove` array, where user can add their custom node prefixes. By default, it contains two elements: "FN" and "FlowNode". Any duplicating elements will be instantly removed with an error notification. + * To optimize the process of prefix removal, nodes' names without prefixes are generated and stored as custom `GeneratedDisplayName` metadata every time the array is changed. `UFlowNode::GetNodeTitle()` method has been modified to return `GeneratedDisplayName` in case the node class does not have `BlueprintDisplayName` and `DisplayName` metadata, and `UFlowNode::bDisplayNodeTitleWithoutPrefix` == true. Similar changes have been made in `UFlowNode::GetNodeToolTip()`, so now it also returns `GeneratedDisplayName` if possible. +* Added a bunch of const keywords to allow usage of LogError/LogWarning/LogNote in const functions without const_cast. (contributed by MaksymKapelianovych) +* Added check to prevent a crash when deleting two or more node assets (if some of them are in undo history). (contributed by MaksymKapelianovych) + +## Specfic Flow Nodes +* Added `UFlowNode_ExecuteComponent` which executes a UActorComponent on the owning actor as if it was a SubGraph. (contributed by LindyHopperGT) +* `UFlowNode_PlayLevelSequence`: prevent multiple output pins with the same name (contributed by Soraphis) + +## Flow Component +* Added support for replicating variables using the Push Model. (contributed by NachoAbril) +* Moved the "Start or Load of the RootFlow" to a virtual function. (contributed by Soraphis) +* Added `virtual` keyword to `StartRootFlow` and `FinishRootFlow` methods. (contributed by dyanikoglu) + +## Misc +* Refactored plugin's code to utilize TObjectPtr for raw UPROPERTY pointers. Some projects might have TObjectPtr enforcement enabled starting from UE 5.5, and this prevented the plugin from compiling. (contributed by dyanikoglu) +* Change the FlowEditor loading phase to `PreDefault` to fix corrupting FlowAssets when DefaultPawnClass for GameMode is set in C++ (as in Epic's templates). (contributed by MaksymKapelianovych) +* Replaced the monolithic header include (PropertyEditing.h) with the corresponding includes. (contributed by SPontadit) +* Fixed compilation for iOS. (contributed by slobodin) +* Fixed compile failure due to default C++ standard below 20. (contributed by Bigotry0) +* Fixed short type name warnings. (contributed by DoubleDeez) diff --git a/docs/Releases/Version21.md b/docs/Releases/Version21.md new file mode 100644 index 000000000..78e4b125f --- /dev/null +++ b/docs/Releases/Version21.md @@ -0,0 +1,94 @@ +--- +title: Flow 2.1 +--- + +June 14, 2025. + +This release includes pull requests from the community: 39M (Nil), Deams51 (Mickaël), fchampoux (pixelchamp), HomerJohnston (Kyle Wilcox), IRSMsoso, jnucc, LindyHopperGT, Maksym Kapelianovych, MichaelCenger (Michael Cenger), Numblaze, Rhillion, Ryan DowlingSoka, Soraphis. + +This is the first release for UE 5.6, and the last for UE 5.4. + +* [Flow 5.6](https://github.com/MothCocoon/FlowGraph/releases/tag/v2.1-5.6) +* [Flow 5.5](https://github.com/MothCocoon/FlowGraph/releases/tag/v2.1-5.5) +* [Flow 5.4](https://github.com/MothCocoon/FlowGraph/releases/tag/v2.1-5.4) + +Flow Solo (sample project) releases +* [Flow Solo 5.6](https://github.com/MothCocoon/FlowSolo/releases/tag/v2.1-5.6) +* [Flow Solo 5.5](https://github.com/MothCocoon/FlowSolo/releases/tag/v2.1-5.5) +* [Flow Solo 5.4](https://github.com/MothCocoon/FlowSolo/releases/tag/v2.1-5.4) + +## Flow Subsystem +* Modified `UFlowSubsystem::RemoveSubFlow`. Now we're invalidating the `AssetInstance->NodeOwningThisAssetInstance` pointer after calling `AssetInstance->FinishFlow`, as this point may be needed in the FinishFlow method. (contributed by fchampoux) +* Exposed `CreateFlowInstance` as a public method. Useful in projects where Root-SubGraph relations are "replaced" with a loose set of graphs, i.e. card games. + * Moved `LoadSynchronous()` calls out of this method, so external code is allowed to use async asset loading. + * Methods operating on asset templates - `AddInstancedTemplate`, `RemoveInstancedTemplate` - turned back to `protected`. It turns out, it doesn't make sense to duplicate the logic of the CreateFlowInstance method. +* Allow multiple instances on loading Root Flow. (contributed by Numblaze) + +## Flow Asset +* Graph refresh refactored. (contributed by HomerJohnston) + * The goal of this PR is to make the graph editor stop refreshing the entire graph during as many events as possible. It also fixes minor bugs related to orphaned pins, running undo/redo commands on edge cases, and unnecessary graph dirtying on edge cases, such as when nodes contain orphaned or invalid pins. + * **BREAKING CHANGE**. Public void `UFlowGraphNode::RefreshContextPins(bool)` changed to protected void `UFlowGraphNode::RefreshContextPins()`. The existing system has some logic that could result in attempts to recursively call ReconstructNode -> RefreshContextPins -> ReconstructNode -> RefreshContextPins. Currently, there are booleans to guard against this, but both methods are exposed and this feels unclean. + * `UFlowGraphNode::PostLoad` now rebuilds the unserialized InputPins and OutputPins arrays using the serialized Pins array. + * `UFlowAsset::HarvestNodeConnections` - made this function able to harvest a single node, or harvest the whole graph (pass in nullptr to harvest the whole graph, or pass in a single node to harvest just that node). Then `UFlowGraphNode::NodeConnectionListChanged` updates node connections for only a single node when its connections are changed. + * Changed the right-click context menu command, "Refresh Context Pins", to "Reconstruct Node". + * Modified the following functions to only refresh individual nodes instead of the whole graph by changing NotifyGraphChanged() calls to NotifyNodeChanged(Node): `FFlowGraphSchemaAction_NewNode::CreateNode`, `UFlowGraphSchema::TryCreateConnection`, `UFlowGraphSchema::BreakNodeLinks`, `UFlowGraphNode::RemoveOrphanedPin`, `UFlowGraphNode::AddInstancePin`, `UFlowGraphNode::RemoveInstancePin`. + * `FlowGraphSchemaAction_NewNode::CreateNode` now runs ReconstructNode() instead of only spawning default pins. + * `UFlowGraphNode::OnGraphRefresh` now runs ReconstructNode() instead of RefreshContextPins(), on all nodes. + * `UFlowGraphNode::RefreshContextPins` - no longer runs if an editor transaction is in progress (running undo/redo). + * `UFlowGraphNode::HavePinsChanged` - now disregards any orphaned pin connections. + * Added a new delegate `UFlowGraphNode::OnReconstructNodeCompleted` to trigger the rebuild of SFlowGraphNode widgets whenever ReconstructNode is called. + * `SFlowGraphNode` added destructor to unbind from UFlowGraphNode delegates. + * `UFlowGraphSchema`added virtual bool ShouldAlwaysPurgeOnModification() const override { return false; } to reduce unnecessary graph refreshes. + * `UFlowNodeAddOn` - GetContextInput/OutputPins functions changed to ignore invalid pins and write a log warning for invalid pins. + * Post-refactor fix: copy-paste breaking reroute node directionality. (contributed by Ryan-DowlingSoka) +* Added "Select" button, which will select and focus the Custom Event node with the same name. If such a node does not exist in the graph, the button will be inactive. (contributed by Maksym Kapelianovych) +* Added menu and toolbar extensibility managers. This allows new options to be added to the menu and toolbar for custom flow asset types without the need to extend/override FFlowAssetEditor. (contributed by MaksymKapelianovych) +* Fixed Linux packaged build by passing by reference. (contributed by IRSMsoso) + +## Flow Node +* `UFlowNode::TriggerOutput` won't execute its logic if a given node is deactivated. (contributed by 39M) +* Fixed `UFlowNode::Branch` never deactivating after checking its condition. (fix suggested by Geckostya) +* Added missing calls to the parent class `Cleanup` method. This fixes calling Cleanup on the attached AddOns. (contributed by Rhillion) +* Fixed: Node pass-through does not work under closed-loop conditions. (contributed by MaksymKapelianovych) +* Several improvements to copy/pasting nodes. (contributed by MaksymKapelianovych) + * Reset EventName in `UFlowNode_CustomInput` after duplicating or copying/pasting. + * Fix pasting nodes into Flow Asset when Flow Asset cannot accept such nodes (node or asset class is denied). + * If among selected nodes there are nodes that cannot be deleted, they will stay in the graph as is, and all "deletable" nodes will be deleted (currently, none of the nodes will be deleted in such a case). +* Exposed ability to override node's Category via `OverridenNodeCategories` list in Flow Graph project settings. This way, you can fully reorganize the Flow Palette to your liking. (inspired by LindyHopperGT's proposal) +* The first-ever update to built-in node categories. (inspired by LindyHopperGT's proposal) + * I partially accepted a proposed update to the category layout. I updated the defaults, which are flatter than proposed, as this would be enough for projects starting with a limited number of project-specific nodes. + * It feels good to refresh categories after a few years of using that palette. The `World` category has been effectively renamed `Actor` since a growing number of projects embrace ECS. It's useful to separate nodes tied to the OOP paradigm from multi-threaded ECS. + * **BREAKING CHANGE**. Flow Node source files have been moved to matching folder names. If your code contains C++ classes referencing built-in Flow Node classes, you might need to update your includes. +* Fixed the ability to paste nodes while some need is still selected. (contributed by jnucc) +* Added custom Make/Break implementation for the `FFlowPin` struct to avoid using the "BlueprintReadWrite" specific - since that would block users from compiling out cosmetic properties in non-editor builds. (contributed by LindyHopperGT) +* Added additional constructors for `FFlowDataPinResult_Enum(`) used in an AI Flow node: "GetBlackboardValues". (contributed by LindyHopperGT) +* `SubGraph` node: added null checks to fix crash while attempting to load Flow Asset removed with "Force Delete" option. +* Removed a deprecated `Call Owner Function` feature. +* Adaptive Build fix for when the entire FlowGraph is built as separate source files. (contributed by jnucc) + +## Flow Debugger +* There are several improvements to Flow breakpoints. + * Node and pin breakpoints are now stored locally for every user. This is thanks to moving data structures out of `UFlowGraphNode`; we no longer need to save assets to remember added breakpoints. Data is now stored in .ini, which was created for this purpose, `UFlowDebuggerSettings`. Logic is handled by the new `UFlowDebuggerSubsystem` class. (contributed by Maksym Kapelianovych) + * Any breakpoints set on older plugin versions will vanish. + * POTENTIALLY BREAKING CHANGE: `FFlowPinTrait` struct has been renamed to `FFlowBreakpoint` and moved to the `FlowDebugger` module. If you utilize this struct in your custom code, please re-add it to your project code. + * Added a new module to the Flow plugin, `FlowDebugger`, which is a `DeveloperTool` module. This is where `UFlowDebuggerSubsystem` lives. This is extended by the pre-existing editor class: `UFlowDebugEditorSubsystem`. + * Refactored old breakpoint logic to open doors to building a separate cook-only graph debugger. If anyone would be willing to implement such a feature, of course. This would require much work, but the Flow Graph community keeps surprising! + * Decoupled triggering breakpoints from the Flow Editor module. `UFlowGraphNode` no longer operates on breakpoints. Instead, `UFlowDebuggerSubsystem` binds directly to the runtime `OnPinTriggered` delegate. + * Pin breakpoint is now identified as `NodeGuid` and `PinName` instead of `UEdGraphPin`. Thanks to this, it's now possible to bind to the `OnPinTriggered` delegate outside of the editor! Events shall be received in the Standalone game, non-shipping game builds. + * Wrapped `UEdGraphNod` pointer in runtime Flow Node class with `WITH_EDITORONLY_DATA` since this isn't even loaded in a Standalone game. + * Now it's possible to add custom logic to "On Pin Triggered" logic outside of the runtime module. You can add what you want by extending UFlowDebuggerSubsystem or UFlowDebugEditorSubsystem. +* Prevented occasional crashes while dispatching permanent error messages. (contributed by jnucc and Deams51) +* Added `CanPlaceBreakpoints()` functionality in `UFlowGraphNode`, return false on `UFlowGraphNode_Reroute`. (contributed by jnucc) + * Added conditions to check if `CanPlaceBreakpoint()` on the selected node is true. Also, since multiple breakpoints on nodes can be set/unset at the same time, added the checks in these functions too. Since they are in a for loop, don't return immediately at the first element, but check if it makes sense to return true on the first item that returns true. +* Fixed breakpoints that could erroneously be added on reroute nodes through the context menu, or through F9, while not being visible at all. + +## Flow Component +* Added `TriggerRootFlowCustomInput` method. (contributed by Soraphis) +* Renamed methods related to Custom Output events, now called `BP_OnRootFlowCustomEvent` and `OnRootFlowCustomEvent`. +* Fixed a few issues with `IdentityTags` replication. (contributed by MichaelCenger) + * Changes to identity tags while offline (NM_Standalone) did not replicate to clients if they went online at some later point. + * Changes to identity tags on the server before BeginPlay was called on the Flow Component would never replicate to clients. + * Multiple calls to `AddIdentityTag(s)` or `RemoveIdentityTag(s)` within a single net update did not get replicated properly. It would only replicate the last of each respective operation. + + + diff --git a/docs/Releases/Version22.md b/docs/Releases/Version22.md new file mode 100644 index 000000000..c9322d7f3 --- /dev/null +++ b/docs/Releases/Version22.md @@ -0,0 +1,187 @@ +--- +title: Flow 2.2 +--- + +February 28, 2026. + +This release includes pull requests from the community: ameaninglessname, bohdon (Bohdon Sayre), Danamarik, DemonViglu, GreggoryAddison-AntiHeroGameStudio (Greggory Addison), gregorhcs (Gregor Sönnichsen), j0tt (Jeff Ott), LindyHopperGT, Maksym Kapelianovych, Numblaze, Rhillion, SilentGodot (Mor Ohana). + +This is the first release for UE 5.7, and the last for UE 5.5. + +* [Flow 5.7](https://github.com/MothCocoon/FlowGraph/releases/tag/v2.2-5.7) +* [Flow 5.6](https://github.com/MothCocoon/FlowGraph/releases/tag/v2.2-5.6) +* [Flow 5.5](https://github.com/MothCocoon/FlowGraph/releases/tag/v2.2-5.5) + +Flow Game (sample project) releases +* [Flow Game 5.7](https://github.com/MothCocoon/FlowGame/releases/tag/v2.2-5.7) +* [Flow Game 5.6](https://github.com/MothCocoon/FlowGame/releases/tag/v2.2-5.6) +* [Flow Game 5.5](https://github.com/MothCocoon/FlowGame/releases/tag/v2.2-5.5) + +## Post-update actions +### Flow plugin is now DISABLED by default +Change suggested Riot Games, but the idea isn't new. This plugin tends to be part of the codebase shared between projects in studios. In that case, it is desired to have this plugin as part of a shared codebase or repository, but it's not desired to have the plugin enabled for every project. + +The easiest way to re-enable the plugin in your project is to edit your .uproject file and add this section to the Plugins list. +``` +{ + "Name": "Flow", + "Enabled": true +}, +``` + +### Resave all Flow Assets +This version automatically updates data related to Data Pins. Many of your graphs will be marked as dirty, even if you don't use this feature. It's best to resave all graphs at once. + +If you're using Data Pins, please: +* Read about related changes below. +* Smoke test if all data has been properly updated. + +## Flow Asset +* Added `UFlowAsset::GatherNodesConnectedToAllInputs` helper function. (contributed by Riot Games) +* Fixed the issue with the unwanted multiplication of the Comment node while copy-pasting it. +* Made `UFlowAsset::IsBoundToWorld()` const. (contributed by gregorhcs) +* Removed the old way of injecting own Main Menu items into the Flow Asset editor, by using `FExtensibilityManager`. The deprecation message tells the programmer how exactly the code should be updated. + +## Flow Subsystem +* Exposed `UFlowSubsystem` runtime state fields to subclasses. (contributed by gregorhcs) + +## Flow Node +* Trigger Outputs deferred while processing an Input Trigger (contributed by Riot Games: LindyHopperGT) + * This is a change to the core Flow input triggering logic to fix a category of sequencing bugs from the previous behavior. It would immediately fully process a triggered input and so on down the chain of Flow Nodes, without allowing the current Flow Node to finish executing, this caused a whole category of problems where the node wasn't able to finish its execution before being interrupted by a retirgger (from downstream) and AddOns wouldn't execute at the same time as their owning flow node reliably. + * Now, Flow Asset will queue any triggers generated while processing a trigger, and flush them when ending the processing of that trigger. + * Also integrated the debugger queued trigger caching mechanism to use the same system. + * Subclasses of `UFlowAsset` that do their own deferred asset triggering can disable this feature, except for the debugger portion, which is still processed using the `UFlowAsset` queue. +* Improvements to node validation. (contributed by Numblaze) + * Added support for node validation via Blueprints using a new `K2_ValidateNode()` function. + * Blueprints can now log validation messages (errors, warnings, notes) using new `LogValidation` functions exposed on `UFlowNodeBase`. + * Flow Asset Validation Logs All Severities. Before the fix, validation logged only warnings and notes if at least one error was present. After fix: UFlowAsset validation logic ensures that all node validation messages are logged, regardless of their severity + * The node validation code has been moved from UFlowNode to UFlowNodeBase to allow validation of AddOns. +* AddOns + * AddOns can now include data pins, which show up as pins on their owning node. (contributed by Riot Games: LindyHopperGT) + * Updated the ForEachAddOn templates to support a parameter to control how the function should recurse into child addons (or not). (contributed by Riot Games: LindyHopperGT) + * Extracted `UFlowNodeAddOn::FindOwningFlowNode()` functionality into a function. (contributed by Riot Games: LindyHopperGT) + * Added AddOn descriptions to node descriptions, with an editor settings option to disable. (contributed by Rhillion) +* Exposed methods to `public` to enable Flow Node status display in runtime debuggers. (contributed by gregorhcs) +* Added option to tone down on-screen error messages. (contributed by gregorhcs) +* Added support for custom overlay icons for Flow Nodes. (contributed by Riot Games: LindyHopperGT) + * `FlowNodeBase::GetCornerIcon()` allows you to easily define an icon to draw in the top-right of the Node and handles positioning for you. + * `FlowNodeBase::GetOverlayIcons()` allows you to define any number of overlay icons and custom positioning. + * Added code in SFlowGraphNode to query and draw the custom overlay icons. +* Added a virtual method to allow saving non-executing flow nodes. (contributed by Riot Games: LindyHopperGT) +* Added transaction for changing the signal mode. (contributed by Maksym Kapelianovych) +* Added an `IsFinishedState()` classifier function for `EFlowNodeState`, to error-proof checking node state for "finished" states. (contributed by Riot Games: LindyHopperGT) +* Made `CanFinishGraph` a BlueprintImplementableEvent. (contributed by Riot Games: EvanC4) +* Added `GetRandomSeed()`. The default version uses the hash from the node's GUID. This can be overridden in subclasses (which we do) to any implementation that suits the client code. (contributed by Riot Games: LindyHopperGT) +* `OnNodeDoubleClicked` logic moved to the Graph Node itself, allowing for overriding the default logic. +* Added `FLOW_API` to `GetFlowPinType()` functions to allow them to be called from extension plugins. (contributed by Riot Games: LindyHopperGT) +* Added enum-based constructors for `FFlowDataPinResult` structs. (contributed by Riot Games: LindyHopperGT) +* Renamed `FFlowNamedDataPinOutputPropertyCustomization`, which is now also used for input pins. (contributed by Riot Games: LindyHopperGT) +* Fixed some Datapin result logic. (contributed by DemonViglu) +* Fixed rare crash in LogError(..) caused by invalid flow node self or owner. (contributed by gregorhcs) + +# Specific Flow Nodes +* Added `bUseAsyncSave` option to `UFlowNode_Checkpoint`. This option can be changed by adding an entry to the `DefaultGame.ini`. (based on changelist submitted by j0tt) +* Fixed `UFlowNode_OnNotifyFromActor` node not using identity tag match type. (contributed by SilentGodot) +* `UFlowNode_Reroute` now supports Data Pins. Previously, only Exec pins were supported. (contributed by Riot Games: LindyHopperGT) +* Added `UFlowNode_FormatText` that formats text using input pins and the FText formatting engine. (contributed by Riot Games: LindyHopperGT) +* Changed the `UFlowNode_Log` to format text (a la `UFlowNode_FormatText`) to generate its logged output string. (contributed by Riot Games: LindyHopperGT) +* Changed `UFlowNode_Log` to inherit from `UFlowNode_DefineProperties`, so that it can have input properties added on the instance. (contributed by Riot Games: LindyHopperGT) +* Renamed `UFlowNode_DefineProperties::OutputProperties` to `NamedProperties`, so that it can be used as the super class for `UFlowNode_FormatText`. (contributed by Riot Games: LindyHopperGT) +* Fixed `UFlowNode_ExecuteComponent` to handle injected components correctly in validation. (contributed by Riot Games: LindyHopperGT) +* Fixed `UFlowNode_ExecuteComponent` to conform to the new style of pin generation, now using ContextPins (the old method didn't work after a refactor with flow graph node reconstruction). (contributed by Riot Games: LindyHopperGT) +* Updated `UFlowNode_ExecuteComponent` to allow the component to supply data pin output values. (contributed by Riot Games: LindyHopperGT) +* Updated FlowNode_Branch: AND/OR selectability and BranchCase support. (contributed by Riot Games: LindyHopperGT) + * Branch node can now be configured with AND/OR for the top-level combination rule (default is still AND). + * Branch node can now have BranchCase AddOns at its root addon level, these will be evaluated and can trigger prior to evaluating the root predicates (which serve as the "else" case for this sort of configuration). + * BranchCase adds a switch/case-like capability where each BranchCase can have its own output exec pin, if that case's predicates pass +* Switch Case AddOns' names are authorable. (contributed by Riot Games: LindyHopperGT) + * If you set the name, it will match the pin 1:1 (unless duplicates). + * Default case name (and pin name) is "Case". + * Duplicates are disambiguated with numbers appended. + * Sets the title to the pin name, so it's easier to match the pin to the case + +## Data Pins refactor and improvements +Benefits +* Arrays are now supported for data pins. +* Cross-conversion between like-types is supported (eg, int<->float, tag<->tag container, etc.) when connecting pins, including all standard data pin types convertible to strings (primarily for logging and dev). +* User-addable type framework. It's definitely an advanced feature, but it is possible to extend Flow via your plugin to add new data pin types. +* Less boilerplate for nodes, etc. that interact with data pins. +* "Mostly" backward compatible with the old `FFlowDataPinProperty` classes and old TryResolveDataPin functions. The data side should auto-convert your flow graph pins without hand-fixes. It "should" work with the older classes and API in blueprint and Flow Graph data-space, but you will want to convert your pin properties to the new wrapper classes `FFlowDataPinValue` and the new, unified results signature. There's a suite of new blueprint functions to make using data pins in blueprint Flow Nodes, etc. easier. The backward compatibility support is designed to preserve authored data through the transition process, and it worked for internal Riot Games use cases. Riot intends to preserve the legacy support code for one UE minor release (~3 months) to allow projects to update. + +Detailed changelog +* Incremented `UFlowGraph::GraphVersion` to 2, created a data migration function (UpgradeAllFlowNodePins). +* `FlowPinType` namespace templates for the bulk of the "Supply/Resolve" pipeline support for data pins. +* Updated standard `FFlowPinType` and `FFlowDataPinValue` subclasses to use them with the new resolve pipeline. +* Reworked FlowSchema's pin compatibility checks to be more orderly, simpler, and data-driven connectivity. +* Created policies for schema connectivity rules for the standard types. +* Updated `FFlowNamedDataPinProperty` to use `FFlowDataPinValue` as its property payload (including migrate functions from the old data). +* Updated` FlowDataPinBlueprintLibrary` with new auto-converts and functions to support data pin manipulation in blueprint. +* Updated `FlowNodeBase` with the new Resolve pathway entry points & related refactors. +* Updated `FlowNode` with the new Supply pathway entry points & related refactors. +* Updated `FlowPin` to deprecate Enum PinType and add ContainerType (for Array data pins) and PinTypeName (the replacement for PinType Enum). +* Removed overrides `TrySupplyDataPinAs...` (now replaced by general version). +* Removed `TrySupplyDataPinAs...` variants from the IFlowDataPinValueSupplierInterface (leaving only the general replacement). +* Ported `TryResolveDataPinAs...` specialized versions to use the general version internally. +* Adapted uses of `TryResolveDataPinAs...` to the general version TryResolveDataPin. +* FlowNode (and AddOn) details customizations now inherit from `TFlowDataPinValueOwnerCustomization`, which adds a RequestRebuild() for rebuilding flow node details in a way that correctly rebuilds the `FFlowDataPinValue` customizations. +* Refactored FlowAsset's automatic pin generation mechanism to be cleaner, simpler, and work with the new system. +* Details customizations for `FFlowDataPinValue` specific subclasses. +* Details customization for `IFlowDataPinValueOwnerInterface` implementers (via template). +* Added some details customization rebuild hooks into `IFlowDataPinValueOwnerInterface` to support `FFlowDataPinValue` details rebuilding. +* Updated some flow nodes to use new Resolve functions (eg, Log, DefineProperties, Start, FormatText, etc.). +* Slighly reworked `FFlowPinSubsystem` API. +* Created new test classes and assets in `FlowGraph_DataPinsTest`. +* Converted FlowDataPinValueSupplierInterface to C++ only. The blueprint functionality for this has been removed. +* Added FlowNode_BlueprintDataPinSupplierBase. This is a thin C++ class that exposes TrySupplyDataPin override capability for blueprint if they need it. +* Flow Asset Params improvements. + * Can now statically assign AssetParams to use on a Subgraph node for the associated Flow Asset. + * Can also dynamically source the AssetParams to use via an input data pin for the Subgraph node. + * If the Subgraph node pins are connected, the connected supplier is preferred over the `FlowAssetParams`. Otherwise, the FlowAssetParams object (if chosen) will supply the value (this is the new part). + * If no assigned FlowAssetParams, the Start node in the subgraph's default values are used (this was pre-existing behavior). + * Fixed some code in and around sourcing subgraph Start node pin values to allow this sourcing to work properly. + * Fixed some issues preventing the tooltip inspection for data pins in subgraphs from correctly finding the correct source value. +* For a reroute node, connecting to a new type with a data pin will change its type and break any incompatible connections to the reroute. Also fixed copy/paste reroute nodes losing their type. + +(Contributed by Riot Games: LindyHopperGT) + +## Breakpoint debugger +* Functional improvements. (based on the changelist submitted by Maksym Kapelianovych) + * Open the Flow Asset editor on breakpoint hit instead of just freezing. + * Added Debug Menu allowing to enable/disable/remove all breakpoints in the graph. + * Added initial support for filtering out inspected instances by world. This is primarily for multiplayer to separate server instances from client ones. + * Select the correct flow asset instance, instead of the first one spawned with the same name in multiplayer. +* Functional improvements. (Contributed by Riot Games: LindyHopperGT) + * Debugger will stop at breakpoints instead of continuing to trigger. + * Data Pin values are visible in the debugger as tooltips. +* Visual improvements. (submitted by Maksym Kapelianovych) + * In the top right corner of the graph, the PIE status will be displayed, based on the selected world/instance. + * Visual changes for breadcrumbs (added trailing delimiter, background color, max width). + * Clicking on a breadcrumb now will focus the `SubGraph` node that created the inspected instance. + * The button `Go to Parent` was removed from the toolbar (the same can be achieved with breadcrumbs), but the command was kept to retain the ability to use a shortcut for this action. + +## Flow Search +* Updated Search UX, displays the source(s) where the search terms were found. +* Multi-asset search capability (same time or all flow assets). +* Increased number of search sources (Eg, tooltips or property values). +* Filters to control which search sources to include. +* Improved inline object, struct and subgraph search capabilities. +* Caching search metadata for each Flow Asset, for faster re-searches, updated when the asset changes. +(Contributed by Riot Games: LindyHopperGT) + +## Misc +* Made `UFlowComponent::NotifyFromGraph` a BlueprintCallable. (contributed by Riot Games: EvanC4) +* Deprecated redundant `UFlowSettings::Get()`, it's cleaner to call `GetDefault()`. +* Deprecated redundant `UFlowGraphSettings::Get()`, it's cleaner to call `GetDefault()`. +* Deprecated redundant `UFlowGraphEditorSettings::Get()`, it's cleaner to call `GetDefault()`. +* Fixed an issue where breakpoint overlay brushes weren't showing properly due to deprecated code in 5.6. (contributed by Greggory Addison) +* Preventing a crash while using Ctrl + Shift + X shortcut of the `BlueprintAssist` plugin. (contributed by Numblaze) +* Exposed some methods related to custom blueprint nodes. (contributed by Danamarik) +* Non-unity build fixes. (contributed by bohdon) +* Fixed: build error when making an installed build. (contributed by ameaninglessname) +* Removed enforcing cpp20 in the `FlowEditor.Build.cs`. This an engine default since UE 5.3. +* Code style update + * Using style for multi-line comments for all comments above method and property declarations. Sentences end with a dot. + * Using only a single asterisk to begin a multi-line comment as this lets us align text better. + * Update class, structs, enums and interfaces descriptions to use the style of multi-line comment. + * Removed parentheses in comments as this actually reduces cognitive load (while using multi-line comment style), I guess? + * Removed empty line between copyright and "#pragma once" to make these lines more compact. diff --git a/docs/Releases/Version23.md b/docs/Releases/Version23.md new file mode 100644 index 000000000..c542d5a5c --- /dev/null +++ b/docs/Releases/Version23.md @@ -0,0 +1,69 @@ +--- +title: Flow 2.3 (in works) +--- + +This is the upcoming release. This page is updated regularly after changes are pushed to the repository. + +This release includes pull requests from the community: Bargestt (Vasilii Bulgakov), Chen-Gary (Gary Chen), dskliarov-gsc, EvanC4, LindyHopperGT (Riot Games). + +This is the first release for UE 5.8, and the last for UE 5.6. + +## Update Notes +### Critical warning for Data Pins users +If you were using Data Pins in your assets prior to Flow 2.2, do not upgrade directly from your current Flow Graph version to the version newer than 2.2. +Version 2.2 came with a huge Data Pins refactor and it requires data migration occuring while loading assets. +* Update first to the Flow Graph 2.2. +* Resave all Flow Graph assets. +* Continue with updating to newer Flow Graph version. + +### Changed Flow Node constructor to GENERATED_BODY +This is BREAKING CHANGE. It requires updating constructors for C++ Flow Nodes. It's simple, but it will take a few minutes for large projects. + +## Flow Node +* Added Paste option to the right-click menu for Flow Nodes. Formerly could only paste onto a Flow Node using Ctrl + V. (contributed by LindyHopperGT) +* Added Attach AddOn drop-down to Flow Node (and AddOn) details. Adds a more convenient method for attaching addons that is fewer-clicks per operation and a bit less hidden. This is in addition to the right-click menu on these nodes. (contributed by LindyHopperGT) +* Setting `UFlowNode` pointer on AddOns more reliably in editor. Updated the node pointer in editor for AddOns so that it is usable any time while in editor, can updated when addons are moved/rebuilt. (contributed by LindyHopperGT) +* Updated Flow Palette filters. (contributed by LindyHopperGT) + * Improved the Flow Palette filtering by category to also check superclasses of the `UFlowAsset` subclass being edited. + * The palette filtering can apply a strict or tentative result, as it crawls up the superclass lineage. +* Refactored the IsInput/OutputConnected interface to be more useful & pin connection change event improvements. + * These new function signatures provide multiple connections when present, and also have `FConnectedPin` structs as their container + Updated existing calls to these functions to use the new signatures. + * Augmented the event for `OnConnectionsChanged` to have an array of changed connections instead of the old connections array. + * This new version `OnEditorPinConnectionsChanged` is called on all of the addons on a Flow Node as well, and has a blueprint signature. + * This change allows nodes to be more reactive to pin connections changing (like changing their Config Text), which was possible before, but not smooth. + * Updated some Flow Nodes (Log, FormatText) to update their config text on pin connection changes. + +## Specific Flow Nodes +* `UFlowNode_Reroute` (contributed by LindyHopperGT) + * Reroute nodes can now retype themselves if connected to a new type (and in doing so, break incompatible connections). + * Copy/paste for data pin reroutes preserves the type of the reroute (was being lost). +* Fixed ExecutionLimit in `FlowNode_LogicalOR`. Surprisingly, it wasn't working with `ExecutionLimit` higher than 1. (contributed by Chen-Gary) +* `UFlowNode_FormatText` + * Fixed crash: prevent Getting the value of an invalid FFlowDataPinValue property. (contributed LindyHopperGT) + * Fixed node not supporting the Format Text input connection. (contributed by dskliarov-gsc) +* Added `PredicateRequireGameplayTags` AddOn: a data pin version. Blackboard version lives in AIFlowGraph plugin. (contributed by LindyHopperGT) +* Added `CompareValues` predicate (for data pins) and auto-generate data pins refactor. (contributed by LindyHopperGT) + * Refactored the auto-generate data pins code so that the CompareValues predicate can get its pins generated, duplicates disambiguated and the results queried. + * Created CompareValues predicate, which is analogous to the Compare Blackboard Values predicate, but for data pins. + +## Flow Asset +* Fixed: assets didn't show dirty and version control statuses. (contributed by Bargestt) +* Fixed dirtying graph on copy. (contributed by Bargestt) +* Fix invalid instance class when pasting. (contributed by Bargestt) +* Refactored LogError/LogWarning/LogNote. Extracted shared LogRuntimeMessage() helper. Three identical copy-pasted functions → one implementation with severity parameter. (contributed by LindyHopperGT, improved by Bargestt) +* Fixed `TryFindActorOwner()` to correctly return the Owner when it is already an AActor, not just when it's a component. Fulfills the documented contract. (contributed by LindyHopperGT) +* Crash fix in `CancelAndWarnForUnflushedDeferredTriggers()`. Null-guard ToNode and FromNode before dereferencing in UE_LOG. Prevents crash during abnormal termination when nodes are already destroyed. (contributed by LindyHopperGT) +* Introduced `FFlowPolicy` instanced-struct policy meant to handle various project-specific policies. Refactored Pin Connection policy to use it. (contributed by LindyHopperGT) + +## SaveGame support +* Refactored SaveGame integration to allow for arbitrary save data container objects. (inspired by gregorhcs) + * Added variants of `OnGameSaved` and `OnGameLoaded`: accepting `TArray` and `TArray` as input parameters. + * Added creating a transient `UFlowSaveGame` object in the new `OnGameLoaded` variant. This way, the plugin can keep operating on `UFlowSaveGame` as a container for these 2 arrays, but projects can store these arrays whenever they want. + * Refactored input parameters of `GetLoadedComponentRecord` and `GetLoadedAssetRecord` for easier integration with project-specific save systems. (contributed by Bargestt) +* Added `CanSave` check to `UFlowComponent`. Allows for transient Flow graphs and components that are never saved. (contributed by Bargestt) + +## Misc +* Fixed one of `UFlowSubsystem::FindComponents` variants which could return no components if method has been called with the following parameters: EGameplayContainerMatchType::All and bExactMatch = false. (contributed by Bargestt) +* In `UFlowGraphSettings`, all occurences of hard refences `TSubclassOf` have been changed to `TSoftClassPtr`. In general, TSubclassOf should be avoided to prevent automatic loading of unnecessary assets. In case of plugins, using hard reference was using issue with loading some assets defined in other plugins. Since `UFlowGraphSettings` could load assets before other plugin is loaded. (contributed by Chen-Gary) +* Fix crash when `OnMapOpened` delegate fires after `SLevelEditorFlow` destruction. (contributed by EvanC4) diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 000000000..e1a08a4a8 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,12 @@ +theme: jekyll-theme-midnight +title: Flow Graph +description: Design-agnostic node system for scripting game’s flow in Unreal Engine. + +defaults: + - scope: + path: "" + values: + layout: default + +sass: + silence_deprecations: [import] diff --git a/docs/_data/navigation.yml b/docs/_data/navigation.yml new file mode 100644 index 000000000..688c8cf19 --- /dev/null +++ b/docs/_data/navigation.yml @@ -0,0 +1,67 @@ +- title: Overview + docs: + - title: Concept + url: /Overview/Concept + - title: Getting Started + url: /Overview/GettingStarted + - title: How To Contribute + url: /Overview/HowToContribute + - title: FAQ + url: /Overview/FAQ + +- title: Features + docs: + - title: Asset Search + url: /Features/AssetSearch + - title: Force Pin Activation + url: /Features/ForcePinActivation + - title: Generic Gameplay Tag Events + url: /Features/GenericGameplayTagEvents + - title: Save Game Support + url: /Features/SaveGameSupport + - title: Signal Modes + url: /Features/SignalModes + +- title: Guides + docs: + - title: Adding Node Spawn Shortcut + url: /Guides/AddingNodeSpawnShortcut + - title: Comparison to GAS + url: /Guides/ComparisonToGAS + +- title: Flow-based + docs: + - title: Games + url: /FlowBased/Games + - title: Plugins + url: /FlowBased/Plugins + +- title: Releases + docs: + - title: Version 2.3 (in works) + url: /Releases/Version23 + - title: Version 2.2 + url: /Releases/Version22 + - title: Version 2.1 + url: /Releases/Version21 + - title: Version 2.0 + url: /Releases/Version20 + - title: Version 1.6 + url: /Releases/Version16 + - title: Version 1.5 + url: /Releases/Version15 + - title: Version 1.4 + url: /Releases/Version14 + - title: Version 1.3 + url: /Releases/Version13 + - title: Version 1.2 + url: /Releases/Version12 + - title: Version 1.1 + url: /Releases/Version11 + - title: Version 1.0 + url: /Releases/Version10 + +- title: Forks + docs: + - title: Notable Pull Requests + url: /Forks/NotablePullRequests diff --git a/docs/_includes/head-custom.html b/docs/_includes/head-custom.html new file mode 100644 index 000000000..51b5659cf --- /dev/null +++ b/docs/_includes/head-custom.html @@ -0,0 +1,112 @@ + diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html new file mode 100644 index 000000000..68428499a --- /dev/null +++ b/docs/_layouts/default.html @@ -0,0 +1,49 @@ + + + + + + + {% seo %} + + + + + {% include head-custom.html %} + + + + +
+ + +
+
+

{{ page.title | default: site.title | default: site.github.repository_name }}

+
+
+ {{ content }} +
+ +
+ + diff --git a/docs/_plugins/ruby34_compat.rb b/docs/_plugins/ruby34_compat.rb new file mode 100644 index 000000000..3a6d010ba --- /dev/null +++ b/docs/_plugins/ruby34_compat.rb @@ -0,0 +1,13 @@ +# Ruby 3.2+ removed Object#tainted? and Object#taint, which Liquid 4.x still uses. +# This shim restores them as no-ops so Jekyll can run on Ruby 3.2+. +if RUBY_VERSION >= "3.2" + module Kernel + def tainted? + false + end + + def taint + self + end + end +end diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 000000000..4651f6d90 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,14 @@ +--- +layout: none +--- + + + + + + + + +

Redirecting to Concept...

+ +