Massive refactoring in FormatMessage and LuaCallNode

This commit is contained in:
2026-03-04 06:47:37 -05:00
parent 6428194393
commit cb6f9ebe1d
7 changed files with 318 additions and 467 deletions

View File

@@ -21,6 +21,7 @@
}
],
"settings": {
"files.autoSave": "afterDelay",
"typescript.tsc.autoDetect": "off",
"lldb.dereferencePointers": false,
"npm.autoDetect": "off",

View File

@@ -1,5 +1,8 @@
#include "FormatDataLibrary.h"
#include "Editor.h"
#include "EdGraphSchema_K2.h"
#include "Internationalization/TextFormatter.h"
#include "Layout/Geometry.h"
#include "Widgets/Layout/Anchors.h"
#include "Common.h"
@@ -49,6 +52,54 @@ void UlxFormatDataLibrary::ScanForConverters()
}
}
UFunction* UlxFormatDataLibrary::GetConverterForPinType(const UEdGraphSchema_K2 *Schema, const FEdGraphPinType& PinType, bool AllowWild)
{
// Special case: wildcard pins are unconnected pins.
if ((PinType.PinCategory == UEdGraphSchema_K2::PC_Wildcard) && AllowWild)
{
return UlxFormatDataLibrary::StaticClass()->FindFunctionByName(GET_MEMBER_NAME_CHECKED(UlxFormatDataLibrary, FormatArgumentDataBlank));
}
// Scan the cached converter list for a matching type.
UlxFormatDataLibrary* FormatDataLib = GEditor->GetEditorSubsystem<UlxFormatDataLibrary>();
for (UFunction* Function : FormatDataLib->Converters)
{
FProperty* ValueProperty = Function->FindPropertyByName(TEXT("AutoConvertedValue"));
FEdGraphPinType ValuePinType;
if (!Schema->ConvertPropertyToPinType(ValueProperty, ValuePinType)) continue;
if (!Schema->ArePinTypesEquivalent(PinType, ValuePinType)) continue;
return Function;
}
// A general handler for Enums.
if ((PinType.PinCategory == UEdGraphSchema_K2::PC_Byte) && (nullptr != Cast<const UEnum>(PinType.PinSubCategoryObject)))
{
return UlxFormatDataLibrary::StaticClass()->FindFunctionByName(GET_MEMBER_NAME_CHECKED(UlxFormatDataLibrary, FormatArgumentDataEnum));
}
// A case for subclasses of 'Object' which are not exactly 'Object'.
if (PinType.PinCategory == UEdGraphSchema_K2::PC_Object)
{
return UlxFormatDataLibrary::StaticClass()->FindFunctionByName(GET_MEMBER_NAME_CHECKED(UlxFormatDataLibrary, FormatArgumentDataObject));
}
return nullptr;
}
ELogVerbosity::Type UlxFormatDataLibrary::ConvertElxFormatLogVerbosity(ElxFormatLogVerbosity Verbosity) {
switch (Verbosity) {
case ElxFormatLogVerbosity::Error: return ELogVerbosity::Error;
case ElxFormatLogVerbosity::Warning: return ELogVerbosity::Warning;
case ElxFormatLogVerbosity::Display: return ELogVerbosity::Display;
case ElxFormatLogVerbosity::Log: return ELogVerbosity::Log;
case ElxFormatLogVerbosity::ThrottledDisplay: return ELogVerbosity::Display;
case ElxFormatLogVerbosity::ThrottledLog: return ELogVerbosity::Log;
case ElxFormatLogVerbosity::Verbose: return ELogVerbosity::Verbose;
case ElxFormatLogVerbosity::VeryVerbose: return ELogVerbosity::VeryVerbose;
case ElxFormatLogVerbosity::Fatal: return ELogVerbosity::Fatal;
}
}
FFormatArgumentData UlxFormatDataLibrary::FormatArgumentDataBool(bool AutoConvertedValue, const FString &Name)
{
FFormatArgumentData Result;
@@ -248,3 +299,55 @@ FFormatArgumentData UlxFormatDataLibrary::FormatArgumentDataEnum(uint8 Value, co
}
return Result;
}
void UlxFormatDataLibrary::FormatLogMessageInternal(UObject *Context, ElxFormatLogVerbosity Verbosity, const FString &InPattern, TArray<FFormatArgumentData> InArgs)
{
// For throttled verbosity levels, suppress repeated messages with the
// same format pattern. We key on the blueprint name + format pattern,
// and allow at most one message per second per key.
//
if ((Verbosity == ElxFormatLogVerbosity::ThrottledDisplay) || (Verbosity == ElxFormatLogVerbosity::ThrottledLog))
{
static TMap<FString, double> LastLogTime;
double Now = FPlatformTime::Seconds();
FString Key = Context->GetClass()->GetName() + TEXT("::") + InPattern;
double &Last = LastLogTime.FindOrAdd(Key, 0.0);
if (Now - Last < 1.0)
{
return;
}
Last = Now;
}
// Generate the formatted string.
//
FText InPatternText(FText::FromString(InPattern));
FText Message = FTextFormatter::Format(MoveTemp(InPatternText), MoveTemp(InArgs), false, false);
FString MessageString = Message.ToString();
// Get the blueprint name.
//
// Normally, the log function expects you to pass in a filename, and a log
// category name. We use the blueprint name for both.
//
// Using the blueprint name as a log category name is not technically
// correct. However, there is no correct way to create log categories
// from inside of blueprints. Doing it this way at least produces a reasonable
// message inside the log. What doesn't work correctly is the log message
// suppression system. Ie, console commands like 'log <category> verbose'
// don't have any effect here. The design of the log message suppression
// system is such that there just is no reasonable way to hook into it from
// inside of blueprints.
//
FString BlueprintNameString = Context->GetClass()->GetName();
auto BlueprintNameAnsi = StringCast<ANSICHAR>(*BlueprintNameString);
FLogCategoryName BlueprintNameLogCategory(Context->GetClass()->GetFName());
// Output to Log
//
ELogVerbosity::Type VerbosityValue = ConvertElxFormatLogVerbosity(Verbosity);
if (VerbosityValue <= ELogVerbosity::COMPILED_IN_MINIMUM_VERBOSITY)
{
FMsg::Logf(BlueprintNameAnsi.Get(), 0, BlueprintNameLogCategory, VerbosityValue, TEXT("%s"), *MessageString);
}
}

