diff --git a/Source/Flow/Private/FlowAsset.cpp b/Source/Flow/Private/FlowAsset.cpp index 14b971f4f..3a9034262 100644 --- a/Source/Flow/Private/FlowAsset.cpp +++ b/Source/Flow/Private/FlowAsset.cpp @@ -23,7 +23,9 @@ #include "Editor/EditorEngine.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) @@ -122,9 +124,21 @@ EDataValidationResult UFlowAsset::ValidateAsset(FFlowMessageLog& MessageLog) } Node.Value->ValidationLog.Messages.Empty(); - if (Node.Value->ValidateNode() == EDataValidationResult::Invalid) + Node.Value->ValidateNode(); + MessageLog.Messages.Append(Node.Value->ValidationLog.Messages); + + // Validate AddOns + for (UFlowNodeAddOn* AddOn : Node.Value->GetFlowNodeAddOnChildren()) { - MessageLog.Messages.Append(Node.Value->ValidationLog.Messages); + if (IsValid(AddOn)) + { + ValidateAddOnTree(*AddOn, MessageLog); + } + else + { + const FString ErrorMsg = FString::Format(*ValidationError_NullAddOnNodeInstance, { *Node.Key.ToString() }); + MessageLog.Error(*ErrorMsg, this); + } } } else @@ -134,7 +148,17 @@ EDataValidationResult UFlowAsset::ValidateAsset(FFlowMessageLog& MessageLog) } } - return MessageLog.Messages.Num() > 0 ? EDataValidationResult::Invalid : EDataValidationResult::Valid; + // if at least one error has been has been logged : mark the asset as invalid + for (const TSharedRef& Msg : MessageLog.Messages) + { + if (Msg->GetSeverity() == EMessageSeverity::Error) + { + return EDataValidationResult::Invalid; + } + } + + // otherwise, the asset is considered valid (even with warnings or notes) + return EDataValidationResult::Valid; } bool UFlowAsset::IsNodeOrAddOnClassAllowed(const UClass* FlowNodeOrAddOnClass, FText* OutOptionalFailureReason) const @@ -239,6 +263,35 @@ bool UFlowAsset::IsFlowNodeClassInDeniedClasses(const UClass& FlowNodeClass) con return false; } +void UFlowAsset::ValidateAddOnTree(UFlowNodeAddOn& AddOn, FFlowMessageLog& MessageLog) +{ + // Filter unauthorized addon nodes + FText FailureReason; + if (!IsNodeOrAddOnClassAllowed(AddOn.GetClass(), &FailureReason)) + { + const FString ErrorMsg = + FailureReason.IsEmpty() + ? FString::Format(*ValidationError_AddOnNodeClassNotAllowed, { *AddOn.GetClass()->GetName() }) + : FailureReason.ToString(); + + MessageLog.Error(*ErrorMsg, AddOn.GetFlowNodeSelfOrOwner()); + } + + // Validate AddOn + AddOn.ValidationLog.Messages.Empty(); + AddOn.ValidateNode(); + MessageLog.Messages.Append(AddOn.ValidationLog.Messages); + + // Validate Children + for (UFlowNodeAddOn* Child : AddOn.GetFlowNodeAddOnChildren()) + { + if (IsValid(Child)) + { + ValidateAddOnTree(*Child, MessageLog); + } + } +} + bool UFlowAsset::IsFlowNodeClassInAllowedClasses(const UClass& FlowNodeClass, const TSubclassOf& RequiredAncestor) const { diff --git a/Source/Flow/Private/FlowMessageLog.cpp b/Source/Flow/Private/FlowMessageLog.cpp index 39c1d2997..0976a0e6d 100644 --- a/Source/Flow/Private/FlowMessageLog.cpp +++ b/Source/Flow/Private/FlowMessageLog.cpp @@ -16,7 +16,11 @@ FFlowGraphToken::FFlowGraphToken(const UFlowAsset* InFlowAsset) } FFlowGraphToken::FFlowGraphToken(const UFlowNodeBase* InFlowNodeBase) - : GraphNode(InFlowNodeBase->GetGraphNode()) + : GraphNode( + InFlowNodeBase->GetParentNode() + ? InFlowNodeBase->GetParentNode()->GetGraphNode() + : nullptr + ) { CachedText = InFlowNodeBase->GetNodeTitle(); } diff --git a/Source/Flow/Private/Nodes/FlowNodeBase.cpp b/Source/Flow/Private/Nodes/FlowNodeBase.cpp index 7c2bdcbdb..698306d94 100644 --- a/Source/Flow/Private/Nodes/FlowNodeBase.cpp +++ b/Source/Flow/Private/Nodes/FlowNodeBase.cpp @@ -260,12 +260,44 @@ TArray UFlowNodeBase::GetContextOutputs() const return ContextOutputs; } +EDataValidationResult UFlowNodeBase::ValidateNode() +{ + if (GetClass()->IsFunctionImplementedInScript(GET_FUNCTION_NAME_CHECKED(UFlowNodeBase, K2_ValidateNode))) + { + return K2_ValidateNode(); + } + + return EDataValidationResult::NotValidated; +} + FString UFlowNodeBase::GetStatusString() const { return K2_GetStatusString(); } + #endif // WITH_EDITOR +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 diff --git a/Source/Flow/Public/AddOns/FlowNodeAddOn.h b/Source/Flow/Public/AddOns/FlowNodeAddOn.h index 47fb5ab4c..b8dc8470c 100644 --- a/Source/Flow/Public/AddOns/FlowNodeAddOn.h +++ b/Source/Flow/Public/AddOns/FlowNodeAddOn.h @@ -22,6 +22,12 @@ class UFlowNodeAddOn : public UFlowNodeBase // (accessible only when initialized, runtime only) UPROPERTY(Transient) TObjectPtr FlowNode; + +#if WITH_EDITORONLY_DATA + // Editor-only pointer to the owning top-level UFlowNode + UPROPERTY(Transient) + TObjectPtr ParentNode; +#endif // Input pins to add to the owning flow node // If defined, ExecuteInput will only be executed for these inputs @@ -78,6 +84,23 @@ class UFlowNodeAddOn : public UFlowNodeBase // by default, uses the seed for the Flow Node that this addon is attached to. FLOW_API virtual int32 GetRandomSeed() const override; + // Sets the parent node. Editor only. + FLOW_API virtual void SetParentNode(UFlowNode* InParent) + { +#if WITH_EDITORONLY_DATA + ParentNode = InParent; +#endif // WITH_EDITOR + } + + // Editor only. + FLOW_API virtual const UFlowNode* GetParentNode() const override + { +#if WITH_EDITORONLY_DATA + if (ParentNode) return ParentNode; +#endif // WITH_EDITOR + return UFlowNodeBase::GetFlowNodeSelfOrOwner(); + } + #if WITH_EDITOR // IFlowContextPinSupplierInterface FLOW_API virtual bool SupportsContextPins() const override { return Super::SupportsContextPins() || (!InputPins.IsEmpty() || !OutputPins.IsEmpty()); } diff --git a/Source/Flow/Public/FlowAsset.h b/Source/Flow/Public/FlowAsset.h index 38b9e206d..5736ebe7b 100644 --- a/Source/Flow/Public/FlowAsset.h +++ b/Source/Flow/Public/FlowAsset.h @@ -103,7 +103,9 @@ class FLOW_API UFlowAsset : public UObject FSimpleDelegate OnDetailsRefreshRequested; static FString ValidationError_NodeClassNotAllowed; + static FString ValidationError_AddOnNodeClassNotAllowed; static FString ValidationError_NullNodeInstance; + static FString ValidationError_NullAddOnNodeInstance; private: UPROPERTY() @@ -126,6 +128,10 @@ class FLOW_API UFlowAsset : public UObject 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 ////////////////////////////////////////////////////////////////////////// diff --git a/Source/Flow/Public/Nodes/FlowNode.h b/Source/Flow/Public/Nodes/FlowNode.h index b9da34695..80a2b9271 100644 --- a/Source/Flow/Public/Nodes/FlowNode.h +++ b/Source/Flow/Public/Nodes/FlowNode.h @@ -58,9 +58,6 @@ class FLOW_API UFlowNode virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; virtual void PostLoad() override; // -- - - virtual EDataValidationResult ValidateNode() { return EDataValidationResult::NotValidated; } - #endif // Inherits Guid after graph node @@ -78,6 +75,11 @@ class FLOW_API UFlowNode // 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); } + + virtual const UFlowNode* GetParentNode() const override + { + return UFlowNodeBase::GetFlowNodeSelfOrOwner(); + } public: virtual bool CanFinishGraph() const { return false; } diff --git a/Source/Flow/Public/Nodes/FlowNodeBase.h b/Source/Flow/Public/Nodes/FlowNodeBase.h index b9f9d5169..1fa511acc 100644 --- a/Source/Flow/Public/Nodes/FlowNodeBase.h +++ b/Source/Flow/Public/Nodes/FlowNodeBase.h @@ -118,6 +118,9 @@ class FLOW_API UFlowNodeBase 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 @@ -335,20 +338,41 @@ class FLOW_API UFlowNodeBase // used when import graph from another asset virtual void PostImport() {} + + void RequestReconstruction() const { (void) OnReconstructionRequested.ExecuteIfBound(); }; + + virtual EDataValidationResult ValidateNode(); // Called by owning FlowNode to add to its Status String. // (may be multi-line) virtual FString GetStatusString() const; - void RequestReconstruction() const { (void) OnReconstructionRequested.ExecuteIfBound(); }; - -#endif +#endif // WITH_EDITOR 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; + // Flow Node Validation : blueprint compatibility + + // 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); + // -- + #if WITH_EDITORONLY_DATA protected: UPROPERTY() diff --git a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp index 50056584d..15dab3c60 100644 --- a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp +++ b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp @@ -1320,6 +1320,27 @@ void UFlowGraphNode::SetParentNodeForSubNode(UFlowGraphNode* InParentNode) { // Once a SubNode, always a SubNode bIsSubNode = true; + +#if WITH_EDITOR + // Attempt to set the parent node if the instance is an AddOn + if (UFlowNodeAddOn* SelfAsAddOn = Cast(NodeInstance)) + { + const UFlowNode* TopLevelOwner = nullptr; + + if (const UFlowNode* ParentFlowNode = Cast(InParentNode->NodeInstance)) + { + // Parent is the top-level flow node + TopLevelOwner = ParentFlowNode; + } + else if (const UFlowNodeAddOn* ParentAddOn = Cast(InParentNode->NodeInstance)) + { + // Bubble up to the top-level flow node + TopLevelOwner = ParentAddOn->GetParentNode(); + } + + SelfAsAddOn->SetParentNode(const_cast(TopLevelOwner)); + } +#endif } ParentNode = InParentNode; @@ -1479,10 +1500,12 @@ void UFlowGraphNode::AddSubNode(UFlowGraphNode* SubNode, class UEdGraph* ParentG // 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->SetParentNodeForSubNode(this); + SubNode->AllocateDefaultPins(); SubNode->AutowireNewNode(nullptr);