From 93800f0ea1ee9cc70fa39440f8b4b82e6707b6a7 Mon Sep 17 00:00:00 2001 From: LindyHopperGT <91915878+LindyHopperGT@users.noreply.github.com> Date: Fri, 3 Oct 2025 12:26:09 -0700 Subject: [PATCH 1/2] [FlowAssetParams] Initial implementation of FlowAssetParams FlowAssetParams are a new data asset, that is optionally managed by the FlowAsset, sourcing properties from the Start node of the graph. These params can be subclassed to create overrides. At runtime, either the base or a compatible subclass may be used to provide data for start data pins in the flow graph. - Added FlowAssetParams - Added support to FlowAsset to optionally create and sync with FlowAssetParams base - Added AssetDefinition_FlorAssetParams to add 'create child', for creating 'subclassed' params that override some elements - Updated FFlowNamedDataPinProperty to have Guid and other properties used in the FlowAssetParams use-case - Added FlowNamedPropertiesSupplierInterface - Added FlowAssetProviderInterface - FFlowAssetParamsPtr - wrapper class for details customization - Added Params support to FlowComponent & FlowSubsystem Also: - Removed stale references to IFlowOwnerInterface - Merged over some deltas from IFlowExtended/Curated Property Customization --- Source/Flow/Flow.Build.cs | 3 +- Source/Flow/Private/FlowAsset.cpp | 134 +++++++++++++++++- Source/Flow/Private/FlowComponent.cpp | 4 +- Source/Flow/Private/FlowSubsystem.cpp | 8 +- Source/Flow/Private/Nodes/FlowNodeBase.cpp | 74 ---------- Source/Flow/Public/FlowAsset.h | 24 ++++ Source/Flow/Public/FlowComponent.h | 15 +- Source/Flow/Public/FlowSubsystem.h | 5 +- .../Public/Interfaces/FlowOwnerInterface.h | 4 +- .../Nodes/Actor/FlowNode_ExecuteComponent.h | 1 - Source/Flow/Public/Nodes/FlowNodeBase.h | 10 -- .../Nodes/Graph/FlowNode_DefineProperties.h | 10 +- .../Public/Nodes/Graph/FlowNode_SubGraph.h | 4 +- .../Flow/Public/Types/FlowDataPinProperties.h | 116 ++++++++++----- .../Asset/AssetDefinition_FlowAsset.cpp | 2 +- .../Private/Asset/FlowObjectDiff.cpp | 2 +- .../FlowEditor/Private/FlowEditorModule.cpp | 6 +- .../Private/Graph/Nodes/FlowGraphNode.cpp | 1 + .../IFlowCuratedNamePropertyCustomization.cpp | 34 ++--- ...IFlowExtendedPropertyTypeCustomization.cpp | 3 - .../FlowEditor/Public/Asset/FlowDiffControl.h | 1 + .../FlowEditor/Public/Asset/FlowObjectDiff.h | 1 + Source/FlowEditor/Public/FlowEditorModule.h | 2 +- .../Public/Graph/Nodes/FlowGraphNode.h | 2 +- .../Public/Graph/Widgets/SFlowGraphNode.h | 7 +- .../IFlowCuratedNamePropertyCustomization.h | 11 +- 26 files changed, 313 insertions(+), 171 deletions(-) diff --git a/Source/Flow/Flow.Build.cs b/Source/Flow/Flow.Build.cs index 74df20a4c..4d8b12253 100644 --- a/Source/Flow/Flow.Build.cs +++ b/Source/Flow/Flow.Build.cs @@ -32,7 +32,8 @@ public Flow(ReadOnlyTargetRules target) : base(target) PublicDependencyModuleNames.AddRange(new[] { "MessageLog", - "UnrealEd" + "UnrealEd", + "SourceControl", }); } } diff --git a/Source/Flow/Private/FlowAsset.cpp b/Source/Flow/Private/FlowAsset.cpp index 521ed3e36..9f52f9edb 100644 --- a/Source/Flow/Private/FlowAsset.cpp +++ b/Source/Flow/Private/FlowAsset.cpp @@ -5,8 +5,9 @@ #include "FlowLogChannels.h" #include "FlowSettings.h" #include "FlowSubsystem.h" - #include "AddOns/FlowNodeAddOn.h" +#include "Asset/FlowAssetParams.h" +#include "Asset/FlowAssetParamsUtils.h" #include "Interfaces/FlowDataPinGeneratorNodeInterface.h" #include "Nodes/FlowNodeBase.h" #include "Nodes/Graph/FlowNode_CustomInput.h" @@ -19,8 +20,18 @@ #include "Serialization/MemoryWriter.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 "ObjectTools.h" +#include "SourceControlHelpers.h" +#include "UObject/ObjectSaveContext.h" +#include "UObject/Package.h" +#include "UObject/SavePackage.h" FString UFlowAsset::ValidationError_NodeClassNotAllowed = TEXT("Node class {0} is not allowed in this asset."); FString UFlowAsset::ValidationError_NullNodeInstance = TEXT("Node with GUID {0} is NULL"); @@ -101,6 +112,127 @@ void UFlowAsset::PostLoad() { UnregisterNode(Guid); } + + ReconcileBaseAssetParams(FFlowAssetParamsUtils::GetLastSavedTimestampForObject(this)); +} + +void UFlowAsset::PreSaveRoot(FObjectPreSaveRootContext ObjectSaveContext) +{ + ReconcileBaseAssetParams(FDateTime::Now()); +} + +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; + } + + 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()); + } +} + +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 + 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; + + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + AssetRegistryModule.Get().AssetCreated(NewParams); + + FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked("ContentBrowser"); + 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"); + } } EDataValidationResult UFlowAsset::ValidateAsset(FFlowMessageLog& MessageLog) diff --git a/Source/Flow/Private/FlowComponent.cpp b/Source/Flow/Private/FlowComponent.cpp index fc1dc359e..0f4a66d01 100644 --- a/Source/Flow/Private/FlowComponent.cpp +++ b/Source/Flow/Private/FlowComponent.cpp @@ -2,6 +2,7 @@ #include "FlowComponent.h" +#include "Asset/FlowAssetParams.h" #include "FlowAsset.h" #include "FlowLogChannels.h" #include "FlowSettings.h" @@ -450,7 +451,8 @@ void UFlowComponent::StartRootFlow() { VerifyIdentityTags(); - FlowSubsystem->StartRootFlow(this, RootFlow, bAllowMultipleInstances); + TScriptInterface RootFlowParamsAsInterface = RootFlowParams.ResolveFlowAssetParams(); + FlowSubsystem->StartRootFlow(this, RootFlow, RootFlowParamsAsInterface, bAllowMultipleInstances); } } } diff --git a/Source/Flow/Private/FlowSubsystem.cpp b/Source/Flow/Private/FlowSubsystem.cpp index 946610632..d8f934a03 100644 --- a/Source/Flow/Private/FlowSubsystem.cpp +++ b/Source/Flow/Private/FlowSubsystem.cpp @@ -76,15 +76,13 @@ void UFlowSubsystem::AbortActiveFlows() RootInstances.Empty(); } -void UFlowSubsystem::StartRootFlow(UObject* Owner, UFlowAsset* FlowAsset, const bool bAllowMultipleInstances /* = true */) +void UFlowSubsystem::StartRootFlow(UObject* Owner, UFlowAsset* FlowAsset, TScriptInterface DataPinValueSupplier, const bool bAllowMultipleInstances) { if (FlowAsset) { if (UFlowAsset* NewFlow = CreateRootFlow(Owner, FlowAsset, bAllowMultipleInstances)) { - // todo: (gtaylor) In the future, we may want to provide a way to set a data pin value supplier - // for the root flow graph. - NewFlow->StartFlow(); + NewFlow->StartFlow(DataPinValueSupplier.GetInterface()); } } #if WITH_EDITOR @@ -664,4 +662,4 @@ void UFlowSubsystem::FindComponents(const FGameplayTagContainer& Tags, const EGa } } -#undef LOCTEXT_NAMESPACE +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/Flow/Private/Nodes/FlowNodeBase.cpp b/Source/Flow/Private/Nodes/FlowNodeBase.cpp index 7c2bdcbdb..2f70c55b0 100644 --- a/Source/Flow/Private/Nodes/FlowNodeBase.cpp +++ b/Source/Flow/Private/Nodes/FlowNodeBase.cpp @@ -319,39 +319,6 @@ UObject* UFlowNodeBase::TryGetRootFlowObjectOwner() const return nullptr; } -IFlowOwnerInterface* UFlowNodeBase::GetFlowOwnerInterface() const -{ - const UFlowAsset* FlowAsset = GetFlowAsset(); - if (!IsValid(FlowAsset)) - { - return nullptr; - } - - const UClass* ExpectedOwnerClass = FlowAsset->GetExpectedOwnerClass(); - if (!IsValid(ExpectedOwnerClass)) - { - return nullptr; - } - - UObject* RootFlowOwner = FlowAsset->GetOwner(); - if (!IsValid(RootFlowOwner)) - { - return nullptr; - } - - if (IFlowOwnerInterface* FlowOwnerInterface = TryGetFlowOwnerInterfaceFromRootFlowOwner(*RootFlowOwner, *ExpectedOwnerClass)) - { - return FlowOwnerInterface; - } - - if (IFlowOwnerInterface* FlowOwnerInterface = TryGetFlowOwnerInterfaceActor(*RootFlowOwner, *ExpectedOwnerClass)) - { - return FlowOwnerInterface; - } - - return nullptr; -} - TArray UFlowNodeBase::BuildFlowNodeBaseAncestorChain(UFlowNodeBase& FromFlowNodeBase, bool bIncludeFromFlowNodeBase) { TArray AncestorChain; @@ -374,47 +341,6 @@ TArray UFlowNodeBase::BuildFlowNodeBaseAncestorChain(UFlowNodeBa return AncestorChain; } -IFlowOwnerInterface* UFlowNodeBase::TryGetFlowOwnerInterfaceFromRootFlowOwner(UObject& RootFlowOwner, const UClass& ExpectedOwnerClass) -{ - const UClass* RootFlowOwnerClass = RootFlowOwner.GetClass(); - if (!IsValid(RootFlowOwnerClass)) - { - return nullptr; - } - - if (!RootFlowOwnerClass->IsChildOf(&ExpectedOwnerClass)) - { - return nullptr; - } - - // If the immediate owner is the expected class type, return its FlowOwnerInterface - return CastChecked(&RootFlowOwner); -} - -IFlowOwnerInterface* UFlowNodeBase::TryGetFlowOwnerInterfaceActor(UObject& RootFlowOwner, const UClass& ExpectedOwnerClass) -{ - // Special case if the immediate owner is a component, also consider the component's owning actor - const UActorComponent* FlowComponent = Cast(&RootFlowOwner); - if (!IsValid(FlowComponent)) - { - return nullptr; - } - - AActor* ActorOwner = FlowComponent->GetOwner(); - if (!IsValid(ActorOwner)) - { - return nullptr; - } - - const UClass* ActorOwnerClass = ActorOwner->GetClass(); - if (!ActorOwnerClass->IsChildOf(&ExpectedOwnerClass)) - { - return nullptr; - } - - return CastChecked(ActorOwner); -} - EFlowAddOnAcceptResult UFlowNodeBase::AcceptFlowNodeAddOnChild_Implementation( const UFlowNodeAddOn* AddOnTemplate, const TArray& AdditionalAddOnsToAssumeAreChildren) const diff --git a/Source/Flow/Public/FlowAsset.h b/Source/Flow/Public/FlowAsset.h index 38b9e206d..e001873c9 100644 --- a/Source/Flow/Public/FlowAsset.h +++ b/Source/Flow/Public/FlowAsset.h @@ -4,6 +4,7 @@ #include "FlowSave.h" #include "FlowTypes.h" +#include "Asset/FlowAssetParamsTypes.h" #include "Nodes/FlowNode.h" #if WITH_EDITOR @@ -21,6 +22,7 @@ class UFlowSubsystem; class UEdGraph; class UEdGraphNode; class UFlowAsset; +class UFlowAssetParams; #if !UE_BUILD_SHIPPING DECLARE_DELEGATE(FFlowGraphEvent); @@ -457,6 +459,28 @@ class FLOW_API UFlowAsset : public UObject UFUNCTION(BlueprintNativeEvent, Category = "SaveGame") bool IsBoundToWorld(); +////////////////////////////////////////////////////////////////////////// +// 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 + // Called before saving the asset. + virtual void PreSaveRoot(FObjectPreSaveRootContext ObjectSaveContext) override; + + // Generates a new params asset from the Start node. + UFlowAssetParams* GenerateParamsFromStartNode(); + + // 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 + ////////////////////////////////////////////////////////////////////////// // Utils diff --git a/Source/Flow/Public/FlowComponent.h b/Source/Flow/Public/FlowComponent.h index d579e2568..333779911 100644 --- a/Source/Flow/Public/FlowComponent.h +++ b/Source/Flow/Public/FlowComponent.h @@ -7,7 +7,8 @@ #include "FlowSave.h" #include "FlowTypes.h" -#include "Interfaces/FlowOwnerInterface.h" +#include "Interfaces/FlowAssetProviderInterface.h" +#include "Asset/FlowAssetParamsTypes.h" #include "FlowComponent.generated.h" class UFlowAsset; @@ -42,7 +43,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, public IFlowOwnerInterface +class FLOW_API UFlowComponent : public UActorComponent, public IFlowAssetProviderInterface { GENERATED_UCLASS_BODY() @@ -167,6 +168,10 @@ class FLOW_API UFlowComponent : public UActorComponent, public IFlowOwnerInterfa UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RootFlow") TObjectPtr RootFlow; + // 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; @@ -197,6 +202,10 @@ class FLOW_API UFlowComponent : public UActorComponent, public IFlowOwnerInterfa 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 @@ -253,4 +262,4 @@ class FLOW_API UFlowComponent : public UActorComponent, public IFlowOwnerInterfa public: UFlowSubsystem* GetFlowSubsystem() const; bool IsFlowNetMode(const EFlowNetMode NetMode) const; -}; +}; \ No newline at end of file diff --git a/Source/Flow/Public/FlowSubsystem.h b/Source/Flow/Public/FlowSubsystem.h index 4852cb519..01790fcd6 100644 --- a/Source/Flow/Public/FlowSubsystem.h +++ b/Source/Flow/Public/FlowSubsystem.h @@ -11,6 +11,7 @@ class UFlowAsset; class UFlowNode_SubGraph; +class IFlowDataPinValueSupplierInterface; DECLARE_DYNAMIC_MULTICAST_DELEGATE(FSimpleFlowEvent); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSimpleFlowComponentEvent, UFlowComponent*, Component); @@ -73,7 +74,7 @@ class FLOW_API UFlowSubsystem : public UGameInstanceSubsystem /* 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, TScriptInterface DataPinValueSupplier, const bool bAllowMultipleInstances = true); virtual UFlowAsset* CreateRootFlow(UObject* Owner, UFlowAsset* FlowAsset, const bool bAllowMultipleInstances = true, const FString& NewInstanceName = FString()); @@ -431,4 +432,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/Interfaces/FlowOwnerInterface.h b/Source/Flow/Public/Interfaces/FlowOwnerInterface.h index 28a00f5e0..5c17b1a62 100644 --- a/Source/Flow/Public/Interfaces/FlowOwnerInterface.h +++ b/Source/Flow/Public/Interfaces/FlowOwnerInterface.h @@ -6,8 +6,8 @@ #include "FlowOwnerInterface.generated.h" -// (optional) interface to enable a Flow owner object to execute CallOwnerFunction nodes -UINTERFACE(MinimalAPI, Blueprintable, BlueprintType) +// (deprecated) interface to enable a Flow owner object to execute CallOwnerFunction nodes +UINTERFACE(MinimalAPI) class UFlowOwnerInterface : public UInterface { GENERATED_BODY() diff --git a/Source/Flow/Public/Nodes/Actor/FlowNode_ExecuteComponent.h b/Source/Flow/Public/Nodes/Actor/FlowNode_ExecuteComponent.h index c4d8a4c58..a4d0f33b5 100644 --- a/Source/Flow/Public/Nodes/Actor/FlowNode_ExecuteComponent.h +++ b/Source/Flow/Public/Nodes/Actor/FlowNode_ExecuteComponent.h @@ -11,7 +11,6 @@ #include "FlowNode_ExecuteComponent.generated.h" // Forward Declarations -class IFlowOwnerInterface; class UFlowInjectComponentsManager; UENUM() diff --git a/Source/Flow/Public/Nodes/FlowNodeBase.h b/Source/Flow/Public/Nodes/FlowNodeBase.h index b9f9d5169..6c54a0a8d 100644 --- a/Source/Flow/Public/Nodes/FlowNodeBase.h +++ b/Source/Flow/Public/Nodes/FlowNodeBase.h @@ -18,7 +18,6 @@ class UFlowNode; class UFlowNodeAddOn; class UFlowSubsystem; class UEdGraphNode; -class IFlowOwnerInterface; class IFlowDataPinValueSupplierInterface; struct FFlowPin; struct FFlowNamedDataPinProperty; @@ -157,17 +156,8 @@ class FLOW_API UFlowNodeBase UFUNCTION(BlueprintCallable, Category = "FlowNode") UObject* TryGetRootFlowObjectOwner() const; - // Returns the IFlowOwnerInterface for the owner object (if implemented) - // NOTE - will consider a UActorComponent owner's owning actor if appropriate - IFlowOwnerInterface* GetFlowOwnerInterface() const; - static TArray BuildFlowNodeBaseAncestorChain(UFlowNodeBase& FromFlowNodeBase, bool bIncludeFromFlowNodeBase); -protected: - // Helper functions for GetFlowOwnerInterface() - static IFlowOwnerInterface* TryGetFlowOwnerInterfaceFromRootFlowOwner(UObject& RootFlowOwner, const UClass& ExpectedOwnerClass); - static IFlowOwnerInterface* TryGetFlowOwnerInterfaceActor(UObject& RootFlowOwner, const UClass& ExpectedOwnerClass); - ////////////////////////////////////////////////////////////////////////// // AddOn support diff --git a/Source/Flow/Public/Nodes/Graph/FlowNode_DefineProperties.h b/Source/Flow/Public/Nodes/Graph/FlowNode_DefineProperties.h index 0573362bc..c40ed2c69 100644 --- a/Source/Flow/Public/Nodes/Graph/FlowNode_DefineProperties.h +++ b/Source/Flow/Public/Nodes/Graph/FlowNode_DefineProperties.h @@ -3,6 +3,7 @@ #pragma once #include "Interfaces/FlowDataPinGeneratorNodeInterface.h" +#include "Interfaces/FlowNamedPropertiesSupplierInterface.h" #include "Nodes/FlowNode.h" #include "Types/FlowDataPinProperties.h" @@ -12,7 +13,10 @@ * 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 IFlowDataPinGeneratorNodeInterface +class FLOW_API UFlowNode_DefineProperties + : public UFlowNode + , public IFlowDataPinGeneratorNodeInterface + , public IFlowNamedPropertiesSupplierInterface { GENERATED_UCLASS_BODY() @@ -35,6 +39,10 @@ class FLOW_API UFlowNode_DefineProperties : public UFlowNode, public IFlowDataPi // IFlowDataPinGeneratorNodeInterface virtual void AutoGenerateDataPins(TMap& PinNameToBoundPropertyMap, TArray& InputDataPins, TArray& OutputDataPins) const override; // -- + + // IFlowNamedPropertiesSupplierInterface + virtual TArray& GetMutableNamedProperties() override { return NamedProperties; } + // -- #endif bool TryFormatTextWithNamedPropertiesAsParameters(const FText& FormatText, FText& OutFormattedText) const; diff --git a/Source/Flow/Public/Nodes/Graph/FlowNode_SubGraph.h b/Source/Flow/Public/Nodes/Graph/FlowNode_SubGraph.h index 5f7f89f0d..24cbed357 100644 --- a/Source/Flow/Public/Nodes/Graph/FlowNode_SubGraph.h +++ b/Source/Flow/Public/Nodes/Graph/FlowNode_SubGraph.h @@ -15,7 +15,7 @@ class FLOW_API UFlowNode_SubGraph : public UFlowNode, public IFlowDataPinGenerat { GENERATED_UCLASS_BODY() -public: +public: friend class UFlowAsset; friend class FFlowNode_SubGraphDetails; friend class UFlowSubsystem; @@ -27,6 +27,8 @@ class FLOW_API UFlowNode_SubGraph : public UFlowNode, public IFlowDataPinGenerat UPROPERTY(EditAnywhere, Category = "Graph") TSoftObjectPtr Asset; + // TODO (gtaylor) Create FlowAssetParams option for the Subgraph & reconcile with connected input pins' values + /* * 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 diff --git a/Source/Flow/Public/Types/FlowDataPinProperties.h b/Source/Flow/Public/Types/FlowDataPinProperties.h index a40a0e24f..75407e7a9 100644 --- a/Source/Flow/Public/Types/FlowDataPinProperties.h +++ b/Source/Flow/Public/Types/FlowDataPinProperties.h @@ -3,6 +3,7 @@ #pragma once #include "GameplayTagContainer.h" +#include "Kismet/BlueprintFunctionLibrary.h" #include "StructUtils/InstancedStruct.h" #include "UObject/Class.h" @@ -17,7 +18,7 @@ struct FFlowDataPinProperty { GENERATED_BODY() - FFlowDataPinProperty() { } + FFlowDataPinProperty() = default; virtual ~FFlowDataPinProperty() { } @@ -44,11 +45,86 @@ struct FFlowDataPinProperty return UnrealType; } - else + + return StructProperty->Struct; + } +#endif +}; + +// 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; + + // DataPinProperty payload + UPROPERTY(EditAnywhere, Category = DataPins, meta = (ExcludeBaseStruct, NoClear)) + TInstancedStruct DataPinProperty; + +#if WITH_EDITORONLY_DATA + // Unique identifier for property tracking + UPROPERTY() + 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 && DataPinProperty.GetPtr() != nullptr; } + + bool IsInputProperty() const; + bool IsOutputProperty() const; + +#if WITH_EDITOR + FFlowPin CreateFlowPin() const { return FFlowDataPinProperty::CreateFlowPin(Name, DataPinProperty); } + + 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) { - return StructProperty->Struct; + Property.ConfigureForFlowAssetStartNode(); } } + #endif }; @@ -446,40 +522,6 @@ struct FFlowDataPinOutputProperty_Class : public FFlowDataPinProperty UClass* GetResolvedClass() const { return Value.ResolveClass(); } }; -// 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) - FName Name; - - // DataPinProperty payload - UPROPERTY(EditAnywhere, Category = DataPins, meta = (ExcludeBaseStruct, NoClear)) - TInstancedStruct DataPinProperty; - -public: - - FFlowNamedDataPinProperty() { } - - bool IsValid() const { return Name != NAME_None && DataPinProperty.GetPtr() != nullptr; } - - bool IsInputProperty() const; - bool IsOutputProperty() const; - -#if WITH_EDITOR - FFlowPin CreateFlowPin() const { return FFlowDataPinProperty::CreateFlowPin(Name, DataPinProperty); } - - FLOW_API FText BuildHeaderText() const; -#endif // WITH_EDITOR -}; - // 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 an Defaulted-Input property (rather than an output property) diff --git a/Source/FlowEditor/Private/Asset/AssetDefinition_FlowAsset.cpp b/Source/FlowEditor/Private/Asset/AssetDefinition_FlowAsset.cpp index 03a513c6d..df88d22cb 100644 --- a/Source/FlowEditor/Private/Asset/AssetDefinition_FlowAsset.cpp +++ b/Source/FlowEditor/Private/Asset/AssetDefinition_FlowAsset.cpp @@ -30,7 +30,7 @@ TConstArrayView UAssetDefinition_FlowAsset::GetAssetCategori { if (UFlowGraphSettings::Get()->bExposeFlowAssetCreation) { - static const auto Categories = {FFLowAssetCategoryPaths::Flow}; + static const auto Categories = {FFlowAssetCategoryPaths::Flow}; return Categories; } diff --git a/Source/FlowEditor/Private/Asset/FlowObjectDiff.cpp b/Source/FlowEditor/Private/Asset/FlowObjectDiff.cpp index 370ff0bd9..7871b3514 100644 --- a/Source/FlowEditor/Private/Asset/FlowObjectDiff.cpp +++ b/Source/FlowEditor/Private/Asset/FlowObjectDiff.cpp @@ -3,8 +3,8 @@ #include "Asset/FlowObjectDiff.h" #include "Asset/FlowDiffControl.h" -#include "Graph/Nodes/FlowGraphNode.h" #include "Nodes/FlowNodeBase.h" +#include "Graph/Nodes/FlowGraphNode.h" #include "DiffResults.h" #include "EdGraph/EdGraph.h" diff --git a/Source/FlowEditor/Private/FlowEditorModule.cpp b/Source/FlowEditor/Private/FlowEditorModule.cpp index 94582f2f7..c036a4651 100644 --- a/Source/FlowEditor/Private/FlowEditorModule.cpp +++ b/Source/FlowEditor/Private/FlowEditorModule.cpp @@ -32,9 +32,11 @@ #include "DetailCustomizations/FlowDataPinProperty_ObjectCustomization.h" #include "DetailCustomizations/FlowPinCustomization.h" #include "DetailCustomizations/FlowNamedDataPinOutputPropertyCustomization.h" +#include "DetailCustomizations/FlowAssetParamsPtrCustomization.h" #include "FlowAsset.h" #include "AddOns/FlowNodeAddOn.h" +#include "Asset/FlowAssetParamsTypes.h" #include "Nodes/Actor/FlowNode_ComponentObserver.h" #include "Nodes/Actor/FlowNode_PlayLevelSequence.h" #include "Nodes/Graph/FlowNode_CustomInput.h" @@ -55,7 +57,7 @@ static FName AssetSearchModuleName = TEXT("AssetSearch"); #define LOCTEXT_NAMESPACE "FlowEditorModule" EAssetTypeCategories::Type FFlowEditorModule::FlowAssetCategory = static_cast(0); -FAssetCategoryPath FFLowAssetCategoryPaths::Flow(LOCTEXT("Flow", "Flow")); +FAssetCategoryPath FFlowAssetCategoryPaths::Flow(LOCTEXT("Flow", "Flow")); void FFlowEditorModule::StartupModule() { @@ -259,6 +261,8 @@ void FFlowEditorModule::RegisterDetailCustomizations() RegisterCustomStructLayout(*FFlowDataPinInputProperty_Class::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowDataPinInputProperty_ClassCustomization::MakeInstance)); RegisterCustomStructLayout(*FFlowDataPinInputProperty_Object::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowDataPinInputProperty_ObjectCustomization::MakeInstance)); + RegisterCustomStructLayout(*FFlowAssetParamsPtr::StaticStruct(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FFlowAssetParamsPtrCustomization::MakeInstance)); + // Consider implementing details customizations... for every EFlowPinType FLOW_ASSERT_ENUM_MAX(EFlowPinType, 16); diff --git a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp index 50056584d..99a8203dc 100644 --- a/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp +++ b/Source/FlowEditor/Private/Graph/Nodes/FlowGraphNode.cpp @@ -28,6 +28,7 @@ #include "Kismet2/KismetEditorUtilities.h" #include "ScopedTransaction.h" #include "SourceCodeNavigation.h" +#include "Subsystems/AssetEditorSubsystem.h" #include "Textures/SlateIcon.h" #include "ToolMenuSection.h" #include "Editor/Transactor.h" diff --git a/Source/FlowEditor/Private/UnrealExtensions/IFlowCuratedNamePropertyCustomization.cpp b/Source/FlowEditor/Private/UnrealExtensions/IFlowCuratedNamePropertyCustomization.cpp index 5ef3afb86..77e64d428 100644 --- a/Source/FlowEditor/Private/UnrealExtensions/IFlowCuratedNamePropertyCustomization.cpp +++ b/Source/FlowEditor/Private/UnrealExtensions/IFlowCuratedNamePropertyCustomization.cpp @@ -59,22 +59,22 @@ void IFlowCuratedNamePropertyCustomization::CreateHeaderRowWidget(FDetailWidgetR .NameContent() [ SAssignNew(HeaderTextBlock, STextBlock) - .Text(BuildHeaderText()) + .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) - ] + .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 @@ -82,7 +82,7 @@ void IFlowCuratedNamePropertyCustomization::CreateHeaderRowWidget(FDetailWidgetR FIsResetToDefaultVisible::CreateSP( this, &IFlowCuratedNamePropertyCustomization::CustomIsResetToDefaultVisible); - FResetToDefaultHandler ResetHandler = + FResetToDefaultHandler ResetHandler = FResetToDefaultHandler::CreateSP( this, &IFlowCuratedNamePropertyCustomization::CustomResetToDefault); @@ -95,7 +95,7 @@ void IFlowCuratedNamePropertyCustomization::CreateHeaderRowWidget(FDetailWidgetR HeaderRow.IsEnabled(IsEnabledAttribute); } -bool IFlowCuratedNamePropertyCustomization::CustomIsResetToDefaultVisible(TSharedPtr Property) const +bool IFlowCuratedNamePropertyCustomization::CustomIsResetToDefaultVisibleImpl(TSharedPtr Property) const { FName CuratedName; if (!TryGetCuratedName(CuratedName)) @@ -106,7 +106,7 @@ bool IFlowCuratedNamePropertyCustomization::CustomIsResetToDefaultVisible(TShare return !CuratedName.IsNone(); } -void IFlowCuratedNamePropertyCustomization::CustomResetToDefault(TSharedPtr Property) +void IFlowCuratedNamePropertyCustomization::CustomResetToDefaultImpl(TSharedPtr Property) { if (TrySetCuratedNameWithSideEffects(NAME_None)) { @@ -114,7 +114,7 @@ void IFlowCuratedNamePropertyCustomization::CustomResetToDefault(TSharedPtrIsEditConst()) { @@ -132,7 +132,7 @@ bool IFlowCuratedNamePropertyCustomization::CustomIsEnabled() const bool IFlowCuratedNamePropertyCustomization::TrySetCuratedNameWithSideEffects(const FName& NewName) { FName ExistingName; - (void) TryGetCuratedName(ExistingName); + (void)TryGetCuratedName(ExistingName); if (ExistingName != NewName) { @@ -196,7 +196,7 @@ void IFlowCuratedNamePropertyCustomization::OnTextListComboBoxOpening() for (TSharedPtr& Text : CachedTextList) { - (void) MapNameToText.FindOrAdd(FName(Text.Get()->ToString()), Text); + (void)MapNameToText.FindOrAdd(FName(Text.Get()->ToString()), Text); } TArray CuratedNameOptions = GetCuratedNameOptions(); diff --git a/Source/FlowEditor/Private/UnrealExtensions/IFlowExtendedPropertyTypeCustomization.cpp b/Source/FlowEditor/Private/UnrealExtensions/IFlowExtendedPropertyTypeCustomization.cpp index d54ab8610..96d42a729 100644 --- a/Source/FlowEditor/Private/UnrealExtensions/IFlowExtendedPropertyTypeCustomization.cpp +++ b/Source/FlowEditor/Private/UnrealExtensions/IFlowExtendedPropertyTypeCustomization.cpp @@ -10,9 +10,6 @@ #include "IDetailPropertyRow.h" #include "Widgets/Text/STextBlock.h" - -// IFlowExtendedPropertyTypeCustomization Implementation - void IFlowExtendedPropertyTypeCustomization::CustomizeHeader(TSharedRef InStructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { StructPropertyHandle = InStructPropertyHandle; diff --git a/Source/FlowEditor/Public/Asset/FlowDiffControl.h b/Source/FlowEditor/Public/Asset/FlowDiffControl.h index 5240636ff..770f3a996 100644 --- a/Source/FlowEditor/Public/Asset/FlowDiffControl.h +++ b/Source/FlowEditor/Public/Asset/FlowDiffControl.h @@ -5,6 +5,7 @@ #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 diff --git a/Source/FlowEditor/Public/Asset/FlowObjectDiff.h b/Source/FlowEditor/Public/Asset/FlowObjectDiff.h index d1896fabe..0bb17be07 100644 --- a/Source/FlowEditor/Public/Asset/FlowObjectDiff.h +++ b/Source/FlowEditor/Public/Asset/FlowObjectDiff.h @@ -2,6 +2,7 @@ #pragma once +#include "Runtime/Launch/Resources/Version.h" #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 7 #include "Editor/Kismet/Private/DiffControl.h" #else diff --git a/Source/FlowEditor/Public/FlowEditorModule.h b/Source/FlowEditor/Public/FlowEditorModule.h index 018bfd314..8dec688cf 100644 --- a/Source/FlowEditor/Public/FlowEditorModule.h +++ b/Source/FlowEditor/Public/FlowEditorModule.h @@ -16,7 +16,7 @@ struct FGraphPanelPinConnectionFactory; class FFlowAssetEditor; class UFlowAsset; -struct FLOWEDITOR_API FFLowAssetCategoryPaths : EAssetCategoryPaths +struct FLOWEDITOR_API FFlowAssetCategoryPaths : EAssetCategoryPaths { static FAssetCategoryPath Flow; }; diff --git a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode.h b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode.h index a9d715862..6c5e3d9ca 100644 --- a/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode.h +++ b/Source/FlowEditor/Public/Graph/Nodes/FlowGraphNode.h @@ -157,7 +157,7 @@ class FLOWEDITOR_API UFlowGraphNode : public UEdGraphNode // -- virtual void OnNodeDoubleClicked() const; - virtual void OnNodeDoubleClickedInPIE() const {}; + virtual void OnNodeDoubleClickedInPIE() const {} /** check if node has any errors, used for assigning colors on graph */ virtual bool HasErrors() const; diff --git a/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode.h b/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode.h index a7977813b..2428d41b9 100644 --- a/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode.h +++ b/Source/FlowEditor/Public/Graph/Widgets/SFlowGraphNode.h @@ -4,6 +4,7 @@ #include "SGraphNode.h" #include "KismetPins/SGraphPinExec.h" +#include "Runtime/Launch/Resources/Version.h" #include "Graph/Nodes/FlowGraphNode.h" @@ -35,10 +36,10 @@ class FLOWEDITOR_API SFlowGraphNode : public SGraphNode virtual void GetNodeInfoPopups(FNodeInfoContext* Context, TArray& Popups) const override; virtual const FSlateBrush* GetShadowBrush(bool bSelected) const override; - #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 6 -virtual void GetOverlayBrushes(bool bSelected, const FVector2D WidgetSize, TArray& Brushes) const override; +#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION < 6 + virtual void GetOverlayBrushes(bool bSelected, const FVector2D WidgetSize, TArray& Brushes) const override; #else -virtual void GetOverlayBrushes(bool bSelected, const FVector2f& WidgetSize, TArray& Brushes) const override; + virtual void GetOverlayBrushes(bool bSelected, const FVector2f& WidgetSize, TArray& Brushes) const override; #endif // -- diff --git a/Source/FlowEditor/Public/UnrealExtensions/IFlowCuratedNamePropertyCustomization.h b/Source/FlowEditor/Public/UnrealExtensions/IFlowCuratedNamePropertyCustomization.h index cabc2636a..6bd95250d 100644 --- a/Source/FlowEditor/Public/UnrealExtensions/IFlowCuratedNamePropertyCustomization.h +++ b/Source/FlowEditor/Public/UnrealExtensions/IFlowCuratedNamePropertyCustomization.h @@ -12,7 +12,7 @@ class FLOWEDITOR_API IFlowCuratedNamePropertyCustomization : public IFlowExtendedPropertyTypeCustomization { protected: - // IExtendedPropertyTypeCustomization + // IFlowExtendedPropertyTypeCustomization virtual void CreateHeaderRowWidget(FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override; // --- @@ -34,9 +34,9 @@ class FLOWEDITOR_API IFlowCuratedNamePropertyCustomization : public IFlowExtende void AddToCachedTextList(const TSharedPtr Text); void InsertAtHeadOfCachedTextList(const TSharedPtr Text); - bool CustomIsResetToDefaultVisible(TSharedPtr Property) const; - void CustomResetToDefault(TSharedPtr Property); - bool CustomIsEnabled() const; + 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; @@ -44,6 +44,9 @@ class FLOWEDITOR_API IFlowCuratedNamePropertyCustomization : public IFlowExtende 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: From 89c1d18605a659b0d6fd910fc86a5f87fa311517 Mon Sep 17 00:00:00 2001 From: LindyHopperGT <91915878+LindyHopperGT@users.noreply.github.com> Date: Fri, 3 Oct 2025 12:28:02 -0700 Subject: [PATCH 2/2] More files for FlowAssetParams --- Source/Flow/Private/Asset/FlowAssetParams.cpp | 570 ++++++++++++++++++ .../Private/Asset/FlowAssetParamsTypes.cpp | 11 + .../Private/Asset/FlowAssetParamsUtils.cpp | 122 ++++ .../Interfaces/FlowAssetProviderInterface.cpp | 9 + Source/Flow/Public/Asset/FlowAssetParams.h | 96 +++ .../Flow/Public/Asset/FlowAssetParamsTypes.h | 67 ++ .../Flow/Public/Asset/FlowAssetParamsUtils.h | 44 ++ .../Interfaces/FlowAssetProviderInterface.h | 29 + .../FlowNamedPropertiesSupplierInterface.h | 30 + .../Asset/AssetDefinition_FlowAssetParams.cpp | 152 +++++ .../FlowAssetParamsPtrCustomization.cpp | 192 ++++++ .../Asset/AssetDefinition_FlowAssetParams.h | 24 + .../FlowAssetParamsPtrCustomization.h | 24 + 13 files changed, 1370 insertions(+) create mode 100644 Source/Flow/Private/Asset/FlowAssetParams.cpp create mode 100644 Source/Flow/Private/Asset/FlowAssetParamsTypes.cpp create mode 100644 Source/Flow/Private/Asset/FlowAssetParamsUtils.cpp create mode 100644 Source/Flow/Private/Interfaces/FlowAssetProviderInterface.cpp create mode 100644 Source/Flow/Public/Asset/FlowAssetParams.h create mode 100644 Source/Flow/Public/Asset/FlowAssetParamsTypes.h create mode 100644 Source/Flow/Public/Asset/FlowAssetParamsUtils.h create mode 100644 Source/Flow/Public/Interfaces/FlowAssetProviderInterface.h create mode 100644 Source/Flow/Public/Interfaces/FlowNamedPropertiesSupplierInterface.h create mode 100644 Source/FlowEditor/Private/Asset/AssetDefinition_FlowAssetParams.cpp create mode 100644 Source/FlowEditor/Private/DetailCustomizations/FlowAssetParamsPtrCustomization.cpp create mode 100644 Source/FlowEditor/Public/Asset/AssetDefinition_FlowAssetParams.h create mode 100644 Source/FlowEditor/Public/DetailCustomizations/FlowAssetParamsPtrCustomization.h diff --git a/Source/Flow/Private/Asset/FlowAssetParams.cpp b/Source/Flow/Private/Asset/FlowAssetParams.cpp new file mode 100644 index 000000000..a30daeede --- /dev/null +++ b/Source/Flow/Private/Asset/FlowAssetParams.cpp @@ -0,0 +1,570 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/FlowAssetParams.h" +#include "FlowAsset.h" +#include "FlowLogChannels.h" +#include "Asset/FlowAssetParamsUtils.h" +#if WITH_EDITOR +#include "SourceControlHelpers.h" +#include "Misc/DataValidation.h" +#endif + +#include UE_INLINE_GENERATED_CPP_BY_NAME(FlowAssetParams) + +void UFlowAssetParams::PostLoad() +{ + Super::PostLoad(); + +#if WITH_EDITOR + const EFlowReconcilePropertiesResult ReconcileResult = ReconcilePropertiesWithParentParams(); + if (EFlowReconcilePropertiesResult_Classifiers::IsErrorResult(ReconcileResult)) + { + UE_LOG(LogFlow, Error, TEXT("Failed to reconcile ParentParams 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.DataPinProperty.IsValid()) + { + Context.AddError(FText::FromString(FString::Printf(TEXT("Property at index %d has invalid DataPinProperty"), 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 && LocalProp->bIsOverride) + { + FFlowNamedDataPinProperty UpdatedProp = *LocalProp; + + // Enforce Parent's name + UpdatedProp.Name = ParentProp.Name; + + NewProperties.Add(UpdatedProp); + + continue; + } + + if (LocalProp && FFlowAssetParamsUtils::ArePropertiesEqual(*LocalProp, ParentProp)) + { + LocalProp->bIsOverride = false; + + // Enforce Parent's name + LocalProp->Name = ParentProp.Name; + + NewProperties.Add(*LocalProp); + + continue; + } + + NewProperties.Add(ParentProp); + } + + for (FFlowNamedDataPinProperty& LocalProp : Properties) + { + if (!FFlowAssetParamsUtils::FindPropertyByGuid(ParentProps, LocalProp.Guid)) + { + LocalProp.bIsOverride = true; + + NewProperties.Add(LocalProp); + } + } + + if (FFlowAssetParamsUtils::ArePropertyArraysEqual(NewProperties, Properties)) + { + return EFlowReconcilePropertiesResult::NoChanges; + } + + Properties = NewProperties; + + (void) TryCheckOutFromSourceControl(); + + 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::TryCheckOutFromSourceControl() const +{ + if (!USourceControlHelpers::IsAvailable()) + { + return true; + } + + const FString FileName = USourceControlHelpers::PackageFilename(GetPathName()); + if (!USourceControlHelpers::CheckOutOrAddFile(FileName)) + { + UE_LOG(LogFlow, Warning, TEXT("%s is not checked out; properties updated in-memory only"), *GetPathName()); + return false; + } + + return true; +} + +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.DataPinProperty); + } + else + { + UE_LOG(LogFlow, Warning, TEXT("Skipping invalid property %s during rebuild for %s"), *Prop.Name.ToString(), *GetPathName()); + } + } +} +#endif + +bool UFlowAssetParams::CanSupplyDataPinValues_Implementation() const +{ + return !PropertyMap.IsEmpty(); +} + +FFlowDataPinResult_Bool UFlowAssetParams::TrySupplyDataPinAsBool_Implementation(const FName& PinName) const +{ + if (const TInstancedStruct* Found = PropertyMap.Find(PinName)) + { + if (Found->GetScriptStruct()->IsChildOf(FFlowDataPinOutputProperty_Bool::StaticStruct())) + { + const FFlowDataPinOutputProperty_Bool& BoolProp = Found->Get(); + return FFlowDataPinResult_Bool(BoolProp.Value); + } + + UE_LOG(LogFlow, Warning, TEXT("Type mismatch for Bool pin %s in %s"), *PinName.ToString(), *GetPathName()); + } + + return FFlowDataPinResult_Bool(EFlowDataPinResolveResult::FailedUnknownPin); +} + +FFlowDataPinResult_Int UFlowAssetParams::TrySupplyDataPinAsInt_Implementation(const FName& PinName) const +{ + if (const TInstancedStruct* Found = PropertyMap.Find(PinName)) + { + const UScriptStruct* Struct = Found->GetScriptStruct(); + if (Struct->IsChildOf(FFlowDataPinOutputProperty_Int64::StaticStruct())) + { + const FFlowDataPinOutputProperty_Int64& IntProp = Found->Get(); + return FFlowDataPinResult_Int(IntProp.Value); + } + else if (Struct->IsChildOf(FFlowDataPinOutputProperty_Int32::StaticStruct())) + { + const FFlowDataPinOutputProperty_Int32& IntProp = Found->Get(); + return FFlowDataPinResult_Int(static_cast(IntProp.Value)); + } + + UE_LOG(LogFlow, Warning, TEXT("Type mismatch for Int pin %s in %s"), *PinName.ToString(), *GetPathName()); + } + + return FFlowDataPinResult_Int(EFlowDataPinResolveResult::FailedUnknownPin); +} + +FFlowDataPinResult_Float UFlowAssetParams::TrySupplyDataPinAsFloat_Implementation(const FName& PinName) const +{ + if (const TInstancedStruct* Found = PropertyMap.Find(PinName)) + { + const UScriptStruct* Struct = Found->GetScriptStruct(); + if (Struct->IsChildOf(FFlowDataPinOutputProperty_Double::StaticStruct())) + { + const FFlowDataPinOutputProperty_Double& FloatProp = Found->Get(); + return FFlowDataPinResult_Float(FloatProp.Value); + } + else if (Struct->IsChildOf(FFlowDataPinOutputProperty_Float::StaticStruct())) + { + const FFlowDataPinOutputProperty_Float& FloatProp = Found->Get(); + return FFlowDataPinResult_Float(static_cast(FloatProp.Value)); + } + + UE_LOG(LogFlow, Warning, TEXT("Type mismatch for Float pin %s in %s"), *PinName.ToString(), *GetPathName()); + } + + return FFlowDataPinResult_Float(EFlowDataPinResolveResult::FailedUnknownPin); +} + +FFlowDataPinResult_Name UFlowAssetParams::TrySupplyDataPinAsName_Implementation(const FName& PinName) const +{ + if (const TInstancedStruct* Found = PropertyMap.Find(PinName)) + { + if (Found->GetScriptStruct()->IsChildOf(FFlowDataPinOutputProperty_Name::StaticStruct())) + { + const FFlowDataPinOutputProperty_Name& NameProp = Found->Get(); + return FFlowDataPinResult_Name(NameProp.Value); + } + + UE_LOG(LogFlow, Warning, TEXT("Type mismatch for Name pin %s in %s"), *PinName.ToString(), *GetPathName()); + } + + return FFlowDataPinResult_Name(EFlowDataPinResolveResult::FailedUnknownPin); +} + +FFlowDataPinResult_String UFlowAssetParams::TrySupplyDataPinAsString_Implementation(const FName& PinName) const +{ + if (const TInstancedStruct* Found = PropertyMap.Find(PinName)) + { + if (Found->GetScriptStruct()->IsChildOf(FFlowDataPinOutputProperty_String::StaticStruct())) + { + const FFlowDataPinOutputProperty_String& StringProp = Found->Get(); + return FFlowDataPinResult_String(StringProp.Value); + } + + UE_LOG(LogFlow, Warning, TEXT("Type mismatch for String pin %s in %s"), *PinName.ToString(), *GetPathName()); + } + + return FFlowDataPinResult_String(EFlowDataPinResolveResult::FailedUnknownPin); +} + +FFlowDataPinResult_Text UFlowAssetParams::TrySupplyDataPinAsText_Implementation(const FName& PinName) const +{ + if (const TInstancedStruct* Found = PropertyMap.Find(PinName)) + { + if (Found->GetScriptStruct()->IsChildOf(FFlowDataPinOutputProperty_Text::StaticStruct())) + { + const FFlowDataPinOutputProperty_Text& TextProp = Found->Get(); + return FFlowDataPinResult_Text(TextProp.Value); + } + + UE_LOG(LogFlow, Warning, TEXT("Type mismatch for Text pin %s in %s"), *PinName.ToString(), *GetPathName()); + } + + return FFlowDataPinResult_Text(EFlowDataPinResolveResult::FailedUnknownPin); +} + +FFlowDataPinResult_Enum UFlowAssetParams::TrySupplyDataPinAsEnum_Implementation(const FName& PinName) const +{ + if (const TInstancedStruct* Found = PropertyMap.Find(PinName)) + { + if (Found->GetScriptStruct()->IsChildOf(FFlowDataPinOutputProperty_Enum::StaticStruct())) + { + const FFlowDataPinOutputProperty_Enum& EnumProp = Found->Get(); + return FFlowDataPinResult_Enum(EnumProp.Value, EnumProp.EnumClass); + } + + UE_LOG(LogFlow, Warning, TEXT("Type mismatch for Enum pin %s in %s"), *PinName.ToString(), *GetPathName()); + } + + return FFlowDataPinResult_Enum(EFlowDataPinResolveResult::FailedUnknownPin); +} + +FFlowDataPinResult_Vector UFlowAssetParams::TrySupplyDataPinAsVector_Implementation(const FName& PinName) const +{ + if (const TInstancedStruct* Found = PropertyMap.Find(PinName)) + { + if (Found->GetScriptStruct()->IsChildOf(FFlowDataPinOutputProperty_Vector::StaticStruct())) + { + const FFlowDataPinOutputProperty_Vector& VectorProp = Found->Get(); + return FFlowDataPinResult_Vector(VectorProp.Value); + } + + UE_LOG(LogFlow, Warning, TEXT("Type mismatch for Vector pin %s in %s"), *PinName.ToString(), *GetPathName()); + } + + return FFlowDataPinResult_Vector(EFlowDataPinResolveResult::FailedUnknownPin); +} + +FFlowDataPinResult_Rotator UFlowAssetParams::TrySupplyDataPinAsRotator_Implementation(const FName& PinName) const +{ + if (const TInstancedStruct* Found = PropertyMap.Find(PinName)) + { + if (Found->GetScriptStruct()->IsChildOf(FFlowDataPinOutputProperty_Rotator::StaticStruct())) + { + const FFlowDataPinOutputProperty_Rotator& RotatorProp = Found->Get(); + return FFlowDataPinResult_Rotator(RotatorProp.Value); + } + + UE_LOG(LogFlow, Warning, TEXT("Type mismatch for Rotator pin %s in %s"), *PinName.ToString(), *GetPathName()); + } + + return FFlowDataPinResult_Rotator(EFlowDataPinResolveResult::FailedUnknownPin); +} + +FFlowDataPinResult_Transform UFlowAssetParams::TrySupplyDataPinAsTransform_Implementation(const FName& PinName) const +{ + if (const TInstancedStruct* Found = PropertyMap.Find(PinName)) + { + if (Found->GetScriptStruct()->IsChildOf(FFlowDataPinOutputProperty_Transform::StaticStruct())) + { + const FFlowDataPinOutputProperty_Transform& TransformProp = Found->Get(); + return FFlowDataPinResult_Transform(TransformProp.Value); + } + + UE_LOG(LogFlow, Warning, TEXT("Type mismatch for Transform pin %s in %s"), *PinName.ToString(), *GetPathName()); + } + + return FFlowDataPinResult_Transform(EFlowDataPinResolveResult::FailedUnknownPin); +} + +FFlowDataPinResult_GameplayTag UFlowAssetParams::TrySupplyDataPinAsGameplayTag_Implementation(const FName& PinName) const +{ + if (const TInstancedStruct* Found = PropertyMap.Find(PinName)) + { + if (Found->GetScriptStruct()->IsChildOf(FFlowDataPinOutputProperty_GameplayTag::StaticStruct())) + { + const FFlowDataPinOutputProperty_GameplayTag& TagProp = Found->Get(); + return FFlowDataPinResult_GameplayTag(TagProp.Value); + } + + UE_LOG(LogFlow, Warning, TEXT("Type mismatch for GameplayTag pin %s in %s"), *PinName.ToString(), *GetPathName()); + } + + return FFlowDataPinResult_GameplayTag(EFlowDataPinResolveResult::FailedUnknownPin); +} + +FFlowDataPinResult_GameplayTagContainer UFlowAssetParams::TrySupplyDataPinAsGameplayTagContainer_Implementation(const FName& PinName) const +{ + if (const TInstancedStruct* Found = PropertyMap.Find(PinName)) + { + if (Found->GetScriptStruct()->IsChildOf(FFlowDataPinOutputProperty_GameplayTagContainer::StaticStruct())) + { + const FFlowDataPinOutputProperty_GameplayTagContainer& ContainerProp = Found->Get(); + return FFlowDataPinResult_GameplayTagContainer(ContainerProp.Value); + } + + UE_LOG(LogFlow, Warning, TEXT("Type mismatch for GameplayTagContainer pin %s in %s"), *PinName.ToString(), *GetPathName()); + } + + return FFlowDataPinResult_GameplayTagContainer(EFlowDataPinResolveResult::FailedUnknownPin); +} + +FFlowDataPinResult_InstancedStruct UFlowAssetParams::TrySupplyDataPinAsInstancedStruct_Implementation(const FName& PinName) const +{ + if (const TInstancedStruct* Found = PropertyMap.Find(PinName)) + { + if (Found->GetScriptStruct()->IsChildOf(FFlowDataPinOutputProperty_InstancedStruct::StaticStruct())) + { + const FFlowDataPinOutputProperty_InstancedStruct& StructProp = Found->Get(); + return FFlowDataPinResult_InstancedStruct(StructProp.Value); + } + + UE_LOG(LogFlow, Warning, TEXT("Type mismatch for InstancedStruct pin %s in %s"), *PinName.ToString(), *GetPathName()); + } + + return FFlowDataPinResult_InstancedStruct(EFlowDataPinResolveResult::FailedUnknownPin); +} + +FFlowDataPinResult_Object UFlowAssetParams::TrySupplyDataPinAsObject_Implementation(const FName& PinName) const +{ + if (const TInstancedStruct* Found = PropertyMap.Find(PinName)) + { + if (Found->GetScriptStruct()->IsChildOf(FFlowDataPinOutputProperty_Object::StaticStruct())) + { + const FFlowDataPinOutputProperty_Object& ObjectProp = Found->Get(); + return FFlowDataPinResult_Object(ObjectProp.GetObjectValue()); + } + + UE_LOG(LogFlow, Warning, TEXT("Type mismatch for Object pin %s in %s"), *PinName.ToString(), *GetPathName()); + } + + return FFlowDataPinResult_Object(EFlowDataPinResolveResult::FailedUnknownPin); +} + +FFlowDataPinResult_Class UFlowAssetParams::TrySupplyDataPinAsClass_Implementation(const FName& PinName) const +{ + if (const TInstancedStruct* Found = PropertyMap.Find(PinName)) + { + if (Found->GetScriptStruct()->IsChildOf(FFlowDataPinOutputProperty_Class::StaticStruct())) + { + const FFlowDataPinOutputProperty_Class& ClassProp = Found->Get(); + return FFlowDataPinResult_Class(ClassProp.GetResolvedClass()); + } + + UE_LOG(LogFlow, Warning, TEXT("Type mismatch for Class pin %s in %s"), *PinName.ToString(), *GetPathName()); + } + + return FFlowDataPinResult_Class(EFlowDataPinResolveResult::FailedUnknownPin); +} \ No newline at end of file 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..d2ad66b99 --- /dev/null +++ b/Source/Flow/Private/Asset/FlowAssetParamsUtils.cpp @@ -0,0 +1,122 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/FlowAssetParamsUtils.h" +#include "Asset/FlowAssetParamsTypes.h" +#include "Types/FlowDataPinProperties.h" +#include "FlowLogChannels.h" +#include "Misc/DateTime.h" +#include "HAL/FileManager.h" +#include "UObject/Class.h" +#include "UObject/UObjectGlobals.h" + +#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.DataPinProperty.GetScriptStruct(); + const UScriptStruct* ScriptStructB = PropB.DataPinProperty.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.DataPinProperty.GetScriptStruct(); + const UScriptStruct* ScriptStructB = B.DataPinProperty.GetScriptStruct(); + if (ScriptStructA != ScriptStructB) + { + return false; + } + + return A.DataPinProperty == B.DataPinProperty; +} + +#endif 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/Public/Asset/FlowAssetParams.h b/Source/Flow/Public/Asset/FlowAssetParams.h new file mode 100644 index 000000000..d8053d108 --- /dev/null +++ b/Source/Flow/Public/Asset/FlowAssetParams.h @@ -0,0 +1,96 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "Engine/DataAsset.h" +#include "Types/FlowDataPinProperties.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. +*/ +UCLASS(BlueprintType) +class FLOW_API UFlowAssetParams : public UDataAsset, public IFlowDataPinValueSupplierInterface, public IFlowAssetProviderInterface +{ + 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 + virtual void PostLoad() override; + virtual void Serialize(FArchive& Ar) override; + // -- + + // IFlowDataPinValueSupplierInterface + virtual bool CanSupplyDataPinValues_Implementation() const override; + virtual FFlowDataPinResult_Bool TrySupplyDataPinAsBool_Implementation(const FName& PinName) const override; + virtual FFlowDataPinResult_Int TrySupplyDataPinAsInt_Implementation(const FName& PinName) const override; + virtual FFlowDataPinResult_Float TrySupplyDataPinAsFloat_Implementation(const FName& PinName) const override; + virtual FFlowDataPinResult_Name TrySupplyDataPinAsName_Implementation(const FName& PinName) const override; + virtual FFlowDataPinResult_String TrySupplyDataPinAsString_Implementation(const FName& PinName) const override; + virtual FFlowDataPinResult_Text TrySupplyDataPinAsText_Implementation(const FName& PinName) const override; + virtual FFlowDataPinResult_Enum TrySupplyDataPinAsEnum_Implementation(const FName& PinName) const override; + virtual FFlowDataPinResult_Vector TrySupplyDataPinAsVector_Implementation(const FName& PinName) const override; + virtual FFlowDataPinResult_Rotator TrySupplyDataPinAsRotator_Implementation(const FName& PinName) const override; + virtual FFlowDataPinResult_Transform TrySupplyDataPinAsTransform_Implementation(const FName& PinName) const override; + virtual FFlowDataPinResult_GameplayTag TrySupplyDataPinAsGameplayTag_Implementation(const FName& PinName) const override; + virtual FFlowDataPinResult_GameplayTagContainer TrySupplyDataPinAsGameplayTagContainer_Implementation(const FName& PinName) const override; + virtual FFlowDataPinResult_InstancedStruct TrySupplyDataPinAsInstancedStruct_Implementation(const FName& PinName) const override; + virtual FFlowDataPinResult_Object TrySupplyDataPinAsObject_Implementation(const FName& PinName) const override; + virtual FFlowDataPinResult_Class TrySupplyDataPinAsClass_Implementation(const 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); + + void ConfigureFlowAssetParams(TSoftObjectPtr OwnerAsset, TSoftObjectPtr InParentParams, const TArray& InProperties); + +protected: + + // Updates properties from ParentParams, handling inheritance and name enforcement. + EFlowReconcilePropertiesResult ReconcilePropertiesWithParentParams(); + + bool TryCheckOutFromSourceControl() const; + + 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..6d8ecf13d --- /dev/null +++ b/Source/Flow/Public/Asset/FlowAssetParamsTypes.h @@ -0,0 +1,67 @@ +// 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(EFlowReconcilePropertiesResult Result) { return FLOW_IS_ENUM_IN_SUBRANGE(Result, EFlowReconcilePropertiesResult::Success); } + FORCEINLINE bool IsModifiedResult(EFlowReconcilePropertiesResult Result) { return FLOW_IS_ENUM_IN_SUBRANGE(Result, EFlowReconcilePropertiesResult::Modified); } + FORCEINLINE bool IsErrorResult(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 FFlowAssetParamsPtr +{ + GENERATED_BODY() + + FFlowAssetParamsPtr() = default; + FFlowAssetParamsPtr(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..9c1f966a1 --- /dev/null +++ b/Source/Flow/Public/Asset/FlowAssetParamsUtils.h @@ -0,0 +1,44 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#pragma once + +#include "Misc/DateTime.h" +#include "Asset/FlowAssetParamsTypes.h" + +#include "FlowAssetParamsUtils.generated.h" + +class UObject; +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); +#endif +}; \ No newline at end of file diff --git a/Source/Flow/Public/Interfaces/FlowAssetProviderInterface.h b/Source/Flow/Public/Interfaces/FlowAssetProviderInterface.h new file mode 100644 index 000000000..29d9bc95a --- /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/FlowNamedPropertiesSupplierInterface.h b/Source/Flow/Public/Interfaces/FlowNamedPropertiesSupplierInterface.h new file mode 100644 index 000000000..b41bd87ac --- /dev/null +++ b/Source/Flow/Public/Interfaces/FlowNamedPropertiesSupplierInterface.h @@ -0,0 +1,30 @@ +// 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: + +#if WITH_EDITOR + // Returns the array of named properties defined by this node. + virtual TArray& GetMutableNamedProperties() = 0; +#endif +}; diff --git a/Source/FlowEditor/Private/Asset/AssetDefinition_FlowAssetParams.cpp b/Source/FlowEditor/Private/Asset/AssetDefinition_FlowAssetParams.cpp new file mode 100644 index 000000000..c42d98ea7 --- /dev/null +++ b/Source/FlowEditor/Private/Asset/AssetDefinition_FlowAssetParams.cpp @@ -0,0 +1,152 @@ +// Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors + +#include "Asset/AssetDefinition_FlowAssetParams.h" +#include "Asset/FlowAssetParams.h" +#include "FlowAsset.h" +#include "FlowEditorLogChannels.h" +#include "FlowEditorModule.h" +#include "Types/FlowDataPinProperties.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetToolsModule.h" +#include "ContentBrowserMenuContexts.h" +#include "ContentBrowserModule.h" +#include "FileHelpers.h" +#include "IContentBrowserSingleton.h" +#include "SourceControlHelpers.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; + } + + FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); + const FString PackagePath = FPackageName::GetLongPackagePath(ParentParams->GetPackage()->GetPathName()); + const FString BaseAssetName = ParentParams->GetName(); + + FString UniquePackageName, UniqueAssetName; + AssetToolsModule.Get().CreateUniqueAssetName(PackagePath + TEXT("/") + BaseAssetName, TEXT(""), UniquePackageName, UniqueAssetName); + if (UniqueAssetName.IsEmpty()) + { + UE_LOG(LogFlowEditor, Error, TEXT("Failed to generate unique asset name for child params of %s"), *BaseAssetName); + return; + } + + UFlowAssetParams* NewParams = Cast( + AssetToolsModule.Get().CreateAsset(UniqueAssetName, PackagePath, ParentParams->GetClass(), nullptr)); + if (!IsValid(NewParams)) + { + UE_LOG(LogFlowEditor, Error, TEXT("Failed to create child Flow Asset Params: %s"), *UniqueAssetName); + return; + } + + if (USourceControlHelpers::IsAvailable()) + { + const FString FileName = USourceControlHelpers::PackageFilename(NewParams->GetPathName()); + if (!USourceControlHelpers::CheckOutOrAddFile(FileName)) + { + UE_LOG(LogFlowEditor, Warning, TEXT("Failed to check out/add %s; saved in-memory only"), *NewParams->GetPathName()); + } + } + + NewParams->ConfigureFlowAssetParams(ParentParams->OwnerFlowAsset, ParentParams, ParentParams->Properties); + + // Save the package (force save even if not prompted) + UPackage* Package = NewParams->GetPackage(); + TArray PackagesToSave = { Package }; + + // Saves without dialog/prompt + const bool bForceSave = true; + if (!UEditorLoadingAndSavingUtils::SavePackages(PackagesToSave, bForceSave)) + { + UE_LOG(LogFlowEditor, Error, TEXT("Failed to save child Flow Asset Params: %s"), *NewParams->GetPathName()); + return; + } + + FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked("ContentBrowser"); + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + AssetRegistryModule.Get().AssetCreated(NewParams); + TArray AssetsToSync = { NewParams }; + ContentBrowserModule.Get().SyncBrowserToAssets(AssetsToSync, true); + } + + 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 \ No newline at end of file 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/Public/Asset/AssetDefinition_FlowAssetParams.h b/Source/FlowEditor/Public/Asset/AssetDefinition_FlowAssetParams.h new file mode 100644 index 000000000..99c8aa74c --- /dev/null +++ b/Source/FlowEditor/Public/Asset/AssetDefinition_FlowAssetParams.h @@ -0,0 +1,24 @@ +// 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 interface + 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/DetailCustomizations/FlowAssetParamsPtrCustomization.h b/Source/FlowEditor/Public/DetailCustomizations/FlowAssetParamsPtrCustomization.h new file mode 100644 index 000000000..2c979633f --- /dev/null +++ b/Source/FlowEditor/Public/DetailCustomizations/FlowAssetParamsPtrCustomization.h @@ -0,0 +1,24 @@ +// 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