diff --git a/Flow.uplugin b/Flow.uplugin index f4eb2d4c..90920bea 100644 --- a/Flow.uplugin +++ b/Flow.uplugin @@ -1,4 +1,4 @@ -{ +{ "FileVersion" : 3, "Version" : 2.2, "FriendlyName" : "Flow", @@ -42,6 +42,10 @@ { "Name": "EngineAssetDefinitions", "Enabled": true + }, + { + "Name": "GameplayAbilities", + "Enabled": true } ] } \ No newline at end of file diff --git a/Source/Flow/Flow.Build.cs b/Source/Flow/Flow.Build.cs index c6847ce4..cc47240d 100644 --- a/Source/Flow/Flow.Build.cs +++ b/Source/Flow/Flow.Build.cs @@ -18,6 +18,7 @@ public Flow(ReadOnlyTargetRules target) : base(target) "CoreUObject", "DeveloperSettings", "Engine", + "GameplayAbilities", // for FGameplayTagRequirements "GameplayTags", "MovieScene", "MovieSceneTracks", diff --git a/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateRequireGameplayTags.cpp b/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateRequireGameplayTags.cpp new file mode 100644 index 00000000..c3ee2d5f --- /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/Nodes/Developer/FlowNode_Log.cpp b/Source/Flow/Private/Nodes/Developer/FlowNode_Log.cpp index 10bb0ecf..e5bb4b90 100644 --- a/Source/Flow/Private/Nodes/Developer/FlowNode_Log.cpp +++ b/Source/Flow/Private/Nodes/Developer/FlowNode_Log.cpp @@ -88,14 +88,23 @@ void UFlowNode_Log::PostEditChangeChainProperty(FPropertyChangedChainEvent& Prop Super::PostEditChangeChainProperty(PropertyChainEvent); } +void UFlowNode_Log::OnEditorPinConnectionsChanged(const TArray& Changes) +{ + Super::OnEditorPinConnectionsChanged(Changes); + + UpdateNodeConfigText(); +} + void UFlowNode_Log::UpdateNodeConfigText_Implementation() { - constexpr bool bErrorIfInputPinNotFound = false; - const bool bIsInputConnected = IsInputConnected(GET_MEMBER_NAME_CHECKED(ThisClass, Message), bErrorIfInputPinNotFound); + constexpr bool bErrorIfInputPinNotFound = true; + + FConnectedPin ConnectedPin; + const bool bIsInputConnected = FindFirstInputPinConnection(GET_MEMBER_NAME_CHECKED(ThisClass, Message), bErrorIfInputPinNotFound, ConnectedPin); if (bIsInputConnected) { - SetNodeConfigText(FText()); + SetNodeConfigText(FText::Format(LOCTEXT("LogFromPin", "Message from: {0}"), { FText::FromString(ConnectedPin.PinName.ToString()) })); } else { diff --git a/Source/Flow/Private/Nodes/FlowNode.cpp b/Source/Flow/Private/Nodes/FlowNode.cpp index ecf2b1d2..923c475b 100644 --- a/Source/Flow/Private/Nodes/FlowNode.cpp +++ b/Source/Flow/Private/Nodes/FlowNode.cpp @@ -6,9 +6,10 @@ #include "FlowAsset.h" #include "FlowSettings.h" #include "Interfaces/FlowNodeWithExternalDataPinSupplierInterface.h" -#include "Types/FlowPinType.h" -#include "Types/FlowDataPinValue.h" #include "Types/FlowAutoDataPinsWorkingData.h" +#include "Types/FlowDataPinValue.h" +#include "Types/FlowPinConnectionChange.h" +#include "Types/FlowPinType.h" #include "Components/ActorComponent.h" #if WITH_EDITOR @@ -99,14 +100,12 @@ void UFlowNode::ValidateFlowPinArrayIsUnique(const TArray& FlowPins, T } } -void UFlowNode::EnsureSetFlowNodeForEditorForAllAddOns() const +void UFlowNode::EnsureAddOnFlowNodePointersForEditor() { - UFlowNode* MutableThis = const_cast(this); - - MutableThis->ForEachAddOn( - [MutableThis](UFlowNodeAddOn& AddOn) -> EFlowForEachAddOnFunctionReturnValue + ForEachAddOn( + [this](UFlowNodeAddOn& AddOn) -> EFlowForEachAddOnFunctionReturnValue { - AddOn.SetFlowNodeForEditor(MutableThis); + AddOn.SetFlowNodeForEditor(this); return EFlowForEachAddOnFunctionReturnValue::Continue; }); } @@ -130,7 +129,7 @@ void UFlowNode::PostLoad() bool UFlowNode::IsSupportedInputPinName(const FName& PinName) const { - const FFlowPin* InputPin = FindFlowPinByName(PinName, InputPins); + const FFlowPin* InputPin = FindInputPinByName(PinName); if (AddOns.IsEmpty()) { @@ -159,6 +158,14 @@ void UFlowNode::AddOutputPins(const TArray& Pins) #if WITH_EDITOR +void UFlowNode::SetupForEditing(UEdGraphNode& EdGraphNode) +{ + Super::SetupForEditing(EdGraphNode); + + // Ensure AddOn editor pointers are correct as soon as we're prepared for editing. + EnsureAddOnFlowNodePointersForEditor(); +} + bool UFlowNode::RebuildPinArray(const TArray& NewPinNames, TArray& InOutPins, const FFlowPin& DefaultPin) { bool bIsChanged; @@ -328,8 +335,6 @@ bool UFlowNode::SupportsContextPins() const TArray UFlowNode::GetContextInputs() const { - EnsureSetFlowNodeForEditorForAllAddOns(); - TArray ContextOutputs = Super::GetContextInputs(); // Add the Auto-Generated DataPins as GetContextInputs @@ -343,8 +348,6 @@ TArray UFlowNode::GetContextInputs() const TArray UFlowNode::GetContextOutputs() const { - EnsureSetFlowNodeForEditorForAllAddOns(); - TArray ContextOutputs = Super::GetContextOutputs(); // Add the Auto-Generated DataPins as ContextOutputs @@ -652,11 +655,15 @@ bool UFlowNode::TryGetFlowDataPinSupplierDatasForPinName(const FName& PinName, T TryAddSupplierDataToArray(NewPinValueSupplier, InOutPinValueSupplierDatas); // If the pin is connected, try to add the connected node as the priority supplier - FFlowPinValueSupplierData ConnectedPinValueSupplier; - FGuid ConnectedNodeGuid; + FConnectedPin ConnectedPin; - if (FindConnectedNodeForPinFast(PinName, &ConnectedNodeGuid, &ConnectedPinValueSupplier.SupplierPinName)) + 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); @@ -732,11 +739,118 @@ void UFlowNode::FixupDataPinTypesForPin(FFlowPin& MutableDataPin) // -- #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; - OnConnectionsChanged(OldConnections); + + // Compute per-pin deltas and broadcast to self + addons + TArray Changes; + BuildConnectionChangeList(*GetFlowAsset(), OldConnections, Connections, Changes); + + if (!Changes.IsEmpty()) + { + BroadcastEditorPinConnectionsChanged(Changes); + } } #endif @@ -766,112 +880,214 @@ FName UFlowNode::GetPinConnectedToNode(const FGuid& OtherNodeGuid) bool UFlowNode::IsInputConnected(const FName& PinName, bool bErrorIfPinNotFound) const { - if (const FFlowPin* FlowPin = FindFlowPinByName(PinName, InputPins)) + // 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 IsInputConnected(*FlowPin); + return FindFirstInputPinConnection(*FlowPin, FirstConnectedPin); } if (bErrorIfPinNotFound) { - LogError(FString::Printf(TEXT("Unknown pin %s"), *PinName.ToString()), EFlowOnScreenMessageType::Temporary); + LogError(FString::Printf(TEXT("Unknown input pin %s"), *PinName.ToString()), EFlowOnScreenMessageType::Temporary); } return false; } -bool UFlowNode::IsOutputConnected(const FName& PinName, bool bErrorIfPinNotFound) const +bool UFlowNode::FindInputPinConnections(const FName& PinName, bool bErrorIfPinNotFound, TArray* ConnectedPins) const { - if (const FFlowPin* FlowPin = FindFlowPinByName(PinName, OutputPins)) + if (const FFlowPin* FlowPin = FindInputPinByName(PinName)) { - return IsOutputConnected(*FlowPin); + return FindInputPinConnections(*FlowPin, ConnectedPins); } if (bErrorIfPinNotFound) { - LogError(FString::Printf(TEXT("Unknown pin %s"), *PinName.ToString()), EFlowOnScreenMessageType::Temporary); + LogError(FString::Printf(TEXT("Unknown input pin %s"), *PinName.ToString()), EFlowOnScreenMessageType::Temporary); } return false; } -FFlowPin* UFlowNode::FindInputPinByName(const FName& PinName) +bool UFlowNode::FindFirstOutputPinConnection(const FName& PinName, bool bErrorIfPinNotFound, FConnectedPin& FirstConnectedPin) const { - if (FFlowPin* FlowPin = FindFlowPinByName(PinName, InputPins)) + if (const FFlowPin* FlowPin = FindOutputPinByName(PinName)) { - return FlowPin; + return FindFirstOutputPinConnection(*FlowPin, FirstConnectedPin); } - return nullptr; + if (bErrorIfPinNotFound) + { + LogError(FString::Printf(TEXT("Unknown output pin %s"), *PinName.ToString()), EFlowOnScreenMessageType::Temporary); + } + + return false; } -FFlowPin* UFlowNode::FindOutputPinByName(const FName& PinName) +bool UFlowNode::FindOutputPinConnections(const FName& PinName, bool bErrorIfPinNotFound, TArray* ConnectedPins) const { - if (FFlowPin* FlowPin = FindFlowPinByName(PinName, OutputPins)) + if (const FFlowPin* FlowPin = FindOutputPinByName(PinName)) { - return FlowPin; + return FindOutputPinConnections(*FlowPin, ConnectedPins); } - return nullptr; + if (bErrorIfPinNotFound) + { + LogError(FString::Printf(TEXT("Unknown output pin %s"), *PinName.ToString()), EFlowOnScreenMessageType::Temporary); + } + + return false; } -bool UFlowNode::IsInputConnected(const FFlowPin& FlowPin, FGuid* FoundGuid, FName* OutConnectedPinName) const +template +bool UFlowNode::FindFirstPinConnection( + const FFlowPin& FlowPin, + const TArray& FlowPinArray, + FConnectedPin& FirstConnectedPin) const { - if (!InputPins.Contains(FlowPin.PinName)) + if (!FlowPinArray.Contains(FlowPin.PinName)) { return false; } - if (FlowPin.IsExecPin()) + const bool bUseCachedPath = (bExecIsCached == FlowPin.IsExecPin()); + if (bUseCachedPath) { - // We don't cache the input exec pins for fast lookup in Connections, so use the slow path for them: - - return FindConnectedNodeForPinSlow(FlowPin.PinName, FoundGuid, OutConnectedPinName); + // Cached category: fast lookup (0/1 connection) + return FindConnectedNodeForPinCached(FlowPin.PinName, FirstConnectedPin); } else { - return FindConnectedNodeForPinFast(FlowPin.PinName, FoundGuid, OutConnectedPinName); + // 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; + } } } -bool UFlowNode::IsOutputConnected(const FFlowPin& FlowPin, FGuid* FirstFoundGuid, FName* OutFirstConnectedPinName) const +template +bool UFlowNode::FindPinConnections(const FFlowPin& FlowPin, const TArray& FlowPinArray, TArray* ConnectedPins) const { - if (!OutputPins.Contains(FlowPin.PinName)) + if (!FlowPinArray.Contains(FlowPin.PinName)) { return false; } - if (FlowPin.IsExecPin()) + const bool bUseCachedPath = bExecIsCached == FlowPin.IsExecPin(); + if (bUseCachedPath) { - return FindConnectedNodeForPinFast(FlowPin.PinName, FirstFoundGuid, OutFirstConnectedPinName); + // 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 input data pins for fast lookup in Connections, so use the slow path for them: + // We don't cache the output data pins for fast lookup in Connections, so use the slow path for them: - return FindConnectedNodeForPinSlow(FlowPin.PinName, FirstFoundGuid, OutFirstConnectedPinName); + return FindConnectedNodeForPinUncached(FlowPin.PinName, ConnectedPins); } } -bool UFlowNode::FindConnectedNodeForPinFast(const FName& PinName, FGuid* OutGuid, FName* OutConnectedPinName) const +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 { - const FConnectedPin* FoundConnectedPin = Connections.Find(PinName); + // 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) { - if (OutGuid) - { - *OutGuid = FoundConnectedPin->NodeGuid; - } + ConnectedPin = *FoundConnectedPin; - if (OutConnectedPinName) - { - *OutConnectedPinName = FoundConnectedPin->PinName; - } + return true; } - return FoundConnectedPin != nullptr; + return false; } -bool UFlowNode::FindConnectedNodeForPinSlow(const FName& PinName, FGuid* OutGuid, FName* OutConnectedPinName) const +bool UFlowNode::FindConnectedNodeForPinUncached(const FName& PinName, TArray* ConnectedPins) const { const UFlowAsset* FlowAsset = GetFlowAsset(); @@ -880,9 +1096,10 @@ bool UFlowNode::FindConnectedNodeForPinSlow(const FName& PinName, FGuid* OutGuid return false; } + check(!ConnectedPins || ConnectedPins->IsEmpty()); + for (const TPair& Pair : ObjectPtrDecay(FlowAsset->Nodes)) { - const FGuid& ConnectedFromGuid = Pair.Key; const UFlowNode* ConnectedFromFlowNode = Pair.Value; if (!IsValid(ConnectedFromFlowNode)) @@ -890,27 +1107,30 @@ bool UFlowNode::FindConnectedNodeForPinSlow(const FName& PinName, FGuid* OutGuid continue; } - for (const TPair& Connection : Pair.Value->Connections) + for (const TPair& Connection : ConnectedFromFlowNode->Connections) { const FConnectedPin& ConnectedPinStruct = Connection.Value; if (ConnectedPinStruct.NodeGuid == NodeGuid && ConnectedPinStruct.PinName == PinName) { - if (OutGuid) + if (ConnectedPins) { - *OutGuid = ConnectedFromGuid; + ConnectedPins->Add(ConnectedPinStruct); } - - if (OutConnectedPinName) + else { - *OutConnectedPinName = Connection.Key; + // Early return if not collecting the ConnectedPins, since only connected true/false matters + return true; } - - return true; } } } + if (ConnectedPins && !ConnectedPins->IsEmpty()) + { + return true; + } + return false; } diff --git a/Source/Flow/Private/Nodes/FlowNodeBase.cpp b/Source/Flow/Private/Nodes/FlowNodeBase.cpp index e4105666..20ccb493 100644 --- a/Source/Flow/Private/Nodes/FlowNodeBase.cpp +++ b/Source/Flow/Private/Nodes/FlowNodeBase.cpp @@ -9,6 +9,7 @@ #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" @@ -264,7 +265,7 @@ TArray UFlowNodeBase::GetContextOutputs() const return ContextOutputs; } -#endif // WITH_EDITOR +#endif void UFlowNodeBase::LogValidationError(const FString& Message) { @@ -409,7 +410,7 @@ EFlowAddOnAcceptResult UFlowNodeBase::CheckAcceptFlowNodeAddOnChild( return CombinedResult; } -#endif // WITH_EDITOR +#endif EFlowForEachAddOnFunctionReturnValue UFlowNodeBase::ForEachAddOnConst( const FConstFlowNodeAddOnFunction& Function, @@ -622,7 +623,7 @@ void UFlowNodeBase::PostEditChangeProperty(FPropertyChangedEvent& PropertyChange UpdateNodeConfigText(); } -#endif // WITH_EDITOR +#endif FString UFlowNodeBase::GetStatusString() const { @@ -846,7 +847,7 @@ FText UFlowNodeBase::GetNodeConfigText() const return DevNodeConfigText; #else return FText::GetEmpty(); -#endif // WITH_EDITORONLY_DATA +#endif } void UFlowNodeBase::SetNodeConfigText(const FText& NodeConfigText) @@ -856,7 +857,7 @@ void UFlowNodeBase::SetNodeConfigText(const FText& NodeConfigText) { DevNodeConfigText = NodeConfigText; } -#endif // WITH_EDITOR +#endif } void UFlowNodeBase::UpdateNodeConfigText_Implementation() diff --git a/Source/Flow/Private/Nodes/Graph/FlowNode_FormatText.cpp b/Source/Flow/Private/Nodes/Graph/FlowNode_FormatText.cpp index 5da5f18b..5968eebd 100644 --- a/Source/Flow/Private/Nodes/Graph/FlowNode_FormatText.cpp +++ b/Source/Flow/Private/Nodes/Graph/FlowNode_FormatText.cpp @@ -66,10 +66,13 @@ void UFlowNode_FormatText::PostEditChangeChainProperty(FPropertyChangedChainEven void UFlowNode_FormatText::UpdateNodeConfigText_Implementation() { - constexpr bool bErrorIfInputPinNotFound = false; - if (IsInputConnected(GET_MEMBER_NAME_CHECKED(ThisClass, FormatText), bErrorIfInputPinNotFound)) + constexpr bool bErrorIfInputPinNotFound = true; + FConnectedPin ConnectedPin; + const bool bIsInputConnected = FindFirstInputPinConnection(GET_MEMBER_NAME_CHECKED(ThisClass, FormatText), bErrorIfInputPinNotFound, ConnectedPin); + + if (bIsInputConnected) { - SetNodeConfigText(FText()); + SetNodeConfigText(FText::Format(LOCTEXT("FormatTextFromPin", "Format from: {0}"), { FText::FromString(ConnectedPin.PinName.ToString()) })); } else { diff --git a/Source/Flow/Private/Nodes/Route/FlowNode_Reroute.cpp b/Source/Flow/Private/Nodes/Route/FlowNode_Reroute.cpp index d04c1a87..1c32d287 100644 --- a/Source/Flow/Private/Nodes/Route/FlowNode_Reroute.cpp +++ b/Source/Flow/Private/Nodes/Route/FlowNode_Reroute.cpp @@ -46,14 +46,13 @@ FFlowDataPinResult UFlowNode_Reroute::TrySupplyDataPin(FName PinName) const return FFlowDataPinResult(EFlowDataPinResolveResult::FailedUnknownPin); } - FGuid FoundGuid; - FName ConnectedPinName; - if (!IsInputConnected(*InputPin, &FoundGuid, &ConnectedPinName)) + FConnectedPin ConnectedPin; + if (!FindFirstInputPinConnection(*InputPin, ConnectedPin)) { return FFlowDataPinResult(EFlowDataPinResolveResult::FailedNotConnected); } - const UFlowNode* ConnectedFlowNodeSupplier = GetFlowAsset()->GetNode(FoundGuid); + const UFlowNode* ConnectedFlowNodeSupplier = GetFlowAsset()->GetNode(ConnectedPin.NodeGuid); if (!IsValid(ConnectedFlowNodeSupplier)) { checkf(IsValid(ConnectedFlowNodeSupplier), TEXT("This node should be valid if IsInputConnected returned true")); @@ -62,5 +61,5 @@ FFlowDataPinResult UFlowNode_Reroute::TrySupplyDataPin(FName PinName) const } // Hand-off to the connected flow node to supply the value - return ConnectedFlowNodeSupplier->TrySupplyDataPin(ConnectedPinName); + return ConnectedFlowNodeSupplier->TrySupplyDataPin(ConnectedPin.PinName); } diff --git a/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateRequireGameplayTags.h b/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateRequireGameplayTags.h new file mode 100644 index 00000000..614aa50b --- /dev/null +++ b/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateRequireGameplayTags.h @@ -0,0 +1,51 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors +#pragma once + +#include "AddOns/FlowNodeAddOn.h" +#include "Interfaces/FlowPredicateInterface.h" + +#include "GameplayEffectTypes.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) + FGameplayTagRequirements Requirements; +}; diff --git a/Source/Flow/Public/Nodes/Developer/FlowNode_Log.h b/Source/Flow/Public/Nodes/Developer/FlowNode_Log.h index 3c04ecce..938ec98d 100644 --- a/Source/Flow/Public/Nodes/Developer/FlowNode_Log.h +++ b/Source/Flow/Public/Nodes/Developer/FlowNode_Log.h @@ -56,6 +56,10 @@ class FLOW_API UFlowNode_Log : public UFlowNode_DefineProperties virtual void PostEditChangeChainProperty(FPropertyChangedChainEvent& PropertyChangedEvent) override; // -- + // UFlowNodeBase + virtual void OnEditorPinConnectionsChanged(const TArray& Changes) override; + // -- + virtual void UpdateNodeConfigText_Implementation() override; #endif diff --git a/Source/Flow/Public/Nodes/FlowNode.h b/Source/Flow/Public/Nodes/FlowNode.h index 90985a18..159faf3d 100644 --- a/Source/Flow/Public/Nodes/FlowNode.h +++ b/Source/Flow/Public/Nodes/FlowNode.h @@ -11,6 +11,7 @@ #include "Interfaces/FlowDataPinValueSupplierInterface.h" #include "Nodes/FlowPin.h" #include "Types/FlowArray.h" +#include "Types/FlowPinConnectionChange.h" #include "FlowNode.generated.h" struct FFlowAutoDataPinsWorkingData; @@ -70,9 +71,15 @@ class FLOW_API UFlowNode : public UFlowNodeBase // -- #if WITH_EDITOR - /* Called before operations on AddOns in the editor - * where we need the AddOns to have a value FlowNode pointer to use. */ - void EnsureSetFlowNodeForEditorForAllAddOns() const; + /* 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: @@ -143,7 +150,7 @@ class FLOW_API UFlowNode : public UFlowNodeBase * 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 // WITH_EDITOR; +#endif /* Always use default range for nodes with user-created outputs i.e. Execution Sequence. */ void SetNumberedInputPins(const uint8 FirstNumber = 0, const uint8 LastNumber = 1); @@ -197,7 +204,6 @@ class FLOW_API UFlowNode : public UFlowNodeBase public: #if WITH_EDITOR void SetConnections(const TMap& InConnections); - virtual void OnConnectionsChanged(const TMap& OldConnections) {} #endif FConnectedPin GetConnection(const FName OutputName) const { return Connections.FindRef(OutputName); } @@ -216,8 +222,24 @@ class FLOW_API UFlowNode : public UFlowNodeBase UFUNCTION(BlueprintPure, Category= "FlowNode") bool IsOutputConnected(const FName& PinName, bool bErrorIfPinNotFound = true) const; - bool IsInputConnected(const FFlowPin& FlowPin, FGuid* FoundGuid = nullptr, FName* OutConnectedPinName = nullptr) const; - bool IsOutputConnected(const FFlowPin& FlowPin, FGuid* FirstFoundGuid = nullptr, FName* OutFirstConnectedPinName = nullptr) const; + // 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); @@ -229,8 +251,14 @@ class FLOW_API UFlowNode : public UFlowNodeBase 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 FindConnectedNodeForPinFast(const FName& FlowPinName, FGuid* FoundGuid = nullptr, FName* OutConnectedPinName = nullptr) const; - bool FindConnectedNodeForPinSlow(const FName& FlowPinName, FGuid* FoundGuid = nullptr, FName* OutConnectedPinName = nullptr) const; + 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. @@ -238,6 +266,17 @@ class FLOW_API UFlowNode : public UFlowNodeBase * 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 diff --git a/Source/Flow/Public/Nodes/FlowNodeBase.h b/Source/Flow/Public/Nodes/FlowNodeBase.h index 6bbd3d9b..82be586a 100644 --- a/Source/Flow/Public/Nodes/FlowNodeBase.h +++ b/Source/Flow/Public/Nodes/FlowNodeBase.h @@ -11,6 +11,7 @@ #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" @@ -157,7 +158,12 @@ class FLOW_API UFlowNodeBase 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 diff --git a/Source/Flow/Public/Types/FlowPinConnectionChange.h b/Source/Flow/Public/Types/FlowPinConnectionChange.h new file mode 100644 index 00000000..264c6d2b --- /dev/null +++ b/Source/Flow/Public/Types/FlowPinConnectionChange.h @@ -0,0 +1,49 @@ +// 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") + UFlowNode* OldConnectedNode = nullptr; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Flow") + FName OldConnectedPinName = NAME_None; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Flow") + UFlowNode* NewConnectedNode = nullptr; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Flow") + FName NewConnectedPinName = NAME_None; +}; diff --git a/Source/FlowEditor/Private/Asset/FlowAssetParamsFactory.cpp b/Source/FlowEditor/Private/Asset/FlowAssetParamsFactory.cpp index 12608a73..194e1d3f 100644 --- a/Source/FlowEditor/Private/Asset/FlowAssetParamsFactory.cpp +++ b/Source/FlowEditor/Private/Asset/FlowAssetParamsFactory.cpp @@ -77,6 +77,12 @@ bool UFlowAssetParamsFactory::ShowParentPickerDialog() SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() + [ + SNew(STextBlock) + .Text(LOCTEXT("CreateChildParamsHelp", "Choose Parent Flow Asset Params:\n")) + ] + + SVerticalBox::Slot() + .FillHeight(1.0f) [ ParamsPicker ] diff --git a/Source/FlowEditor/Private/DetailCustomizations/FlowDetailsAddOnUI.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowDetailsAddOnUI.cpp new file mode 100644 index 00000000..1286546d --- /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/FlowNodeAddOn_Details.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowNodeAddOn_Details.cpp index 3ce051f4..82947792 100644 --- a/Source/FlowEditor/Private/DetailCustomizations/FlowNodeAddOn_Details.cpp +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowNodeAddOn_Details.cpp @@ -1,9 +1,17 @@ // 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) { @@ -14,6 +22,57 @@ void FFlowNodeAddOn_Details::CustomizeDetails(IDetailLayoutBuilder& DetailLayout 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/DetailCustomizations/FlowNode_Details.cpp b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_Details.cpp index 41c09eb6..9a699f8b 100644 --- a/Source/FlowEditor/Private/DetailCustomizations/FlowNode_Details.cpp +++ b/Source/FlowEditor/Private/DetailCustomizations/FlowNode_Details.cpp @@ -1,9 +1,17 @@ // 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) { @@ -13,6 +21,55 @@ void FFlowNode_Details::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) 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/Graph/FlowGraphNodesPolicy.cpp b/Source/FlowEditor/Private/Graph/FlowGraphNodesPolicy.cpp index 45c038df..29ea839f 100644 --- a/Source/FlowEditor/Private/Graph/FlowGraphNodesPolicy.cpp +++ b/Source/FlowEditor/Private/Graph/FlowGraphNodesPolicy.cpp @@ -7,11 +7,11 @@ #include UE_INLINE_GENERATED_CPP_BY_NAME(FlowGraphNodesPolicy) #if WITH_EDITOR -bool FFlowGraphNodesPolicy::IsNodeAllowedByPolicy(const UFlowNodeBase* FlowNodeBase) const +EFlowGraphPolicyResult FFlowGraphNodesPolicy::IsNodeAllowedByPolicy(const UFlowNodeBase* FlowNodeBase) const { if (!IsValid(FlowNodeBase)) { - return false; + return EFlowGraphPolicyResult::TentativeForbidden; } const FString NodeCategoryString = UFlowGraphSettings::GetNodeCategoryForNode(*FlowNodeBase); @@ -19,23 +19,23 @@ bool FFlowGraphNodesPolicy::IsNodeAllowedByPolicy(const UFlowNodeBase* FlowNodeB const bool bIsInAllowedCategory = !AllowedCategories.IsEmpty() && IsAnySubcategory(NodeCategoryString, AllowedCategories); if (bIsInAllowedCategory) { - return true; + return EFlowGraphPolicyResult::Allowed; } const bool bIsInDisallowedCategory = !DisallowedCategories.IsEmpty() && IsAnySubcategory(NodeCategoryString, DisallowedCategories); if (bIsInDisallowedCategory) { - return false; + return EFlowGraphPolicyResult::Forbidden; } if (AllowedCategories.IsEmpty()) { // If the AllowedCategories is empty, then we consider any node that isn't disallowed, as allowed - return true; + return EFlowGraphPolicyResult::TentativeAllowed; } else { - return false; + return EFlowGraphPolicyResult::TentativeForbidden; } } diff --git a/Source/FlowEditor/Private/Graph/FlowGraphSchema.cpp b/Source/FlowEditor/Private/Graph/FlowGraphSchema.cpp index 575fbf7d..b660570c 100644 --- a/Source/FlowEditor/Private/Graph/FlowGraphSchema.cpp +++ b/Source/FlowEditor/Private/Graph/FlowGraphSchema.cpp @@ -1170,17 +1170,36 @@ void UFlowGraphSchema::ApplyNodeOrAddOnFilter(const UFlowAsset* EditedFlowAsset, return; } + using namespace EFlowGraphPolicyResult_Classifiers; + UFlowNodeBase* FlowNodeBaseCDO = FlowNodeClass->GetDefaultObject(); - if (const FFlowGraphNodesPolicy* FlowAssetPolicy = GraphSettings->PerAssetSubclassFlowNodePolicies.Find(FSoftClassPath(EditedFlowAsset->GetClass()))) + UClass* CurFlowAssetClass = EditedFlowAsset->GetClass(); + + // Crawl up the superclass parentage until we find a strict result, otherwise accept the best tentative result + EFlowGraphPolicyResult BestResult = EFlowGraphPolicyResult::Invalid; + while (IsValid(CurFlowAssetClass) && CurFlowAssetClass->IsChildOf()) { - const bool bIsAllowedByPolicy = FlowAssetPolicy->IsNodeAllowedByPolicy(FlowNodeBaseCDO); - if (!bIsAllowedByPolicy) + if (const FFlowGraphNodesPolicy* FlowAssetPolicy = GraphSettings->PerAssetSubclassFlowNodePolicies.Find(FSoftClassPath(CurFlowAssetClass))) { - return; + const EFlowGraphPolicyResult CurPolicyResult = FlowAssetPolicy->IsNodeAllowedByPolicy(FlowNodeBaseCDO); + + // Choose the most applicable result for this class + BestResult = MergePolicyResult(BestResult, CurPolicyResult); + + if (IsStrictPolicyResult(BestResult)) + { + // A strict policy stops the crawl up the superclass parentage + break; + } } + + CurFlowAssetClass = CurFlowAssetClass->GetSuperClass(); } - FilteredNodes.Emplace(FlowNodeBaseCDO); + if (IsAnyAllowedPolicyResult(BestResult)) + { + FilteredNodes.Emplace(FlowNodeBaseCDO); + } } void UFlowGraphSchema::GetFlowNodeActions(FGraphActionMenuBuilder& ActionMenuBuilder, const UFlowAsset* EditedFlowAsset, const FString& CategoryName) diff --git a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp index fab76e8e..c146263a 100644 --- a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp +++ b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp @@ -320,9 +320,15 @@ void UFlowGraphNode::ReconstructNode() return; } - bIsReconstructingNode = true; + TGuardValue GuardIsResonstructingNode(bIsReconstructingNode, true); + FScopedTransaction Transaction(LOCTEXT("ReconstructNode", "Reconstruct Node"), !GUndo); + if (UFlowNode* FlowNode = Cast(NodeInstance)) + { + FlowNode->SetupForEditing(*this); + } + 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 @@ -357,16 +363,9 @@ void UFlowGraphNode::ReconstructNode() bNeedsFullReconstruction = false; } - if (UFlowNode* FlowNode = Cast(NodeInstance)) - { - FlowNode->UpdateNodeConfigText(); - } - // 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(); - - bIsReconstructingNode = false; } void UFlowGraphNode::AllocateDefaultPins() @@ -533,6 +532,7 @@ void UFlowGraphNode::GetNodeContextMenuActions(class UToolMenu* Menu, class UGra Section.AddMenuEntry(GenericCommands.Cut); Section.AddMenuEntry(GenericCommands.Copy); Section.AddMenuEntry(GenericCommands.Duplicate); + Section.AddMenuEntry(GenericCommands.Paste); Section.AddMenuEntry(GraphCommands.BreakNodeLinks); @@ -1848,7 +1848,7 @@ bool UFlowGraphNode::TryUpdateNodePins() const } // Ensure the AddOns for this FlowNode have their FlowNode pointer set - FlowNodeInstance->EnsureSetFlowNodeForEditorForAllAddOns(); + 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 @@ -1878,7 +1878,7 @@ bool UFlowGraphNode::TryUpdateNodePins() const // Fix up old pins on the CDO UFlowNode* MutableCDO = const_cast(FlowNodeCDO); - MutableCDO->EnsureSetFlowNodeForEditorForAllAddOns(); + MutableCDO->EnsureAddOnFlowNodePointersForEditor(); MutableCDO->FixupDataPinTypes(); const bool bIsRerouteGraphNode = (Cast(this) != nullptr); diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowDetailsAddOnUI.h b/Source/FlowEditor/Public/DetailCustomizations/FlowDetailsAddOnUI.h new file mode 100644 index 00000000..799f8942 --- /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/FlowNodeAddOn_Details.h b/Source/FlowEditor/Public/DetailCustomizations/FlowNodeAddOn_Details.h index 9d69651a..4f980bc8 100644 --- a/Source/FlowEditor/Public/DetailCustomizations/FlowNodeAddOn_Details.h +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowNodeAddOn_Details.h @@ -18,4 +18,7 @@ class FFlowNodeAddOn_Details final : public TFlowDataPinValueOwnerCustomization< // IDetailCustomization virtual void CustomizeDetails(IDetailLayoutBuilder& DetailLayout) override; // -- -}; + +private: + TWeakObjectPtr EditedAddOn = nullptr; +}; \ No newline at end of file diff --git a/Source/FlowEditor/Public/DetailCustomizations/FlowNode_Details.h b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_Details.h index 7d4148f4..2b2b9205 100644 --- a/Source/FlowEditor/Public/DetailCustomizations/FlowNode_Details.h +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowNode_Details.h @@ -18,4 +18,7 @@ class FFlowNode_Details final : public TFlowDataPinValueOwnerCustomization EditedNode = nullptr; +}; \ No newline at end of file diff --git a/Source/FlowEditor/Public/Graph/FlowGraphNodesPolicy.h b/Source/FlowEditor/Public/Graph/FlowGraphNodesPolicy.h index c6178264..5684006f 100644 --- a/Source/FlowEditor/Public/Graph/FlowGraphNodesPolicy.h +++ b/Source/FlowEditor/Public/Graph/FlowGraphNodesPolicy.h @@ -1,10 +1,57 @@ // 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 { @@ -21,9 +68,10 @@ struct FFlowGraphNodesPolicy #if WITH_EDITOR public: - bool IsNodeAllowedByPolicy(const UFlowNodeBase* FlowNodeBase) const; + EFlowGraphPolicyResult IsNodeAllowedByPolicy(const UFlowNodeBase* FlowNodeBase) const; protected: static bool IsAnySubcategory(const FString& CheckCategory, const TArray& Categories); #endif }; +