View File

@@ -36,6 +36,52 @@
#include "FormatDataLibrary.generated.h"
////////////////////////////////////////////////////////////
//
// ElxFormatLogVerbosity
//
// Controls the ELogVerbosity of the UE_LOG directive inside
// FormatLogMessage. Also controls the throttling of log
// messages.
//
// Fatal is deliberately placed at the end so that the
// editor defaults to Error (value 0) when the dropdown is
// uninitialized. The numeric values don't match
// ELogVerbosity, so a conversion function is needed.
//
////////////////////////////////////////////////////////////
UENUM(BlueprintType)
enum class ElxFormatLogVerbosity : uint8 {
/** Prints an error to the console and log file. The editor collects and reports errors. */
Error,
/** Prints a warning to the console and log file. The editor collects and reports warnings. */
Warning,
/** Prints a message to the console and log file. */
Display,
/** Prints a message to the log file, but not to the console. */
Log,
/** Like Display, but suppresses repeated messages with the same format pattern (at most once per second). */
ThrottledDisplay,
/** Like Log, but suppresses repeated messages with the same format pattern (at most once per second). */
ThrottledLog,
/** Prints a message to the log file only if Verbose logging is enabled for the given category. */
Verbose,
/** Prints a message to the log file only if VeryVerbose logging is enabled. */
VeryVerbose,
/** Prints a fatal error to the console and log file, then crashes (this crashes the editor too). */
Fatal,
};
////////////////////////////////////////////////////////////
//
// UlxFormatDataLibrary
@@ -51,13 +97,24 @@ class UlxFormatDataLibrary : public UEditorSubsystem
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
// Get all converter functions (functions with an
// "AutoConvertedValue" parameter) found across all
// loaded classes.
// Given a pin type, find a converter function that can turn
// that type into an FFormatArgumentData. If AllowWild is true,
// unconnected wildcard pins will use the Blank converter.
// Returns nullptr if no converter is found.
//
const TArray<UFunction*>& GetConverters() const { return Converters; }
static UFunction* GetConverterForPinType(const UEdGraphSchema_K2 *Schema, const FEdGraphPinType& PinType, bool AllowWild);
// Format a message using FTextFormatter::Format, and send
// it to UE_LOG. The Context object's name is used as the
// log category. Meant to be used internally by the Format
// Log Message K2Node.
//
UFUNCTION(BlueprintCallable, meta=(WorldContext = "Context", BlueprintInternalUseOnly = "true"))
static void FormatLogMessageInternal(UObject *Context, ElxFormatLogVerbosity Verbosity, const FString &InPattern, TArray<FFormatArgumentData> InArgs);
private:
static ELogVerbosity::Type ConvertElxFormatLogVerbosity(ElxFormatLogVerbosity Verbosity);
// Scan all loaded classes for converter functions.
//
void ScanForConverters();

View File

