From c8d665e5ca4ceade6335fbb26d2552ae8cbba388 Mon Sep 17 00:00:00 2001 From: LindyHopperGT <91915878+LindyHopperGT@users.noreply.github.com> Date: Thu, 12 Mar 2026 15:44:38 -0700 Subject: [PATCH] Updated CompareValues predicate to support the remainder of the pin types --- .../FlowNodeAddOn_PredicateCompareValues.cpp | 414 ++++++++++++------ .../FlowNodeAddOn_PredicateCompareValues.h | 93 +++- 2 files changed, 363 insertions(+), 144 deletions(-) diff --git a/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateCompareValues.cpp b/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateCompareValues.cpp index 01790d29..513f2811 100644 --- a/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateCompareValues.cpp +++ b/Source/Flow/Private/AddOns/FlowNodeAddOn_PredicateCompareValues.cpp @@ -152,6 +152,41 @@ bool UFlowNodeAddOn_PredicateCompareValues::IsGameplayTagLikeTypeName(const FNam TypeName == FFlowPinTypeNamesStandard::PinTypeNameGameplayTagContainer; } +bool UFlowNodeAddOn_PredicateCompareValues::IsBoolTypeName(const FName& TypeName) +{ + return TypeName == FFlowPinTypeNamesStandard::PinTypeNameBool; +} + +bool UFlowNodeAddOn_PredicateCompareValues::IsVectorTypeName(const FName& TypeName) +{ + return TypeName == FFlowPinTypeNamesStandard::PinTypeNameVector; +} + +bool UFlowNodeAddOn_PredicateCompareValues::IsRotatorTypeName(const FName& TypeName) +{ + return TypeName == FFlowPinTypeNamesStandard::PinTypeNameRotator; +} + +bool UFlowNodeAddOn_PredicateCompareValues::IsTransformTypeName(const FName& TypeName) +{ + return TypeName == FFlowPinTypeNamesStandard::PinTypeNameTransform; +} + +bool UFlowNodeAddOn_PredicateCompareValues::IsObjectTypeName(const FName& TypeName) +{ + return TypeName == FFlowPinTypeNamesStandard::PinTypeNameObject; +} + +bool UFlowNodeAddOn_PredicateCompareValues::IsClassTypeName(const FName& TypeName) +{ + return TypeName == FFlowPinTypeNamesStandard::PinTypeNameClass; +} + +bool UFlowNodeAddOn_PredicateCompareValues::IsInstancedStructTypeName(const FName& TypeName) +{ + return TypeName == FFlowPinTypeNamesStandard::PinTypeNameInstancedStruct; +} + #if WITH_EDITOR void UFlowNodeAddOn_PredicateCompareValues::PostEditChangeChainProperty(FPropertyChangedChainEvent& PropertyChangedEvent) @@ -177,13 +212,99 @@ void UFlowNodeAddOn_PredicateCompareValues::OnPostEditEnsureAllNamedPropertiesPi } } +EDataValidationResult UFlowNodeAddOn_PredicateCompareValues::ValidateNode() +{ + EDataValidationResult Result = Super::ValidateNode(); + + // Validate that both values are configured + if (!LeftValue.IsValid()) + { + LogValidationError(TEXT("LeftValue is not configured (missing name or pin type).")); + Result = EDataValidationResult::Invalid; + } + + if (!RightValue.IsValid()) + { + LogValidationError(TEXT("RightValue is not configured (missing name or pin type).")); + Result = EDataValidationResult::Invalid; + } + + // Remaining checks require both values to be valid + if (!LeftValue.IsValid() || !RightValue.IsValid()) + { + return Result; + } + + const FFlowPinTypeName LeftPinTypeName = LeftValue.DataPinValue.Get().GetPinTypeName(); + const FFlowPinTypeName RightPinTypeName = RightValue.DataPinValue.Get().GetPinTypeName(); + + // Validate pin type names are set + if (LeftPinTypeName.IsNone()) + { + LogValidationError(TEXT("LeftValue has an unknown or unset pin type.")); + Result = EDataValidationResult::Invalid; + } + + if (RightPinTypeName.IsNone()) + { + LogValidationError(TEXT("RightValue has an unknown or unset pin type.")); + Result = EDataValidationResult::Invalid; + } + + if (LeftPinTypeName.IsNone() || RightPinTypeName.IsNone()) + { + return Result; + } + + // Check type compatibility + const FName LeftTypeName = LeftPinTypeName.Name; + const FName RightTypeName = RightPinTypeName.Name; + + const bool bSameType = (LeftTypeName == RightTypeName); + + if (!bSameType && !AreComparableStandardPinTypes(LeftTypeName, RightTypeName)) + { + LogValidationError(FString::Printf( + TEXT("Pin types are not comparable: '%s' vs '%s'."), + *LeftTypeName.ToString(), + *RightTypeName.ToString())); + Result = EDataValidationResult::Invalid; + } + + // Validate arithmetic operators are only used with numeric types + if (IsArithmeticOp() && !(IsNumericTypeName(LeftTypeName) && IsNumericTypeName(RightTypeName))) + { + LogValidationError(FString::Printf( + TEXT("Arithmetic operator '%s' is only supported for numeric pin types (Int/Int64/Float/Double). Current types: '%s' vs '%s'."), + *EFlowPredicateCompareOperatorType_Classifiers::GetOperatorSymbolString(OperatorType), + *LeftTypeName.ToString(), + *RightTypeName.ToString())); + Result = EDataValidationResult::Invalid; + } + + // Warn if both sides have the same authored name (potential user confusion) + if (GetAuthoredValueName(LeftValue) == GetAuthoredValueName(RightValue)) + { + LogValidationWarning(FString::Printf( + TEXT("LeftValue and RightValue have the same name '%s'. This may cause confusion with pin disambiguation."), + *GetAuthoredValueName(LeftValue).ToString())); + } + + if (Result == EDataValidationResult::NotValidated) + { + Result = EDataValidationResult::Valid; + } + + return Result; +} + FText UFlowNodeAddOn_PredicateCompareValues::K2_GetNodeTitle_Implementation() const { using namespace EFlowPredicateCompareOperatorType_Classifiers; const bool bIsClassDefault = HasAnyFlags(RF_ArchetypeObject | RF_ClassDefaultObject); - if (!bIsClassDefault && + if (!bIsClassDefault && GetDefault()->bUseAdaptiveNodeTitles) { const FText LeftDisplayName = FText::FromName(GetAuthoredValueName(LeftValue)); @@ -230,9 +351,30 @@ bool UFlowNodeAddOn_PredicateCompareValues::AreComparableStandardPinTypes(const return true; } + // Note: Bool, Vector, Rotator, Transform, Object, Class, InstancedStruct are all + // only comparable with themselves (handled by the LeftPinTypeName == RightPinTypeName check above). + // Unknown/user types also fall into same-type comparison via the fallback path in EvaluatePredicate. + return false; } +bool UFlowNodeAddOn_PredicateCompareValues::CacheTypeNames(FCachedTypeNames& OutCache) const +{ + OutCache.Reset(); + + if (!LeftValue.IsValid() || !RightValue.IsValid()) + { + LogError(TEXT("Compare Values requires both LeftValue and RightValue to be configured.")); + return false; + } + + OutCache.LeftTypeName = LeftValue.DataPinValue.Get().GetPinTypeName().Name; + OutCache.RightTypeName = RightValue.DataPinValue.Get().GetPinTypeName().Name; + OutCache.bIsValid = true; + + return true; +} + bool UFlowNodeAddOn_PredicateCompareValues::TryCheckGameplayTagsEqual(bool& bOutIsEqual) const { // Compare both sides as containers; pin type templates should allow tag->container conversion. @@ -264,102 +406,38 @@ bool UFlowNodeAddOn_PredicateCompareValues::TryCheckGameplayTagsEqual(bool& bOut return true; } -bool UFlowNodeAddOn_PredicateCompareValues::TryCheckTextEqual(bool& bOutIsEqual) const +bool UFlowNodeAddOn_PredicateCompareValues::TryCheckFallbackStringEqual(bool& bOutIsEqual) const { - // Compare both sides as Text; pin type templates should allow Name/String/Enum -> Text conversion. - FText LeftText; - { - const EFlowDataPinResolveResult ResolveResult = - TryResolveDataPinValue(GetDisambiguatedValueName(LeftValue), LeftText, SingleFromArray); + // Fallback path: try to convert both sides to string via their FFlowDataPinValue::TryConvertValuesToString. + // This enables user-added pin types (from other plugins) to participate in equality comparisons + // as long as they implement TryConvertValuesToString on their FFlowDataPinValue subclass. - if (!FlowPinType::IsSuccess(ResolveResult)) - { - LogError(TEXT("Failed to resolve LeftValue as Text.")); - return false; - } - } + const FFlowDataPinValue* LeftDataPinValue = LeftValue.DataPinValue.GetPtr(); + const FFlowDataPinValue* RightDataPinValue = RightValue.DataPinValue.GetPtr(); - FText RightText; + if (!LeftDataPinValue || !RightDataPinValue) { - const EFlowDataPinResolveResult ResolveResult = - TryResolveDataPinValue(GetDisambiguatedValueName(RightValue), RightText, SingleFromArray); - - if (!FlowPinType::IsSuccess(ResolveResult)) - { - LogError(TEXT("Failed to resolve RightValue as Text.")); - return false; - } + return false; } - bOutIsEqual = LeftText.EqualTo(RightText); - return true; -} - -bool UFlowNodeAddOn_PredicateCompareValues::TryCheckStringEqual(bool& bOutIsEqual) const -{ - // Compare both sides as String; templates can handle Name/Text/Enum -> String if allowed. FString LeftString; + if (!LeftDataPinValue->TryConvertValuesToString(LeftString)) { - const EFlowDataPinResolveResult ResolveResult = - TryResolveDataPinValue(GetDisambiguatedValueName(LeftValue), LeftString, SingleFromArray); - - if (!FlowPinType::IsSuccess(ResolveResult)) - { - LogError(TEXT("Failed to resolve LeftValue as String.")); - return false; - } + LogError(TEXT("Failed to convert LeftValue to String for fallback comparison.")); + return false; } FString RightString; + if (!RightDataPinValue->TryConvertValuesToString(RightString)) { - const EFlowDataPinResolveResult ResolveResult = - TryResolveDataPinValue(GetDisambiguatedValueName(RightValue), RightString, SingleFromArray); - - if (!FlowPinType::IsSuccess(ResolveResult)) - { - LogError(TEXT("Failed to resolve RightValue as String.")); - return false; - } + LogError(TEXT("Failed to convert RightValue to String for fallback comparison.")); + return false; } bOutIsEqual = (LeftString == RightString); return true; } -bool UFlowNodeAddOn_PredicateCompareValues::TryCheckNameEqual(bool& bOutIsEqual) const -{ - // Compare case-insensitively if either side is Name-like. - // We resolve both sides as String and compare IgnoreCase, because: - // - FName itself is case-sensitive in operator==, but your requirement is case-insensitive for Name-like. - // - FlowPinType templates can source Enum values (FName) into string as needed. - FString LeftString; - { - const EFlowDataPinResolveResult ResolveResult = - TryResolveDataPinValue(GetDisambiguatedValueName(LeftValue), LeftString, SingleFromArray); - - if (!FlowPinType::IsSuccess(ResolveResult)) - { - LogError(TEXT("Failed to resolve LeftValue for Name-like comparison.")); - return false; - } - } - - FString RightString; - { - const EFlowDataPinResolveResult ResolveResult = - TryResolveDataPinValue(GetDisambiguatedValueName(RightValue), RightString, SingleFromArray); - - if (!FlowPinType::IsSuccess(ResolveResult)) - { - LogError(TEXT("Failed to resolve RightValue for Name-like comparison.")); - return false; - } - } - - bOutIsEqual = LeftString.Equals(RightString, ESearchCase::IgnoreCase); - return true; -} - bool UFlowNodeAddOn_PredicateCompareValues::CompareDoubleUsingOperator(double LeftValueAsDouble, double RightValueAsDouble) const { FLOW_ASSERT_ENUM_MAX(EFlowPredicateCompareOperatorType, 6); @@ -490,19 +568,40 @@ bool UFlowNodeAddOn_PredicateCompareValues::TryCompareAsInt64() const return CompareInt64UsingOperator(LeftInt64, RightInt64); } +bool UFlowNodeAddOn_PredicateCompareValues::EvaluateEqualityBlock(const TCHAR* TypeLabel, const TFunctionRef CompareFunc) const +{ + if (!IsEqualityOp()) + { + LogError(FString::Printf(TEXT("Arithmetic operators are not supported for %s comparisons."), TypeLabel)); + return false; + } + + bool bIsEqual = false; + if (!CompareFunc(bIsEqual)) + { + return false; + } + + return (OperatorType == EFlowPredicateCompareOperatorType::Equal) == bIsEqual; +} + bool UFlowNodeAddOn_PredicateCompareValues::EvaluatePredicate_Implementation() const { - // All failures are errors and return false. - if (!LeftValue.IsValid() || !RightValue.IsValid()) + // Cache type names once to avoid repeated TInstancedStruct::Get() virtual dispatch. + FCachedTypeNames Cache; + if (!CacheTypeNames(Cache)) { - LogError(TEXT("Compare Values requires both LeftValue and RightValue to be configured.")); return false; } - const FName LeftTypeName = LeftValue.DataPinValue.Get().GetPinTypeName().Name; - const FName RightTypeName = RightValue.DataPinValue.Get().GetPinTypeName().Name; + const FName& LeftTypeName = Cache.LeftTypeName; + const FName& RightTypeName = Cache.RightTypeName; + + const bool bSameType = (LeftTypeName == RightTypeName); - if (!AreComparableStandardPinTypes(LeftTypeName, RightTypeName)) + // Type compatibility gate. + // Same-type unknowns are allowed through for the fallback path at the bottom. + if (!bSameType && !AreComparableStandardPinTypes(LeftTypeName, RightTypeName)) { LogError(FString::Printf( TEXT("Compare Values pin types are not comparable: '%s' vs '%s'."), @@ -512,21 +611,16 @@ bool UFlowNodeAddOn_PredicateCompareValues::EvaluatePredicate_Implementation() c return false; } - // Arithmetic operators: numeric only - if (IsArithmeticOp()) + // Arithmetic operators: numeric only (fast reject before the cascade) + if (IsArithmeticOp() && !(IsNumericTypeName(LeftTypeName) && IsNumericTypeName(RightTypeName))) { - if (!(IsNumericTypeName(LeftTypeName) && IsNumericTypeName(RightTypeName))) - { - LogError(TEXT("Arithmetic operators are only supported for numeric pin types (Int/Int64/Float/Double).")); - return false; - } + LogError(TEXT("Arithmetic operators are only supported for numeric pin types (Int/Int64/Float/Double).")); + return false; } - // Numeric + // Numeric (full operator set) if (IsNumericTypeName(LeftTypeName) && IsNumericTypeName(RightTypeName)) { - // Prefer Int64 if both are integer types (or can be upscaled to int64 precisely). - // Use Double if either side is floating point. if (IsFloatingPointType(LeftTypeName) || IsFloatingPointType(RightTypeName)) { return TryCompareAsDouble(); @@ -535,67 +629,111 @@ bool UFlowNodeAddOn_PredicateCompareValues::EvaluatePredicate_Implementation() c return TryCompareAsInt64(); } - // Gameplay tags: compare as container (superset). Equality ops only (as per enum). + // Gameplay tags: compare as container (superset). Equality ops only. if (IsGameplayTagLikeTypeName(LeftTypeName) || IsGameplayTagLikeTypeName(RightTypeName)) { - if (!IsEqualityOp()) - { - LogError(TEXT("Arithmetic operators are not supported for Gameplay Tags.")); - return false; - } - - bool bIsEqual = false; - if (!TryCheckGameplayTagsEqual(bIsEqual)) - { - return false; - } - - return (OperatorType == EFlowPredicateCompareOperatorType::Equal) == bIsEqual; + return EvaluateEqualityBlock(TEXT("Gameplay Tag"), + [this](bool& bIsEqual) { return TryCheckGameplayTagsEqual(bIsEqual); }); } // String-like (including enums-as-names). Equality ops only. if (IsAnyStringLikeTypeName(LeftTypeName) || IsAnyStringLikeTypeName(RightTypeName)) { - if (!IsEqualityOp()) + // Dispatch order is significant: + // 1) Name-like (Name OR Enum) => case-insensitive compare via FString + // 2) Text => FText::EqualTo (culture-aware) + // 3) String => exact FString equality + if (IsNameLikeType(LeftTypeName) || IsNameLikeType(RightTypeName)) { - LogError(TEXT("Arithmetic operators are not supported for Name/Text/String/Enum comparisons.")); - return false; + return EvaluateEqualityBlock(TEXT("Name/Enum"), + [this](bool& bIsEqual) + { + return TryCheckResolvedValuesEqual(bIsEqual, TEXT("String (Name-like)"), + [](const FString& L, const FString& R) { return L.Equals(R, ESearchCase::IgnoreCase); }); + }); } - // Order is significant: - // 1) Name-like (Name OR Enum) => case-insensitive compare - // 2) Text => FText equality (localized) - // 3) String => FString equality - bool bIsEqual = false; - - if (IsNameLikeType(LeftTypeName) || IsNameLikeType(RightTypeName)) + if (IsTextType(LeftTypeName) || IsTextType(RightTypeName)) { - if (!TryCheckNameEqual(bIsEqual)) - { - return false; - } + return EvaluateEqualityBlock(TEXT("Text"), + [this](bool& bIsEqual) + { + return TryCheckResolvedValuesEqual(bIsEqual, TEXT("Text"), + [](const FText& L, const FText& R) { return L.EqualTo(R); }); + }); } - else if (IsTextType(LeftTypeName) || IsTextType(RightTypeName)) - { - if (!TryCheckTextEqual(bIsEqual)) + + return EvaluateEqualityBlock(TEXT("String"), + [this](bool& bIsEqual) { - return false; - } - } - else - { - if (!TryCheckStringEqual(bIsEqual)) + return TryCheckResolvedValuesEqual(bIsEqual, TEXT("String")); + }); + } + + // Bool. Equality ops only. + if (IsBoolTypeName(LeftTypeName) && IsBoolTypeName(RightTypeName)) + { + return EvaluateEqualityBlock(TEXT("Bool"), + [this](bool& bIsEqual) { return TryCheckResolvedValuesEqual(bIsEqual, TEXT("Bool")); }); + } + + // Vector. Equality ops only, strict comparison (no tolerance). + if (IsVectorTypeName(LeftTypeName) && IsVectorTypeName(RightTypeName)) + { + return EvaluateEqualityBlock(TEXT("Vector"), + [this](bool& bIsEqual) { return TryCheckResolvedValuesEqual(bIsEqual, TEXT("Vector")); }); + } + + // Rotator. Equality ops only, strict comparison (no tolerance). + if (IsRotatorTypeName(LeftTypeName) && IsRotatorTypeName(RightTypeName)) + { + return EvaluateEqualityBlock(TEXT("Rotator"), + [this](bool& bIsEqual) { return TryCheckResolvedValuesEqual(bIsEqual, TEXT("Rotator")); }); + } + + // Transform. Equality ops only, strict comparison (zero tolerance). + if (IsTransformTypeName(LeftTypeName) && IsTransformTypeName(RightTypeName)) + { + return EvaluateEqualityBlock(TEXT("Transform"), + [this](bool& bIsEqual) { - return false; - } - } + return TryCheckResolvedValuesEqual(bIsEqual, TEXT("Transform"), + [](const FTransform& L, const FTransform& R) { return L.Equals(R, 0.0); }); + }); + } + + // Object. Equality ops only, pointer identity. + if (IsObjectTypeName(LeftTypeName) && IsObjectTypeName(RightTypeName)) + { + return EvaluateEqualityBlock(TEXT("Object"), + [this](bool& bIsEqual) { return TryCheckResolvedValuesEqual(bIsEqual, TEXT("Object")); }); + } - return (OperatorType == EFlowPredicateCompareOperatorType::Equal) == bIsEqual; + // Class. Equality ops only, strict class identity (not "is derived from"). + if (IsClassTypeName(LeftTypeName) && IsClassTypeName(RightTypeName)) + { + return EvaluateEqualityBlock(TEXT("Class"), + [this](bool& bIsEqual) { return TryCheckResolvedValuesEqual(bIsEqual, TEXT("Class")); }); + } + + // InstancedStruct. Equality ops only, struct type + data equality. + if (IsInstancedStructTypeName(LeftTypeName) && IsInstancedStructTypeName(RightTypeName)) + { + return EvaluateEqualityBlock(TEXT("InstancedStruct"), + [this](bool& bIsEqual) { return TryCheckResolvedValuesEqual(bIsEqual, TEXT("InstancedStruct")); }); + } + + // Fallback: same-type comparison via string conversion. + // This supports user-added types from other plugins as long as they + // implement TryConvertValuesToString on their FFlowDataPinValue subclass. + if (bSameType) + { + return EvaluateEqualityBlock(*LeftTypeName.ToString(), + [this](bool& bIsEqual) { return TryCheckFallbackStringEqual(bIsEqual); }); } - // TODO (gtaylor) Add Object, Class, InstancedStruct, Vector... etc. support LogError(FString::Printf( - TEXT("Compare Values does not support comparing pin types '%s' and '%s' yet."), + TEXT("Compare Values does not support comparing pin types '%s' and '%s'."), *LeftTypeName.ToString(), *RightTypeName.ToString())); diff --git a/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateCompareValues.h b/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateCompareValues.h index cec8f1ba..b20b2329 100644 --- a/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateCompareValues.h +++ b/Source/Flow/Public/AddOns/FlowNodeAddOn_PredicateCompareValues.h @@ -1,6 +1,8 @@ // Copyright https://github.com/MothCocoon/FlowGraph/graphs/contributors #pragma once +#include + #include "AddOns/FlowNodeAddOn.h" #include "Interfaces/FlowPredicateInterface.h" #include "Types/FlowBranchEnums.h" @@ -28,11 +30,12 @@ class UFlowNodeAddOn_PredicateCompareValues // -- // UFlowNodeBase + virtual EDataValidationResult ValidateNode() override; virtual FText K2_GetNodeTitle_Implementation() const override; // -- /* Utility function for subclasses, if they want to force a named property to be Input or Output. - * Unused in this class. */ + * Unused in this class. */ void OnPostEditEnsureAllNamedPropertiesPinDirection(const FProperty& Property, bool bIsInput); #endif @@ -77,11 +80,32 @@ class UFlowNodeAddOn_PredicateCompareValues static bool IsAnyStringLikeTypeName(const FName& TypeName); static bool IsGameplayTagLikeTypeName(const FName& TypeName); - // Domain comparisons (these return true if they successfully compared; result is via out param) + static bool IsBoolTypeName(const FName& TypeName); + static bool IsVectorTypeName(const FName& TypeName); + static bool IsRotatorTypeName(const FName& TypeName); + static bool IsTransformTypeName(const FName& TypeName); + static bool IsObjectTypeName(const FName& TypeName); + static bool IsClassTypeName(const FName& TypeName); + static bool IsInstancedStructTypeName(const FName& TypeName); + + // ----------------------------------------------------------------------- + // Domain equality comparisons + // (these return true if they successfully compared; equality result is via out param) + // ----------------------------------------------------------------------- + + /* Generic equality check: resolve both sides as TFlowPinType, compare with Comparator. + * Works for any pin type whose ValueType is supported by the comparator. + * ErrorLabel is used in LogError messages (e.g. "Bool", "Vector", "Object"). + * ComparatorFn defaults to std::equal_to<> (transparent), which uses operator==. */ + template > + bool TryCheckResolvedValuesEqual(bool& bOutIsEqual, const TCHAR* ErrorLabel, ComparatorFn Comparator = {}) const; + + // Domain comparisons that need special handling beyond simple resolve-and-compare bool TryCheckGameplayTagsEqual(bool& bOutIsEqual) const; - bool TryCheckTextEqual(bool& bOutIsEqual) const; - bool TryCheckStringEqual(bool& bOutIsEqual) const; - bool TryCheckNameEqual(bool& bOutIsEqual) const; + + /* Fallback: both sides convert to string via TryConvertValuesToString. + * This supports user-added pin types from other plugins, so long as they implement TryConvertValuesToString. */ + bool TryCheckFallbackStringEqual(bool& bOutIsEqual) const; // Numeric comparisons support full operator set bool TryCompareAsDouble() const; @@ -91,6 +115,11 @@ class UFlowNodeAddOn_PredicateCompareValues bool CompareDoubleUsingOperator(double LeftValueAsDouble, double RightValueAsDouble) const; bool CompareInt64UsingOperator(int64 LeftValueAsInt64, int64 RightValueAsInt64) const; + /* Helper for equality-only type blocks in EvaluatePredicate. + * Guards against arithmetic operators, calls CompareFunc, applies Equal/NotEqual flip. + * Returns the final predicate result, or false on error. */ + bool EvaluateEqualityBlock(const TCHAR* TypeLabel, const TFunctionRef CompareFunc) const; + // These are the DataPinNamedProperty property names // (ie, the name of the property itself, eg "LeftValue") const FName& GetLeftValuePropertyName() const; @@ -100,6 +129,58 @@ class UFlowNodeAddOn_PredicateCompareValues FORCEINLINE const FName& GetAuthoredValueName(const FFlowNamedDataPinProperty& NamedDataPinProperty) const { return NamedDataPinProperty.Name; } /* This is the authored value after being disambiguated (for duplicates). - * Example: how it is presented and indexed on the owning Flow Node. */ + * Example: how it is presented and indexed on the owning Flow Node. */ FORCEINLINE const FName& GetDisambiguatedValueName(const FFlowNamedDataPinProperty& NamedDataPinProperty) const { return NamedDataPinProperty.DataPinValue.Get().PropertyPinName; } + +private: + + /* Cached type names for the current evaluation, to avoid repeated TInstancedStruct::Get() calls. + * Only valid during a single call to EvaluatePredicate_Implementation or ValidateNode. */ + struct FCachedTypeNames + { + FName LeftTypeName; + FName RightTypeName; + bool bIsValid = false; + + void Reset() { bIsValid = false; } + }; + + /* Populate cached type names from the current LeftValue/RightValue. + * Returns false (and logs error) if either value is not configured. */ + bool CacheTypeNames(FCachedTypeNames& OutCache) const; }; + +// ----------------------------------------------------------------------- +// Template implementations +// ----------------------------------------------------------------------- + +template +bool UFlowNodeAddOn_PredicateCompareValues::TryCheckResolvedValuesEqual(bool& bOutIsEqual, const TCHAR* ErrorLabel, ComparatorFn Comparator) const +{ + typename TFlowPinType::ValueType LeftResolved{}; + { + const EFlowDataPinResolveResult ResolveResult = + TryResolveDataPinValue(GetDisambiguatedValueName(LeftValue), LeftResolved, SingleFromArray); + + if (!FlowPinType::IsSuccess(ResolveResult)) + { + LogError(FString::Printf(TEXT("Failed to resolve LeftValue as %s."), ErrorLabel)); + return false; + } + } + + typename TFlowPinType::ValueType RightResolved{}; + { + const EFlowDataPinResolveResult ResolveResult = + TryResolveDataPinValue(GetDisambiguatedValueName(RightValue), RightResolved, SingleFromArray); + + if (!FlowPinType::IsSuccess(ResolveResult)) + { + LogError(FString::Printf(TEXT("Failed to resolve RightValue as %s."), ErrorLabel)); + return false; + } + } + + bOutIsEqual = Comparator(LeftResolved, RightResolved); + return true; +} \ No newline at end of file