diff --git a/Integration.code-workspace.tpl.json b/Integration.code-workspace.tpl.json index 971612df..413eb5b1 100644 --- a/Integration.code-workspace.tpl.json +++ b/Integration.code-workspace.tpl.json @@ -21,6 +21,7 @@ } ], "settings": { + "files.autoSave": "afterDelay", "typescript.tsc.autoDetect": "off", "lldb.dereferencePointers": false, "npm.autoDetect": "off", diff --git a/Source/Integration/FormatDataLibrary.cpp b/Source/Integration/FormatDataLibrary.cpp index fd32a9a4..dd4f1d1c 100644 --- a/Source/Integration/FormatDataLibrary.cpp +++ b/Source/Integration/FormatDataLibrary.cpp @@ -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(); + 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(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 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 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 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(*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); + } +} diff --git a/Source/Integration/FormatDataLibrary.h b/Source/Integration/FormatDataLibrary.h index 496df5ce..4aaa9863 100644 --- a/Source/Integration/FormatDataLibrary.h +++ b/Source/Integration/FormatDataLibrary.h @@ -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& 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 InArgs); private: + static ELogVerbosity::Type ConvertElxFormatLogVerbosity(ElxFormatLogVerbosity Verbosity); + // Scan all loaded classes for converter functions. // void ScanForConverters(); diff --git a/Source/Integration/FormatMessage.cpp b/Source/Integration/FormatMessage.cpp index 71dfc6c9..a672bfc3 100644 --- a/Source/Integration/FormatMessage.cpp +++ b/Source/Integration/FormatMessage.cpp @@ -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")); -static const FName FormatPinName(TEXT("Format")); -static bool IsFormatPin(const UEdGraphPin *Pin) { - return (Pin->PinName == FormatPinName); -} -static const FName ResultPinName(TEXT("Result")); -static bool IsResultPin(const UEdGraphPin *Pin) { - return (Pin->PinName == ResultPinName); -} +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; + } + Super::ReconstructNode(); +} void UK2Node_FormatMessage::AllocateDefaultPins() { + Pins.Reset(); Super::AllocateDefaultPins(); - CreateCorrectPins(); -} -void UK2Node_FormatMessage::CreateCorrectPins() -{ - if (FindPin(UEdGraphSchema_K2::PN_Execute) == nullptr) { - CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Execute); - } + // 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 PinNames; + FText::GetFormatPatternParameters(FText::FromString(FormatPattern), PinNames); - if (FindPin(UEdGraphSchema_K2::PN_Then) == nullptr) { - CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Then); - } + CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Execute); + 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); - } + 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(), VerbosityPinName); - P->DefaultValue = TEXT("Error"); - P->AutogeneratedDefaultValue = P->DefaultValue; - } + UEdGraphPin *P = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Byte, StaticEnum(), VerbosityPinName); + P->DefaultValue = TEXT("Error"); + P->AutogeneratedDefaultValue = P->DefaultValue; } - // Transfer all Existing Argument pins to the Old Pins Map. - // - TMap 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(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')) { - 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); + // Just return the pin name, stripping prefix if present. + return FText::FromName(RemovePrefix(Pin->PinName)); } 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) - { - // Potentially update an argument pin type - SynchronizeArgumentPinType(CurrentPin); - } + if (IsTemplate() || (GetGraph() == nullptr)) return; + for (UEdGraphPin* Pin : Pins) + { + 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)); + CompilerContext.MessageLog.Error(TEXT("Cannot convert Pin to a Format Argument")); + return nullptr; } - // Scan the cached converter list for a matching type. - // - UlxFormatDataLibrary* FormatDataLib = GEditor->GetEditorSubsystem(); - for (UFunction* Function : FormatDataLib->GetConverters()) + 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) { - 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; + CompilerContext.MovePinLinksToIntermediate(*ArgumentPin, *ValuePin); } - // 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(PinType.PinSubCategoryObject))) + UEdGraphPin *SubCategoryObjectPin = ConvertNode->FindPin(TEXT("PinSubCategoryObject")); + if (SubCategoryObjectPin != nullptr) { - return UlxFormatDataLibrary::StaticClass()->FindFunctionByName(GET_MEMBER_NAME_CHECKED(UlxFormatDataLibrary, FormatArgumentDataEnum)); + SubCategoryObjectPin->DefaultObject = Cast(ArgumentPin->PinType.PinSubCategoryObject); } - // 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. - // - return nullptr; -}; - - + 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(this, SourceGraph); - MakeArrayNode->AllocateDefaultPins(); - CompilerContext.MessageLog.NotifyIntermediateObjectCreation(MakeArrayNode, this); + // Parse the format pattern to extract argument names. + TArray 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(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(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(int32 ArgIdx = 0; ArgIdx < PinNames.Num(); ++ArgIdx) + // 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(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(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(GetSchema()); - if( !K2Schema || K2Schema->IsSelfPin(*NewPin) || K2Schema->ArePinTypesCompatible(OldPin->PinType, NewPin->PinType) ) - { - RedirectType = ERedirectType_Name; - } - else - { - RedirectType = ERedirectType_None; - } - } + return ERedirectType_Name; } - else - { - // try looking for a redirect if it's a K2 node - if (UK2Node* Node = Cast(NewPin->GetOwningNode())) - { - // if you don't have matching pin, now check if there is any redirect param set - TArray 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(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 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 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 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(*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 diff --git a/Source/Integration/FormatMessage.h b/Source/Integration/FormatMessage.h index f18bbafe..d14a8ef3 100644 --- a/Source/Integration/FormatMessage.h +++ b/Source/Integration/FormatMessage.h @@ -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 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 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; } }; diff --git a/Source/Integration/LuaCallNode.cpp b/Source/Integration/LuaCallNode.cpp index 4d38854b..0cb2959d 100644 --- a/Source/Integration/LuaCallNode.cpp +++ b/Source/Integration/LuaCallNode.cpp @@ -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)) diff --git a/Source/Integration/LuaCallNode.h b/Source/Integration/LuaCallNode.h index f8b9c289..6f232ca7 100644 --- a/Source/Integration/LuaCallNode.h +++ b/Source/Integration/LuaCallNode.h @@ -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"); };