@@ -2,7 +2,6 @@
#include "FormatMessage.h"
#include "Editor.h"
#include "Internationalization/TextFormatter.h"
#include "BlueprintActionDatabaseRegistrar.h"
@@ -44,136 +43,75 @@
// All argument pins will have Names that start with "A:"
static const FName VerbosityPinName(TEXT("Verbosity"));
static bool IsVerbosityPin(const UEdGraphPin *Pin) {
return (Pin->PinName == VerbosityPinName);
const FName UK2Node_FormatMessage::VerbosityPinName(TEXT("Verbosity"));
const FName UK2Node_FormatMessage::FormatPinName(TEXT("Format"));
const FName UK2Node_FormatMessage::ResultPinName(TEXT("Result"));
void UK2Node_FormatMessage::ReconstructNode()
{
// Save the value of the Format Pin before it gets reconstructed.
UEdGraphPin* FormatPin = FindPin(FormatPinName);
if (FormatPin != nullptr)
{
FormatPattern = FormatPin->DefaultValue;
}
static const FName FormatPinName(TEXT("Format"));
static bool IsFormatPin(const UEdGraphPin *Pin) {
return (Pin->PinName == FormatPinName);
Super::ReconstructNode();
}
static const FName ResultPinName(TEXT("Result"));
static bool IsResultPin(const UEdGraphPin *Pin) {
return (Pin->PinName == ResultPinName);
}
void UK2Node_FormatMessage::AllocateDefaultPins()
{
Pins.Reset();
Super::AllocateDefaultPins();
CreateCorrectPins();
}
void UK2Node_FormatMessage::CreateCorrectPins()
{
if (FindPin(UEdGraphSchema_K2::PN_Execute) == nullptr) {
// Parse the format pattern to extract argument names.
// Note that we use the saved value of the format pattern,
// because the format pin itself has been deleted at this point.
TArray<FString> PinNames;
FText::GetFormatPatternParameters(FText::FromString(FormatPattern), PinNames);
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(FormatPinName, EGPD_Input) == nullptr) {
UEdGraphPin *P = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_String, FormatPinName);
P->DefaultValue = TEXT("Message");
}
UEdGraphPin *FormatPin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_String, FormatPinName);
FormatPin->DefaultValue = FormatPattern;
// If this is a FormatMessage node, create a pin to output the result as text.
//
if (!IsFormatErrorMessage())
{
if (FindPin(ResultPinName, EGPD_Output) == nullptr) {
CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Text, ResultPinName);
}
}
// If this is a FormatErrorMessage node, create pins that control the log verbosity
//
if (IsFormatErrorMessage())
{
if (FindPin(VerbosityPinName, EGPD_Input) == nullptr) {
UEdGraphPin *P = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Byte, StaticEnum<ElxFormatLogVerbosity>(), VerbosityPinName);
P->DefaultValue = TEXT("Error");
P->AutogeneratedDefaultValue = P->DefaultValue;
}
}
// Transfer all Existing Argument pins to the Old Pins Map.
//
TMap<FString, UEdGraphPin *> OldPins;
for (auto It = Pins.CreateIterator(); It; ++It)
{
UEdGraphPin* CheckPin = *It;
if (HasPrefix(CheckPin->PinName, 'A'))
{
OldPins.Add(RemovePrefix(CheckPin->PinName).ToString(), CheckPin);
It.RemoveCurrent();
}
}
// Create Argument pins in the correct order, reusing old pins where possible.
//
// Create argument pins.
for (const FString& Name : PinNames)
{
UEdGraphPin **OldPin = OldPins.Find(Name);
if (OldPin != nullptr) {
Pins.Emplace(*OldPin);
OldPins.Remove(Name);
} else {
FName PrefixedName = AddPrefix(Name, 'A');
CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Wildcard, PrefixedName);
CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Wildcard, AddPrefix(Name, 'A'));
}
}
// Delete any unused pins.
//
for (auto &iter : OldPins)
{
iter.Value->Modify();
iter.Value->MarkAsGarbage();
}
OldPins.Empty();
}
void UK2Node_FormatMessage::SynchronizeArgumentPinType(UEdGraphPin* Pin)
{
if (HasPrefix(Pin->PinName, 'A'))
{
const UEdGraphSchema_K2* K2Schema = Cast<const UEdGraphSchema_K2>(GetSchema());
bool bPinTypeChanged = false;
if (Pin->LinkedTo.Num() == 0)
FEdGraphPinType DesiredType;
DesiredType.PinCategory = UEdGraphSchema_K2::PC_Wildcard;
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* ArgumentSourcePin = Pin->LinkedTo[0];
// Take the type of the connected pin
if (Pin->PinType != ArgumentSourcePin->PinType)
{
Pin->PinType = ArgumentSourcePin->PinType;
bPinTypeChanged = true;
}
DesiredType = Pin->LinkedTo[0]->PinType;
}
if (bPinTypeChanged)
if (Pin->PinType != DesiredType)
{
// Let the graph know to refresh
Pin->PinType = DesiredType;
GetGraph()->NotifyNodeChanged(this);
UBlueprint* Blueprint = GetBlueprint();
if (!Blueprint->bBeingCompiled)
{
@@ -204,7 +142,7 @@ FText UK2Node_FormatMessage::GetPinDisplayName(const UEdGraphPin* Pin) const
}
// Many pins can go unlabeled if they have default values.
if (IsFormatPin(Pin) || IsVerbosityPin(Pin))
if ((Pin->PinName == FormatPinName) || (Pin->PinName == VerbosityPinName))
{
if (Pin->LinkedTo.Num() == 0)
{
@@ -212,142 +150,103 @@ FText UK2Node_FormatMessage::GetPinDisplayName(const UEdGraphPin* Pin) const
}
}
// For argument pins, we must strip off the Argument Pin Prefix.
if (HasPrefix(Pin->PinName, 'A')) {
// Just return the pin name, stripping prefix if present.
return FText::FromName(RemovePrefix(Pin->PinName));
}
// Otherwise, just return the Pin Name the normal way.
return FText::FromName(Pin->PinName);
}
void UK2Node_FormatMessage::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent)
{
const FName PropertyName = (PropertyChangedEvent.Property ? PropertyChangedEvent.Property->GetFName() : NAME_None);
if (PropertyName == GET_MEMBER_NAME_CHECKED(UK2Node_FormatMessage, PinNames))
{
ReconstructNode();
}
Super::PostEditChangeProperty(PropertyChangedEvent);
GetGraph()->NotifyNodeChanged(this);
}
void UK2Node_FormatMessage::PinConnectionListChanged(UEdGraphPin* Pin)
{
Modify();
Super::PinConnectionListChanged(Pin);
SynchronizeArgumentPinType(Pin);
}
void UK2Node_FormatMessage::PinDefaultValueChanged(UEdGraphPin* Pin)
{
if(IsFormatPin(Pin))
if ((Pin->PinName == FormatPinName) && (Pin->DefaultValue != FormatPattern))
{
PinNames.Empty();
FText::GetFormatPatternParameters(FText::FromString(Pin->DefaultValue), PinNames);
CreateCorrectPins();
GetGraph()->NotifyNodeChanged(this);
ReconstructNode();
}
}
void UK2Node_FormatMessage::PinTypeChanged(UEdGraphPin* Pin)
{
SynchronizeArgumentPinType(Pin);
Super::PinTypeChanged(Pin);
}
FText UK2Node_FormatMessage::GetTooltipText() const
{
return NodeTooltip;
if (IsFormatErrorMessage())
{
static FText Tooltip = LOCTEXT("LogTooltip",
"Output an error, warning, or informational message to the log file.\n"
"\n"
" \u2022 Use {ArgName} to denote format arguments, giving each argument a different ArgName.\n"
"\n"
"It is often desirable to use this in conjunction with a separate utility that\n"
"pauses the execution of the blueprint whenever an error is logged.");
return Tooltip;
}
else
{
static FText Tooltip = LOCTEXT("FormatTooltip",
"Format a message, and output it as Text.\n"
"\n"
" \u2022 Use {ArgName} to denote format arguments, giving each argument a different ArgName.\n"
"\n");
return Tooltip;
}
}
void UK2Node_FormatMessage::PostReconstructNode()
{
Super::PostReconstructNode();
UEdGraph* OuterGraph = GetGraph();
if (!IsTemplate() && OuterGraph && OuterGraph->Schema) {
for (UEdGraphPin* CurrentPin : Pins)
if (IsTemplate() || (GetGraph() == nullptr)) return;
for (UEdGraphPin* Pin : Pins)
{
// Potentially update an argument pin type
SynchronizeArgumentPinType(CurrentPin);
}
SynchronizeArgumentPinType(Pin);
}
}
// Get a function that can convert the specified type into a FFormatArgumentData.
//
// For example:
// * if you pass in a pin of type 'String', it will return UlxFormatDataLibrary::FormatArgumentDataString
// * if you pass in a pin of type 'Vector', it will return UlxFormatDataLibrary::FormatArgumentDataVector
// and so forth.
//
UFunction *ToFormatArgumentData(const UEdGraphSchema_K2 *Schema, const FEdGraphPinType& PinType, bool AllowWild)
UEdGraphPin* UK2Node_FormatMessage::ExpandArgumentPin(UEdGraphPin* ArgumentPin, FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph)
{
// Special case. Wildcard Pins are unconnected pins.
//
if (PinType.PinCategory == UEdGraphSchema_K2::PC_Wildcard && AllowWild)
UFunction *Converter = UlxFormatDataLibrary::GetConverterForPinType(CompilerContext.GetSchema(), ArgumentPin->PinType, true);
if (Converter == nullptr)
{
return UlxFormatDataLibrary::StaticClass()->FindFunctionByName(GET_MEMBER_NAME_CHECKED(UlxFormatDataLibrary, FormatArgumentDataBlank));
}
// Scan the cached converter list for a matching type.
//
UlxFormatDataLibrary* FormatDataLib = GEditor->GetEditorSubsystem<UlxFormatDataLibrary>();
for (UFunction* Function : FormatDataLib->GetConverters())
{
FProperty* ValueProperty = Function->FindPropertyByName(TEXT("AutoConvertedValue"));
FEdGraphPinType ValuePinType;
bool Convertible = Schema->ConvertPropertyToPinType(ValueProperty, ValuePinType);
if (!Convertible) continue;
if (!Schema->ArePinTypesEquivalent(PinType, ValuePinType)) continue;
return Function;
}
// A general handler for Enums. You can override this for specific enums by
// putting that particular enum into any class with an AutoConvertedValue function.
//
if ((PinType.PinCategory == UEdGraphSchema_K2::PC_Byte) && (nullptr != Cast<const UEnum>(PinType.PinSubCategoryObject)))
{
return UlxFormatDataLibrary::StaticClass()->FindFunctionByName(GET_MEMBER_NAME_CHECKED(UlxFormatDataLibrary, FormatArgumentDataEnum));
}
// A case for subclasses of 'Object' which are not exactly 'Object'
//
if (PinType.PinCategory == UEdGraphSchema_K2::PC_Object)
{
return UlxFormatDataLibrary::StaticClass()->FindFunctionByName(GET_MEMBER_NAME_CHECKED(UlxFormatDataLibrary, FormatArgumentDataObject));
}
// We don't have a match.
//
CompilerContext.MessageLog.Error(TEXT("Cannot convert Pin to a Format Argument"));
return nullptr;
};
}
FName OriginalName = RemovePrefix(ArgumentPin->PinName);
UK2Node_CallFunction* ConvertNode = MakeCallFunctionNode(CompilerContext, SourceGraph, Converter);
ConvertNode->GetSchema()->TrySetDefaultValue(*ConvertNode->FindPinChecked(TEXT("Name")), OriginalName.ToString());
UEdGraphPin *ValuePin = ConvertNode->FindPin(TEXT("AutoConvertedValue"));
if (ValuePin != nullptr)
{
CompilerContext.MovePinLinksToIntermediate(*ArgumentPin, *ValuePin);
}
UEdGraphPin *SubCategoryObjectPin = ConvertNode->FindPin(TEXT("PinSubCategoryObject"));
if (SubCategoryObjectPin != nullptr)
{
SubCategoryObjectPin->DefaultObject = Cast<UObject>(ArgumentPin->PinType.PinSubCategoryObject);
}
return ConvertNode->GetReturnValuePin();
}
void UK2Node_FormatMessage::ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph)
{
Super::ExpandNode(CompilerContext, SourceGraph);
/**
At the end of this, the UK2Node_FormatMessage will not be a part of the Blueprint, it merely handles connecting
the other nodes into the Blueprint.
*/
const UEdGraphSchema_K2* CCSchema = CompilerContext.GetSchema();
const UEdGraphSchema_K2* Schema = CompilerContext.GetSchema();
// Create a "Make Array" node to compile the list of arguments into an array for the Format function being called
UK2Node_MakeArray* MakeArrayNode = CompilerContext.SpawnIntermediateNode<UK2Node_MakeArray>(this, SourceGraph);
MakeArrayNode->AllocateDefaultPins();
CompilerContext.MessageLog.NotifyIntermediateObjectCreation(MakeArrayNode, this);
// Parse the format pattern to extract argument names.
TArray<FString> PinNames;
FText::GetFormatPatternParameters(FText::FromString(FormatPattern), PinNames);
// Decide which formatting function we're going to call.
UFunction *FormatFunction;
if (IsFormatErrorMessage())
{
FormatFunction = UK2Node_FormatMessage::StaticClass()->FindFunctionByName(GET_MEMBER_NAME_CHECKED(UK2Node_FormatMessage, FormatLogMessageInternal));
FormatFunction = UlxFormatDataLibrary::StaticClass()->FindFunctionByName(GET_MEMBER_NAME_CHECKED(UlxFormatDataLibrary, FormatLogMessageInternal));
}
else
{
@@ -355,69 +254,27 @@ void UK2Node_FormatMessage::ExpandNode(class FKismetCompilerContext& CompilerCon
}
// This is the node that does all the Format work and outputs the message.
UK2Node_CallFunction* CallFormatFunction = CompilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, SourceGraph);
CallFormatFunction->SetFromFunction(FormatFunction);
CallFormatFunction->AllocateDefaultPins();
CompilerContext.MessageLog.NotifyIntermediateObjectCreation(CallFormatFunction, this);
UK2Node_CallFunction* CallFormatFunction = MakeCallFunctionNode(CompilerContext, SourceGraph, FormatFunction);
// Connect the output of the "Make Array" pin to the function's "InArgs" pin
// Create a "Make Array" node to compile the list of arguments into an array for the Format function being called.
UK2Node_MakeArray* MakeArrayNode = CompilerContext.SpawnIntermediateNode<UK2Node_MakeArray>(this, SourceGraph);
MakeArrayNode->NumInputs = PinNames.Num();
MakeArrayNode->AllocateDefaultPins();
CompilerContext.MessageLog.NotifyIntermediateObjectCreation(MakeArrayNode, this);
UEdGraphPin* ArrayOut = MakeArrayNode->GetOutputPin();
ArrayOut->MakeLinkTo(CallFormatFunction->FindPinChecked(TEXT("InArgs")));
// This will set the "Make Array" node's type, only works if one pin is connected.
MakeArrayNode->PinConnectionListChanged(ArrayOut);
// For each argument, we will need to add in a "Make Struct" node.
// For each argument, expand it into a converter node and link to the array.
for (int32 ArgIdx = 0; ArgIdx < PinNames.Num(); ++ArgIdx)
{
FString OriginalName = PinNames[ArgIdx];
UEdGraphPin* ArgumentPin = FindPin(AddPrefix(OriginalName, 'A'), EGPD_Input);
// Find a function that can convert the input into an FFormatArgumentData.
UFunction *Converter = ToFormatArgumentData(Schema, ArgumentPin->PinType, true);
if (Converter == nullptr)
{
CompilerContext.MessageLog.Error(TEXT("Cannot convert Pin to a Format Argument"));
continue;
}
// Add a node to call the converter.
UK2Node_CallFunction* ConvertNode = CompilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, SourceGraph);
ConvertNode->SetFromFunction(Converter);
ConvertNode->AllocateDefaultPins();
CompilerContext.MessageLog.NotifyIntermediateObjectCreation(ConvertNode, this);
UEdGraphPin *ValuePin = ConvertNode->FindPin(TEXT("AutoConvertedValue"));
UEdGraphPin *NamePin = ConvertNode->FindPinChecked(TEXT("Name"));
UEdGraphPin *SubCategoryObjectPin = ConvertNode->FindPin(TEXT("PinSubCategoryObject"));
// Set a value for the 'Name' pin of the converter.
ConvertNode->GetSchema()->TrySetDefaultValue(*NamePin, OriginalName);
// Connect the Value pin of the converter, if any.
if (ValuePin != nullptr)
{
CompilerContext.MovePinLinksToIntermediate(*ArgumentPin, *ValuePin);
}
// If the converter wants to know the PinSubCategoryObject, pass it in.
if (SubCategoryObjectPin != nullptr)
{
SubCategoryObjectPin->DefaultObject = Cast<UObject>(ArgumentPin->PinType.PinSubCategoryObject);
}
// The "Make Array" node already has one pin available, so don't create one for ArgIdx == 0
if(ArgIdx > 0)
{
MakeArrayNode->AddInputPin();
}
// Find the input pin on the "Make Array" node by index.
UEdGraphPin* ArgumentPin = FindPin(AddPrefix(PinNames[ArgIdx], 'A'), EGPD_Input);
UEdGraphPin* ConvertedPin = ExpandArgumentPin(ArgumentPin, CompilerContext, SourceGraph);
if (ConvertedPin == nullptr) continue;
const FString PinName = FString::Printf(TEXT("[%d]"), ArgIdx);
UEdGraphPin* InputPin = MakeArrayNode->FindPinChecked(PinName);
// Find the output for the pin's "Make Struct" node and link it to the corresponding pin on the "Make Array" node.
ConvertNode->GetReturnValuePin()->MakeLinkTo(InputPin);
ConvertedPin->MakeLinkTo(MakeArrayNode->FindPinChecked(PinName));
}
// Connect up other pins to the Formatting node.
@@ -441,65 +298,30 @@ void UK2Node_FormatMessage::ExpandNode(class FKismetCompilerContext& CompilerCon
UK2Node::ERedirectType UK2Node_FormatMessage::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))
if (IsTemplate() || (GetGraph() == nullptr)) return ERedirectType_None;
if ((NewPin->PinName == OldPin->PinName) &&
(NewPin->Direction == OldPin->Direction) &&
((NewPin->PinType == OldPin->PinType) || (NewPin->PinType.PinCategory == UEdGraphSchema_K2::PC_Wildcard)))
{
// 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;
return 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;
return ERedirectType_None;
}
bool UK2Node_FormatMessage::IsConnectionDisallowed(const UEdGraphPin* MyPin, const UEdGraphPin* OtherPin, FString& OutReason) const
{
// The following pins cannot be connected. They are meant to be hardwired constants.
if (IsFormatPin(MyPin))
if (MyPin->PinName == FormatPinName)
{
OutReason = LOCTEXT("Error_FormatStringMustBeHardwired", "Format string must be a hardwired constant.").ToString();
return true;
}
// Argument input pins may only be connected to Byte, Integer, Float, Text, and ETextGender pins...
// Argument input pins may only be connected to convertible types.
if (HasPrefix(MyPin->PinName, 'A'))
{
const UEdGraphSchema_K2* K2Schema = Cast<const UEdGraphSchema_K2>(GetSchema());
const FName& OtherPinCategory = OtherPin->PinType.PinCategory;
UFunction *Converter = ToFormatArgumentData(K2Schema, OtherPin->PinType, false);
UFunction *Converter = UlxFormatDataLibrary::GetConverterForPinType(K2Schema, OtherPin->PinType, false);
if (Converter == nullptr)
{
OutReason = LOCTEXT("Error_InvalidArgumentType", "Data cannot be converted to text.").ToString();
@@ -513,15 +335,7 @@ bool UK2Node_FormatMessage::IsConnectionDisallowed(const UEdGraphPin* MyPin, con
void UK2Node_FormatMessage::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());
@@ -536,94 +350,5 @@ FText UK2Node_FormatMessage::GetMenuCategory() const
return FEditorCategoryUtils::GetCommonCategory(FCommonEditorCategory::Text);
}
UK2Node_FormatMessage::UK2Node_FormatMessage(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
NodeTooltip = LOCTEXT("NodeTooltip",
"Format a message, and output it as Text.\n"
"\n"
" \u2022 Use {ArgName} to denote format arguments, giving each argument a different ArgName.\n"
"\n"
);
}
UK2Node_FormatLogMessage::UK2Node_FormatLogMessage(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
NodeTooltip = LOCTEXT("NodeTooltip",
"Output an error, warning, or informational message to the log file.\n"
"\n"
" \u2022 Use {ArgName} to denote format arguments, giving each argument a different ArgName.\n"
"\n"
"It is often desirable to use this in conjunction with a separate utility that\n"
"pauses the execution of the blueprint whenever an error is logged."
);
}
ELogVerbosity::Type UK2Node_FormatMessage::ConvertElxFormatLogVerbosity(ElxFormatLogVerbosity Verbosity) {
switch (Verbosity) {
case ElxFormatLogVerbosity::Error: return ELogVerbosity::Error;
case ElxFormatLogVerbosity::Warning: return ELogVerbosity::Warning;
case ElxFormatLogVerbosity::Display: return ELogVerbosity::Display;
case ElxFormatLogVerbosity::Log: return ELogVerbosity::Log;
case ElxFormatLogVerbosity::ThrottledDisplay: return ELogVerbosity::Display;
case ElxFormatLogVerbosity::ThrottledLog: return ELogVerbosity::Log;
case ElxFormatLogVerbosity::Verbose: return ELogVerbosity::Verbose;
case ElxFormatLogVerbosity::VeryVerbose: return ELogVerbosity::VeryVerbose;
case ElxFormatLogVerbosity::Fatal: return ELogVerbosity::Fatal;
}
}
void UK2Node_FormatMessage::FormatLogMessageInternal(UObject *Context, ElxFormatLogVerbosity Verbosity, const FString &InPattern, TArray<FFormatArgumentData> InArgs)
{
// For throttled verbosity levels, suppress repeated messages with the
// same format pattern. We key on the blueprint name + format pattern,
// and allow at most one message per second per key.
//
if (Verbosity == ElxFormatLogVerbosity::ThrottledDisplay || Verbosity == ElxFormatLogVerbosity::ThrottledLog)
{
static TMap<FString, double> LastLogTime;
double Now = FPlatformTime::Seconds();
FString Key = Context->GetClass()->GetName() + TEXT("::") + InPattern;
double &Last = LastLogTime.FindOrAdd(Key, 0.0);
if (Now - Last < 1.0)
{
return;
}
Last = Now;
}
// Generate the formatted string.
//
FText InPatternText(FText::FromString(InPattern));
FText Message = FTextFormatter::Format(MoveTemp(InPatternText), MoveTemp(InArgs), false, false);
FString MessageString = Message.ToString();
// Get the blueprint name.
//
// Normally, the log function expects you to pass in a filename, and a log
// category name. We use the blueprint name for both.
//
// Using the blueprint name as a log category name is not technically
// correct. However, there is no correct way to create log categories
// from inside of blueprints. Doing it this way at least produces a reasonable
// message inside the log. What doesn't work correctly is the log message
// suppression system. Ie, console commands like 'log <category> verbose'
// don't have any effect here. The design of the log message suppression
// system is such that there just is no reasonable way to hook into it from
// inside of blueprints.
//
FString BlueprintNameString = Context->GetClass()->GetName();
auto BlueprintNameAnsi = StringCast<ANSICHAR>(*BlueprintNameString);
FLogCategoryName BlueprintNameLogCategory(Context->GetClass()->GetFName());
// Output to Log
//
ELogVerbosity::Type VerbosityValue = ConvertElxFormatLogVerbosity(Verbosity);
if (VerbosityValue <= ELogVerbosity::COMPILED_IN_MINIMUM_VERBOSITY)
{
FMsg::Logf(BlueprintNameAnsi.Get(), 0, BlueprintNameLogCategory, VerbosityValue, TEXT("%s"), *MessageString);
}
}
#undef LOCTEXT_NAMESPACE

View File

@@ -19,52 +19,6 @@
#include "FormatMessage.generated.h"
////////////////////////////////////////////////////////////
//
// ElxFormatLogVerbosity
//
// Controls the ELogVerbosity of the UE_LOG directive inside
// FormatLogMessage. Also controls the throttling of log
// messages.
//
// Fatal is deliberately placed at the end so that the
// editor defaults to Error (value 0) when the dropdown is
// uninitialized. The numeric values don't match
// ELogVerbosity, so a conversion function is needed.
//
////////////////////////////////////////////////////////////
UENUM(BlueprintType)
enum class ElxFormatLogVerbosity : uint8 {
/** Prints an error to the console and log file. The editor collects and reports errors. */
Error,
/** Prints a warning to the console and log file. The editor collects and reports warnings. */
Warning,
/** Prints a message to the console and log file. */
Display,
/** Prints a message to the log file, but not to the console. */
Log,
/** Like Display, but suppresses repeated messages with the same format pattern (at most once per second). */
ThrottledDisplay,
/** Like Log, but suppresses repeated messages with the same format pattern (at most once per second). */
ThrottledLog,
/** Prints a message to the log file only if Verbose logging is enabled for the given category. */
Verbose,
/** Prints a message to the log file only if VeryVerbose logging is enabled. */
VeryVerbose,
/** Prints a fatal error to the console and log file, then crashes (this crashes the editor too). */
Fatal,
};
class FBlueprintActionDatabaseRegistrar;
class FString;
class UEdGraph;
@@ -79,25 +33,22 @@ class UObject;
UCLASS(MinimalAPI)
class UK2Node_FormatMessage : public UlxK2Node
{
GENERATED_UCLASS_BODY()
//~ Begin UObject Interface
virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override;
//~ End UObject Interface
GENERATED_BODY()
public:
//~ Begin UEdGraphNode Interface.
virtual void AllocateDefaultPins() override;
virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override;
virtual bool ShouldShowNodeProperties() const override { return true; }
virtual void PinConnectionListChanged(UEdGraphPin* Pin) override;
virtual void PinDefaultValueChanged(UEdGraphPin* Pin) override;
virtual void PinTypeChanged(UEdGraphPin* Pin) override;
virtual FText GetTooltipText() const override;
virtual FText GetPinDisplayName(const UEdGraphPin* Pin) const override;
//~ End UEdGraphNode Interface.
//~ Begin UK2Node Interface.
virtual bool IsNodePure() const override { return false; }
virtual void ReconstructNode() override;
virtual void PostReconstructNode() override;
virtual bool NodeCausesStructuralBlueprintChange() const override { return true; }
virtual void ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) override;
@@ -109,41 +60,38 @@ class UK2Node_FormatMessage : public UlxK2Node
//~ End UK2Node Interface.
protected:
// Create all necessary pins.
//
void CreateCorrectPins();
// Synchronize the type of the given argument pin
// with the type its connected to, or reset it to
// a wildcard pin if there's no connection.
//
void SynchronizeArgumentPinType(UEdGraphPin* Pin);
// During ExpandNode, expand a single argument pin into
// a converter node. Returns the converter's output pin.
//
UEdGraphPin* ExpandArgumentPin(UEdGraphPin* ArgumentPin, class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph);
// Derived class sets this to true, altering
// the behavior of this K2Node.
//
virtual bool IsFormatErrorMessage() const { return false; }
// When IsFormatErrorMessage is true, the K2Node
// macroexpands to call this function, which
// formats the message and outputs it to the log.
//
UFUNCTION(BlueprintCallable, meta=(WorldContext = "Context", BlueprintInternalUseOnly = "true"))
static void FormatLogMessageInternal(UObject *Context, ElxFormatLogVerbosity Verbosity, const FString &InPattern, TArray<FFormatArgumentData> InArgs);
private:
static ELogVerbosity::Type ConvertElxFormatLogVerbosity(ElxFormatLogVerbosity Verbosity);
static const FName VerbosityPinName;
static const FName FormatPinName;
static const FName ResultPinName;
protected:
// Argument names added to the node, generated as pins
// during construction.
// Whenever the format pin value changes, we call
// ReconstructNode, which backs up the value into this
// property. This cache is needed because during
// ReconstructNode, we blow away the format pin. The
// format pin is also absent when the node is first
// created.
//
UPROPERTY()
TArray<FString> PinNames;
FString FormatPattern = TEXT("Message");
// Tooltip text for this node.
//
FText NodeTooltip;
};
////////////////////////////////////////////////////////////
@@ -159,7 +107,7 @@ protected:
UCLASS(MinimalAPI)
class UK2Node_FormatLogMessage : public UK2Node_FormatMessage
{
GENERATED_UCLASS_BODY()
GENERATED_BODY()
virtual bool IsFormatErrorMessage() const override { return true; }
};

View File

@@ -51,17 +51,27 @@ FText UK2Node_LuaInvoke::GetTooltipText() const
return Tooltip;
}
void UK2Node_LuaInvoke::ReconstructNode()
{
// Save the value of the Prototype Pin before it gets reconstructed.
UEdGraphPin* FunctionPin = FindPin(FunctionPinName);
if (FunctionPin != nullptr)
{
LuaFunctionPrototype = FunctionPin->DefaultValue;
}
Super::ReconstructNode();
}
void UK2Node_LuaInvoke::AllocateDefaultPins()
{
Pins.Reset();
Super::AllocateDefaultPins();
if (LuaFunctionPrototype.IsEmpty())
{
LuaFunctionPrototype = TEXT("class.func(int arg1, int arg2) : int ret1, int ret2");
}
// Parse the lua function prototype.
// Note that we use the saved value of the function
// prototype, because the function prototype pin
// has been deleted at this point.
FlxParsedProto ParsedProto(LuaFunctionPrototype);
if (!ParsedProto.ErrorMessage.IsEmpty())
{
@@ -142,7 +152,6 @@ void UK2Node_LuaInvoke::PinDefaultValueChanged(UEdGraphPin* Pin)
{
if ((Pin->PinName == FunctionPinName) && (Pin->DefaultValue != LuaFunctionPrototype))
{
LuaFunctionPrototype = Pin->DefaultValue;
ReconstructNode();
}
}
@@ -246,6 +255,7 @@ void UK2Node_LuaInvoke::ExpandNode(class FKismetCompilerContext& CompilerContext
UK2Node::ERedirectType UK2Node_LuaInvoke::DoPinsMatchForReconstruction(const UEdGraphPin* NewPin, int32 NewPinIndex, const UEdGraphPin* OldPin, int32 OldPinIndex) const
{
if (IsTemplate() || (GetGraph() == nullptr)) return ERedirectType_None;
if ((NewPin->PinName == OldPin->PinName) &&
(NewPin->Direction == OldPin->Direction) &&
(NewPin->PinType == OldPin->PinType))

View File

@@ -31,6 +31,7 @@ public:
//~ Begin UK2Node Interface.
virtual bool IsNodePure() const override { return false; }
virtual void ReconstructNode() override;
virtual bool NodeCausesStructuralBlueprintChange() const override { return true; }
virtual void ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) override;
virtual ERedirectType DoPinsMatchForReconstruction(const UEdGraphPin* NewPin, int32 NewPinIndex, const UEdGraphPin* OldPin, int32 OldPinIndex) const override;
@@ -51,9 +52,15 @@ private:
static const FName ErrorPinName;
private:
/** The lua function prototype, which must be saved as a property **/
// Whenever the function value changes, we call
// ReconstructNode, which backs up the value into this
// property. This cache is needed because during
// ReconstructNode, we blow away the function pin. The
// function pin is also absent when the node is first
// created.
//
UPROPERTY()
FString LuaFunctionPrototype;
FString LuaFunctionPrototype = TEXT("class.func(int arg1, int arg2) : int ret1, int ret2");
};