Lots of work on lua call interface, also improved makefiles

This commit is contained in:
2025-02-24 16:46:05 -05:00
parent 4023d19247
commit bed4f3e805
10 changed files with 1870 additions and 101 deletions

View File

@@ -0,0 +1,697 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "LuaCallNode.h"
#include "BlueprintActionDatabaseRegistrar.h"
#include "BlueprintNodeSpawner.h"
#include "Containers/EnumAsByte.h"
#include "Containers/UnrealString.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphSchema.h"
#include "EdGraphSchema_K2.h"
#include "EdGraphSchema_K2_Actions.h"
#include "EditorCategoryUtils.h"
#include "Engine/Blueprint.h"
#include "HAL/PlatformCrt.h"
#include "Internationalization/Internationalization.h"
#include "K2Node_CallFunction.h"
#include "K2Node_MakeArray.h"
#include "K2Node_MakeStruct.h"
#include "Kismet/KismetMathLibrary.h"
#include "Kismet/KismetTextLibrary.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/CompilerResultsLog.h"
#include "KismetCompiler.h"
#include "LuaCall.h"
#include "Math/Vector2D.h"
#include "Misc/AssertionMacros.h"
#include "Misc/CString.h"
#include "ScopedTransaction.h"
#include "Templates/Casts.h"
#include "Templates/SubclassOf.h"
#include "UObject/Class.h"
#include "UObject/ObjectPtr.h"
#include "UObject/Package.h"
#include "UObject/UnrealNames.h"
#include "UObject/UnrealType.h"
#include "UObject/WeakObjectPtr.h"
#include "UObject/WeakObjectPtrTemplates.h"
#define LOCTEXT_NAMESPACE "LuaCall"
// All argument pins will have internal Names that start with "A:"
static bool IsArgumentPin(const UEdGraphPin *Pin) {
TCHAR pname[FName::StringBufferSize];
Pin->PinName.ToString(pname);
return pname[0] == 'A' && pname[1] == ':';
}
static FName ArgumentNameAddPrefix(const FString &name) {
FString Prefixed = FString("A:") + name;
return FName(*Prefixed);
}
static FString ArgumentNameRemovePrefix(const FName &name) {
return name.ToString().Mid(2, FName::StringBufferSize);
}
// All return value pins will have internal Names that start with "R:"
static bool IsReturnValuePin(const UEdGraphPin *Pin) {
TCHAR pname[FName::StringBufferSize];
Pin->PinName.ToString(pname);
return pname[0] == 'R' && pname[1] == ':';
}
static FName ReturnValueNameAddPrefix(const FString &name) {
FString Prefixed = FString("R:") + name;
return FName(*Prefixed);
}
static FString ReturnValueNameRemovePrefix(const FName &name) {
return name.ToString().Mid(2, FName::StringBufferSize);
}
// Builtin pins will have names with no prefixes.
static const FName FunctionPinName(TEXT("Lua Function Prototype"));
static bool IsFunctionPin(const UEdGraphPin *Pin) {
return (Pin->PinName == FunctionPinName);
}
static const FName InvokeOrProbePinName(TEXT("Invoke or Probe"));
static bool IsInvokeOrProbePin(const UEdGraphPin *Pin) {
return (Pin->PinName == InvokeOrProbePinName);
}
static const FName PlacePinName(TEXT("Place Tangible"));
static bool IsPlacePin(const UEdGraphPin *Pin) {
return (Pin->PinName == PlacePinName);
}
// A parser for lua function prototypes.
//
struct FlxParsedProto {
FString ErrorMessage;
TArray<FString> Tokens;
int NextToken;
FString ClassName;
FString FunctionName;
TArray<FString> Arguments;
TArray<FString> ReturnValues;
bool ExtraReturnValues;
// Check the next token to see if it's exactly equal to text.
//
bool IsLiteral(const TCHAR *text);
// Check the next token to see if it's an identifier.
//
bool IsIdent();
// Empty out the FlxParsedProto.
//
void Empty();
// Make a syntax error message, using the tokens.
//
void Syntax();
// Parse a function prototype.
//
void Parse(const FString &proto);
// Construct with a prototype.
//
FlxParsedProto(const FString &str) { Parse(str); }
};
bool FlxParsedProto::IsLiteral(const TCHAR *text) {
return ((NextToken < Tokens.Num()) && (Tokens[NextToken] == text));
}
bool FlxParsedProto::IsIdent() {
return ((NextToken < Tokens.Num()) && (FChar::IsAlpha(Tokens[NextToken][0])));
}
void FlxParsedProto::Empty() {
ErrorMessage = TEXT("");
Tokens.Empty();
NextToken = 0;
ClassName = TEXT("");
FunctionName = TEXT("");
Arguments.Empty();
ReturnValues.Empty();
ExtraReturnValues = false;
}
void FlxParsedProto::Syntax() {
FString Message;
if (Tokens.Num() == 0) {
Message = TEXT("Function prototype cannot be blank");
}
for (int i = 0; i < Tokens.Num(); i++) {
if (i == NextToken) {
Message.Append(TEXT(" ? "));
} else {
if ((i > 0) && (FChar::IsAlpha(Tokens[i][0])) && (FChar::IsAlpha(Tokens[i-1][0]))) {
Message.Append(TEXT(" "));
}
}
Message.Append(Tokens[i]);
}
Empty();
ErrorMessage = Message;
}
void FlxParsedProto::Parse(const FString &str) {
Empty();
// Step one: tokenize.
int offset = 0;
while (offset < str.Len()) {
TCHAR c = str[offset];
if (FChar::IsWhitespace(c)) {
offset++;
} else if (FChar::IsAlpha(c)) {
int lo = offset;
while ((offset < str.Len()) && FChar::IsAlnum(str[offset])) offset++;
Tokens.Add(str.Mid(lo, offset-lo));
} else if (str.Mid(offset, 3) == TEXT("...")) {
Tokens.Add(str.Mid(offset, 3));
offset += 3;
} else if (FChar::IsPunct(c)) {
Tokens.Add(str.Mid(offset, 1));
offset += 1;
} else {
Empty();
ErrorMessage = FString::Printf(TEXT("%s ? %s"), *str.Mid(0, offset), *str.Mid(offset));
return;
}
}
NextToken = 0;
// Step two: Parse.
if (!IsLiteral(TEXT("function"))) return Syntax();
NextToken++;
if (!IsLiteral(TEXT("*")) && !IsIdent()) return Syntax();
ClassName = Tokens[NextToken++];
if (!IsLiteral(TEXT("."))) return Syntax();
NextToken++;
if (!IsIdent()) return Syntax();
FunctionName = Tokens[NextToken++];
if (!IsLiteral(TEXT("("))) return Syntax();
NextToken++;
if (IsIdent()) {
while (true) {
if (!IsIdent()) return Syntax();
Arguments.Add(Tokens[NextToken++]);
if (!IsLiteral(TEXT(","))) break;
NextToken++;
}
}
if (!IsLiteral(TEXT(")"))) return Syntax();
NextToken++;
if (IsLiteral(TEXT(":"))) {
NextToken++;
while (true) {
if (IsLiteral(TEXT("..."))) {
ExtraReturnValues = true;
NextToken++;
break;
} else if (IsIdent()) {
ReturnValues.Add(Tokens[NextToken++]);
if (!IsLiteral(TEXT(","))) break;
NextToken++;
} else {
return Syntax();
}
}
}
if (NextToken != Tokens.Num()) return Syntax();
}
UK2Node_LuaCall::UK2Node_LuaCall(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
NodeTooltip = LOCTEXT("NodeTooltip",
"Probe or Invoke a Lua function.\n"
"\n"
"The lua function prototype must be a hardwired string which must look like\n"
"one of the following:"
"\n"
" function cname.fname(arg1, arg2)"
" function cname.fname(arg1, arg2) : ret1, ret2\n"
" function cname.fname(arg1, arg2) : ret1, ret2, ...\n"
"\n"
"The prototype is parsed to determine what lua function to call.\n"
"The lua call node will automatically add pins for the arguments and\n"
"return values. If the function has no return values, you can omit those\n"
"from the proto. If you put an ellipsis at the end of the return values,\n"
"then any additional return values will be collected into a\n"
"dynamically typed array of values which you can iterate over later.\n"
"\n"
"Optionally, you may use the * wildcard for the classname. In that\n"
"case, the class of the 'place' tangible will be used.\n"
"\n"
"Argument and return value pins have wildcard types initially, you can\n"
"hook them to inputs and outputs of the following types:\n"
"\n"
" string, name, float, boolean, vector\n"
"\n");
}
void UK2Node_LuaCall::AllocateDefaultPins()
{
Super::AllocateDefaultPins();
CreateCorrectPins();
}
void UK2Node_LuaCall::CreateCorrectPins()
{
if (LuaFunctionPrototype.IsEmpty())
{
LuaFunctionPrototype = TEXT("function class.func(arg1, arg2) : ret1, ret2");
}
if (FindPin(UEdGraphSchema_K2::PN_Execute) == nullptr) {
CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Execute);
}
if (FindPin(UEdGraphSchema_K2::PN_Then) == nullptr) {
CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Then);
}
if (FindPin(FunctionPinName, EGPD_Input) == nullptr) {
UEdGraphPin *P = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_String, FunctionPinName);
P->DefaultValue = LuaFunctionPrototype;
}
if (FindPin(InvokeOrProbePinName, EGPD_Input) == nullptr) {
UEdGraphPin *P = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Byte, StaticEnum<ElxInvokeOrProbe>(), InvokeOrProbePinName);
P->DefaultValue = TEXT("Probe");
P->AutogeneratedDefaultValue = P->DefaultValue;
}
if (FindPin(PlacePinName, EGPD_Input) == nullptr) {
UEdGraphPin *P = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Object, AActor::StaticClass(), PlacePinName);
}
// Parse the lua function prototype.
FlxParsedProto ParsedProto(LuaFunctionPrototype);
if (!ParsedProto.ErrorMessage.IsEmpty())
{
bHasCompilerMessage = true;
ErrorType = EMessageSeverity::Error;
ErrorMsg = FString::Printf(TEXT("Syntax error in lua function prototype: %s"), *ParsedProto.ErrorMessage);
}
// Transfer all Existing argument and return value pins to the Old Pins Maps.
TMap<FName, UEdGraphPin *> OldArgumentPins;
TMap<FName, UEdGraphPin *> OldReturnValuePins;
for (auto It = Pins.CreateIterator(); It; ++It)
{
UEdGraphPin* CheckPin = *It;
if (IsArgumentPin(CheckPin)) {
OldArgumentPins.Add(CheckPin->PinName, CheckPin);
It.RemoveCurrent();
}
if (IsReturnValuePin(CheckPin)) {
OldReturnValuePins.Add(CheckPin->PinName, CheckPin);
It.RemoveCurrent();
}
}
// Create Argument pins in the correct order, reusing old pins where possible.
for (const FString& Name : ParsedProto.Arguments)
{
FName PrefixedName = ArgumentNameAddPrefix(Name);
UEdGraphPin **OldPin = OldArgumentPins.Find(PrefixedName);
if (OldPin != nullptr) {
Pins.Emplace(*OldPin);
OldArgumentPins.Remove(PrefixedName);
} else {
CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Wildcard, PrefixedName);
}
}
// Create ReturnValue pins in the correct order, reusing old pins where possible.
for (const FString& Name : ParsedProto.ReturnValues)
{
FName PrefixedName = ReturnValueNameAddPrefix(Name);
UEdGraphPin **OldPin = OldReturnValuePins.Find(PrefixedName);
if (OldPin != nullptr) {
Pins.Emplace(*OldPin);
OldReturnValuePins.Remove(PrefixedName);
} else {
CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Wildcard, PrefixedName);
}
}
// Delete any unused pins.
for (auto &iter : OldArgumentPins)
{
iter.Value->Modify();
iter.Value->MarkAsGarbage();
}
for (auto &iter : OldReturnValuePins)
{
iter.Value->Modify();
iter.Value->MarkAsGarbage();
}
OldArgumentPins.Empty();
OldReturnValuePins.Empty();
}
void UK2Node_LuaCall::SynchronizePinType(UEdGraphPin* Pin)
{
if (IsArgumentPin(Pin) || IsReturnValuePin(Pin))
{
const UEdGraphSchema_K2* K2Schema = Cast<const UEdGraphSchema_K2>(GetSchema());
bool bPinTypeChanged = false;
if (Pin->LinkedTo.Num() == 0)
{
static const FEdGraphPinType WildcardPinType = FEdGraphPinType(UEdGraphSchema_K2::PC_Wildcard, NAME_None, nullptr, EPinContainerType::None, false, FEdGraphTerminalType());
// Ensure wildcard
if (Pin->PinType != WildcardPinType)
{
Pin->PinType = WildcardPinType;
bPinTypeChanged = true;
}
}
else
{
UEdGraphPin* OtherPin = Pin->LinkedTo[0];
// Take the type of the connected pin
if (Pin->PinType != OtherPin->PinType)
{
Pin->PinType = OtherPin->PinType;
bPinTypeChanged = true;
}
}
if (bPinTypeChanged)
{
// Let the graph know to refresh
GetGraph()->NotifyNodeChanged(this);
UBlueprint* Blueprint = GetBlueprint();
if (!Blueprint->bBeingCompiled)
{
FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
}
}
}
}
FText UK2Node_LuaCall::GetNodeTitle(ENodeTitleType::Type TitleType) const
{
return LOCTEXT("LuaCall_Title", "Probe or Invoke a Lua Function");
}
FText UK2Node_LuaCall::GetPinDisplayName(const UEdGraphPin* Pin) const
{
// The exec pins don't need labels.
if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec)
{
return FText::GetEmpty();
}
// Many pins can go unlabeled if they have default values.
if (IsFunctionPin(Pin) || IsInvokeOrProbePin(Pin))
{
if (Pin->LinkedTo.Num() == 0)
{
return FText::GetEmpty();
}
}
// For argument pins, we must strip off the Argument Pin Prefix.
if (IsArgumentPin(Pin)) {
return FText::FromString(ArgumentNameRemovePrefix(Pin->PinName));
}
// For return value pins, we must strip off the Return Value Pin Prefix.
if (IsReturnValuePin(Pin)) {
return FText::FromString(ReturnValueNameRemovePrefix(Pin->PinName));
}
// Otherwise, just return the Pin Name the normal way.
return FText::FromName(Pin->PinName);
}
void UK2Node_LuaCall::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent)
{
Super::PostEditChangeProperty(PropertyChangedEvent);
GetGraph()->NotifyNodeChanged(this);
}
void UK2Node_LuaCall::PinConnectionListChanged(UEdGraphPin* Pin)
{
Modify();
SynchronizePinType(Pin);
}
void UK2Node_LuaCall::PinDefaultValueChanged(UEdGraphPin* Pin)
{
if(IsFunctionPin(Pin))
{
LuaFunctionPrototype = Pin->DefaultValue;
CreateCorrectPins();
GetGraph()->NotifyNodeChanged(this);
}
}
void UK2Node_LuaCall::PinTypeChanged(UEdGraphPin* Pin)
{
SynchronizePinType(Pin);
Super::PinTypeChanged(Pin);
}
FText UK2Node_LuaCall::GetTooltipText() const
{
return NodeTooltip;
}
void UK2Node_LuaCall::PostReconstructNode()
{
Super::PostReconstructNode();
UEdGraph* OuterGraph = GetGraph();
if (!IsTemplate() && OuterGraph && OuterGraph->Schema) {
for (UEdGraphPin* CurrentPin : Pins)
{
SynchronizePinType(CurrentPin);
}
}
CreateCorrectPins();
}
#define LuaCallLibraryFunction(name) (UlxLuaCallLibrary::StaticClass()->FindFunctionByName(GET_MEMBER_NAME_CHECKED(UlxLuaCallLibrary, name)))
static UFunction *GetArgumentPackingFunction(const FEdGraphPinType &Type)
{
if (Type.PinCategory == UEdGraphSchema_K2::PC_Real)
{
return LuaCallLibraryFunction(LuaCallAddFloatParameter);
}
if (Type.PinCategory == UEdGraphSchema_K2::PC_Int)
{
return LuaCallLibraryFunction(LuaCallAddIntParameter);
}
else if (Type.PinCategory == UEdGraphSchema_K2::PC_Boolean)
{
return LuaCallLibraryFunction(LuaCallAddBooleanParameter);
}
else if (Type.PinCategory == UEdGraphSchema_K2::PC_Name)
{
return LuaCallLibraryFunction(LuaCallAddNameParameter);
}
else if (Type.PinCategory == UEdGraphSchema_K2::PC_String)
{
return LuaCallLibraryFunction(LuaCallAddStringParameter);
}
else if ((Type.PinCategory == UEdGraphSchema_K2::PC_Struct) && (Type.PinSubCategoryObject == TBaseStructure<FVector>::Get()))
{
return LuaCallLibraryFunction(LuaCallAddVectorParameter);
}
else if ((Type.PinCategory == UEdGraphSchema_K2::PC_Struct) && (Type.PinSubCategoryObject == TBaseStructure<FVector2D>::Get()))
{
return LuaCallLibraryFunction(LuaCallAddVector2DParameter);
}
else
{
return nullptr;
}
}
void UK2Node_LuaCall::ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph)
{
Super::ExpandNode(CompilerContext, SourceGraph);
const UEdGraphSchema_K2* Schema = CompilerContext.GetSchema();
// Define a local function that creates a CallFunctionNode
auto MakeCallFunctionNode = [&](UFunction *func)
{
UK2Node_CallFunction* CallNode = CompilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, SourceGraph);
CallNode->SetFromFunction(func);
CallNode->AllocateDefaultPins();
CompilerContext.MessageLog.NotifyIntermediateObjectCreation(CallNode, this);
return CallNode;
};
// The BeginNode function packs the class name and function name into the call buffer.
FlxParsedProto ParsedProto(LuaFunctionPrototype);
UK2Node_CallFunction* BeginNode = MakeCallFunctionNode(LuaCallLibraryFunction(LuaCallBegin));
Schema->TrySetDefaultValue(*BeginNode->FindPinChecked(TEXT("ClassName")), ParsedProto.ClassName);
Schema->TrySetDefaultValue(*BeginNode->FindPinChecked(TEXT("FunctionName")), ParsedProto.FunctionName);
UK2Node_CallFunction* PrevNode = BeginNode;
// Add Packing operations for all argument pins.
for (UEdGraphPin* Pin : Pins)
{
if (IsArgumentPin(Pin))
{
UFunction *PackingFunc = GetArgumentPackingFunction(Pin->PinType);
if (PackingFunc != nullptr)
{
UK2Node_CallFunction *PackNode = MakeCallFunctionNode(PackingFunc);
CompilerContext.MovePinLinksToIntermediate(*Pin, *PackNode->FindPinChecked(TEXT("Value")));
PrevNode->GetThenPin()->MakeLinkTo(PackNode->GetExecPin());
PrevNode = PackNode;
}
else
{
FText PinName = GetPinDisplayName(Pin);
FText PinType = FText::FromName(Pin->PinType.PinCategory);
FText Error = FText::Format(LOCTEXT("Error_UnexpectedPinType", "Pin '{0}' has an unexpected type: {1}"), PinName, PinType);
CompilerContext.MessageLog.Error(*Error.ToString());
}
}
}
// Add the invoke or probe node.
bool IsInvoke = (FindPin(InvokeOrProbePinName, EGPD_Input)->DefaultValue == TEXT("Invoke"));
UFunction *Action = IsInvoke ? LuaCallLibraryFunction(LuaCallInvoke) : LuaCallLibraryFunction(LuaCallProbe);
UK2Node_CallFunction* ActionNode = MakeCallFunctionNode(Action);
CompilerContext.MovePinLinksToIntermediate(*FindPin(PlacePinName, EGPD_Input), *ActionNode->FindPinChecked(TEXT("Place")));
PrevNode->GetThenPin()->MakeLinkTo(ActionNode->GetExecPin());
// Link up the Exec pins.
CompilerContext.MovePinLinksToIntermediate(*GetExecPin(), *BeginNode->GetExecPin());
CompilerContext.MovePinLinksToIntermediate(*GetThenPin(), *ActionNode->GetThenPin());
BreakAllNodeLinks();
}
UK2Node::ERedirectType UK2Node_LuaCall::DoPinsMatchForReconstruction(const UEdGraphPin* NewPin, int32 NewPinIndex, const UEdGraphPin* OldPin, int32 OldPinIndex) const
{
ERedirectType RedirectType = ERedirectType_None;
// if the pin names do match
if (NewPin->PinName.ToString().Equals(OldPin->PinName.ToString(), ESearchCase::CaseSensitive))
{
// Make sure we're not dealing with a menu node
UEdGraph* OuterGraph = GetGraph();
if( OuterGraph && OuterGraph->Schema )
{
const UEdGraphSchema_K2* K2Schema = Cast<const UEdGraphSchema_K2>(GetSchema());
if( !K2Schema || K2Schema->IsSelfPin(*NewPin) || K2Schema->ArePinTypesCompatible(OldPin->PinType, NewPin->PinType) )
{
RedirectType = ERedirectType_Name;
}
else
{
RedirectType = ERedirectType_None;
}
}
}
else
{
// try looking for a redirect if it's a K2 node
if (UK2Node* Node = Cast<UK2Node>(NewPin->GetOwningNode()))
{
// if you don't have matching pin, now check if there is any redirect param set
TArray<FString> OldPinNames;
GetRedirectPinNames(*OldPin, OldPinNames);
FName NewPinName;
RedirectType = ShouldRedirectParam(OldPinNames, /*out*/ NewPinName, Node);
// make sure they match
if ((RedirectType != ERedirectType_None) && (!NewPin->PinName.ToString().Equals(NewPinName.ToString(), ESearchCase::CaseSensitive)))
{
RedirectType = ERedirectType_None;
}
}
}
return RedirectType;
}
bool UK2Node_LuaCall::IsConnectionDisallowed(const UEdGraphPin* MyPin, const UEdGraphPin* OtherPin, FString& OutReason) const
{
// The function pin cannot be connected.
if (IsFunctionPin(MyPin))
{
OutReason = LOCTEXT("Error_FunctionPrototypeMustBeHardwired", "Lua function prototype must be a hardwired constant.").ToString();
return true;
}
// The invoke-or-probe pin cannot be connected.
if (IsInvokeOrProbePin(MyPin))
{
OutReason = LOCTEXT("Error_InvokeOrProbeMustBeHardwired", "Invoke vs Probe must be a hardwired constant.").ToString();
return true;
}
// Argument input pins may only be connected to packable types.
if (IsArgumentPin(MyPin))
{
UFunction *Packer = GetArgumentPackingFunction(OtherPin->PinType);
if (Packer == nullptr)
{
OutReason = LOCTEXT("Error_InvalidArgumentType", "Lua Call Arguments may be float, boolean, string, name, or vector.").ToString();
return true;
}
}
return Super::IsConnectionDisallowed(MyPin, OtherPin, OutReason);
}
void UK2Node_LuaCall::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);
}
}
FText UK2Node_LuaCall::GetMenuCategory() const
{
return FEditorCategoryUtils::GetCommonCategory(FCommonEditorCategory::Text);
}
#undef LOCTEXT_NAMESPACE