Files
UnrealEngine/Engine/Source/Editor/BlueprintGraph/Private/K2Node_MathExpression.cpp
2025-05-18 13:04:45 +08:00

2924 lines
98 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "K2Node_MathExpression.h"
#include "BasicTokenParser.h"
#include "BlueprintActionDatabaseRegistrar.h"
#include "BlueprintNodeSpawner.h"
#include "Containers/Array.h"
#include "Containers/EnumAsByte.h"
#include "Containers/Map.h"
#include "Delegates/Delegate.h"
#include "DiffResults.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraph/EdGraphSchema.h"
#include "EdGraphSchema_K2.h"
#include "EdGraphSchema_K2_Actions.h"
#include "EdGraphUtilities.h"
#include "Engine/Blueprint.h"
#include "Engine/MemberReference.h"
#include "HAL/PlatformCrt.h"
#include "Internationalization/Internationalization.h"
#include "K2Node_CallFunction.h"
#include "K2Node_EditablePinBase.h"
#include "K2Node_Tunnel.h"
#include "K2Node_VariableGet.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/CompilerResultsLog.h"
#include "Kismet2/Kismet2NameValidators.h"
#include "Logging/TokenizedMessage.h"
#include "Math/UnrealMathSSE.h"
#include "Math/Vector2D.h"
#include "MathExpressionHandler.h"
#include "Misc/AssertionMacros.h"
#include "Misc/CString.h"
#include "Misc/DefaultValueHelper.h"
#include "Misc/Guid.h"
#include "Serialization/Archive.h"
#include "Templates/Casts.h"
#include "Templates/SubclassOf.h"
#include "Templates/UnrealTemplate.h"
#include "UObject/Class.h"
#include "UObject/NameTypes.h"
#include "UObject/ObjectPtr.h"
#include "UObject/Script.h"
#include "UObject/Stack.h"
#include "UObject/UObjectIterator.h"
#include "UObject/UnrealNames.h"
#include "UObject/UnrealType.h"
#include "UObject/WeakObjectPtrTemplates.h"
class FExpressionParser;
class UObject;
#define LOCTEXT_NAMESPACE "K2Node"
/*******************************************************************************
* Static Helpers
*******************************************************************************/
/**
* Helper function for deleting all the nodes from a specified graph. Does not
* delete any tunnel in/out nodes (to preserve the tunnel).
*
* @param Graph The graph you want to delete nodes in.
*/
static void DeleteGeneratedNodesInGraph(UEdGraph* Graph)
{
UBlueprint* Blueprint = FBlueprintEditorUtils::FindBlueprintForGraphChecked(Graph);
for (int32 NodeIndex = 0; NodeIndex < Graph->Nodes.Num(); )
{
UEdGraphNode* Node = Graph->Nodes[NodeIndex];
if (ExactCast<UK2Node_Tunnel>(Node) != NULL)
{
++NodeIndex;
}
else
{
FBlueprintEditorUtils::RemoveNode(Blueprint, Node, true);
}
}
}
/**
* If the specified type is a "byte" type, then this will modify it to
* an "int". Helps when trying to match function signatures.
*
* @param InOutType The type you want to attempt to promote.
* @return True if the type was modified, false if not.
*/
static bool PromoteByteToInt(FEdGraphPinType& InOutType)
{
if (InOutType.PinCategory == UEdGraphSchema_K2::PC_Byte)
{
InOutType.PinCategory = UEdGraphSchema_K2::PC_Int;
InOutType.PinSubCategoryObject = nullptr;
return true;
}
return false;
}
/**
* If the specified type is a "int" type, then this will modify it to
* a "double". Helps when trying to match function signatures.
*
* @param InOutType The type you want to attempt to promote.
* @return True if the type was modified, false if not.
*/
static bool PromoteIntToDouble(FEdGraphPinType& InOutType)
{
if (InOutType.PinCategory == UEdGraphSchema_K2::PC_Int)
{
InOutType.PinCategory = UEdGraphSchema_K2::PC_Real;
InOutType.PinSubCategory = UEdGraphSchema_K2::PC_Double;
InOutType.PinSubCategoryObject = nullptr;
return true;
}
return false;
}
/**
* Sets or clears the error on a specific node. If the ErrorText is empty, then
* it resets the error state on the node. If actual error text is supplied,
* then the node is flagged as having an error, and the string is appended to
* the node's error message.
*
* @param Node The node you wish to modify.
* @param ErrorText The text you want to append to the node's error message (empty if you want to clear any errors).
*/
static void SetNodeError(UEdGraphNode* Node, FText const& ErrorText)
{
if (ErrorText.IsEmpty())
{
Node->ErrorMsg.Empty();
Node->ErrorType = EMessageSeverity::Info;
Node->bHasCompilerMessage = false;
}
else if (Node->bHasCompilerMessage)
{
Node->ErrorMsg += TEXT("\n") + ErrorText.ToString();
Node->ErrorType = EMessageSeverity::Error;
}
else
{
Node->ErrorMsg = ErrorText.ToString();
Node->ErrorType = EMessageSeverity::Error;
Node->bHasCompilerMessage = true;
}
}
/*******************************************************************************
* FExpressionVisitor
*******************************************************************************/
/**
* This is the base class used for IFExpressionNode tree traversal (set up to
* handle different node types... new node types should have a Visit() method
* added for them).
*/
class FExpressionVisitor
{
public:
virtual ~FExpressionVisitor() { }
/**
* FExpressionNodes determine when a traverser (FExpressionVisitor) has access
* to the node. There are a couple hook points, allowing the traverser to pick
* either a pre-order or post-order tree traversal. These values let the
* FExpressionVisitor where we are in the tree search.
*/
enum EVisitPhase
{
VISIT_Pre, // the node being visited has yet to visit its children (and will next, starting with the left)
VISIT_Post, // the node being visited has finished visiting its children (and is about to return up, to its parent)
VISIT_Leaf // the node being visited is a leaf (no children will be visited)
};
/**
* Intended to be overridden by sub-classes, for special handling of
* explicit node types (new node types should have one added for them).
*
* @param Node The current node in the tree's traversal.
* @param Phase Where we currently are in traversing the specified node (before children vs. after)
* @return True to continue traversing the tree, false to abort.
*/
virtual bool Visit(class IFExpressionNode& Node, EVisitPhase Phase) { return VisitUnhandled(Node, Phase); }
virtual bool Visit(class FTokenWrapperNode& Node, EVisitPhase Phase) { return VisitUnhandled((class IFExpressionNode&)Node, Phase); }
virtual bool Visit(class FBinaryOperator& Node, EVisitPhase Phase) { return VisitUnhandled((class IFExpressionNode&)Node, Phase); }
virtual bool Visit(class FUnaryOperator& Node, EVisitPhase Phase) { return VisitUnhandled((class IFExpressionNode&)Node, Phase); }
virtual bool Visit(class FConditionalOperator& Node, EVisitPhase Phase) { return VisitUnhandled((class IFExpressionNode&)Node, Phase); }
virtual bool Visit(class FExpressionList& Node, EVisitPhase Phase) { return VisitUnhandled((class IFExpressionNode&)Node, Phase); }
virtual bool Visit(class FFunctionExpression& Node, EVisitPhase Phase) { return VisitUnhandled((class IFExpressionNode&)Node, Phase); }
protected:
/**
* Called by all the base Visit() methods, a good point for sub-classes to
* hook into for handling EVERY IFExpressionNode type (unless they override
* a Visit method)
*
* @param Node The current node in the tree's traversal.
* @param Phase Where we currently are in traversing the specified node (before children vs. after)
* @return True to continue traversing the tree, false to abort.
*/
virtual bool VisitUnhandled(class IFExpressionNode& Node, EVisitPhase Phase)
{
// if we end up here, then the subclass decided not to handle the
// specific node type, and therefore doesn't care about it
return true;
}
};
/*******************************************************************************
* IFExpressionNode Types
*******************************************************************************/
/**
* Base class for all expression-tree nodes that are generated from parsing an
* expression string. Represents either a single value/variable, or an operation
* on other IFExpressionNode(s).
*/
class IFExpressionNode
{
public:
/**
* Entry point for traversing the expression-tree, should either pass the
* Visitor along to sub child nodes (in the case of a branch node), or
* simply let the Visitor "visit" the leaf node.
*
* @return True to continue traversing the tree, false to abort.
*/
virtual bool Accept(FExpressionVisitor& Visitor) = 0;
/**
* For debug purposes, intended to help visualize what this node represents
* (for reconstructing a pseudo expression).
*/
virtual FString ToString() const = 0;
/**
* Variable Guid's are stored in the internal expression and must be
* converted back to their name when showing the expression in the node's title
*/
virtual FString ToDisplayString(UBlueprint* InBlueprint) const { return ToString(); };
protected:
IFExpressionNode() {}
};
/**
* Leaf node for the expression-tree. Encapsulates either a literal constant
* (FBasicToken::TOKEN_Const), or a variable identifier (FBasicToken::TOKEN_Identifier).
*/
class FTokenWrapperNode : public IFExpressionNode
{
public:
FTokenWrapperNode(FBasicToken InToken)
: Token(InToken)
{
}
virtual ~FTokenWrapperNode() {}
/**
* Gives the FExpressionVisitor access to this node (lets it "visit" this,
* in tree traversal terms).
*
* @return True to continue traversing the tree, false to abort.
*/
virtual bool Accept(FExpressionVisitor& Visitor) override
{
return Visitor.Visit(*this, FExpressionVisitor::VISIT_Leaf);
}
/** For debug purposes, constructs a textual representation of this expression */
virtual FString ToString() const override
{
if (Token.TokenType == FBasicToken::TOKEN_Identifier || Token.TokenType == FBasicToken::TOKEN_Guid)
{
return FString::Printf(TEXT("%s"), Token.Identifier);
}
else if (Token.TokenType == FBasicToken::TOKEN_Const)
{
return FString::Printf(TEXT("%s"), *Token.GetConstantValue());
}
else
{
return FString::Printf(TEXT("(UnexpectedTokenType)%s"), Token.Identifier);
}
}
virtual FString ToDisplayString(UBlueprint* InBlueprint) const
{
if (Token.TokenType == FBasicToken::TOKEN_Guid)
{
FGuid VariableGuid;
if(FGuid::Parse(FString(Token.Identifier), VariableGuid))
{
FName VariableName = FBlueprintEditorUtils::FindMemberVariableNameByGuid(InBlueprint, VariableGuid);
if (VariableName.IsNone())
{
VariableName = FBlueprintEditorUtils::FindLocalVariableNameByGuid(InBlueprint, VariableGuid);
}
return VariableName.ToString();
}
}
return ToString();
}
public:
/** The base token which this leaf node represents */
FBasicToken Token;
};
/**
* Branch node that represents a binary operation, where its children are the
* left and right operands:
*
* <operator>
* / \
* <left-expression> <right-expression>
*/
class FBinaryOperator : public IFExpressionNode
{
public:
FBinaryOperator(const FString& InOperator, TSharedRef<IFExpressionNode> InLHS, TSharedRef<IFExpressionNode> InRHS)
: Operator(InOperator)
, LHS(InLHS)
, RHS(InRHS)
{
}
virtual ~FBinaryOperator() {}
/**
* Gives the FExpressionVisitor access to this node, and passes it along to
* traverse the children.
*
* @return True to continue traversing the tree, false to abort.
*/
virtual bool Accept(FExpressionVisitor& Visitor) override
{
bool bAbort = !Visitor.Visit(*this, FExpressionVisitor::VISIT_Pre);
if (bAbort || !LHS->Accept(Visitor) || !RHS->Accept(Visitor))
{
return false;
}
return Visitor.Visit(*this, FExpressionVisitor::VISIT_Post);
}
/** For debug purposes, constructs a textual representation of this expression */
virtual FString ToString() const override
{
const FString LeftStr = LHS->ToString();
const FString RightStr = RHS->ToString();
return FString::Printf(TEXT("(%s %s %s)"), *LeftStr, *Operator, *RightStr);
}
virtual FString ToDisplayString(UBlueprint* InBlueprint) const
{
const FString LeftStr = LHS->ToDisplayString(InBlueprint);
const FString RightStr = RHS->ToDisplayString(InBlueprint);
return FString::Printf(TEXT("(%s %s %s)"), *LeftStr, *Operator, *RightStr);
}
public:
FString Operator;
TSharedRef<IFExpressionNode> LHS;
TSharedRef<IFExpressionNode> RHS;
};
/**
* Branch node that represents a unary (prefix) operation, where its child is
* the right operand:
*
* <unary-operator>
* \
* <operand-expression>
*/
class FUnaryOperator : public IFExpressionNode
{
public:
FUnaryOperator(const FString& InOperator, TSharedRef<IFExpressionNode> InRHS)
: Operator(InOperator)
, RHS(InRHS)
{
}
virtual ~FUnaryOperator() {}
/**
* Gives the FExpressionVisitor access to this node, and passes it along to
* traverse its child.
*
* @return True to continue traversing the tree, false to abort.
*/
virtual bool Accept(FExpressionVisitor& Visitor) override
{
bool bAbort = !Visitor.Visit(*this, FExpressionVisitor::VISIT_Pre);
if (bAbort || !RHS->Accept(Visitor))
{
return false;
}
return Visitor.Visit(*this, FExpressionVisitor::VISIT_Post);
}
/** For debug purposes, constructs a textual representation of this expression */
virtual FString ToString() const override
{
const FString RightStr = RHS->ToString();
return FString::Printf(TEXT("(%s%s)"), *Operator, *RightStr);
}
virtual FString ToDisplayString(UBlueprint* InBlueprint) const
{
const FString RightStr = RHS->ToDisplayString(InBlueprint);
return FString::Printf(TEXT("(%s%s)"), *Operator, *RightStr);
}
public:
FString Operator;
TSharedRef<IFExpressionNode> RHS;
};
/**
* Branch node that represents a ternary conditional (if-then-else) operation
* (c ? a : b), where its children are the condition, the "then" expression,
* and the "else" expression:
*
* <conditional-operator>
* / | \
* <condition> <then-exression> <else-expression>
*/
class FConditionalOperator : public IFExpressionNode
{
public:
FConditionalOperator(TSharedRef<IFExpressionNode> InCondition, TSharedRef<IFExpressionNode> InTruePart, TSharedRef<IFExpressionNode> InFalsePart)
: Condition(InCondition)
, TruePart(InTruePart)
, FalsePart(InFalsePart)
{
}
virtual ~FConditionalOperator() {}
/**
* Gives the FExpressionVisitor access to this node, and passes it along to
* traverse the children.
*
* @return True to continue traversing the tree, false to abort.
*/
virtual bool Accept(FExpressionVisitor& Visitor) override
{
bool bAbort = !Visitor.Visit(*this, FExpressionVisitor::VISIT_Pre);
// @TODO: what about the Condition?
if (bAbort || !TruePart->Accept(Visitor) || !FalsePart->Accept(Visitor))
{
return false;
}
return Visitor.Visit(*this, FExpressionVisitor::VISIT_Post);
}
/** For debug purposes, constructs a textual representation of this expression */
virtual FString ToString() const override
{
const FString ConditionStr = Condition->ToString();
const FString TrueStr = TruePart->ToString();
const FString FalseStr = FalsePart->ToString();
return FString::Printf(TEXT("(%s ? %s : %s)"), *ConditionStr, *TrueStr, *FalseStr);
}
virtual FString ToDisplayString(UBlueprint* InBlueprint) const
{
const FString ConditionStr = Condition->ToDisplayString(InBlueprint);
const FString TrueStr = TruePart->ToDisplayString(InBlueprint);
const FString FalseStr = FalsePart->ToDisplayString(InBlueprint);
return FString::Printf(TEXT("(%s ? %s : %s)"), *ConditionStr, *TrueStr, *FalseStr);
}
public:
TSharedRef<IFExpressionNode> Condition;
TSharedRef<IFExpressionNode> TruePart;
TSharedRef<IFExpressionNode> FalsePart;
};
/**
* Branch node that represents an n-dimentional list of sub-expressions (like
* for vector parameter lists, etc.). Each child is a separate sub-expression:
*
* <list-node>
* / | \
* <sub-expression0> | <sub-expression2>
* |
* <sub-expression1>
*/
class FExpressionList : public IFExpressionNode
{
public:
/**
* Gives the FExpressionVisitor access to this node, and passes it along to
* traverse all children.
*
* @return True to continue traversing the tree, false to abort.
*/
virtual bool Accept(FExpressionVisitor& Visitor) override
{
bool bAbort = !Visitor.Visit(*this, FExpressionVisitor::VISIT_Pre);
for (TSharedRef<IFExpressionNode> Child : Children)
{
if (bAbort || !Child->Accept(Visitor))
{
return false;
}
}
return Visitor.Visit(*this, FExpressionVisitor::VISIT_Post);
}
/** For debug purposes, constructs a textual representation of this expression */
virtual FString ToString() const override
{
FString AsString("(");
if (Children.Num() > 0)
{
for (TSharedRef<IFExpressionNode> Child : Children)
{
AsString += Child->ToString();
if (Child == Children.Last())
{
AsString += ")";
}
else
{
AsString += ", ";
}
}
}
else
{
AsString += ")";
}
return AsString;
}
virtual FString ToDisplayString(UBlueprint* InBlueprint) const
{
FString AsString("(");
if (Children.Num() > 0)
{
for (TSharedRef<IFExpressionNode> Child : Children)
{
AsString += Child->ToDisplayString(InBlueprint);
if (Child == Children.Last())
{
AsString += ")";
}
else
{
AsString += ", ";
}
}
}
else
{
AsString += ")";
}
return AsString;
}
virtual ~FExpressionList() {}
public:
TArray< TSharedRef<IFExpressionNode> > Children;
};
/**
* Branch node that represents some function (like sin(), cos(), etc.), could
* also represent some structure (conceptually the constructor), like vector,
* rotator, etc. Its child is a single FExpressionList (which wraps all the params).
*/
class FFunctionExpression : public IFExpressionNode
{
public:
FFunctionExpression(const FString& InFuncName, TSharedRef<FExpressionList> InParamList)
: FuncName(InFuncName)
, ParamList(InParamList)
{
}
virtual ~FFunctionExpression() {}
/**
* Gives the FExpressionVisitor access to this node, and passes it along to
* traverse its child.
*
* @return True to continue traversing the tree, false to abort.
*/
virtual bool Accept(FExpressionVisitor& Visitor) override
{
bool bAbort = !Visitor.Visit(*this, FExpressionVisitor::VISIT_Pre);
if (bAbort || !ParamList->Accept(Visitor))
{
return false;
}
return Visitor.Visit(*this, FExpressionVisitor::VISIT_Post);
}
/** For debug purposes, constructs a textual representation of this expression */
virtual FString ToString() const override
{
const FString ParamsString = ParamList->ToString();
return FString::Printf(TEXT("(%s%s)"), *FuncName, *ParamsString);
}
virtual FString ToDisplayString(UBlueprint* InBlueprint) const
{
const FString ParamsString = ParamList->ToDisplayString(InBlueprint);
return FString::Printf(TEXT("(%s%s)"), *FuncName, *ParamsString);
}
public:
FString FuncName;
TSharedRef<FExpressionList> ParamList;
};
/*******************************************************************************
* FLayoutVisitor
*******************************************************************************/
/**
* This class is utilized to help layout math expression nodes by traversing the
* expression tree and cataloging each expression node's depth. From the tree's
* depth we can determine the width of the the graph (an where to place each K2 node):
*
* _
* | [_]---[_]
* | /
* height [_]-- [_]--[_]---[_]
* | \ /
* |_ [_]---[_]
*
* ^-------depth/width-------^
*/
class FLayoutVisitor : public FExpressionVisitor
{
public:
/** Tracks the horizontal (depth) placement for each expression node encountered */
TMap<IFExpressionNode*, int32> DepthChart;
/** Tracks the vertical (height) placement for each expression node encountered */
TMap<IFExpressionNode*, int32> HeightChart;
/** Tracks the total height (value) at each depth (key) */
TMap<int32, int32> DepthHeightLookup;
/** */
FLayoutVisitor()
: CurrentDepth(0)
, MaximumDepth(0)
{
}
/**
* Retrieves the total depth (or graph width) of the previously traversed
* expression tree.
*/
int32 GetMaximumDepth() const
{
return MaximumDepth;
}
/**
* Resets this tree visitor so that it can accurately parse another
* expression tree (else the results would stack up).
*/
void Clear()
{
CurrentDepth = 0;
MaximumDepth = 0;
DepthChart.Empty();
HeightChart.Empty();
DepthHeightLookup.Empty();
}
private:
/**
* From the FExpressionVisitor interface, a generic choke point for visiting
* all expression nodes.
*
* @return True to continue traversing the tree, false to abort.
*/
virtual bool VisitUnhandled(class IFExpressionNode& Node, EVisitPhase Phase) override
{
if (Phase == FExpressionVisitor::VISIT_Pre)
{
++CurrentDepth;
MaximumDepth = FMath::Max(CurrentDepth, MaximumDepth);
}
else
{
if (Phase == FExpressionVisitor::VISIT_Post)
{
--CurrentDepth;
}
// else leaf
// CurrentHeight represents how many nodes have already been placed
// at this depth
int32& CurrentHeight = DepthHeightLookup.FindOrAdd(CurrentDepth);
DepthChart.Add(&Node, CurrentDepth);
HeightChart.Add(&Node, CurrentHeight);
// since we just placed another node at this depth, increase the
// height count
++CurrentHeight;
}
// let the tree traversal continue! don't abort it!
return true;
}
private:
int32 CurrentDepth;
int32 MaximumDepth;
};
/*******************************************************************************
* FOperatorTable
*******************************************************************************/
/**
* This class acts as a lookup table for mapping operator strings (like "+",
* "*", etc.) to corresponding functions that can be turned into blueprint
* nodes. It builds itself (so users don't have to add mappings themselves).
*/
class FOperatorTable
{
public:
FOperatorTable() { Rebuild(); }
/**
* Checks to see if there are any functions associated with the specified
* operator.
*/
bool Contains(const FString& Operator) const
{
return LookupTable.Contains(Operator);
}
/**
* Attempts to lookup a function matching the supplied signature (where
* 'Operator' identifies the function's name and 'InputTypeList' defines
* the desired parameters). If one can't be found, it attempts to find a
* match by promoting the input types (like from int to double, etc.)
*
* @param Operator The operator you want to find a function for.
* @param InputTypeList A list of parameter types you want to feed the function.
* @return A pointer to the matching function (if one was found), otherwise nullptr.
*/
UFunction* FindMatchingFunction(const FString& Operator, const TArray<FEdGraphPinType>& InputTypeList) const
{
// make a local copy of the desired input types so that we can promote
// those types as needed
TArray<FEdGraphPinType> ParamTypeList = InputTypeList;
// try to find the function
UFunction* MatchingFunc = FindFunctionInternal(Operator, ParamTypeList);
// if we didn't find a function that matches the supplied function
// signature, then try to promote the parameters (like from int to
// double), and see if we can lookup a function with those types
for (int32 promoterIndex = 0; (promoterIndex < OrderedTypePromoters.Num()) && (MatchingFunc == NULL); ++promoterIndex)
{
const FTypePromoter& PromotionOperator = OrderedTypePromoters[promoterIndex];
// Apply the promotion operator to any values that match
bool bMadeChanges = false;
for (FEdGraphPinType& ParamType : ParamTypeList)
{
bMadeChanges |= PromotionOperator.Execute(ParamType);
}
// since we've promoted some of the params, attempt to find the
// function again (maybe there's one that matches these param types)
if (bMadeChanges)
{
MatchingFunc = FindFunctionInternal(Operator, ParamTypeList);
// if we found a function to match this time around, no need to
// continue with
if (MatchingFunc != NULL)
{
break;
}
}
}
return MatchingFunc;
}
/**
* Flags the specified function as one associated with the supplied
* operator.
*/
void Add(const FString& Operator, UFunction* OperatorFunc)
{
LookupTable.FindOrAdd(Operator).Add(OperatorFunc);
}
/**
* Rebuilds the lookup table, mapping operator strings (like "+" or "*") to
* associated functions (searches through function libraries for operator
* functions).
*/
void Rebuild()
{
LookupTable.Empty();
OrderedTypePromoters.Empty();
// run through all blueprint function libraries and build up a list of
// functions that have good operator info
for (TObjectIterator<UClass> ClassIt; ClassIt; ++ClassIt)
{
UClass* TestClass = *ClassIt;
if (TestClass->IsChildOf(UBlueprintFunctionLibrary::StaticClass()) && (!TestClass->HasAnyClassFlags(CLASS_Abstract)))
{
for (TFieldIterator<UFunction> FuncIt(TestClass, EFieldIteratorFlags::ExcludeSuper); FuncIt; ++FuncIt)
{
UFunction* TestFunction = *FuncIt;
if (!TestFunction->HasAnyFunctionFlags(FUNC_BlueprintPure) || (TestFunction->GetReturnProperty() == nullptr))
{
continue;
}
FString FunctionName = TestFunction->GetName();
const TArray<FString>& OperatorAliases = GetOperatorAliases(FunctionName);
// if there are aliases, use those instead of the function's standard name
if (OperatorAliases.Num() > 0)
{
for (const FString& Alias : OperatorAliases)
{
Add(Alias, TestFunction);
}
}
else
{
if (TestFunction->HasMetaData(FBlueprintMetadata::MD_CompactNodeTitle))
{
FunctionName = TestFunction->GetMetaData(FBlueprintMetadata::MD_CompactNodeTitle);
}
else if (TestFunction->HasMetaData(FBlueprintMetadata::MD_DisplayName))
{
FunctionName = TestFunction->GetMetaData(FBlueprintMetadata::MD_DisplayName);
}
// Remove spaces from display name as the parser cannot handle it
FunctionName = FDefaultValueHelper::RemoveWhitespaces(FunctionName);
Add(FunctionName, TestFunction);
}
}
}
}
FTypePromoter ByteToIntPromoter;
ByteToIntPromoter.BindStatic(&PromoteByteToInt);
OrderedTypePromoters.Add(ByteToIntPromoter);
FTypePromoter IntToDoublePromoter;
IntToDoublePromoter.BindStatic(&PromoteIntToDouble);
OrderedTypePromoters.Add(IntToDoublePromoter);
}
private:
/**
* Attempts to lookup a function matching the supplied signature (where
* 'Operator' identifies the function's name and 'InputTypeList' defines
* the desired parameters into that function).
*
* @param Operator The operator you want to find a function for.
* @param InputTypeList A list of parameter types you want to feed the function.
* @return A pointer to the matching function (if one was found), otherwise nullptr.
*/
UFunction* FindFunctionInternal(const FString& Operator, const TArray<FEdGraphPinType>& InputTypeList) const
{
UFunction* MatchedFunction = nullptr;
const FFunctionsList* OperatorFunctions = LookupTable.Find(Operator);
if (OperatorFunctions != nullptr)
{
const UEdGraphSchema_K2* K2Schema = GetDefault<UEdGraphSchema_K2>();
for (UFunction* TestFunction : *OperatorFunctions)
{
int32 ArgumentIndex = 0;
for (TFieldIterator<FProperty> PropIt(TestFunction); PropIt && (PropIt->PropertyFlags & CPF_Parm); ++PropIt)
{
FProperty* Param = *PropIt;
if (!Param->HasAnyPropertyFlags(CPF_ReturnParm))
{
if (ArgumentIndex < InputTypeList.Num())
{
FEdGraphPinType ParamType;
if (K2Schema->ConvertPropertyToPinType(Param, /*out*/ParamType))
{
const FEdGraphPinType& TypeToMatch = InputTypeList[ArgumentIndex];
if (!K2Schema->ArePinTypesCompatible(TypeToMatch, ParamType))
{
break; // type mismatch
}
}
else
{
break; // function has a non-K2 type as a parameter
}
}
else
{
break; // ran out of arguments; no match
}
++ArgumentIndex;
}
}
if (ArgumentIndex == InputTypeList.Num())
{
// success!
MatchedFunction = TestFunction;
break;
}
}
}
return MatchedFunction;
}
/**
* Here we overwrite and map multiplies names to specific functions (for
* example "MultiplyMultiply_FloatFloat" and "^2" are not the sort of names
* we'd expect a user to input in a mathematical expression). We can
* replace a function name with a single value, or a series of values
* (could setup "asin" and "arcsin" both as aliases for the ASin() method).
*
* @param FunctionName The raw name of the function you're looking to replace (not the friendly name)
* @return A reference to the array of aliases for the specified function (an empty array if none were found).
*/
static const TArray<FString>& GetOperatorAliases(const FString& FunctionName)
{
#define FUNC_ALIASES_BEGIN(FuncName) \
if (FunctionName == FString(TEXT(FuncName))) \
{ \
static TArray<FString> AliasTable; \
if (AliasTable.Num() == 0) \
{
#define ADD_ALIAS(AliasStr) \
AliasTable.Add(TEXT(AliasStr));
#define FUNC_ALIASES_END \
} \
return AliasTable; \
}
FUNC_ALIASES_BEGIN("BooleanAND")
ADD_ALIAS("&&")
FUNC_ALIASES_END
FUNC_ALIASES_BEGIN("BooleanOR")
ADD_ALIAS("||")
FUNC_ALIASES_END
FUNC_ALIASES_BEGIN("BooleanXOR")
ADD_ALIAS("^")
FUNC_ALIASES_END
FUNC_ALIASES_BEGIN("Not_PreBool")
ADD_ALIAS("!")
FUNC_ALIASES_END
// keep the compact node title of "^2" from being the required key
FUNC_ALIASES_BEGIN("Square")
ADD_ALIAS("SQUARE")
FUNC_ALIASES_END
FUNC_ALIASES_BEGIN("FClamp")
ADD_ALIAS("CLAMP")
FUNC_ALIASES_END
FUNC_ALIASES_BEGIN("MultiplyMultiply_FloatFloat")
ADD_ALIAS("POWER")
ADD_ALIAS("POW")
FUNC_ALIASES_END
FUNC_ALIASES_BEGIN("FCEIL")
ADD_ALIAS("FCEIL")
ADD_ALIAS("CEIL")
FUNC_ALIASES_END
FUNC_ALIASES_BEGIN("ASin")
// have to add "ASin" back, because this overwrites the function's
// name and we still want it as a viable option
ADD_ALIAS("ASIN")
ADD_ALIAS("ARCSIN")
FUNC_ALIASES_END
FUNC_ALIASES_BEGIN("ACos")
ADD_ALIAS("ACOS")
ADD_ALIAS("ARCCOS")
FUNC_ALIASES_END
FUNC_ALIASES_BEGIN("ATan")
ADD_ALIAS("ATAN")
ADD_ALIAS("ARCTAN")
FUNC_ALIASES_END
FUNC_ALIASES_BEGIN("DegAtan")
ADD_ALIAS("DEGATAN")
ADD_ALIAS("DEGARCTAN")
FUNC_ALIASES_END
FUNC_ALIASES_BEGIN("DegAtan2")
ADD_ALIAS("DEGATAN2")
ADD_ALIAS("DEGARCTAN2")
FUNC_ALIASES_END
FUNC_ALIASES_BEGIN("DegreesToRadians")
ADD_ALIAS("DEGTORAD")
ADD_ALIAS("D2R")
FUNC_ALIASES_END
FUNC_ALIASES_BEGIN("RadiansToDegrees")
ADD_ALIAS("RADTODEG")
ADD_ALIAS("R2D")
FUNC_ALIASES_END
FUNC_ALIASES_BEGIN("ATan2")
ADD_ALIAS("ATAN2")
ADD_ALIAS("ARCTAN2")
FUNC_ALIASES_END
FUNC_ALIASES_BEGIN("MakeVector")
ADD_ALIAS("VECTOR")
ADD_ALIAS("VEC")
ADD_ALIAS("VECT")
FUNC_ALIASES_END
FUNC_ALIASES_BEGIN("MakeVector2D")
ADD_ALIAS("VECTOR2D")
ADD_ALIAS("VEC2D")
ADD_ALIAS("VECT2D")
FUNC_ALIASES_END
FUNC_ALIASES_BEGIN("MakeRotator")
ADD_ALIAS("ROTATOR")
ADD_ALIAS("ROT")
FUNC_ALIASES_END
FUNC_ALIASES_BEGIN("MakeTransform")
ADD_ALIAS("TRANSFORM")
ADD_ALIAS("XFORM")
FUNC_ALIASES_END
FUNC_ALIASES_BEGIN("MakeColor")
ADD_ALIAS("COLOR")
ADD_ALIAS("LINEARCOLOR")
ADD_ALIAS("COLOUR") // long live the empire!
FUNC_ALIASES_END
FUNC_ALIASES_BEGIN("RandomFloat")
ADD_ALIAS("RandomFloat")
ADD_ALIAS("RAND")
ADD_ALIAS("RANDOM")
FUNC_ALIASES_END
FUNC_ALIASES_BEGIN("Dot_VectorVector")
ADD_ALIAS("Dot")
FUNC_ALIASES_END
FUNC_ALIASES_BEGIN("Cross_VectorVector")
ADD_ALIAS("Cross")
FUNC_ALIASES_END
// if none of the above aliases returned, then we don't have any for
// this function (use its regular name)
static TArray<FString> NoAliases;
return NoAliases;
#undef FUNC_ALIASES_END
#undef ADD_ALIAS
#undef FUNC_ALIASES_END
}
private:
/**
* A single operator can have multiple functions associated with it; usually
* for handling different types (int*int, vs. int*vector), hence this array.
*/
typedef TArray<UFunction*> FFunctionsList;
/**
* A lookup table, mapping operator strings (like "+", "*", etc.) to a list
* of associated functions.
*/
TMap<FString, FFunctionsList> LookupTable;
/**
* When looking to match parameters, there are some implicit conversions we
* can make to try and find a match (like converting from int to double).
* This holds an ordered list of delegates that will try and promote the
* supplied types.
*/
DECLARE_DELEGATE_RetVal_OneParam(bool, FTypePromoter, FEdGraphPinType&);
TArray<FTypePromoter> OrderedTypePromoters;
};
/*******************************************************************************
* FCodeGenFragments
*******************************************************************************/
/**
* FCodeGenFragments facilitate the making of pin connections/defaults. When
* turning an expression tree into a network of UK2Nodes, you traverse the tree,
* working backwards from the expression's result node. This means that when you
* spawn a UK2Node, you don't have the node (or literals) that should be plugged
* into it, that is why these fragments are created (to track the associated
* UK2Node/literal, and provide an easy interface for connecting it later with
* other fragments/nodes).
*/
class FCodeGenFragment
{
public:
FCodeGenFragment(FEdGraphPinType InType)
: FragmentType(InType)
{
}
virtual ~FCodeGenFragment()
{
}
/**
* Takes the input to some other fragment, and plugs the result of this one
* into it.
*
* @param InputPin Either an input into some parent expression, or the final output for the entire math expression.
* @return True is the connection was made, otherwise false.
*/
virtual bool ConnectToInput(UEdGraphPin* InputPin, FCompilerResultsLog& MessageLog) = 0;
/**
* As it stands, all the math nodes/literals that can be generated have a
* singular output (hence why we have a basic "connect this fragment to an
* input" function). This message retrieves that output type.
*
* @return The pin type of this fragment's output.
*/
const FEdGraphPinType& GetOutputType() const
{
return FragmentType;
}
protected:
/**
* Utility method for sub-classes to use when attempting a connection
* between two pins. Tries to connect two pins, verifying the type/etc, and
* reporting a failure if there is one.
*
* @param OutputPin The output pin (probably from this fragment).
* @param InputPin The input pin (probably from some other fragment).
* @return True if the connection was made, false if the pins weren't compatible.
*/
bool SafeConnectPins(UEdGraphPin* OutputPin, UEdGraphPin* InputPin, FCompilerResultsLog& MessageLog)
{
const UEdGraphSchema* Schema = InputPin->GetSchema();
bool bSuccess = Schema->TryCreateConnection(OutputPin, InputPin);
if (!bSuccess)
{
MessageLog.Error(*LOCTEXT("PinsNotCompatible", "Output pin '@@ 'is not compatible with input: '@@'").ToString(),
OutputPin, InputPin);
}
return bSuccess;
}
private:
/** All fragments have a singular output type, this is considered the fragment's type as a whole. */
FEdGraphPinType FragmentType;
};
/**
* If the user uses a variable name that already exists in the blueprint, then
* we use that instead of adding an extra input. This fragment wraps a
* VariableGet node that was generated in that scenario.
*/
class FCodeGenFragment_VariableGet : public FCodeGenFragment
{
public:
FCodeGenFragment_VariableGet(UK2Node_VariableGet* InNode, const FEdGraphPinType& InType)
: FCodeGenFragment(InType)
, GeneratedNode(InNode)
{
check(GeneratedNode != nullptr);
}
virtual ~FCodeGenFragment_VariableGet() {}
/// Begin FCodeGenFragment Interface
virtual bool ConnectToInput(UEdGraphPin* InputPin, FCompilerResultsLog& MessageLog) override
{
bool bSuccess = false;
if (UEdGraphPin* VariablePin = GeneratedNode->FindPin(GeneratedNode->VariableReference.GetMemberName()))
{
bSuccess = SafeConnectPins(VariablePin, InputPin, MessageLog);
}
else
{
FText ErrorText = FText::Format(LOCTEXT("NoVariablePin", "Failed to find the '{0}' pin for: '@@'"),
FText::FromName(GeneratedNode->VariableReference.GetMemberName()));
MessageLog.Error(*ErrorText.ToString(), GeneratedNode);
}
return bSuccess;
}
/// End FCodeGenFragment Interface
private:
UK2Node_VariableGet* GeneratedNode;
};
/**
* All operators in the mathematical expression correspond to library functions,
* which in turn generate CallFunction nodes. This fragment wraps one of those
* operation nodes and connects it with the given input (when prompted to).
*/
class FCodeGenFragment_FuntionCall : public FCodeGenFragment
{
public:
FCodeGenFragment_FuntionCall(UK2Node_CallFunction* InNode, const FEdGraphPinType& InType)
: FCodeGenFragment(InType)
, GeneratedNode(InNode)
{
check(GeneratedNode != nullptr);
}
virtual ~FCodeGenFragment_FuntionCall() {}
/// Begin FCodeGenFragment Interface
virtual bool ConnectToInput(UEdGraphPin* InputPin, FCompilerResultsLog& MessageLog) override
{
bool bSuccess = false;
if (UEdGraphPin* ResultPin = GeneratedNode->GetReturnValuePin())
{
bSuccess = SafeConnectPins(ResultPin, InputPin, MessageLog);
}
else
{
MessageLog.Error(*LOCTEXT("NoRetValPin", "Failed to find an output pin for: '@@'").ToString(),
GeneratedNode);
}
return bSuccess;
}
/// End FCodeGenFragment Interface
private:
UK2Node_CallFunction* GeneratedNode;
};
/**
* This fragment doesn't have a corresponding UK2Node, instead it represents a
* constant value that should be entered into another node's input field. When
* "connected", it modifies the connecting pin's DefaultValue.
*/
class FCodeGenFragment_Literal : public FCodeGenFragment
{
public:
FCodeGenFragment_Literal(const FString& LiteralVal, const FEdGraphPinType& ResultType)
: FCodeGenFragment(ResultType)
, DefaultValue(LiteralVal)
{}
virtual ~FCodeGenFragment_Literal() {}
/// Begin FCodeGenFragment Interface
virtual bool ConnectToInput(UEdGraphPin* InputPin, FCompilerResultsLog& MessageLog) override
{
const UEdGraphSchema_K2* K2Schema = Cast<UEdGraphSchema_K2>(InputPin->GetSchema());
bool bSuccess = true;//K2Schema->ArePinTypesCompatible(GetOutputType(), InputPin->PinType);
if (bSuccess)
{
K2Schema->TrySetDefaultValue(*InputPin, DefaultValue, false);
}
else
{
const FText ErrorText = FText::Format(LOCTEXT("LiteralNotCompatible", "Literal type ({0}) is incompatible with pin: '@@'"),
FText::FromName(GetOutputType().PinCategory));
MessageLog.Error(*ErrorText.ToString(), InputPin);
}
return bSuccess;
}
/// End FCodeGenFragment Interface
private:
FString DefaultValue;
};
/**
* This fragment corresponds to an input pin that was added to the
* MathExpression node. Input pins are generated when the user enters variable
* names (like "x", or "y"... ones that aren't variables on the blueprint).
*/
class FCodeGenFragment_InputPin : public FCodeGenFragment
{
public:
FCodeGenFragment_InputPin(UEdGraphPin* InTunnelInputPin)
: FCodeGenFragment(InTunnelInputPin->PinType)
, TunnelInputPin(InTunnelInputPin)
{
}
virtual ~FCodeGenFragment_InputPin() {}
/// Begin FCodeGenFragment Interface
virtual bool ConnectToInput(UEdGraphPin* InputPin, FCompilerResultsLog& MessageLog) override
{
return SafeConnectPins(TunnelInputPin, InputPin, MessageLog);
}
/// End FCodeGenFragment Interface
private:
UEdGraphPin* TunnelInputPin;
};
/*******************************************************************************
* FMathGraphGenerator
*******************************************************************************/
/**
* Takes the root of an expression tree and instantiates blueprint nodes/pins
* for the specified UK2Node_MathExpression (which is a tunnel node, similar to
* how collapsed composite nodes work).
*/
class FMathGraphGenerator final : public FExpressionVisitor
{
public:
FMathGraphGenerator(UK2Node_MathExpression* InNode)
: CompilingNode(InNode)
, TargetBlueprint(FBlueprintEditorUtils::FindBlueprintForGraphChecked(InNode->BoundGraph))
, ActiveMessageLog(nullptr)
{
}
/**
* Takes an expression tree and converts expression nodes into UK2Nodes,
* connecting them, and adding them under the math expression node that
* this was instantiated with.
*
* @param ExpressionRoot The root of the expression tree that you want converted into a UK2Node network.
*/
bool GenerateCode(TSharedRef<IFExpressionNode> ExpressionRoot, FCompilerResultsLog& MessageLog)
{
ActiveMessageLog = &MessageLog;
// want to track if we generated any errors from this pass, so we need to know how many we started with
int32 StartingErrorCount = MessageLog.NumErrors;
InputPinNames.Empty();
LayoutMapper.Clear();
// map the depth/height of expression tree (so we can position nodes prettily)
ExpressionRoot->Accept(LayoutMapper);
// reset the bounds tracking, so we can adjust it as we spawn nodes
GraphXBounds.X = +LayoutMapper.GetMaximumDepth();
GraphXBounds.Y = -LayoutMapper.GetMaximumDepth();
// traverse the expression tree, spawning nodes as we go along
ExpressionRoot->Accept(*this);
UK2Node_Tunnel* EntryNode = CompilingNode->GetEntryNode();
UK2Node_Tunnel* ExitNode = CompilingNode->GetExitNode();
TSharedPtr<FCodeGenFragment> RootFragment = CompiledFragments.FindRef(&ExpressionRoot.Get());
if (RootFragment.IsValid())
{
// connect the final node of the expression with the math-node's output
UEdGraphPin* ReturnPin = ExitNode->CreateUserDefinedPin(UEdGraphSchema_K2::PN_ReturnValue, RootFragment->GetOutputType(), EGPD_Input);
if (!RootFragment->ConnectToInput(ReturnPin, MessageLog))
{
MessageLog.Error(*LOCTEXT("ResultConnectError", "Failed to connect the generated nodes with expression's result pin: '@@'").ToString(),
ReturnPin);
}
}
else
{
if (MessageLog.NumErrors == 0)
{
MessageLog.Error(*LOCTEXT("NoGraphGenerated", "No root node generated from the expression: '@@'").ToString(), CompilingNode);
}
}
// position the entry and exit nodes somewhere sane
{
const FVector2D EntryPos = GetNodePosition(static_cast<int32>(GraphXBounds.X - 1), 0);
EntryNode->NodePosX = static_cast<int32>(EntryPos.X);
EntryNode->NodePosY = static_cast<int32>(EntryPos.Y);
const FVector2D ExitPos = GetNodePosition(static_cast<int32>(GraphXBounds.Y + 1), 0);
ExitNode->NodePosX = static_cast<int32>(ExitPos.X);
ExitNode->NodePosY = static_cast<int32>(ExitPos.Y);
}
bool bHasErrors = ((MessageLog.NumErrors - StartingErrorCount) > 0);
ActiveMessageLog = nullptr;
return !bHasErrors;
}
/**
* When the node gen is over, we need to clear any old pins that weren't
* reused. This query method helps in identifying those that were utilized.
*
* @return True if the pin's name was used in the most recent expression, false if not.
*/
bool IsPinInUse(TSharedPtr<FUserPinInfo> PinInfo)
{
return (InputPinNames.Find(PinInfo->PinName) != INDEX_NONE);
}
/**
* Overloaded, part of the FExpressionVisitor interface; attempts to
* generate either a vaiable-get node, an input pin, or a literal fragment
* from the supplied FTokenWrapperNode (all depends on the token's type).
*
* @return True to continue travesing the expression tree, false to stop.
*/
virtual bool Visit(FTokenWrapperNode& ExpressionNode, EVisitPhase Phase) override
{
check(ActiveMessageLog != nullptr);
const UEdGraphSchema_K2* K2Schema = GetDefault<UEdGraphSchema_K2>();
if (ExpressionNode.Token.TokenType == FBasicToken::TOKEN_Identifier || ExpressionNode.Token.TokenType == FBasicToken::TOKEN_Guid)
{
const FString VariableIdentifier = ExpressionNode.Token.Identifier;
// first we try to match up variables with existing variable properties on the blueprint
FMemberReference VariableReference;
FName VariableName;
FGuid VariableGuid;
if (ExpressionNode.Token.TokenType == FBasicToken::TOKEN_Guid && FGuid::Parse(VariableIdentifier, VariableGuid))
{
// First look the variable up as a Member variable
VariableName = FBlueprintEditorUtils::FindMemberVariableNameByGuid(TargetBlueprint, VariableGuid);
// If the variable was not found, look it up as a local variable
if (VariableName.IsNone())
{
VariableName = FBlueprintEditorUtils::FindLocalVariableNameByGuid(TargetBlueprint, VariableGuid);
VariableReference.SetLocalMember(VariableName, CompilingNode->GetGraph()->GetName(), VariableGuid);
}
else
{
VariableReference.SetSelfMember(VariableName);
}
}
else
{
VariableName = *VariableIdentifier;
// First look the variable up as a Member variable
VariableGuid = FBlueprintEditorUtils::FindMemberVariableGuidByName(TargetBlueprint, VariableName);
// If the variable was not found, look it up as a local variable
if (!VariableGuid.IsValid())
{
VariableGuid = FBlueprintEditorUtils::FindLocalVariableGuidByName(TargetBlueprint, CompilingNode->GetGraph(), VariableName);
if (VariableGuid.IsValid())
{
VariableReference.SetLocalMember(VariableName, CompilingNode->GetGraph()->GetName(), VariableGuid);
}
}
else
{
VariableReference.SetSelfMember(VariableName);
}
// If we found a valid Guid, change the expression's identifier to be the Guid
if (VariableGuid.IsValid())
{
FCString::Strncpy(ExpressionNode.Token.Identifier, *VariableGuid.ToString(EGuidFormats::DigitsWithHyphensInBraces), NAME_SIZE);
ExpressionNode.Token.TokenType = FBasicToken::TOKEN_Guid;
}
}
if (FProperty* VariableProperty = VariableReference.ResolveMember<FProperty>(TargetBlueprint))
{
TSharedPtr<FCodeGenFragment_VariableGet> VariableGetFragment = GeneratePropertyFragment(ExpressionNode, VariableProperty, VariableReference, *ActiveMessageLog);
if (VariableGetFragment.IsValid())
{
CompiledFragments.Add(&ExpressionNode, VariableGetFragment);
}
}
// if a variable-get couldn't be created for it, it needs to be an input to the math node
else if(ExpressionNode.Token.TokenType != FBasicToken::TOKEN_Guid)
{
CompiledFragments.Add(&ExpressionNode, GenerateInputPinFragment(*VariableIdentifier));
}
}
else if (ExpressionNode.Token.TokenType == FBasicToken::TOKEN_Const)
{
CompiledFragments.Add(&ExpressionNode, GenerateLiteralFragment(ExpressionNode.Token, *ActiveMessageLog));
}
else // TOKEN_Symbol
{
FText ErrorText = FText::Format(LOCTEXT("UhandledTokenType", "Unhandled token '{0}' in expression: '@@'"),
FText::FromString(ExpressionNode.Token.Identifier));
ActiveMessageLog->Error(*ErrorText.ToString(), CompilingNode);
}
// keep traversing the expression tree... we should handle cascading
// errors that result from ones incurred here, gathering them all as we
// go, presenting them to the user later
return true;
}
/**
* Overloaded, part of the FExpressionVisitor interface... On VISIT_Post,
* attempts to generate a UK2Node_CallFunction node for the specified
* FBinaryOperator.
*
* @return True to continue traversing the expression tree, false to stop.
*/
virtual bool Visit(FBinaryOperator& ExpressionNode, EVisitPhase Phase) override
{
check(ActiveMessageLog != nullptr);
// we only care about the "Post" visit, after the operands fragments have been generated
if (Phase == FExpressionVisitor::VISIT_Post)
{
TSharedPtr<FCodeGenFragment> LHS = CompiledFragments.FindRef(&(ExpressionNode.LHS.Get()));
TSharedPtr<FCodeGenFragment> RHS = CompiledFragments.FindRef(&(ExpressionNode.RHS.Get()));
TArray< TSharedPtr<FCodeGenFragment> > ArgumentList;
ArgumentList.Add(LHS);
ArgumentList.Add(RHS);
TSharedPtr<FCodeGenFragment_FuntionCall> FunctionFragment = GenerateFunctionFragment(ExpressionNode, ExpressionNode.Operator, ArgumentList, *ActiveMessageLog);
if (FunctionFragment.IsValid())
{
CompiledFragments.Add(&ExpressionNode, FunctionFragment);
}
}
// keep traversing the expression tree... we should handle cascading
// errors that result from ones incurred here, gathering them all as we
// go, presenting them to the user later
return true;
}
/**
* Does nothing (but had to prevent this expression node from being flagged
* as "unhandled"). Expression lists are handled by whatever expression
* they're contained within.
*
* @return Always true, it is expected that cascading errors are handled (and all should be logged).
*/
virtual bool Visit(FExpressionList& ExpressionNode, EVisitPhase Phase) override
{
// no fragments are generated from a list node, it mostly acts as a link
// from a parent node to some set of sub-expressions
// keep traversing the expression tree... if there are any errors,
// they'll be caught in the children nodes (or maybe in the parent)
return true;
}
/**
* Overloaded, part of the FExpressionVisitor interface... On VISIT_Post,
* attempts to generate a UK2Node_CallFunction node for the specified
* FFunctionExpression.
*
* @return True to continue traversing the expression tree, false to stop.
*/
virtual bool Visit(FFunctionExpression& ExpressionNode, EVisitPhase Phase) override
{
check(ActiveMessageLog != nullptr);
// we only care about the "Post" visit, after the function's parameter fragments have been generated
if (Phase == FExpressionVisitor::VISIT_Post)
{
TArray< TSharedPtr<FCodeGenFragment> > ArgumentList;
for (TSharedRef<IFExpressionNode> Param : ExpressionNode.ParamList->Children)
{
TSharedPtr<FCodeGenFragment> ParamFragment = CompiledFragments.FindRef(&(Param.Get()));
ArgumentList.Add(ParamFragment);
}
TSharedPtr<FCodeGenFragment_FuntionCall> FunctionFragment = GenerateFunctionFragment(ExpressionNode, ExpressionNode.FuncName, ArgumentList, *ActiveMessageLog);
if (FunctionFragment.IsValid())
{
CompiledFragments.Add(&ExpressionNode, FunctionFragment);
}
}
// keep traversing the expression tree... we should handle cascading
// errors that result from ones incurred here, gathering them all as we
// go, presenting them to the user later
return true;
}
/**
* From the FExpressionVisitor interface; where we would handle prefixed
* unary operators. Currently support for those is unimplemented, so we just
* log a descriptive error and return.
*
* @return Always true, it is expected that cascading errors are handled (and all should be logged).
*/
virtual bool Visit(FUnaryOperator& ExpressionNode, EVisitPhase Phase) override
{
// don't want to double up on the error message (in the "Post" phase)
if (Phase == VISIT_Pre)
{
FText ErrorText = FText::Format(LOCTEXT("UnaryExpressionError", "Currently, unary operators {0} are prohibited in expressions: '@@'"),
FText::FromString(ExpressionNode.ToString()));
ActiveMessageLog->Error(*ErrorText.ToString(), CompilingNode);
}
// keep traversing the expression tree... we should handle cascading
// errors that result from this, and gather them all to present to the user
return true;
}
/**
* From the FExpressionVisitor interface; where we would handle conditional
* ?: operators. Currently support for those is unimplemented, so we just
* log a descriptive error and return.
*
* @return Always true, it is expected that cascading errors are handled (and all should be logged).
*/
virtual bool Visit(FConditionalOperator& ExpressionNode, EVisitPhase Phase) override
{
check(ActiveMessageLog != nullptr);
// don't want to double up on the error message (in the "Post" phase)
if (Phase == VISIT_Pre)
{
FText ErrorText = FText::Format(LOCTEXT("ConditionalExpressionError", "Currently, conditional operators {0} are prohibited in expressions: '@@'"),
FText::FromString(ExpressionNode.ToString()));
ActiveMessageLog->Error(*ErrorText.ToString(), CompilingNode);
}
// keep traversing the expression tree... we should handle cascading
// errors that result from this, and gather them all to present to the user
return true;
}
private:
/**
* Another FExpressionVisitor interface function... A Generic catch all for
* any expression nodes that we don't explicitly handle. Simply logs an
* error, and returns.
*
* @return Always true, it is expected that cascading errors can be handled (and all should be logged).
*/
virtual bool VisitUnhandled(IFExpressionNode& ExpressionNode, EVisitPhase Phase) override
{
check(ActiveMessageLog != nullptr);
if (Phase == VISIT_Leaf || Phase == VISIT_Pre)
{
FText ErrorText = FText::Format(LOCTEXT("UnhandledExpressionNode", "Unsupported operation ({0}) in the expression: '@@'"),
FText::FromString(ExpressionNode.ToString()));
ActiveMessageLog->Error(*ErrorText.ToString(), CompilingNode);
}
// keep traversing the expression tree... we should handle cascading
// errors that result from this, and gather them all to present to the user
return true;
}
/**
* Either adds a new pin, or finds an existing one on the MathExpression
* node. From that, a fragment is generated (to track the pin, so
* connections can be made later).
*
* @param VariableIdentifier The name of the pin to generate a fragment for.
* @return A new input pin fragment (associated with a pin on the math expression's entry node).
*/
TSharedPtr<FCodeGenFragment_InputPin> GenerateInputPinFragment(const FName VariableIdentifier)
{
TSharedPtr<FCodeGenFragment_InputPin> InputPinFragment;
const UEdGraphSchema_K2* K2Schema = GetDefault<UEdGraphSchema_K2>();
UK2Node_Tunnel* EntryNode = CompilingNode->GetEntryNode();
// if a pin under this name already exists, use that
if (UEdGraphPin* InputPin = EntryNode->FindPin(VariableIdentifier))
{
InputPinFragment = MakeShareable(new FCodeGenFragment_InputPin(InputPin));
}
// otherwise, a new input pin needs to be created for it
else
{
// Create an input pin (using the default guessed type)
FEdGraphPinType DefaultType;
// currently, generated expressions ALWAYS take a double (it is the most versatile type)
DefaultType.PinCategory = UEdGraphSchema_K2::PC_Real;
DefaultType.PinSubCategory = UEdGraphSchema_K2::PC_Double;
UEdGraphPin* NewInputPin = EntryNode->CreateUserDefinedPin(VariableIdentifier, DefaultType, EGPD_Output);
InputPinFragment = MakeShareable(new FCodeGenFragment_InputPin(NewInputPin));
}
// when regenerating a node, we need to clear any old pins that weren't
// reused (can't do this before the node gen because the user may have
// altered a pin to how they want it), so here we track the ones that
// were used by the latest expression
InputPinNames.Add(VariableIdentifier);
return InputPinFragment;
}
/**
* Attempts to generate a VariableGet node for the blueprint graph. If one
* isn't generated, then this function logs an error (and returns an empty
* pointer). However, if one is successfully created, then a fragment
* wrapper is created and returned (to aid in linking the node later).
*
* @param ExpressionContext The expression node that we're generating this fragment for.
* @param VariableProperty The variable that the generated UK2Node should access.
* @param VariableReference Variable reference to assign to the node
* @return An empty pointer if we failed to generate something, otherwise new variable-get fragment.
*/
TSharedPtr<FCodeGenFragment_VariableGet> GeneratePropertyFragment(FTokenWrapperNode& ExpressionContext, FProperty* VariableProperty, FMemberReference& MemberReference, FCompilerResultsLog& MessageLog)
{
check(ExpressionContext.Token.TokenType == FBasicToken::TOKEN_Identifier || ExpressionContext.Token.TokenType == FBasicToken::TOKEN_Guid);
check(VariableProperty != nullptr);
const UEdGraphSchema_K2* K2Schema = GetDefault<UEdGraphSchema_K2>();
TSharedPtr<FCodeGenFragment_VariableGet> VariableGetFragment;
UClass* VariableAccessClass = TargetBlueprint->SkeletonGeneratedClass;
if (MemberReference.IsLocalScope() || UEdGraphSchema_K2::CanUserKismetAccessVariable(VariableProperty, VariableAccessClass, UEdGraphSchema_K2::CannotBeDelegate))
{
FEdGraphPinType VarType;
if (K2Schema->ConvertPropertyToPinType(VariableProperty, /*out*/VarType))
{
UK2Node_VariableGet* NodeTemplate = NewObject<UK2Node_VariableGet>();
NodeTemplate->VariableReference = MemberReference;
UK2Node_VariableGet* VariableGetNode = SpawnNodeFromTemplate<UK2Node_VariableGet>(&ExpressionContext, NodeTemplate);
VariableGetFragment = MakeShareable(new FCodeGenFragment_VariableGet(VariableGetNode, VarType));
}
else
{
FText ErrorText = FText::Format(LOCTEXT("IncompatibleVarError", "Blueprint '{0}' variable is incompatible with graph pins in the expression: '@@'"),
FText::FromName(VariableProperty->GetFName()));
MessageLog.Error(*ErrorText.ToString(), CompilingNode);
}
}
else
{
FText ErrorText = FText::Format(LOCTEXT("InaccessibleVarError", "Cannot access the blueprint's '{0}' variable from the expression: '@@'"),
FText::FromName(VariableProperty->GetFName()));
MessageLog.Error(*ErrorText.ToString(), CompilingNode);
}
return VariableGetFragment;
}
/**
* Spawns a fragment which wraps a literal value. No graph-node or pin is
* created for this fragment; instead, it saves the literal value for later,
* when this fragment is connected with another (it then enters the literal
* value as the connecting pin's default).
*
* @param ExpressionNode The expression node that we're generating this fragment for.
* @return A new literal fragment.
*/
TSharedPtr<FCodeGenFragment_Literal> GenerateLiteralFragment(const FBasicToken& Token, FCompilerResultsLog& MessageLog)
{
check(Token.TokenType == FBasicToken::TOKEN_Const);
FEdGraphPinType LiteralType;
switch (Token.ConstantType)
{
case CPT_Bool:
LiteralType.PinCategory = UEdGraphSchema_K2::PC_Boolean;
break;
case CPT_Double:
LiteralType.PinCategory = UEdGraphSchema_K2::PC_Real;
LiteralType.PinSubCategory = UEdGraphSchema_K2::PC_Double;
break;
case CPT_Int:
LiteralType.PinCategory = UEdGraphSchema_K2::PC_Int;
break;
case CPT_String:
LiteralType.PinCategory = UEdGraphSchema_K2::PC_String;
break;
default:
MessageLog.Error(*FText::Format(LOCTEXT("UnhandledLiteralType", "Unknown literal type in expression: '@@'"),
FText::AsNumber(Token.ConstantType)).ToString(),
CompilingNode);
break;
};
return MakeShareable(new FCodeGenFragment_Literal(Token.GetConstantValue(), LiteralType));
}
/**
* Attempts to find a coresponding fuction (in this class's FOperatorTable),
* one that matches the supplied operator name and the set of arguments. If
* a matching function is found, then a wrapping UK2Node_CallFunction is
* spawned and linked with the supplied arguments (otherwise, errors will
* be logged and an empty pointer will be returned).
*
* @param ExpressionContext The expression node that we're generating this fragment for.
* @param FunctionName The name of the operator (the key we're going to use looking up into FOperatorTable).
* @param ArgumentList A set of other fragments that will plug in as parameters into function.
* @return An empty pointer if we failed to generate something, otherwise the new function fragment.
*/
TSharedPtr<FCodeGenFragment_FuntionCall> GenerateFunctionFragment(IFExpressionNode& ExpressionContext, FString FunctionName, TArray< TSharedPtr<FCodeGenFragment> > ArgumentList, FCompilerResultsLog& MessageLog)
{
bool bMissingArgument = false;
TArray<FEdGraphPinType> TypeList;
// create a type list from the argument fragments (so we can find a matching function signature)
for (int32 Index = 0; Index < ArgumentList.Num(); ++Index)
{
if (!ArgumentList[Index].IsValid())
{
FText ErrorText = FText::Format(LOCTEXT("MissingArgument", "Failed to generate argument #{0} for the '{1}' function, in the expression: '@@'"),
FText::AsNumber(Index + 1),
FText::FromString(FunctionName));
MessageLog.Error(*ErrorText.ToString(), CompilingNode);
bMissingArgument = true;
continue;
}
TypeList.Add(ArgumentList[Index]->GetOutputType());
}
TSharedPtr<FCodeGenFragment_FuntionCall> FunctionFragment;
if (!OperatorLookup.Contains(FunctionName))
{
FText ErrorText = FText::Format(LOCTEXT("UnknownFuncError", "Unknown function '{0}' in the expression: '@@'"), FText::FromString(FunctionName));
MessageLog.Error(*ErrorText.ToString(), CompilingNode);
}
else if (bMissingArgument)
{
// don't execute the other if-branches, head them off if there is already an error
}
else if (UFunction* MatchingFunction = OperatorLookup.FindMatchingFunction(FunctionName, TypeList))
{
FProperty* ReturnProperty = MatchingFunction->GetReturnProperty();
if (ReturnProperty == nullptr)
{
FText ErrorText = FText::Format(LOCTEXT("NoReturnTypeError", "The '{0}' function returns nothing, it cannot be used in the expression: '@@'"),
FText::FromString(FunctionName));
MessageLog.Error(*ErrorText.ToString(), CompilingNode);
}
else
{
const UEdGraphSchema_K2* K2Schema = GetDefault<UEdGraphSchema_K2>();
FEdGraphPinType ReturnType;
if (K2Schema->ConvertPropertyToPinType(ReturnProperty, /*out*/ReturnType))
{
UK2Node_CallFunction* NodeTemplate = NewObject<UK2Node_CallFunction>(CompilingNode->GetGraph());
NodeTemplate->SetFromFunction(MatchingFunction);
UK2Node_CallFunction* FunctionCall = SpawnNodeFromTemplate<UK2Node_CallFunction>(&ExpressionContext, NodeTemplate);
int32 InitialErrorCount = MessageLog.NumErrors;
// connect this fragment to its children fragments
int32 PinWireIndex = 0;
for (auto PinIt = FunctionCall->Pins.CreateConstIterator(); PinIt; ++PinIt)
{
UEdGraphPin* InputPin = *PinIt;
if (!K2Schema->IsMetaPin(*InputPin) && (InputPin->Direction == EGPD_Input))
{
if (PinWireIndex < ArgumentList.Num())
{
TSharedPtr<FCodeGenFragment>& ArgumentNode = ArgumentList[PinWireIndex];
// try to make the connection (which might cause an error internally)
if (!ArgumentNode->ConnectToInput(InputPin, MessageLog))
{
FText ErrorText = FText::Format(LOCTEXT("ConnectPinError", "Failed to connect parameter #{0} with input on '@@'"),
FText::AsNumber(PinWireIndex + 1));
MessageLog.Error(*ErrorText.ToString(), FunctionCall);
}
}
else if (InputPin->DefaultValue.IsEmpty()) // there is an ErrorTolerance parameter with a default value in EqualEqual_VectorVector
{
// too many pins - shouldn't be possible due to the checking in FindMatchingFunction() above
FText ErrorText = LOCTEXT("ConnectPinError_RequiresMoreParameters", "The '@@' function requires more parameters than were provided");
MessageLog.Error(*ErrorText.ToString(), FunctionCall);
break;
}
++PinWireIndex;
}
}
bool bConnectionErrors = (InitialErrorCount < MessageLog.NumErrors);
if (bConnectionErrors)
{
MessageLog.Error(*LOCTEXT("InternalExpressionError", "Internal node error for expression: '@@'").ToString(), CompilingNode);
}
FunctionFragment = MakeShareable(new FCodeGenFragment_FuntionCall(FunctionCall, ReturnType));
}
else
{
FText ErrorText = FText::Format(LOCTEXT("ReturnTypeError", "The '{0}' function's return type is incompatible with graph pins in the expression: '@@'"),
FText::FromString(FunctionName));
MessageLog.Error(*ErrorText.ToString(), CompilingNode);
}
}
}
else
{
FText ErrorText = FText::Format(LOCTEXT("OperatorParamsError", "Cannot find a '{0}' function that takes the supplied param types, for expression: '@@'"),
FText::FromString(FunctionName));
MessageLog.Error(*ErrorText.ToString(), CompilingNode);
}
return FunctionFragment;
}
/**
* Utility method to turn an FLayoutVisitor coordinate into graph coordinates.
* FLayoutVisitor coordinates are in terms of nodes (so a depth of 1, would
* mean one node to the right of the initial node).
*
* @param Depth Horizontal coordinate (how many nodes away from the initial node).
* @param Height Vertical coordinate (how many nodes down from the initial node).
* @return A 2D graph position for the center of a node to be placed at.
*/
FVector2D GetNodePosition(int32 Depth, int32 Height) const
{
// get a count of how many nodes there are at this specific depth
int32 TotalHeight = LayoutMapper.DepthHeightLookup.FindRef(LayoutMapper.GetMaximumDepth() - Depth);
const float MiddleHeight = FMath::Max(TotalHeight, 1) * 0.5f;
const float HeightPerNode = 140.0f;
const float DepthPerNode = 240.0f;
return FVector2D(Depth * DepthPerNode, (Height - MiddleHeight + 0.5f) * HeightPerNode);
}
/**
* Templatized function for turning an expression node into a UK2Node. This
* takes the expression node's position in the expression tree and turns it
* into a blueprint graph position (placing the new UK2Node there).
*
* @param ForExpression The expression node that the will UK2Node represent.
* @param Template An instance of the UK2Node type you wish to spawn.
* @return The newly created UK2Node.
*/
template<typename NodeType>
NodeType* SpawnNodeFromTemplate(IFExpressionNode* ForExpression, NodeType* Template)
{
const int32 Y = LayoutMapper.HeightChart.FindRef(ForExpression);
const int32 X = LayoutMapper.GetMaximumDepth() - LayoutMapper.DepthChart.FindRef(ForExpression);
GraphXBounds.X = FMath::Min((int32)GraphXBounds.X, X);
GraphXBounds.Y = FMath::Max((int32)GraphXBounds.Y, X);
constexpr bool bShouldSelectNewNode = false;
const FVector2D Location = GetNodePosition(X, Y);
return FEdGraphSchemaAction_K2NewNode::SpawnNodeFromTemplate<NodeType>(CompilingNode->BoundGraph, Template, Location, bShouldSelectNewNode);
}
private:
/** The node that we're generating sub-nodes and pins for */
UK2Node_MathExpression* CompilingNode;
/**
* The blueprint that CompilingNode belongs to (the blueprint this will
* generate a graph for).
*/
UBlueprint* TargetBlueprint;
/** List of known operators, and mappings from them to associated functions */
FOperatorTable OperatorLookup;
/**
* An FLayoutVisitor that charts the depth of the expression tree (and what
* depth/height each expression node is at). Used to layout the graph nicely.
*/
FLayoutVisitor LayoutMapper;
/**
* Supplements LayoutMapper, tracks where nodes were actually placed (sometimes
* the depth of an expression node doesn't map one-to-one with the fragment
* in the graph), so you have the min and max x locations of spawned graph nodes.
*/
FVector2D GraphXBounds;
/**
* Fragments that represent spawned UK2Nodes or literals that were generated
* from traversing the expression tree... These fragments facilitate
* connections between each other (that's why we need to track them).
*/
TMap<IFExpressionNode*, TSharedPtr<FCodeGenFragment> > CompiledFragments;
/**
* Used so the various Visit() methods have a way to log errors, null when
* not in the middle of GenerateCode().
*/
FCompilerResultsLog* ActiveMessageLog;
/**
* After the code generation, we want to clear any old pins that
* weren't reused, so here we track the ones in use.
*/
TArray<FName> InputPinNames;
};
/*******************************************************************************
* FExpressionParser
*******************************************************************************/
#define PARSE_HELPER_BEGIN(NestedRuleName) \
TSharedRef<IFExpressionNode> LHS = NestedRuleName(); \
Begin:
#define PARSE_HELPER_ENTRY(NestedRuleName, DesiredToken) \
if (IsValid() && MatchSymbol(DesiredToken)) \
{ \
TSharedRef<IFExpressionNode> RHS = NestedRuleName(); \
LHS = MakeShareable(new FBinaryOperator(DesiredToken, LHS, RHS)); \
goto Begin; \
} \
#define PARSE_HELPER_END \
{ return LHS; }
/**
* Recursively builds an IFExpressionNode tree, where leaf nodes represent tokens
* (constants, literals, or identifiers), and branch nodes represent operations
* on the attached children. The chaining order of expression functions is what
* determines operator precedence.
*/
class FExpressionParser : public FBasicTokenParser
{
public:
/**
* Takes a string and parses a mathematical expression out of it, returning
* the head of an expression tree that was generated as a result.
*
* @param InExpression The string you wish to parse.
* @return An expression node, which serves as the root of an expression tree representing the provided string.
*/
TSharedRef<IFExpressionNode> ParseExpression(FString InExpression)
{
ExpressionString = InExpression;
ResetParser(*ExpressionString);
TSharedRef<IFExpressionNode> FullExpression = Expression();
// if we didn't parse the full expression and the parser doesn't have
// an error, then there is some unhandled string postfixed to the
// expression (something like "2.x" or "5var")
if (InputPos < InputLen && IsValid())
{
FText ErrorText = FText::Format(LOCTEXT("UnhandledPostfixError", "Unhandled trailing '{0}' at the end of the expression"),
FText::FromString(&Input[InputPos]));
SetError(FErrorState::ParseError, ErrorText);
}
return FullExpression;
}
private:
/**
* Starting point for parsing full expressions (sets off on parsing out
* operations according to operator precedence)... Could be used for the
* initial root expression, or various other sub-expressions (like those
* encapsulated in parentheses, etc.).
*
* @return Root node of an expression tree that was generated from where we
* are in parsing ExpressionString (at time of calling).
*/
TSharedRef<IFExpressionNode> Expression()
{
// AssignmentExpression has the lowest precedence, start with it (it
// will attempt to parse out higher precedent operations first)
return AssignmentExpression();
}
/**
* Intended to support assignment within the expression (setting temp or
* external variables equal to some value, so they can be used later in the
* expression).
*
* @TODO Implement!
* @return Root node of an expression tree that was generated from where we
* are in parsing ExpressionString (at time of calling).
*/
TSharedRef<IFExpressionNode> AssignmentExpression()
{
// ConditionalExpression takes precedence over a assignment operation, parse it first
return ConditionalExpression();
}
/**
* Looks for a conditional ternary statement (c ? a : b) to parse, and
* tokenizes the operands.
*
* @return Root node of an expression tree that was generated from where we
* are in parsing ExpressionString (at time of calling).
*/
TSharedRef<IFExpressionNode> ConditionalExpression()
{
// LogicalOrExpression takes precedence over a conditional operation, parse it first
TSharedRef<IFExpressionNode> MainPart = LogicalOrExpression();
if (IsValid() && MatchSymbol(TEXT("?")))
{
TSharedRef<IFExpressionNode> TruePart = Expression();
RequireSymbol(TEXT(":"), TEXT("?: operator"));
TSharedRef<IFExpressionNode> FalsePart = ConditionalExpression();
return MakeShareable(new FConditionalOperator(MainPart, TruePart, FalsePart));
}
else
{
return MainPart;
}
}
/**
* Looks for a binary logical-or statement (a || b) to parse, and
* tokenizes the operands.
*
* @return Root node of an expression tree that was generated from where we
* are in parsing ExpressionString (at time of calling).
*/
TSharedRef<IFExpressionNode> LogicalOrExpression()
{
// LogicalOrExpression takes precedence over an or operation, parse it first
PARSE_HELPER_BEGIN(LogicalAndExpression)
PARSE_HELPER_ENTRY(LogicalAndExpression, TEXT("||"))
PARSE_HELPER_END
}
/**
* Looks for a binary logical-and statement (a && b) to parse, and
* tokenizes the operands.
*
* @return Root node of an expression tree that was generated from where we
* are in parsing ExpressionString (at time of calling).
*/
TSharedRef<IFExpressionNode> LogicalAndExpression()
{
// InclusiveOrExpression takes precedence over an and operation, parse it first
PARSE_HELPER_BEGIN(InclusiveOrExpression)
PARSE_HELPER_ENTRY(InclusiveOrExpression, TEXT("&&"))
PARSE_HELPER_END
}
/**
* Looks for a binary bitwise-or statement (a | b) to parse, and
* tokenizes the operands.
*
* @return Root node of an expression tree that was generated from where we
* are in parsing ExpressionString (at time of calling).
*/
TSharedRef<IFExpressionNode> InclusiveOrExpression()
{
// ExclusiveOrExpression takes precedence over an inclusive or operation, parse it first
PARSE_HELPER_BEGIN(ExclusiveOrExpression)
PARSE_HELPER_ENTRY(ExclusiveOrExpression, TEXT("|"))
PARSE_HELPER_END
}
/**
* Looks for a binary exclusive-or statement (a ^ b) to parse, and
* tokenizes the operands.
*
* @return Root node of an expression tree that was generated from where we
* are in parsing ExpressionString (at time of calling).
*/
TSharedRef<IFExpressionNode> ExclusiveOrExpression()
{
// AndExpression takes precedence over an exclusive or operation, parse it first
PARSE_HELPER_BEGIN(AndExpression)
PARSE_HELPER_ENTRY(AndExpression, TEXT("^"))
PARSE_HELPER_END
}
/**
* Looks for a binary bitwise-and statement (a & b) to parse, and
* tokenizes the operands.
*
* @return Root node of an expression tree that was generated from where we
* are in parsing ExpressionString (at time of calling).
*/
TSharedRef<IFExpressionNode> AndExpression()
{
// EqualityExpression takes precedence over an and operation, parse it first
PARSE_HELPER_BEGIN(EqualityExpression)
PARSE_HELPER_ENTRY(EqualityExpression, TEXT("&"))
PARSE_HELPER_END
}
/**
* Looks for a binary equality statement (like [a == b], or [a != b]) to
* parse, and tokenizes the operands.
*
* @return Root node of an expression tree that was generated from where we
* are in parsing ExpressionString (at time of calling).
*/
TSharedRef<IFExpressionNode> EqualityExpression()
{
// RelationalExpression takes precedence over an equality expression, parse it first
PARSE_HELPER_BEGIN(RelationalExpression)
PARSE_HELPER_ENTRY(RelationalExpression, TEXT("=="))
PARSE_HELPER_ENTRY(RelationalExpression, TEXT("!="))
PARSE_HELPER_END
}
/**
* Looks for a binary comparison statement to parse (like [a > b], [a <= b],
* etc.), and tokenizes the operands.
*
* @return Root node of an expression tree that was generated from where we
* are in parsing ExpressionString (at time of calling).
*/
TSharedRef<IFExpressionNode> RelationalExpression()
{
// ShiftExpression takes precedence over a relational expression, parse it first
PARSE_HELPER_BEGIN(ShiftExpression)
PARSE_HELPER_ENTRY(ShiftExpression, TEXT("<"))
PARSE_HELPER_ENTRY(ShiftExpression, TEXT(">"))
PARSE_HELPER_ENTRY(ShiftExpression, TEXT("<="))
PARSE_HELPER_ENTRY(ShiftExpression, TEXT(">="))
PARSE_HELPER_END
}
/**
* Looks for a binary bitwise shift statement to parse (like [a << b], or
* [a >> b]), and tokenizes the operands.
*
* @return Root node of an expression tree that was generated from where we
* are in parsing ExpressionString (at time of calling).
*/
TSharedRef<IFExpressionNode> ShiftExpression()
{
// AdditiveExpression takes precedence over a shift, parse it first
PARSE_HELPER_BEGIN(AdditiveExpression)
PARSE_HELPER_ENTRY(AdditiveExpression, TEXT("<<"))
PARSE_HELPER_ENTRY(AdditiveExpression, TEXT(">>"))
PARSE_HELPER_END
}
/**
* Looks for a binary addition/subtraction statement to parse ([a + b], or
* [a - b]), and tokenizes the operands.
*
* @return Root node of an expression tree that was generated from where we
* are in parsing ExpressionString (at time of calling).
*/
TSharedRef<IFExpressionNode> AdditiveExpression()
{
// MultiplicativeExpression takes precedence over an add/subtract, parse it first
PARSE_HELPER_BEGIN(MultiplicativeExpression)
PARSE_HELPER_ENTRY(MultiplicativeExpression, TEXT("+"))
PARSE_HELPER_ENTRY(MultiplicativeExpression, TEXT("-"))
PARSE_HELPER_END
}
/**
* Looks for a binary multiplication/division/modulus statement to parse
* ([a * b], [a / b], or [a % b]), and tokenizes the operands.
*
* @return Root node of an expression tree that was generated from where we
* are in parsing ExpressionString (at time of calling).
*/
TSharedRef<IFExpressionNode> MultiplicativeExpression()
{
// CastExpression takes precedence over a multiply/division/modulus, parse it first
PARSE_HELPER_BEGIN(CastExpression)
PARSE_HELPER_ENTRY(CastExpression, TEXT("*"))
PARSE_HELPER_ENTRY(CastExpression, TEXT("/"))
PARSE_HELPER_ENTRY(CastExpression, TEXT("%"))
PARSE_HELPER_END
}
/**
* Intended to handle type-casts (like from double to int, etc.).
*
* @TODO Implement!
* @return Root node of an expression tree that was generated from where we
* are in parsing ExpressionString (at time of calling).
*/
TSharedRef<IFExpressionNode> CastExpression()
{
// @TODO: support casts (currently this is too greedy, and messes up "4*(5)" interpreting (5) as a cast)
// if (MatchSymbol(TEXT("(")))
// {
// //@TODO: Need to support qualifiers / typedefs / what have you
// FBasicToken TypeName;
// GetToken(TypeName);
// TSharedRef<IFExpressionNode> TypeExpression = MakeShareable(new FTokenWrapperNode(TypeName));
//
// RequireSymbol(TEXT(")"), TEXT("Closing ) in cast"));
//
// TSharedRef<IFExpressionNode> ValueExpression = CastExpression(Context);
//
// return MakeShareable(new FCastOperator(TypeExpression, ValueExpression));
// }
// else
{
return UnaryExpression();
}
}
/**
* Attempts to parse various unary statements (like positive/negative
* markers, logical negation, pre increment/decrement, etc.)
*
* @return Root node of an expression tree that was generated from where we
* are in parsing ExpressionString (at time of calling).
*/
TSharedRef<IFExpressionNode> UnaryExpression()
{
// prefix increment: ++<unary-expression>
// prefix decrement: --<unary-expression>
// bitwise not: ~<unary-expression>
// logical not: !<unary-expression>
// positive sign: +<unary-expression>
// negative sign: -<unary-expression>
// reference: &<unary-expression>
// dereference: *<unary-expression>
// negative sign: -<unary-expression>
// allocation: new <unary-expression>
// deallocation: delete <unary-expression>
// parameter pack: sizeof <unary-expression>
// C-style cast: (type) <unary-expression>
//
// check for the various prefix operators and jump back to
// CastExpression() for parsing the right operand...
if (MatchSymbol(TEXT("&")))
{
return MakeShareable(new FUnaryOperator(TEXT("&"), CastExpression()));
}
else if (MatchSymbol(TEXT("+")))
{
// would return pre-increment operators like so:
// unaryOp(+).RHS = unaryOp(+)
return MakeShareable(new FUnaryOperator(TEXT("+"), CastExpression()));
}
else if (MatchSymbol(TEXT("-")))
{
// would return post-increment operators like so:
// unaryOp(-).RHS = unaryOp(-)
return MakeShareable(new FUnaryOperator(TEXT("-"), CastExpression()));
}
else if (MatchSymbol(TEXT("~")))
{
return MakeShareable(new FUnaryOperator(TEXT("~"), CastExpression()));
}
else if (MatchSymbol(TEXT("!")))
{
return MakeShareable(new FUnaryOperator(TEXT("!"), CastExpression()));
}
else
{
return PostfixExpression();
}
}
/**
* Intended to handle postfix operations (like post increment/decrement,
* array subscripting, member access, etc.).
*
* @TODO Implement!
* @return Root node of an expression tree that was generated from where we
* are in parsing ExpressionString (at time of calling).
*/
TSharedRef<IFExpressionNode> PostfixExpression()
{
// if (MatchSymbol(TEXT("[")))
// {
// // Array indexing
// TSharedRef<IFExpressionNode> IndexExpression = Expression(Context);
// RequireSymbol(TEXT("]"), TEXT("Closing ] in array indexing"));
//
// return NullExpression; // @TODO: return a valid expression node
// }
// else if (MatchSymbol(TEXT("(")))
// {
// while (!PeekSymbol(TEXT(")")))
// {
// TSharedRef<IFExpressionNode> Item = AssignmentExpression(Context);
// }
//
// RequireSymbol(TEXT(")"), TEXT("Closing ) in function call"));
//
// return NullExpression; // @TODO: return a valid expression node
// }
// else if (MatchSymbol(TEXT(".")) || MatchSymbol(TEXT("->")))
// {
// // Member reference
// FBasicToken Identifier;
// if (GetIdentifier(Identifier, /*bNoConsts=*/ true))
// {
// //@TODO: Do stuffs
// }
// else
// {
// // @TODO: error
// }
//
// return NullExpression; // @TODO: return a valid expression node
// }
// else
{
return PrimaryExpression();
}
}
/**
* End of the line, where we attempt to generate a leaf node (an identifier,
* const literal, or a string). However, here we also look for the start of
* a sub-expression (one encapsulated in parentheses).
*
* @return Either a leaf node (representing a variable, or literal), or another
* branch node, representing a sub-expression encapsulated by parentheses.
*/
TSharedRef<IFExpressionNode> PrimaryExpression()
{
if (MatchSymbol(TEXT("(")))
{
TSharedRef<IFExpressionNode> Result = Expression();
RequireSymbol(TEXT(")"), TEXT("group closing"));
return Result;
}
else
{
// identifier, constant, or string
FBasicToken Token;
GetToken(Token);
// or maybe a function call?
if (MatchSymbol(TEXT("(")))
{
TSharedPtr<FExpressionList> FuncArguments;
// if this is an empty function (takes no parameters)
if (PeekSymbol(TEXT(")")))
{
FuncArguments = MakeShareable(new FExpressionList);
}
else
{
FuncArguments = ListExpression();
}
TSharedRef<IFExpressionNode> FuncExpression = MakeShareable(new FFunctionExpression(Token.Identifier, FuncArguments.ToSharedRef()));
FText RequireError = FText::Format(LOCTEXT("MissingFuncClose", "'{0}' closing"), FText::FromString(Token.Identifier));
RequireSymbol(TEXT(")"), *RequireError.ToString());
return FuncExpression;
}
else
{
return MakeShareable(new FTokenWrapperNode(Token));
}
}
}
/**
* Parses out a comma separated list of sub-expressions (arguments for a
* function or struct).
*
* @return A branch FExpressionList node, which holds a series of sub-expressions.
*/
TSharedRef<FExpressionList> ListExpression()
{
TSharedRef<FExpressionList> ListNode = MakeShareable(new FExpressionList);
do
{
ListNode->Children.Add(Expression());
} while (MatchSymbol(TEXT(",")));
return ListNode;
}
private:
/** The intact expression string that this is currently in charge of parsing */
FString ExpressionString;
};
/*******************************************************************************
* UK2Node_MathExpression
*******************************************************************************/
//------------------------------------------------------------------------------
UK2Node_MathExpression::UK2Node_MathExpression(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
// renaming the node rebuilds the expression (the node name is where they
// specify the math equation)
bCanRenameNode = true;
bMadeAfterRotChange = false;
OrphanedPinSaveMode = ESaveOrphanPinMode::SaveAll;
}
void UK2Node_MathExpression::Serialize(FArchive& Ar)
{
UK2Node_Composite::Serialize(Ar);
if (Ar.IsLoading() && !bMadeAfterRotChange)
{
// remember that this logic has been run, we only want to run it once:
bMadeAfterRotChange = true;
// We need to reorder the parameters to MakeRot/MakeRotator/Rotator/Rot, to filter this expensive logic I'm just searching
// expressions for 'rot':
if (Expression.Contains(TEXT("Rot")))
{
// Now parse the expression and look for function expressions to the old MakeRot function:
FExpressionParser Parser;
TSharedPtr<IFExpressionNode> ExpressionRoot = Parser.ParseExpression(Expression);
struct FMakeRotFixupVisitor : public FExpressionVisitor
{
virtual bool Visit(class FFunctionExpression& Node, EVisitPhase Phase)
{
if (Phase != VISIT_Pre)
{
return false;
}
const bool bIsMakeRot = (Node.FuncName == TEXT("makerot"));
if (bIsMakeRot ||
Node.FuncName == TEXT("rotator") ||
Node.FuncName == TEXT("rot"))
{
// reorder parameters to match new order of MakeRotator:
if (Node.ParamList->Children.Num() == 3)
{
TSharedRef<IFExpressionNode> OldPitch = Node.ParamList->Children[0];
TSharedRef<IFExpressionNode> OldYaw = Node.ParamList->Children[1];
TSharedRef<IFExpressionNode> OldRoll = Node.ParamList->Children[2];
Node.ParamList->Children[0] = OldRoll;
Node.ParamList->Children[1] = OldPitch;
Node.ParamList->Children[2] = OldYaw;
}
// MakeRot also needs to be updated to the new name:
if (bIsMakeRot)
{
Node.FuncName = TEXT("MakeRotator");
}
}
return true;
}
};
// perform the update:
FMakeRotFixupVisitor Fixup;
ExpressionRoot->Accept(Fixup);
// reform the expression with the updated parameter order/function names:
Expression = ExpressionRoot->ToString();
}
}
}
//------------------------------------------------------------------------------
void UK2Node_MathExpression::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent)
{
Super::PostEditChangeProperty(PropertyChangedEvent);
const FName PropertyName = (PropertyChangedEvent.Property != NULL) ? PropertyChangedEvent.Property->GetFName() : NAME_None;
if (PropertyName == GET_MEMBER_NAME_CHECKED(UK2Node_MathExpression, Expression))
{
RebuildExpression(Expression);
}
}
//------------------------------------------------------------------------------
void UK2Node_MathExpression::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const
{
// actions get registered under specific object-keys; the idea is that
// actions might have to be updated (or deleted) if their object-key is
// mutated (or removed)... here we use the node's class (so if the node
// type disappears, then the action should go with it)
UClass* ActionKey = GetClass();
// to keep from needlessly instantiating a UBlueprintNodeSpawner, first
// check to make sure that the registrar is looking for actions of this type
// (could be regenerating actions for a specific asset, and therefore the
// registrar would only accept actions corresponding to that asset)
if (ActionRegistrar.IsOpenForRegistration(ActionKey))
{
UBlueprintNodeSpawner* NodeSpawner = UBlueprintNodeSpawner::Create(GetClass());
check(NodeSpawner != nullptr);
ActionRegistrar.AddBlueprintAction(ActionKey, NodeSpawner);
}
}
//------------------------------------------------------------------------------
FNodeHandlingFunctor* UK2Node_MathExpression::CreateNodeHandler(class FKismetCompilerContext& CompilerContext) const
{
return new FKCHandler_MathExpression(CompilerContext);
}
//------------------------------------------------------------------------------
bool UK2Node_MathExpression::ShouldExpandInsteadCompile() const
{
const int32 TunnelNodesNum = 2;
if (!BoundGraph || (TunnelNodesNum >= BoundGraph->Nodes.Num()))
{
return true;
}
if ((TunnelNodesNum + 1) == BoundGraph->Nodes.Num())
{
TArray<UEdGraphNode*> InnerNodes = BoundGraph->Nodes;
InnerNodes.RemoveSingleSwap(GetEntryNode(), EAllowShrinking::No);
InnerNodes.RemoveSingleSwap(GetExitNode(), EAllowShrinking::No);
const bool bTheOnlyNodeIsNotAFunctionCall = (1 == InnerNodes.Num())
&& (nullptr != InnerNodes[0])
&& !InnerNodes[0]->IsA<UK2Node_CallFunction>();
if (bTheOnlyNodeIsNotAFunctionCall)
{
return true;
}
}
return false;
}
//------------------------------------------------------------------------------
TSharedPtr<class INameValidatorInterface> UK2Node_MathExpression::MakeNameValidator() const
{
// we'll let our parser mark the node for errors after the face (once the
// name is submitted)... parsing it with every character could be slow
return MakeShareable(new FDummyNameValidator(EValidatorResult::Ok));
}
//------------------------------------------------------------------------------
void UK2Node_MathExpression::OnRenameNode(const FString& NewName)
{
RebuildExpression(NewName);
CachedNodeTitle.MarkDirty();
}
//------------------------------------------------------------------------------
void UK2Node_MathExpression::RebuildExpression(FString InExpression)
{
static bool bIsAlreadyRebuilding = false;
// the rebuild can invoke a ReconstructNode(), which triggers this again,
// so this combined with the following
if (!bIsAlreadyRebuilding)
{
TGuardValue<bool> RecursionGuard(bIsAlreadyRebuilding, true);
ClearExpression();
Expression = InExpression;
// This should not be sanitized, if anything fails to occur, what the user inputed should be what is displayed
CachedDisplayExpression.SetCachedText(FText::FromString(Expression), this);
CachedNodeTitle.SetCachedText(GetFullTitle(CachedDisplayExpression), this);
if (!InExpression.IsEmpty()) // @TODO: is this needed?
{
// build a expression tree from the string
FExpressionParser Parser;
TSharedPtr<IFExpressionNode> ExpressionRoot = Parser.ParseExpression(InExpression);
// if the parser successfully chewed through the string
if (Parser.IsValid())
{
FMathGraphGenerator GraphGenerator(this);
// generate new nodes from the expression tree (could result in
// a series of errors being attached to the node).
if (!GraphGenerator.GenerateCode(ExpressionRoot.ToSharedRef(), *CachedMessageLog))
{
if (CachedMessageLog->NumErrors == 0)
{
CachedMessageLog->Error(*LOCTEXT("MathExprGFailedGen", "Failed to generate full expression graph for: '@@'").ToString(), this);
}
}
else
{
Expression = ExpressionRoot->ToString();
CachedDisplayExpression.SetCachedText(FText::FromString(SanitizeDisplayExpression(ExpressionRoot->ToDisplayString(GetBlueprint()))), this);
CachedNodeTitle.SetCachedText(GetFullTitle(CachedDisplayExpression), this);
}
if (UK2Node_Tunnel* EntryNode = GetEntryNode())
{
// iterate backwards so we can remove as we go... we want to
// clear any pins that weren't used by the expression (if we
// clear any, then they were probably remnants from the last
// expression... we can't delete them before, because the
// user may have mutated one for the new expression)
for (int32 PinIndex = EntryNode->UserDefinedPins.Num() - 1; PinIndex >= 0; --PinIndex)
{
TSharedPtr<FUserPinInfo> PinInfo = EntryNode->UserDefinedPins[PinIndex];
if (!GraphGenerator.IsPinInUse(PinInfo))
{
EntryNode->RemoveUserDefinedPin(PinInfo);
}
}
}
}
else
{
FText ErrorText = FText::Format(LOCTEXT("MathExprParseError", "PARSE ERROR in '@@': {0}"),
Parser.GetErrorState().Description);
CachedMessageLog->Error(*ErrorText.ToString(), this);
}
}
// notify any listeners that the bound graph has changed
if (BoundGraph)
{
BoundGraph->NotifyGraphChanged();
}
// refresh the node since the connections may have changed, this won't be reentrant due to bool above
ReconstructNode();
// finally, recompile
UBlueprint* Blueprint = FBlueprintEditorUtils::FindBlueprintForNodeChecked(this);
FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
}
}
//------------------------------------------------------------------------------
void UK2Node_MathExpression::ClearExpression()
{
// clear any errors
SetNodeError(this, FText::GetEmpty());
// clear out old nodes
DeleteGeneratedNodesInGraph(BoundGraph);
// delete the old return pins (they will always be regenerated)... save the
// input pins though (because someone may have changed the input type to
// something other than a float)
if (UK2Node_Tunnel* ExitNode = GetExitNode())
{
// iterate backwards so we can remove as we go
for (int32 PinIndex = ExitNode->UserDefinedPins.Num() - 1; PinIndex >= 0; --PinIndex)
{
TSharedPtr<FUserPinInfo> PinInfo = ExitNode->UserDefinedPins[PinIndex];
ExitNode->RemoveUserDefinedPin(PinInfo);
}
}
// passing true to FCompilerResultsLog's constructor would make this the
// primary compiler log (it is not) - the idea being that upon destruction
// the primary log prints a summary; well, since this isn't destructed at
// the end of compilation, and it blocks the full compiler log from
// becoming the "CurrentEventTarget", we pass false - we append logs
// collected by this one to the full compiler log later on anyways (so they won't be missed)
CachedMessageLog = MakeShareable(new FCompilerResultsLog(/*bIsCompatibleWithEvents =*/false));
CachedMessageLog->bSilentMode = true;
Expression.Empty();
}
//------------------------------------------------------------------------------
void UK2Node_MathExpression::ValidateNodeDuringCompilation(FCompilerResultsLog& MessageLog) const
{
Super::ValidateNodeDuringCompilation(MessageLog);
if (CachedMessageLog.IsValid())
{
MessageLog.Append(*CachedMessageLog, true);
}
// else, this may be some intermediate node in the compile, let's look at the errors from the original...
else
{
if(const UObject* SourceObject = MessageLog.FindSourceObject(this))
{
const UK2Node_MathExpression* MathExpression = MessageLog.FindSourceObjectTypeChecked<UK2Node_MathExpression>(this);
// Should always be able to find the source math expression
check(MathExpression);
// if the expressions match, then the errors should
check(MathExpression->Expression == Expression);
// take the same errors from the original node (so we don't have to
// re-parse/re-gen to fish out the same errors)
if (MathExpression->CachedMessageLog.IsValid())
{
MessageLog.Append(*MathExpression->CachedMessageLog, true);
}
}
}
}
//------------------------------------------------------------------------------
FText UK2Node_MathExpression::GetNodeTitle(ENodeTitleType::Type TitleType) const
{
if (Expression.IsEmpty() && (TitleType == ENodeTitleType::MenuTitle))
{
return LOCTEXT("AddMathExprMenuOption", "Add Math Expression...");
}
else if (TitleType != ENodeTitleType::FullTitle)
{
if (CachedDisplayExpression.IsOutOfDate(this))
{
FExpressionParser Parser;
TSharedPtr<IFExpressionNode> ExpressionRoot = Parser.ParseExpression(Expression);
if(Parser.IsValid())
{
CachedDisplayExpression.SetCachedText(FText::FromString(SanitizeDisplayExpression(ExpressionRoot->ToDisplayString(GetBlueprint()))), this);
}
else
{
// Fallback and display the expression in it's raw form
CachedDisplayExpression.SetCachedText(FText::FromString(Expression), this);
}
}
return CachedDisplayExpression;
}
else if (CachedNodeTitle.IsOutOfDate(this))
{
FExpressionParser Parser;
TSharedPtr<IFExpressionNode> ExpressionRoot = Parser.ParseExpression(Expression);
if(Parser.IsValid())
{
CachedDisplayExpression.SetCachedText(FText::FromString(SanitizeDisplayExpression(ExpressionRoot->ToDisplayString(GetBlueprint()))), this);
}
CachedNodeTitle.SetCachedText(GetFullTitle(CachedDisplayExpression), this);
}
return CachedNodeTitle;
}
//------------------------------------------------------------------------------
void UK2Node_MathExpression::PostPlacedNewNode()
{
bMadeAfterRotChange = true;
Super::PostPlacedNewNode();
FEdGraphUtilities::RenameGraphToNameOrCloseToName(BoundGraph, "MathExpression");
}
//------------------------------------------------------------------------------
void UK2Node_MathExpression::ReconstructNode()
{
if (!HasAnyFlags(RF_NeedLoad))
{
RebuildExpression(Expression);
}
// Call the super ReconstructNode, preserving our error message since we never want it automatically cleared
const FString OldErrorMessage = ErrorMsg;
Super::ReconstructNode();
ErrorMsg = OldErrorMessage;
// Mark our input pins as not saved, but we want to save orphaned output pins to show compile errors
for (UEdGraphPin* Pin : Pins)
{
// Recombine the sub pins back into the OptionPin
if (Pin->Direction == EEdGraphPinDirection::EGPD_Input)
{
Pin->SetSavePinIfOrphaned(false);
}
else
{
Pin->SetSavePinIfOrphaned(true);
}
}
}
//------------------------------------------------------------------------------
FString UK2Node_MathExpression::SanitizeDisplayExpression(FString InExpression) const
{
// We do not want the outermost parentheses in the display expression, they add nothing to the logical comprehension
InExpression.RemoveFromStart(TEXT("("));
InExpression.RemoveFromEnd(TEXT(")"));
return InExpression;
}
FText UK2Node_MathExpression::GetFullTitle(FText InExpression) const
{
// FText::Format() is slow, so we cache this to save on performance
return FText::Format(LOCTEXT("MathExpressionSecondTitleLine", "{0}\nMath Expression"), InExpression);
}
void UK2Node_MathExpression::FindDiffs(class UEdGraphNode* OtherNode, struct FDiffResults& Results )
{
UK2Node_MathExpression* MathExpression1 = this;
UK2Node_MathExpression* MathExpression2 = Cast<UK2Node_MathExpression>(OtherNode);
// Compare the visual display of a math expression (the visual display involves consolidating variable Guid's into readable parameters)
FText Expression1 = MathExpression1->GetNodeTitle(ENodeTitleType::EditableTitle);
FText Expression2 = MathExpression2->GetNodeTitle(ENodeTitleType::EditableTitle);
if (Expression1.CompareTo(Expression2) != 0)
{
FDiffSingleResult Diff;
Diff.Node1 = MathExpression2;
Diff.Node2 = MathExpression1;
Diff.Diff = EDiffType::NODE_PROPERTY;
FText NodeName = GetNodeTitle(ENodeTitleType::ListView);
FFormatNamedArguments Args;
Args.Add(TEXT("Expression1"), Expression1);
Args.Add(TEXT("Expression2"), Expression2);
Diff.ToolTip = FText::Format(LOCTEXT("DIF_MathExpressionToolTip", "Math Expression '{Expression1}' changed to '{Expression2}'"), Args);
Diff.Category = EDiffType::MODIFICATION;
Diff.DisplayString = FText::Format(LOCTEXT("DIF_MathExpression", "Math Expression '{Expression1}' changed to '{Expression2}'"), Args);
Results.Add(Diff);
}
}
#undef LOCTEXT_NAMESPACE