// Copyright Epic Games, Inc. All Rights Reserved. #include "FormatMessage.h" #include "Internationalization/TextFormatter.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 "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 "FormatMessage" // All argument pins will have 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); } static const FName VerbosityPinName(TEXT("Verbosity")); static bool IsVerbosityPin(const UEdGraphPin *Pin) { return (Pin->PinName == VerbosityPinName); } 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::AllocateDefaultPins() { 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); } 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"); } // 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(), 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 (IsArgumentPin(CheckPin)) { OldPins.Add(ArgumentNameRemovePrefix(CheckPin->PinName), CheckPin); It.RemoveCurrent(); } } // Create Argument pins in the correct order, reusing old pins where possible. // for (const FString& Name : PinNames) { UEdGraphPin **OldPin = OldPins.Find(Name); if (OldPin != nullptr) { Pins.Emplace(*OldPin); OldPins.Remove(Name); } else { FName PrefixedName = ArgumentNameAddPrefix(Name); CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Wildcard, PrefixedName); } } // Delete any unused pins. // for (auto &iter : OldPins) { iter.Value->Modify(); iter.Value->MarkAsGarbage(); } OldPins.Empty(); } void UK2Node_FormatMessage::SynchronizeArgumentPinType(UEdGraphPin* Pin) { if (IsArgumentPin(Pin)) { const UEdGraphSchema_K2* K2Schema = Cast(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* ArgumentSourcePin = Pin->LinkedTo[0]; // Take the type of the connected pin if (Pin->PinType != ArgumentSourcePin->PinType) { Pin->PinType = ArgumentSourcePin->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_FormatMessage::GetNodeTitle(ENodeTitleType::Type TitleType) const { if (IsFormatErrorMessage()) { return LOCTEXT("FormatErrorMessage_Title", "Format Log Message"); } else { return LOCTEXT("FormatMessage_Title", "Format Message"); } } FText UK2Node_FormatMessage::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 (IsFormatPin(Pin) || IsVerbosityPin(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)); } // 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(); SynchronizeArgumentPinType(Pin); } void UK2Node_FormatMessage::PinDefaultValueChanged(UEdGraphPin* Pin) { if(IsFormatPin(Pin)) { PinNames.Empty(); FText::GetFormatPatternParameters(FText::FromString(Pin->DefaultValue), PinNames); CreateCorrectPins(); GetGraph()->NotifyNodeChanged(this); } } void UK2Node_FormatMessage::PinTypeChanged(UEdGraphPin* Pin) { SynchronizeArgumentPinType(Pin); Super::PinTypeChanged(Pin); } FText UK2Node_FormatMessage::GetTooltipText() const { return NodeTooltip; } 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); } } } // 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) { // 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)); } // Try to find a match in the UlxFormatDataLibrary. // for (auto It = TFieldIterator(UlxFormatDataLibrary::StaticClass()); It; ++It) { UFunction* Function = *It; 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 the UlxFormatDataLibrary. // 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)); } // We don't have a match. // return nullptr; }; 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* 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); // 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)); } else { FormatFunction = UKismetTextLibrary::StaticClass()->FindFunctionByName(GET_MEMBER_NAME_CHECKED(UKismetTextLibrary, Format)); } // 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); // Connect the output of the "Make Array" pin to the function's "InArgs" pin 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) { FString OriginalName = PinNames[ArgIdx]; UEdGraphPin* ArgumentPin = FindPin(ArgumentNameAddPrefix(OriginalName), 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. 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); } // Connect up other pins to the Formatting node. CompilerContext.MovePinLinksToIntermediate(*FindPinChecked(FormatPinName), *CallFormatFunction->FindPinChecked(TEXT("InPattern"))); if (IsFormatErrorMessage()) { CompilerContext.MovePinLinksToIntermediate(*FindPinChecked(VerbosityPinName), *CallFormatFunction->FindPinChecked(TEXT("Verbosity"))); } else { CompilerContext.MovePinLinksToIntermediate(*FindPinChecked(ResultPinName), *CallFormatFunction->GetReturnValuePin()); } // Link up the Exec pins. CompilerContext.MovePinLinksToIntermediate(*GetExecPin(), *CallFormatFunction->GetExecPin()); CompilerContext.MovePinLinksToIntermediate(*GetThenPin(), *CallFormatFunction->GetThenPin()); BreakAllNodeLinks(); } 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)) { // 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; } } } 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; } 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)) { 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... if (IsArgumentPin(MyPin)) { const UEdGraphSchema_K2* K2Schema = Cast(GetSchema()); const FName& OtherPinCategory = OtherPin->PinType.PinCategory; UFunction *Converter = ToFormatArgumentData(K2Schema, OtherPin->PinType, false); if (Converter == nullptr) { OutReason = LOCTEXT("Error_InvalidArgumentType", "Data cannot be converted to text.").ToString(); return true; } } return Super::IsConnectionDisallowed(MyPin, OtherPin, OutReason); } 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()); check(NodeSpawner != nullptr); ActionRegistrar.AddBlueprintAction(ActionKey, NodeSpawner); } } 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." ); } void UK2Node_FormatMessage::FormatLogMessageInternal(UObject *Context, ElxLogVerbosity 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 == ElxLogVerbosity::ThrottledDisplay || Verbosity == ElxLogVerbosity::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 = UlxBlueprintErrorLibrary::ConvertElxLogVerbosity(Verbosity); if (VerbosityValue <= ELogVerbosity::COMPILED_IN_MINIMUM_VERBOSITY) { FMsg::Logf(BlueprintNameAnsi.Get(), 0, BlueprintNameLogCategory, VerbosityValue, TEXT("%s"), *MessageString); } } #undef LOCTEXT_NAMESPACE