// Copyright Epic Games, Inc. All Rights Reserved. #include "Kismet2/KismetDebugUtilities.h" #include "Engine/BlueprintGeneratedClass.h" #include "GameFramework/Actor.h" #include "UObject/PropertyPortFlags.h" #include "UObject/TextProperty.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/SWidget.h" #include "Layout/WidgetPath.h" #include "Framework/Application/MenuStack.h" #include "Framework/Application/SlateApplication.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Text/SMultiLineEditableText.h" #include "Widgets/Layout/SScrollBox.h" #include "Styling/AppStyle.h" #include "Engine/Blueprint.h" #include "EdGraph/EdGraph.h" #include "Editor/UnrealEdEngine.h" #include "Settings/EditorExperimentalSettings.h" #include "CallStackViewer.h" #include "WatchPointViewer.h" #include "Animation/AnimBlueprintGeneratedClass.h" #include "UnrealEdGlobals.h" #include "Kismet2/Breakpoint.h" #include "Kismet2/WatchedPin.h" #include "ActorEditorUtils.h" #include "EdGraphSchema_K2.h" #include "K2Node.h" #include "K2Node_Tunnel.h" #include "K2Node_FunctionEntry.h" #include "K2Node_CallFunction.h" #include "K2Node_Knot.h" #include "K2Node_MacroInstance.h" #include "K2Node_Composite.h" #include "K2Node_Message.h" #include "Kismet2/KismetEditorUtilities.h" #include "Kismet2/BlueprintEditorUtils.h" #include "Logging/TokenizedMessage.h" #include "Logging/MessageLog.h" #include "Misc/UObjectToken.h" #include "AnimGraphNode_Base.h" #include "UObject/UnrealType.h" #include "AnimationGraphSchema.h" #include "BlueprintEditorSettings.h" #include "Blueprint/BlueprintExceptionInfo.h" #define LOCTEXT_NAMESPACE "BlueprintDebugging" enum class EKismetDebuggingMode : uint8 { None, Engine, // for Blutility World, // for PIE }; /** Per-thread data for use by FKismetDebugUtilities functions */ class FKismetDebugUtilitiesData : public TThreadSingleton { public: FKismetDebugUtilitiesData() : TargetGraphNodes() , CurrentInstructionPointer(nullptr) , MostRecentBreakpointInstructionPointer(nullptr) , MostRecentStoppedNode(nullptr) , CurrentDebuggingWorld(nullptr) , TargetGraphStackDepth(INDEX_NONE) , MostRecentBreakpointGraphStackDepth(INDEX_NONE) , MostRecentBreakpointInstructionOffset(INDEX_NONE) , StackFrameAtIntraframeDebugging(nullptr) , TraceStackSamples(FKismetDebugUtilities::MAX_TRACE_STACK_SAMPLES) , CurrentDebuggingMode(EKismetDebuggingMode::None) , bIsSingleStepping(false) , bIsSteppingOut(false) { } void Reset() { TargetGraphNodes.Empty(); CurrentInstructionPointer = nullptr; MostRecentStoppedNode = nullptr; CurrentDebuggingWorld = nullptr; TargetGraphStackDepth = INDEX_NONE; MostRecentBreakpointGraphStackDepth = INDEX_NONE; MostRecentBreakpointInstructionOffset = INDEX_NONE; StackFrameAtIntraframeDebugging = nullptr; CurrentDebuggingMode = EKismetDebuggingMode::None; bIsSingleStepping = false; bIsSteppingOut = false; } // List of graph nodes that the user wants to stop at, at the current TargetGraphStackDepth. Used for Step Over: TArray< TWeakObjectPtr< class UEdGraphNode> > TargetGraphNodes; // Current node: TWeakObjectPtr< class UEdGraphNode > CurrentInstructionPointer; // The current instruction encountered if we are stopped at a breakpoint; NULL otherwise TWeakObjectPtr< class UEdGraphNode > MostRecentBreakpointInstructionPointer; // The last node that we decided to break on for any reason (e.g. breakpoint, exception, or step operation): TWeakObjectPtr< class UEdGraphNode > MostRecentStoppedNode; // The PlayWorld that generated TWeakObjectPtr CurrentDebuggingWorld; // The target graph call stack depth. INDEX_NONE if not active int32 TargetGraphStackDepth; // The graph stack depth that a breakpoint was hit at, used to ensure that breakpoints // can be hit multiple times in the case of recursion int32 MostRecentBreakpointGraphStackDepth; // The instruction that we hit a breakpoint at, this is used to ensure that a given node // can be stepped over reliably (but still break multiple times in the case of recursion): int32 MostRecentBreakpointInstructionOffset; // The last message that an exception delivered FText LastExceptionMessage; // Only valid inside intraframe debugging const FFrame* StackFrameAtIntraframeDebugging; // This data is used for the 'marching ants' display in the blueprint editor TSimpleRingBuffer TraceStackSamples; // The type of current debugging EKismetDebuggingMode CurrentDebuggingMode; // This flag controls whether we're trying to 'step in' to a function bool bIsSingleStepping; // This flag controls whether we're trying to 'step out' of a graph bool bIsSteppingOut; }; ////////////////////////////////////////////////////////////////////////// // FKismetDebugUtilities void FKismetDebugUtilities::EndOfScriptExecution(const FBlueprintContextTracker& BlueprintContext) { if(BlueprintContext.GetScriptEntryTag() == 1) { // if this is our last VM frame, then clear stepping data: FKismetDebugUtilitiesData& Data = FKismetDebugUtilitiesData::Get(); Data.Reset(); } } void FKismetDebugUtilities::RequestAbortingExecution() { check(IsInGameThread()); FKismetDebugUtilitiesData& Data = FKismetDebugUtilitiesData::Get(); if (Data.StackFrameAtIntraframeDebugging) { const_cast(Data.StackFrameAtIntraframeDebugging)->bAbortingExecution = true; } } void FKismetDebugUtilities::RequestSingleStepIn() { FKismetDebugUtilitiesData& Data = FKismetDebugUtilitiesData::Get(); Data.bIsSingleStepping = true; } void FKismetDebugUtilities::RequestStepOver() { FKismetDebugUtilitiesData& Data = FKismetDebugUtilitiesData::Get(); TArrayView ScriptStack = FBlueprintContextTracker::Get().GetCurrentScriptStack(); if (ScriptStack.Num() > 0) { Data.TargetGraphStackDepth = ScriptStack.Num(); if (const UEdGraphNode* StoppedNode = Data.MostRecentStoppedNode.Get()) { if (StoppedNode->IsA() || StoppedNode->IsA() || StoppedNode->IsA()) { for (const UEdGraphPin* Pin : StoppedNode->Pins) { // add any nodes connected via execs as TargetGraphNodes: if (Pin->Direction == EGPD_Output && Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec && Pin->LinkedTo.Num() > 0) { for (UEdGraphPin* LinkedTo : Pin->LinkedTo) { UEdGraphNode* GraphNode = LinkedTo->GetOwningNode(); if (UK2Node_Knot* Knot = Cast(GraphNode)) { // search the knot chain to find the actual node: GraphNode = Knot->GetExecTerminal(); } if (GraphNode) { Data.TargetGraphNodes.AddUnique(GraphNode); } } } } return; } } Data.bIsSingleStepping = false; Data.bIsSteppingOut = true; } } void FKismetDebugUtilities::RequestStepOut() { FKismetDebugUtilitiesData& Data = FKismetDebugUtilitiesData::Get(); TArrayView ScriptStack = FBlueprintContextTracker::Get().GetCurrentScriptStack(); Data.bIsSingleStepping = false; if (ScriptStack.Num() > 1) { Data.bIsSteppingOut = true; Data.TargetGraphStackDepth = ScriptStack.Num() - 1; } } void FKismetDebugUtilities::OnScriptException(const UObject* ActiveObject, const FFrame& StackFrame, const FBlueprintExceptionInfo& Info) { FKismetDebugUtilitiesData& Data = FKismetDebugUtilitiesData::Get(); struct Local { static void OnMessageLogLinkActivated(const class TSharedRef& Token) { if( Token->GetType() == EMessageToken::Object ) { const TSharedRef UObjectToken = StaticCastSharedRef(Token); if(UObjectToken->GetObject().IsValid()) { FKismetEditorUtilities::BringKismetToFocusAttentionOnObject(UObjectToken->GetObject().Get()); } } } }; checkSlow(ActiveObject != nullptr); const UClass* ClassContainingCode = FindClassForNode(ActiveObject, StackFrame.Node); UBlueprint* BlueprintObj = (ClassContainingCode ? Cast(ClassContainingCode->ClassGeneratedBy) : nullptr); if (BlueprintObj) { const FBlueprintExceptionInfo* ExceptionInfo = &Info; bool bResetObjectBeingDebuggedWhenFinished = false; UObject* ObjectBeingDebugged = BlueprintObj->GetObjectBeingDebugged(); if (UClass* GeneratedClass = BlueprintObj->GeneratedClass; ObjectBeingDebugged == nullptr && BPTYPE_FunctionLibrary == BlueprintObj->BlueprintType && GeneratedClass) { ObjectBeingDebugged = GeneratedClass->GetDefaultObject(false); } auto IsAPreviewOrInactiveObject = [](const UObject* InObject) { UWorld* World = InObject ? InObject->GetWorld() : nullptr; return World && (World->WorldType == EWorldType::EditorPreview || World->WorldType == EWorldType::Inactive); }; // Ignore script exceptions for preview objects that are not already being debugged if (IsAPreviewOrInactiveObject(ActiveObject) && ObjectBeingDebugged != ActiveObject) { return; } UObject* SavedObjectBeingDebugged = ObjectBeingDebugged; UWorld* WorldBeingDebugged = BlueprintObj->GetWorldBeingDebugged(); const FString& PathToDebug = BlueprintObj->GetObjectPathToDebug(); if (ObjectBeingDebugged == nullptr && !PathToDebug.IsEmpty()) { // Check if we need to update the object being debugged UObject* ObjectToDebug = FindObjectSafe(nullptr, *PathToDebug); if (IsValid(ObjectToDebug)) { // If the path to debug matches a newly-spawned object, set the hard reference now ObjectBeingDebugged = ObjectToDebug; BlueprintObj->SetObjectBeingDebugged(ObjectBeingDebugged); } } const int32 BreakpointOffset = StackFrame.Code - StackFrame.Node->Script.GetData() - 1; bool bShouldBreakExecution = false; bool bForceToCurrentObject = false; bool bIsStepping = Data.bIsSingleStepping || Data.TargetGraphStackDepth != INDEX_NONE; switch (Info.GetType()) { case EBlueprintExceptionType::Breakpoint: bShouldBreakExecution = true; break; case EBlueprintExceptionType::Tracepoint: bShouldBreakExecution = bIsStepping && TracepointBreakAllowedOnOwningWorld(ActiveObject); break; case EBlueprintExceptionType::WireTracepoint: break; case EBlueprintExceptionType::AccessViolation: case EBlueprintExceptionType::UserRaisedError: if ( GIsEditor && GIsPlayInEditorWorld ) { // declared as its own variable since it's flushed (logs pushed to std output) on destruction // we want the full message constructed before it's logged TSharedRef Message = FTokenizedMessage::Create(EMessageSeverity::Error); Message->AddToken(FTextToken::Create(FText::Format(LOCTEXT("RuntimeErrorMessageFmt", "Blueprint Runtime Error: \"{0}\"."), Info.GetDescription()))); #if WITH_EDITORONLY_DATA // to protect access to GeneratedClass->DebugData const UBlueprintGeneratedClass* GeneratedClass = Cast(ClassContainingCode); if ((GeneratedClass != nullptr) && GeneratedClass->DebugData.IsValid()) { UEdGraphNode* BlueprintNode = GeneratedClass->DebugData.FindSourceNodeFromCodeLocation(StackFrame.Node, BreakpointOffset, true); // if instead, there is a node we can point to... if (BlueprintNode != nullptr) { Message->AddToken(FTextToken::Create(LOCTEXT("RuntimeErrorBlueprintNodeLabel", "Node: "))); Message->AddToken(FUObjectToken::Create(BlueprintNode, BlueprintNode->GetNodeTitle(ENodeTitleType::ListView)) ->OnMessageTokenActivated(FOnMessageTokenActivated::CreateStatic(&Local::OnMessageLogLinkActivated)) ); Message->AddToken(FTextToken::Create(LOCTEXT("RuntimeErrorBlueprintGraphLabel", "Graph: "))); Message->AddToken(FUObjectToken::Create(BlueprintNode->GetGraph(), FText::FromString(GetNameSafe(BlueprintNode->GetGraph()))) ->OnMessageTokenActivated(FOnMessageTokenActivated::CreateStatic(&Local::OnMessageLogLinkActivated)) ); } } #endif // WITH_EDITORONLY_DATA // NOTE: StackFrame.Node is not a blueprint node like you may think ("Node" has some legacy meaning) Message->AddToken(FTextToken::Create(LOCTEXT("RuntimeErrorBlueprintFunctionLabel", "Function: "))); Message->AddToken(FUObjectToken::Create(StackFrame.Node, StackFrame.Node->GetDisplayNameText()) ->OnMessageTokenActivated(FOnMessageTokenActivated::CreateStatic(&Local::OnMessageLogLinkActivated)) ); Message->AddToken(FTextToken::Create(LOCTEXT("RuntimeErrorBlueprintObjectLabel", "Blueprint: "))); Message->AddToken(FUObjectToken::Create(BlueprintObj, FText::FromString(BlueprintObj->GetName())) ->OnMessageTokenActivated(FOnMessageTokenActivated::CreateStatic(&Local::OnMessageLogLinkActivated)) ); FMessageLog("PIE").AddMessage(Message); } bForceToCurrentObject = true; bShouldBreakExecution = GetDefault()->bBreakOnExceptions; break; case EBlueprintExceptionType::InfiniteLoop: bForceToCurrentObject = true; bShouldBreakExecution = GetDefault()->bBreakOnExceptions; break; default: bForceToCurrentObject = true; bShouldBreakExecution = GetDefault()->bBreakOnExceptions; break; } if (!bForceToCurrentObject && bIsStepping) { // If we're stepping, temporarily override the selected debug object so step into always works) bForceToCurrentObject = true; } // If we are debugging a specific world, the object needs to be in it if (WorldBeingDebugged != nullptr && !ActiveObject->IsIn(WorldBeingDebugged)) { // Might be a streaming level case, so find the real world to see const UObject *ObjOuter = ActiveObject; const UWorld *ObjWorld = nullptr; bool FailedWorldCheck = true; while(ObjWorld == nullptr && ObjOuter != nullptr) { ObjOuter = ObjOuter->GetOuter(); ObjWorld = Cast(ObjOuter); } if (ObjWorld && ObjWorld->PersistentLevel) { if (ObjWorld->PersistentLevel->OwningWorld == WorldBeingDebugged) { // Its ok, the owning world is the world being debugged FailedWorldCheck = false; } } if (FailedWorldCheck) { bForceToCurrentObject = false; bShouldBreakExecution = false; } } if (bShouldBreakExecution) { if ((PathToDebug.IsEmpty()) || (bForceToCurrentObject)) { // If there was nothing being debugged, treat this as a one-shot, temporarily set this object as being debugged, // and continue allowing any breakpoint to hit later on bResetObjectBeingDebuggedWhenFinished = true; ObjectBeingDebugged = const_cast(ActiveObject); BlueprintObj->SetObjectBeingDebugged(ObjectBeingDebugged); } } if (ObjectBeingDebugged == ActiveObject) { // Record into the trace log FKismetTraceSample& Tracer = Data.TraceStackSamples.WriteNewElementUninitialized(); Tracer.Context = MakeWeakObjectPtr(const_cast(ActiveObject)); Tracer.Function = StackFrame.Node; Tracer.Offset = BreakpointOffset; //@TODO: Might want to make this a parameter of Info Tracer.ObservationTime = FPlatformTime::Seconds(); // Find the node that generated the code which we hit UEdGraphNode* NodeStoppedAt = FindSourceNodeForCodeLocation(ActiveObject, StackFrame.Node, BreakpointOffset, /*bAllowImpreciseHit=*/ true); if (NodeStoppedAt && (Info.GetType() == EBlueprintExceptionType::Tracepoint || Info.GetType() == EBlueprintExceptionType::Breakpoint)) { // Handle Node stepping and update the stack CheckBreakConditions(NodeStoppedAt, Info.GetType() == EBlueprintExceptionType::Breakpoint, BreakpointOffset, bShouldBreakExecution); } // Can't do intraframe debugging when the editor is actively stopping if (GEditor->ShouldEndPlayMap()) { bShouldBreakExecution = false; } // Handle a breakpoint or single-step if (bShouldBreakExecution) { AttemptToBreakExecution(BlueprintObj, ActiveObject, StackFrame, *ExceptionInfo, NodeStoppedAt, BreakpointOffset); } } // Reset the object being debugged if we forced it to be something different if (bResetObjectBeingDebuggedWhenFinished) { if (BlueprintObj->GetObjectBeingDebugged() == ObjectBeingDebugged) { // Only reset if it's still what we expected, if the user picked a new object from the UI we want to respect that BlueprintObj->SetObjectBeingDebugged(SavedObjectBeingDebugged); } } const auto ShowScriptExceptionError = [&](const FText& InExceptionErrorMsg) { if (GUnrealEd->PlayWorld != nullptr) { GEditor->RequestEndPlayMap(); FSlateApplication::Get().LeaveDebuggingMode(); } // Launch a message box notifying the user why they have been booted { // Callback to display a pop-up showing the Callstack, the user can highlight and copy this if needed auto DisplayCallStackLambda = [](const FText CallStack) { TSharedPtr TextBlock; TSharedRef DisplayWidget = SNew(SBox) .MaxDesiredHeight(512.0f) .MaxDesiredWidth(512.0f) .Content() [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) [ SNew(SScrollBox) + SScrollBox::Slot() [ SAssignNew(TextBlock, SMultiLineEditableText) .AutoWrapText(true) .IsReadOnly(true) .Text(CallStack) ] ] ]; FSlateApplication::Get().PushMenu( FSlateApplication::Get().GetActiveTopLevelWindow().ToSharedRef(), FWidgetPath(), DisplayWidget, FSlateApplication::Get().GetCursorPos(), FPopupTransitionEffect(FPopupTransitionEffect::TypeInPopup) ); FSlateApplication::Get().SetKeyboardFocus(TextBlock); }; TSharedRef Message = FTokenizedMessage::Create(EMessageSeverity::Error); // Display the main error message Message->AddToken(FTextToken::Create(InExceptionErrorMsg)); // Display a link to the UObject and the UFunction that is crashing { // Get the name of the Blueprint FString BlueprintName; BlueprintObj->GetName(BlueprintName); Message->AddToken(FTextToken::Create(LOCTEXT("ShowScriptExceptionError_BlueprintLabel", "Blueprint: "))); Message->AddToken(FUObjectToken::Create(BlueprintObj, FText::FromString(BlueprintName))); } { // If a source node is found, that's the token we want to link, otherwise settle with the UFunction const int32 BreakpointOpCodeOffset = StackFrame.Code - StackFrame.Node->Script.GetData() - 1; //@TODO: Might want to make this a parameter of Info UEdGraphNode* SourceNode = FindSourceNodeForCodeLocation(ActiveObject, StackFrame.Node, BreakpointOpCodeOffset, /*bAllowImpreciseHit=*/ true); Message->AddToken(FTextToken::Create(LOCTEXT("ShowScriptExceptionError_FunctionLabel", "Function: "))); if (SourceNode) { Message->AddToken(FUObjectToken::Create(SourceNode, SourceNode->GetNodeTitle(ENodeTitleType::ListView))); } else { Message->AddToken(FUObjectToken::Create(StackFrame.Node, StackFrame.Node->GetDisplayNameText())); } } // Display a pop-up that will display the complete script callstack Message->AddToken(FTextToken::Create(LOCTEXT("ShowScriptExceptionError_CallStackLabel", "Call Stack: "))); Message->AddToken(FActionToken::Create(LOCTEXT("ShowScriptExceptionError_ShowCallStack", "Show"), LOCTEXT("ShowScriptExceptionError_ShowCallStackDesc", "Displays the underlying callstack, tracing what function calls led to the assert occuring."), FOnActionTokenExecuted::CreateStatic(DisplayCallStackLambda, FText::FromString(StackFrame.GetStackTrace())))); FMessageLog("PIE").AddMessage(Message); } }; // Extra cleanup after potential interactive handling switch (Info.GetType()) { case EBlueprintExceptionType::FatalError: ShowScriptExceptionError(FText::Format(LOCTEXT("ShowScriptExceptionError_FatalErrorFmt", "Fatal error detected: \"{0}\"."), Info.GetDescription())); break; case EBlueprintExceptionType::InfiniteLoop: ShowScriptExceptionError(LOCTEXT("ShowScriptExceptionError_InfiniteLoop", "Infinite loop detected.")); break; default: // Left empty intentionally break; } } } bool FKismetDebugUtilities::TracepointBreakAllowedOnOwningWorld(const UObject* ObjOuter) { bool bAllowTracepointBreak = true; const UWorld* ObjWorld = ObjOuter->GetWorld(); // Disable tracepoints on EditorPreviews or Inactive worlds if (ObjWorld && (ObjWorld->WorldType == EWorldType::EditorPreview || ObjWorld->WorldType == EWorldType::Inactive)) { bAllowTracepointBreak = false; } return bAllowTracepointBreak; } const UClass* FKismetDebugUtilities::FindClassForNode(const UObject* Object, const UFunction* Function) { if (NULL != Function) { UClass* FunctionOwner = Function->GetOwnerClass(); return FunctionOwner; } if(NULL != Object) { UClass* ObjClass = Object->GetClass(); return ObjClass; } return NULL; } const TSimpleRingBuffer& FKismetDebugUtilities::GetTraceStack() { return FKismetDebugUtilitiesData::Get().TraceStackSamples; } UEdGraphNode* FKismetDebugUtilities::FindSourceNodeForCodeLocation(const UObject* Object, const UFunction* Function, int32 DebugOpcodeOffset, bool bAllowImpreciseHit) { if (Object != NULL) { // Find the blueprint that corresponds to the object if (const UBlueprintGeneratedClass* Class = Cast(FindClassForNode(Object, Function))) { return Class->GetDebugData().FindSourceNodeFromCodeLocation(Function, DebugOpcodeOffset, bAllowImpreciseHit); } } return NULL; } void FKismetDebugUtilities::CheckBreakConditions(UEdGraphNode* NodeStoppedAt, bool bHitBreakpoint, int32 BreakpointOffset, bool& InOutBreakExecution) { FKismetDebugUtilitiesData& Data = FKismetDebugUtilitiesData::Get(); TArrayView ScriptStack = FBlueprintContextTracker::Get().GetCurrentScriptStack(); if (NodeStoppedAt) { const bool bIsTryingToBreak = bHitBreakpoint || Data.TargetGraphStackDepth != INDEX_NONE || Data.bIsSingleStepping; if(bIsTryingToBreak) { // Update the TargetGraphStackDepth if we're on the same node - this handles things like // event nodes in the Event Graph, which will push another frame on to the stack: if(NodeStoppedAt == Data.MostRecentStoppedNode && Data.MostRecentBreakpointGraphStackDepth < ScriptStack.Num() && Data.TargetGraphStackDepth != INDEX_NONE) { // when we recurse, when a node increases stack depth itself we want to increase our // target depth to compensate: Data.TargetGraphStackDepth += 1; } else if(NodeStoppedAt != Data.MostRecentStoppedNode) { Data.MostRecentStoppedNode = nullptr; } // We should only actually break execution when we're on a new node or we've recursed to the same // node. We detect recursion by checking for a deeper stack and an earlier instruction: InOutBreakExecution = NodeStoppedAt != Data.MostRecentStoppedNode || ( Data.MostRecentBreakpointGraphStackDepth < ScriptStack.Num() && Data.MostRecentBreakpointInstructionOffset >= BreakpointOffset ); // If we have a TargetGraphStackDepth, don't break if we haven't reached that stack depth, or if we've stepped // in to a collapsed graph/macro instance: if(InOutBreakExecution && Data.TargetGraphStackDepth != INDEX_NONE && !bHitBreakpoint) { InOutBreakExecution = Data.TargetGraphStackDepth >= ScriptStack.Num(); if(InOutBreakExecution && Data.TargetGraphStackDepth == ScriptStack.Num()) { // If we have Data.TargetGraphNodes.Num() > 0, see if we can find a BlueprintNode matching a TargetGraphNode by iterating // up the Blueprint class hierarchy of our CurrentFrame->Object calling FindSourceNodeFromCodeLocation at each level. if (Data.TargetGraphNodes.Num() > 0) { // we're at the same stack depth, don't break if we've entered a different graph, but do break if we left the // graph that we were trying to step over.. const FFrame* CurrentFrame = ScriptStack.Last(); if (CurrentFrame->Object) { UClass* BPClass = nullptr; while (true) { BPClass = (BPClass == nullptr) ? CurrentFrame->Object->GetClass() : BPClass->GetSuperClass(); if (BPClass == nullptr) { InOutBreakExecution = false; break; } const UBlueprintGeneratedClass* BPGC = Cast(BPClass); if (BPGC == nullptr) { InOutBreakExecution = false; break; } const UEdGraphNode* BlueprintNode = BPGC->DebugData.FindSourceNodeFromCodeLocation(CurrentFrame->Node, BreakpointOffset, true); if (Data.TargetGraphNodes.Contains(BlueprintNode)) { break; } } } } } } } else if (NodeStoppedAt != Data.MostRecentStoppedNode) { Data.MostRecentStoppedNode = nullptr; } } if (InOutBreakExecution) { Data.MostRecentStoppedNode = NodeStoppedAt; Data.MostRecentBreakpointGraphStackDepth = ScriptStack.Num(); Data.MostRecentBreakpointInstructionOffset = BreakpointOffset; Data.TargetGraphStackDepth = INDEX_NONE; Data.TargetGraphNodes.Empty(); Data.bIsSteppingOut = false; } else if(Data.TargetGraphStackDepth != INDEX_NONE && Data.bIsSteppingOut) { UK2Node_Tunnel* AsTunnel = Cast(NodeStoppedAt); if(AsTunnel) { // if we go through a tunnel entry/exit node update the target stack depth... if(AsTunnel->bCanHaveInputs) { Data.TargetGraphStackDepth += 1; } else if(AsTunnel->bCanHaveOutputs) { Data.TargetGraphStackDepth -= 1; } } } } void FKismetDebugUtilities::AttemptToBreakExecution(UBlueprint* BlueprintObj, const UObject* ActiveObject, const FFrame& StackFrame, const FBlueprintExceptionInfo& Info, UEdGraphNode* NodeStoppedAt, int32 DebugOpcodeOffset) { checkSlow(BlueprintObj->GetObjectBeingDebugged() == ActiveObject); check(IsInGameThread()); FKismetDebugUtilitiesData& Data = FKismetDebugUtilitiesData::Get(); // Cannot have re-entrancy while processing a breakpoint; return from this call stack before resuming execution! check(!GIntraFrameDebuggingGameThread); check(Data.CurrentDebuggingMode == EKismetDebuggingMode::None); TGuardValue SignalGameThreadBeingDebugged(GIntraFrameDebuggingGameThread, true); TGuardValue ResetStackFramePointer(Data.StackFrameAtIntraframeDebugging, &StackFrame); // Should we pump Slate messages from this callstack, allowing intra-frame debugging? bool bShouldInStackDebug = false; if (NodeStoppedAt != NULL) { bShouldInStackDebug = true; Data.CurrentInstructionPointer = NodeStoppedAt; Data.MostRecentBreakpointInstructionPointer = NULL; // Find the breakpoint object for the node, assuming we hit one if (Info.GetType() == EBlueprintExceptionType::Breakpoint) { FBlueprintBreakpoint* Breakpoint = FindBreakpointForNode(NodeStoppedAt, BlueprintObj); if (Breakpoint != NULL) { Data.MostRecentBreakpointInstructionPointer = NodeStoppedAt; UpdateBreakpointStateWhenHit(NodeStoppedAt, BlueprintObj); //@TODO: K2: DEBUGGING: Debug print text can go eventually UE_LOG(LogBlueprintDebug, Warning, TEXT("Hit breakpoint on node '%s', from offset %d"), *(NodeStoppedAt->GetDescriptiveCompiledName()), DebugOpcodeOffset); UE_LOG(LogBlueprintDebug, Log, TEXT("\n%s"), *StackFrame.GetStackTrace()); } else { UE_LOG(LogBlueprintDebug, Warning, TEXT("Unknown breakpoint hit at node %s in object %s:%04X"), *NodeStoppedAt->GetDescriptiveCompiledName(), *StackFrame.Node->GetFullName(), DebugOpcodeOffset); } } // Turn off single stepping; we've hit a node if (Data.bIsSingleStepping) { Data.bIsSingleStepping = false; } } else if(UEdGraphNode* PreviousNode = FKismetDebugUtilities::GetCurrentInstruction()) { if (UK2Node_Message* MessageNode = Cast(PreviousNode)) { //Looks like object not implement one of their interfaces UE_LOG(LogBlueprintDebug, Warning, TEXT("Can't break execution on function '%s'. Possibly interface '%s' in class '%s' was not fully implemented."), *(PreviousNode->GetDocumentationExcerptName()), //Function name *(MessageNode->GetTargetFunction()->GetOuterUClass()->GetName()), //Interface name *(ActiveObject->GetClass()->GetName())); //Current object class name } else { UE_LOG(LogBlueprintDebug, Warning, TEXT("Can't break execution on function '%s'. Possibly it was not implemented in class '%s'."), *(PreviousNode->GetDocumentationExcerptName()), //Function name *(ActiveObject->GetClass()->GetName())); //Current object class name } } else { UE_LOG(LogBlueprintDebug, Warning, TEXT("Tried to break execution in an unknown spot at object %s:%04X"), *StackFrame.Node->GetFullName(), StackFrame.Code - StackFrame.Node->Script.GetData()); } // A check to !GIsAutomationTesting was removed from here as it seemed redundant. // Breakpoints have to be explicitly enabled by the user which shouldn't happen // under automation and this was preventing debugging on automation test bp's. if ((GUnrealEd->PlayWorld != NULL) && NodeStoppedAt) { Data.CurrentDebuggingMode = EKismetDebuggingMode::World; Data.CurrentDebuggingWorld = GUnrealEd->PlayWorld; // Pause the simulation GUnrealEd->PlayWorld->bDebugPauseExecution = true; GUnrealEd->PlayWorld->bDebugFrameStepExecution = false; bShouldInStackDebug = true; } else if (NodeStoppedAt) { Data.CurrentDebuggingMode = EKismetDebuggingMode::Engine; } else { Data.CurrentDebuggingMode = EKismetDebuggingMode::None; bShouldInStackDebug = false; } // Now enter within-the-frame debugging mode if (bShouldInStackDebug) { FTemporaryPlayInEditorIDOverride GuardDisablePIE(INDEX_NONE); TArrayView ScriptStack = FBlueprintContextTracker::Get().GetCurrentScriptStack(); Data.LastExceptionMessage = Info.GetDescription(); FKismetEditorUtilities::BringKismetToFocusAttentionOnObject(NodeStoppedAt); CallStackViewer::UpdateDisplayedCallstack(ScriptStack); FSlateApplication::Get().EnterDebuggingMode(); } Data.CurrentDebuggingMode = EKismetDebuggingMode::None; Data.CurrentDebuggingWorld.Reset(); } UEdGraphNode* FKismetDebugUtilities::GetCurrentInstruction() { // If paused at the end of the frame, or while not paused, there is no 'current instruction' to speak of // It only has meaning during intraframe debugging. if (GIntraFrameDebuggingGameThread) { return FKismetDebugUtilitiesData::Get().CurrentInstructionPointer.Get(); } else { return nullptr; } } UEdGraphNode* FKismetDebugUtilities::GetMostRecentBreakpointHit() { // If paused at the end of the frame, or while not paused, there is no 'current instruction' to speak of // It only has meaning during intraframe debugging. if (GIntraFrameDebuggingGameThread) { return FKismetDebugUtilitiesData::Get().MostRecentBreakpointInstructionPointer.Get(); } else { return nullptr; } } UWorld* FKismetDebugUtilities::GetCurrentDebuggingWorld() { // If paused at the end of the frame, or while not paused, there is no 'current instruction' to speak of // It only has meaning during intraframe debugging. if (GIntraFrameDebuggingGameThread) { const FKismetDebugUtilitiesData& Data = FKismetDebugUtilitiesData::Get(); return Data.CurrentDebuggingMode == EKismetDebuggingMode::World ? Data.CurrentDebuggingWorld.Get() : nullptr; } else { return nullptr; } } // Notify the debugger of the start of the game frame void FKismetDebugUtilities::NotifyDebuggerOfStartOfGameFrame(UWorld* CurrentWorld) { } // Notify the debugger of the end of the game frame void FKismetDebugUtilities::NotifyDebuggerOfEndOfGameFrame(UWorld* CurrentWorld) { FKismetDebugUtilitiesData::Get().bIsSingleStepping = false; } bool FKismetDebugUtilities::IsSingleStepping() { const FKismetDebugUtilitiesData& Data = FKismetDebugUtilitiesData::Get(); return Data.bIsSingleStepping || Data.bIsSteppingOut || Data.TargetGraphStackDepth != INDEX_NONE; } FPerBlueprintSettings* FKismetDebugUtilities::GetPerBlueprintSettings(const UBlueprint* Blueprint) { UBlueprintEditorSettings* Settings = GetMutableDefault(); check(Settings); return Settings->PerBlueprintSettings.Find(Blueprint->GetPathName()); } TArray* FKismetDebugUtilities::GetBreakpoints(const UBlueprint* Blueprint) { FPerBlueprintSettings* Settings = GetPerBlueprintSettings(Blueprint); // return nullptr if there's no breakpoints associated w/ this blueprint return (!Settings || Settings->Breakpoints.IsEmpty()) ? nullptr : &Settings->Breakpoints; } TArray* FKismetDebugUtilities::GetWatchedPins(const UBlueprint* Blueprint) { FPerBlueprintSettings* Settings = GetPerBlueprintSettings(Blueprint); // return nullptr if there's no breakpoints associated w/ this blueprint return (!Settings || Settings->WatchedPins.IsEmpty()) ? nullptr : &Settings->WatchedPins; } void FKismetDebugUtilities::SaveBlueprintEditorSettings() { UBlueprintEditorSettings* Settings = GetMutableDefault(); check(Settings); Settings->SaveConfig(); } void FKismetDebugUtilities::CleanupBreakpoints(const UBlueprint* Blueprint) { RemoveBreakpointsByPredicate( Blueprint, [Blueprint](const FBlueprintBreakpoint& Breakpoint) { if (Breakpoint.GetLocation() == nullptr) { UE_LOG(LogBlueprintDebug, Display, TEXT("Encountered a blueprint breakpoint in %s without an associated node. The blueprint breakpoint has been removed"), *Blueprint->GetPathName()); return true; } return false; } ); } void FKismetDebugUtilities::CleanupWatches(const UBlueprint* Blueprint) { RemovePinWatchesByPredicate( Blueprint, [Blueprint](const UEdGraphPin* Pin)->bool { if(Pin) { if(UEdGraphNode* Node = Pin->GetOwningNode()) { if(UEdGraph* Graph = Node->GetGraph()) { TArray BPGraphs; Blueprint->GetAllGraphs(BPGraphs); if(BPGraphs.Contains(Graph)) { return false; } } } } return true; } ); } void FKismetDebugUtilities::RemoveEmptySettings(const FString& BlueprintPath) { UBlueprintEditorSettings* Settings = GetMutableDefault(); check(Settings); if(FPerBlueprintSettings *PerBlueprintSettings = Settings->PerBlueprintSettings.Find(BlueprintPath)) { // if all settings data is default, we can remove it from the map if(*PerBlueprintSettings == FPerBlueprintSettings()) { Settings->PerBlueprintSettings.Remove(BlueprintPath); } SaveBlueprintEditorSettings(); } } ////////////////////////////////////////////////////////////////////////// // Breakpoint // Is the node a valid breakpoint target? (i.e., the node is impure and ended up generating code) bool FKismetDebugUtilities::IsBreakpointValid(const FBlueprintBreakpoint& Breakpoint) { // Breakpoints on impure nodes in a macro graph are always considered valid UK2Node* K2Node = Cast(Breakpoint.GetLocation()); if (K2Node) { UBlueprint* Blueprint = Cast(K2Node->GetOuter()->GetOuter()); if (Blueprint && Blueprint->BlueprintType == BPTYPE_MacroLibrary) { return K2Node->IsA() || (!K2Node->IsNodePure() && !K2Node->IsA()); } } TArray InstallSites; GetBreakpointInstallationSites(Breakpoint, InstallSites); return InstallSites.Num() > 0; } // Set the node that the breakpoint should focus on void FKismetDebugUtilities::SetBreakpointLocation(FBlueprintBreakpoint& Breakpoint, UEdGraphNode* NewNode) { if (NewNode != Breakpoint.Node) { // Uninstall it from the old site if needed SetBreakpointInternal(Breakpoint, false); // Make the new site accurate Breakpoint.Node = NewNode; SetBreakpointInternal(Breakpoint, Breakpoint.bEnabled); } } void FKismetDebugUtilities::SetBreakpointEnabled(FBlueprintBreakpoint& Breakpoint, bool bIsEnabled) { if (Breakpoint.bStepOnce && !bIsEnabled) { // Want to be disabled, but the single-stepping is keeping it enabled bIsEnabled = true; Breakpoint.bStepOnce_WasPreviouslyDisabled = true; } Breakpoint.bEnabled = bIsEnabled; SetBreakpointInternal(Breakpoint, Breakpoint.bEnabled); SaveBlueprintEditorSettings(); } // Set or clear the enabled flag for the breakpoint void FKismetDebugUtilities::SetBreakpointEnabled(const UEdGraphNode* OwnerNode, const UBlueprint* OwnerBlueprint, bool bIsEnabled) { if(FBlueprintBreakpoint* Breakpoint = FindBreakpointForNode(OwnerNode, OwnerBlueprint)) { SetBreakpointEnabled(*Breakpoint, bIsEnabled); } } // Sets this breakpoint up as a single-step breakpoint (will disable or delete itself after one go if the breakpoint wasn't already enabled) void FKismetDebugUtilities::SetBreakpointEnabledForSingleStep(FBlueprintBreakpoint& Breakpoint, bool bDeleteAfterStep) { Breakpoint.bStepOnce = true; Breakpoint.bStepOnce_RemoveAfterHit = bDeleteAfterStep; Breakpoint.bStepOnce_WasPreviouslyDisabled = !Breakpoint.bEnabled; SetBreakpointEnabled(Breakpoint, true); } void FKismetDebugUtilities::ReapplyBreakpoint(FBlueprintBreakpoint& Breakpoint) { SetBreakpointInternal(Breakpoint, Breakpoint.IsEnabled()); } void FKismetDebugUtilities::RemoveBreakpointFromNode(const UEdGraphNode* OwnerNode, const UBlueprint* OwnerBlueprint) { #if WITH_EDITORONLY_DATA RemoveBreakpointsByPredicate( OwnerBlueprint, [OwnerNode](const FBlueprintBreakpoint& Breakpoint) { return Breakpoint.GetLocation() == OwnerNode; } ); #endif //#if WITH_EDITORONLY_DATA } // Update the internal state of the breakpoint when it got hit void FKismetDebugUtilities::UpdateBreakpointStateWhenHit(const UEdGraphNode* OwnerNode, const UBlueprint* OwnerBlueprint) { if (FBlueprintBreakpoint* Breakpoint = FindBreakpointForNode(OwnerNode, OwnerBlueprint)) { // Handle single-step breakpoints if (Breakpoint->bStepOnce) { Breakpoint->bStepOnce = false; if (Breakpoint->bStepOnce_RemoveAfterHit) { RemoveBreakpointFromNode(Breakpoint->GetLocation(), OwnerBlueprint); } else if (Breakpoint->bStepOnce_WasPreviouslyDisabled) { SetBreakpointEnabled(*Breakpoint, false); } } } } // Install/uninstall the breakpoint into/from the script code for the generated class that contains the node void FKismetDebugUtilities::SetBreakpointInternal(FBlueprintBreakpoint& Breakpoint, bool bShouldBeEnabled) { TArray InstallSites; GetBreakpointInstallationSites(Breakpoint, InstallSites); for (int i = 0; i < InstallSites.Num(); ++i) { if (uint8* InstallSite = InstallSites[i]) { *InstallSite = bShouldBeEnabled ? EX_Breakpoint : EX_Tracepoint; } } } // Returns the installation site(s); don't cache these pointers! void FKismetDebugUtilities::GetBreakpointInstallationSites(const FBlueprintBreakpoint& Breakpoint, TArray& InstallSites) { InstallSites.Empty(); #if WITH_EDITORONLY_DATA if (Breakpoint.Node != NULL) { UBlueprint* Blueprint = FBlueprintEditorUtils::FindBlueprintForNode(Breakpoint.GetLocation()); if ((Blueprint != NULL) && (Blueprint->GeneratedClass != NULL)) { if (UBlueprintGeneratedClass* Class = Cast(*Blueprint->GeneratedClass)) { // Find the insertion point from the debugging data Class->GetDebugData().FindBreakpointInjectionSites(Breakpoint.GetLocation(), InstallSites); } } } #endif //#if WITH_EDITORONLY_DATA } // Returns the set of valid breakpoint locations for the given macro instance node void FKismetDebugUtilities::GetValidBreakpointLocations(const UK2Node_MacroInstance* MacroInstanceNode, TArray& BreakpointLocations) { check(MacroInstanceNode); BreakpointLocations.Empty(); // Gather information from the macro graph associated with the macro instance node bool bIsMacroPure = false; UK2Node_Tunnel* MacroEntryNode = NULL; UK2Node_Tunnel* MacroResultNode = NULL; UEdGraph* InstanceNodeMacroGraph = MacroInstanceNode->GetMacroGraph(); if (ensure(InstanceNodeMacroGraph != nullptr)) { FKismetEditorUtilities::GetInformationOnMacro(InstanceNodeMacroGraph, MacroEntryNode, MacroResultNode, bIsMacroPure); } if (!bIsMacroPure && MacroEntryNode) { // Get the execute pin outputs on the entry node for (const UEdGraphPin* ExecPin : MacroEntryNode->Pins) { if (ExecPin && ExecPin->Direction == EGPD_Output && ExecPin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec) { // For each pin linked to each execute pin, collect the node that owns it for (const UEdGraphPin* LinkedToPin : ExecPin->LinkedTo) { check(LinkedToPin); const UEdGraphNode* LinkedToNode = LinkedToPin->GetOwningNode(); check(LinkedToNode); if (LinkedToNode->IsA()) { // Recursively descend into macro instance nodes encountered in a macro graph TArray SubLocations; GetValidBreakpointLocations(Cast(LinkedToNode), SubLocations); BreakpointLocations.Append(SubLocations); } else { BreakpointLocations.AddUnique(LinkedToNode); } } } } } } void FKismetDebugUtilities::CreateBreakpoint(const UBlueprint* Blueprint, UEdGraphNode* Node, bool bIsEnabled) { UBlueprintEditorSettings* Settings = GetMutableDefault(); check(Settings); FPerBlueprintSettings &BlueprintSettings = Settings->PerBlueprintSettings.FindOrAdd(Blueprint->GetPathName()); // ensure that this node doesn't already contain a breakpoint checkSlow(!BlueprintSettings.Breakpoints.ContainsByPredicate( [Node](const FBlueprintBreakpoint& Other) { return Other.Node == Node; } )); BlueprintSettings.Breakpoints.Emplace(); SetBreakpointEnabled(BlueprintSettings.Breakpoints.Top(), bIsEnabled); SetBreakpointLocation(BlueprintSettings.Breakpoints.Top(), Node); SaveBlueprintEditorSettings(); } void FKismetDebugUtilities::ForeachBreakpoint(const UBlueprint* Blueprint, const TFunctionRef Task) { if(TArray* Breakpoints = GetBreakpoints(Blueprint)) { for(FBlueprintBreakpoint& Breakpoint : *Breakpoints) { Task(Breakpoint); } } } void FKismetDebugUtilities::RemoveBreakpointsByPredicate(const UBlueprint* Blueprint, const TFunctionRef Predicate) { if(TArray* Breakpoints = GetBreakpoints(Blueprint)) { // notify the debugger of the breakpoints being removed for(FBlueprintBreakpoint& Breakpoint : *Breakpoints) { if(Predicate(Breakpoint)) { SetBreakpointLocation(Breakpoint, nullptr); } } // remove the breakpoints from the data if(Breakpoints->RemoveAllSwap(Predicate, EAllowShrinking::No)) { if(Breakpoints->IsEmpty()) { // keeps the ini file clean by removing empty arrays ClearBreakpoints(Blueprint); } SaveBlueprintEditorSettings(); } } } FBlueprintBreakpoint* FKismetDebugUtilities::FindBreakpointByPredicate(const UBlueprint* Blueprint, const TFunctionRef Predicate) { if(TArray* Breakpoints = GetBreakpoints(Blueprint)) { for(FBlueprintBreakpoint& Breakpoint : *Breakpoints) { if(Predicate(Breakpoint)) { return &Breakpoint; } } } return nullptr; } // Finds a breakpoint for a given node if it exists, or returns NULL FBlueprintBreakpoint* FKismetDebugUtilities::FindBreakpointForNode(const UEdGraphNode* OwnerNode, const UBlueprint* OwnerBlueprint, bool bCheckSubLocations) { // remove expired data from deleted nodes and such CleanupBreakpoints(OwnerBlueprint); // find breakpoint return FindBreakpointByPredicate( OwnerBlueprint, [OwnerNode, bCheckSubLocations](const FBlueprintBreakpoint &Breakpoint) { UEdGraphNode* BreakpointLoaction = Breakpoint.GetLocation(); // Return this breakpoint if the location matches the given node if (BreakpointLoaction == OwnerNode) { return true; } if (bCheckSubLocations) { // If this breakpoint is set on a macro instance node, check the set of valid breakpoint locations. If we find a // match in the returned set, return the breakpoint that's set on the macro instance node. This allows breakpoints // to be set and hit on macro instance nodes contained in a macro graph that will be expanded during compile. const UK2Node_MacroInstance* MacroInstanceNode = Cast(BreakpointLoaction); if (MacroInstanceNode) { TArray ValidBreakpointLocations; GetValidBreakpointLocations(MacroInstanceNode, ValidBreakpointLocations); if (ValidBreakpointLocations.Contains(OwnerNode)) { return true; } } } return false; } ); } void FKismetDebugUtilities::RestoreBreakpointsOnLoad(const UBlueprint* Blueprint) { if (!Blueprint) { return; } #if WITH_EDITORONLY_DATA // Remove stale breakpoints RemoveBreakpointsByPredicate( Blueprint, [Blueprint](const FBlueprintBreakpoint& Breakpoint) { const UEdGraphNode* const Location = Breakpoint.GetLocation(); return !Location || Location->GetPackage()->GetPersistentGuid() != Blueprint->GetPackage()->GetPersistentGuid(); } ); #endif // Restore breakpoints based on preferred method const UBlueprintEditorSettings* BlueprintEditorSettings = GetDefault(); switch (BlueprintEditorSettings->BreakpointReloadMethod) { case EBlueprintBreakpointReloadMethod::RestoreAllAndDisable: ForeachBreakpoint( Blueprint, [](FBlueprintBreakpoint& Breakpoint) { SetBreakpointEnabled(Breakpoint, false); } ); break; case EBlueprintBreakpointReloadMethod::DiscardAll: ClearBreakpoints(Blueprint); break; case EBlueprintBreakpointReloadMethod::RestoreAll: default: break; } } bool FKismetDebugUtilities::HasDebuggingData(const UBlueprint* Blueprint) { return Cast(*Blueprint->GeneratedClass)->GetDebugData().IsValid(); } ////////////////////////////////////////////////////////////////////////// // Blueprint utils void FKismetDebugUtilities::PostDuplicateBlueprint(UBlueprint* SrcBlueprint, UBlueprint* DupBlueprint, const TArray& DupNodes) { // Duplicate Breakpoints from the source blueprint if (TArray* Breakpoints = GetBreakpoints(SrcBlueprint)) { for (FBlueprintBreakpoint& Breakpoint : *Breakpoints) { if (UEdGraphNode* Location = Breakpoint.GetLocation()) { UEdGraphNode* const* NewLocation = DupNodes.FindByPredicate( [Location](const UEdGraphNode* Node) -> bool { return Node->NodeGuid == Location->NodeGuid; } ); if (NewLocation) { CreateBreakpoint(DupBlueprint, *NewLocation, Breakpoint.IsEnabled()); } } } } // Duplicate Watched Pins if (TArray* WatchedPins = GetWatchedPins(SrcBlueprint)) { for (FBlueprintWatchedPin& WatchedPin : *WatchedPins) { if (UEdGraphPin* Pin = WatchedPin.Get()) { UEdGraphNode* const* NewOwningNode = DupNodes.FindByPredicate( [Pin](const UEdGraphNode* Node) -> bool { return Node->NodeGuid == Pin->GetOwningNode()->NodeGuid; } ); if (NewOwningNode) { if (const UEdGraphPin* NewPin = (*NewOwningNode)->FindPin(Pin->PinName)) { AddPinWatch(DupBlueprint, NewPin); } } } } } } bool FKismetDebugUtilities::BlueprintHasBreakpoints(const UBlueprint* Blueprint) { return GetBreakpoints(Blueprint) != nullptr; } // Looks thru the debugging data for any class variables associated with the node FProperty* FKismetDebugUtilities::FindClassPropertyForPin(UBlueprint* Blueprint, const UEdGraphPin* Pin) { FProperty* FoundProperty = nullptr; if (Pin) { // Input Pins linked to a reroute node should use debug property from the reroute node's input pin if (Pin->Direction == EGPD_Input && Pin->LinkedTo.Num() == 1) { if (const UK2Node_Knot* LinkedKnot = Cast(Pin->LinkedTo[0]->GetOwningNode())) { return FindClassPropertyForPin(Blueprint, LinkedKnot->GetInputPin()); } } // Reroute nodes should always use the input pin, not the output pin if (const UK2Node_Knot* OwningKnot = Cast(Pin->GetOwningNode())) { if (Pin->Direction == EGPD_Output) { return FindClassPropertyForPin(Blueprint, OwningKnot->GetInputPin()); } } } UClass* Class = Blueprint->GeneratedClass; while (UBlueprintGeneratedClass* BlueprintClass = Cast(Class)) { FoundProperty = BlueprintClass->GetDebugData().FindClassPropertyForPin(Pin); if (FoundProperty != nullptr) { break; } Class = BlueprintClass->GetSuperClass(); } return FoundProperty; } // Looks thru the debugging data for any class variables associated with the node (e.g., temporary variables or timelines) FProperty* FKismetDebugUtilities::FindClassPropertyForNode(UBlueprint* Blueprint, const UEdGraphNode* Node) { if (UBlueprintGeneratedClass* Class = Cast(*Blueprint->GeneratedClass)) { return Class->GetDebugData().FindClassPropertyForNode(Node); } return NULL; } void FKismetDebugUtilities::ClearBreakpoints(const UBlueprint* Blueprint) { ClearBreakpointsForPath(Blueprint->GetPathName()); } void FKismetDebugUtilities::ClearBreakpointsForPath(const FString& BlueprintPath) { UBlueprintEditorSettings* Settings = GetMutableDefault(); check(Settings); if(FPerBlueprintSettings *PerBlueprintSettings = Settings->PerBlueprintSettings.Find(BlueprintPath)) { for(FBlueprintBreakpoint& Breakpoint : PerBlueprintSettings->Breakpoints) { // notify debugger that the breakpont has been removed SetBreakpointLocation(Breakpoint, nullptr); } PerBlueprintSettings->Breakpoints.Empty(); RemoveEmptySettings(BlueprintPath); SaveBlueprintEditorSettings(); } } FKismetDebugUtilities::FOnWatchedPinsListChanged FKismetDebugUtilities::WatchedPinsListChangedEvent; bool FKismetDebugUtilities::CanWatchPin(const UBlueprint* Blueprint, const UEdGraphPin* Pin, const TArray& InPathToProperty) { // Forward to schema if (const UEdGraphNode* Node = Pin->GetOwningNode()) { if (const UAnimationGraphSchema* AnimationGraphSchema = Cast(Node->GetSchema())) { // Anim graphs need to respect whether they have a binding as they are effectively unlinked bool bHasBinding = false; if (UAnimGraphNode_Base* AnimGraphNode = Cast(Pin->GetOwningNode())) { if(AnimGraphNode->HasBinding(Pin->GetFName())) { bHasBinding = true; } } UEdGraph* Graph = Pin->GetOwningNode()->GetGraph(); // We allow input pins to be watched only if they have bindings, otherwise we need to follow to output pins const bool bNotAnInputOrBound = (Pin->Direction != EGPD_Input) || bHasBinding; return !AnimationGraphSchema->IsMetaPin(*Pin) && bNotAnInputOrBound && !IsPinBeingWatched(Blueprint, Pin, InPathToProperty); } else if (const UEdGraphSchema_K2* K2Schema = Cast(Node->GetSchema())) { UEdGraph* Graph = Pin->GetOwningNode()->GetGraph(); // Inputs should always be followed to their corresponding output in the world above const bool bNotAnInput = (Pin->Direction != EGPD_Input); //@TODO: Make watching a schema-allowable/denyable thing const bool bCanWatchThisGraph = true; return bCanWatchThisGraph && !K2Schema->IsMetaPin(*Pin) && bNotAnInput && !IsPinBeingWatched(Blueprint, Pin, InPathToProperty); } } return false; } bool FKismetDebugUtilities::IsPinBeingWatched(const UBlueprint* Blueprint, const UEdGraphPin* Pin, const TArray& InPathToProperty) { if (TArray* WatchedPins = GetWatchedPins(Blueprint)) { for (const FBlueprintWatchedPin& WatchedPin : *WatchedPins) { if (WatchedPin.Get() == Pin && WatchedPin.GetPathToProperty() == InPathToProperty) { return true; } } } return false; } bool FKismetDebugUtilities::DoesPinHaveWatches(const UBlueprint* Blueprint, const UEdGraphPin* Pin) { if (TArray* WatchedPins = GetWatchedPins(Blueprint)) { for (const FBlueprintWatchedPin& WatchedPin : *WatchedPins) { if (WatchedPin.Get() == Pin) { return true; } } } return false; } bool FKismetDebugUtilities::RemovePinWatch(const UBlueprint* Blueprint, const UEdGraphPin* Pin, const TArray& InPathToProperty) { return RemovePinPropertyWatchesByPredicate( Blueprint, [Pin, &InPathToProperty](const FBlueprintWatchedPin& Other) { return Other.Get()->PinId == Pin->PinId && Other.GetPathToProperty() == InPathToProperty; } ); } void FKismetDebugUtilities::AddPinWatch(const UBlueprint* Blueprint, FBlueprintWatchedPin&& WatchedPin) { UBlueprintEditorSettings* Settings = GetMutableDefault(); check(Settings); FPerBlueprintSettings& BlueprintSettings = Settings->PerBlueprintSettings.FindOrAdd(Blueprint->GetPathName()); BlueprintSettings.WatchedPins.Emplace(MoveTemp(WatchedPin)); SaveBlueprintEditorSettings(); WatchedPinsListChangedEvent.Broadcast(const_cast(Blueprint)); } void FKismetDebugUtilities::TogglePinWatch(const UBlueprint* Blueprint, const UEdGraphPin* Pin) { if (IsPinBeingWatched(Blueprint, Pin)) { RemovePinWatch(Blueprint, Pin); } else { AddPinWatch(Blueprint, Pin); } } void FKismetDebugUtilities::ClearPinWatches(const UBlueprint* Blueprint) { UBlueprintEditorSettings* Settings = GetMutableDefault(); check(Settings); if(FPerBlueprintSettings *PerBlueprintSettings = Settings->PerBlueprintSettings.Find(Blueprint->GetPathName())) { PerBlueprintSettings->WatchedPins.Empty(); RemoveEmptySettings(Blueprint->GetPathName()); SaveBlueprintEditorSettings(); WatchedPinsListChangedEvent.Broadcast(const_cast(Blueprint)); } } bool FKismetDebugUtilities::BlueprintHasPinWatches(const UBlueprint* Blueprint) { return GetWatchedPins(Blueprint) != nullptr; } void FKismetDebugUtilities::ForeachPinWatch(const UBlueprint* Blueprint, TFunctionRef Task) { if(TArray* WatchedPins = GetWatchedPins(Blueprint)) { for(FBlueprintWatchedPin& WatchedPin : *WatchedPins) { if (UEdGraphPin* Pin = WatchedPin.Get()) { Task(Pin); } } } } void FKismetDebugUtilities::ForeachPinPropertyWatch(const UBlueprint* Blueprint, TFunctionRef Task) { if (TArray* WatchedPins = GetWatchedPins(Blueprint)) { for (FBlueprintWatchedPin& WatchedPin : *WatchedPins) { Task(WatchedPin); } } } bool FKismetDebugUtilities::RemovePinWatchesByPredicate(const UBlueprint* Blueprint, const TFunctionRef Predicate) { auto ModifiedPedicate = [Predicate](FBlueprintWatchedPin& WatchedPin) { const UEdGraphPin* Pin = WatchedPin.Get(); return Pin && Predicate(Pin); }; if(TArray* WatchedPins = GetWatchedPins(Blueprint)) { if(WatchedPins->RemoveAllSwap(ModifiedPedicate, EAllowShrinking::No)) { if(WatchedPins->IsEmpty()) { // keeps the ini file clean by removing empty arrays ClearPinWatches(Blueprint); } SaveBlueprintEditorSettings(); WatchedPinsListChangedEvent.Broadcast(const_cast(Blueprint)); return true; } } return false; } bool FKismetDebugUtilities::RemovePinPropertyWatchesByPredicate(const UBlueprint* Blueprint, const TFunctionRef Predicate) { auto ModifiedPedicate = [Predicate](FBlueprintWatchedPin& WatchedPin) { const UEdGraphPin* Pin = WatchedPin.Get(); return Pin && Predicate(WatchedPin); }; if (TArray* WatchedPins = GetWatchedPins(Blueprint)) { if (WatchedPins->RemoveAllSwap(ModifiedPedicate, EAllowShrinking::No)) { if (WatchedPins->IsEmpty()) { // keeps the ini file clean by removing empty arrays ClearPinWatches(Blueprint); } SaveBlueprintEditorSettings(); WatchedPinsListChangedEvent.Broadcast(const_cast(Blueprint)); return true; } } return false; } UEdGraphPin* FKismetDebugUtilities::FindPinWatchByPredicate(const UBlueprint* Blueprint, const TFunctionRef Predicate) { if(TArray* WatchedPins = GetWatchedPins(Blueprint)) { for(FBlueprintWatchedPin& WatchedPin : *WatchedPins) { UEdGraphPin* Pin = WatchedPin.Get(); if(Pin && Predicate(Pin)) { return Pin; } } } return nullptr; } // Gets the watched tooltip for a specified site FKismetDebugUtilities::EWatchTextResult FKismetDebugUtilities::GetWatchText(FString& OutWatchText, UBlueprint* Blueprint, UObject* ActiveObject, const UEdGraphPin* WatchPin) { const FProperty* PropertyToDebug = nullptr; const void* DataPtr = nullptr; const void* DeltaPtr = nullptr; UObject* ParentObj = nullptr; TArray SeenObjects; bool bIsDirectPtr = false; FKismetDebugUtilities::EWatchTextResult Result = FindDebuggingData(Blueprint, ActiveObject, WatchPin, PropertyToDebug, DataPtr, DeltaPtr, ParentObj, SeenObjects, &bIsDirectPtr); if (Result == FKismetDebugUtilities::EWatchTextResult::EWTR_Valid) { // If this came from an out parameter it isn't in a property container, so must be accessed directly if (bIsDirectPtr) { PropertyToDebug->ExportText_Direct(/*inout*/ OutWatchText, DataPtr, DeltaPtr, ParentObj, PPF_PropertyWindow | PPF_BlueprintDebugView); } else { PropertyToDebug->ExportText_InContainer(/*ArrayElement=*/ 0, /*inout*/ OutWatchText, DataPtr, DeltaPtr, /*Parent=*/ ParentObj, PPF_PropertyWindow | PPF_BlueprintDebugView); } } return Result; } bool FKismetDebugUtilities::CanInspectPinValue(const UEdGraphPin* Pin) { const UBlueprintEditorSettings* BlueprintEditorSettings = GetDefault(); if (!BlueprintEditorSettings->bEnablePinValueInspectionTooltips) { return false; } // Can't inspect the value on an invalid pin object. if (!Pin || Pin->IsPendingKill()) { return false; } // Can't inspect the value on an orphaned pin object. if (Pin->bOrphanedPin) { return false; } // Can't inspect the value on an unknown pin object or if the owning node is disabled. const UEdGraphNode* OwningNode = Pin->GetOwningNodeUnchecked(); if (!OwningNode || !OwningNode->IsNodeEnabled()) { return false; } // Can't inspect exec pins or delegate pins; their values are not defined. // Disallow non-K2 Schemas (like ControlRig) const UEdGraphSchema_K2* K2Schema = Cast(OwningNode->GetSchema()); if (!K2Schema || !K2Schema->CanShowDataTooltipForPin(*Pin)) { return false; } // Can't inspect the value if there is no active debug context. const UBlueprint* Blueprint = FBlueprintEditorUtils::FindBlueprintForNode(OwningNode); if (!Blueprint || !Blueprint->GetObjectBeingDebugged()) { return false; } // Can't inspect if a debug object isn't selected const UObject* Object = Blueprint->GetObjectBeingDebugged(); if (!Object) { return false; } // Can't inspect if not in PIE const UWorld* OwningWorld = Object->GetWorld(); if (!OwningWorld || !(OwningWorld->IsPlayInEditor() || OwningWorld->IsPreviewWorld())) { return false; } return true; } FKismetDebugUtilities::EWatchTextResult FKismetDebugUtilities::GetDebugInfo(TSharedPtr &OutDebugInfo, UBlueprint* Blueprint, UObject* ActiveObject, const UEdGraphPin* WatchPin) { const void* DataPtr = nullptr; const void* DeltaPtr = nullptr; UObject* ParentObj = nullptr; TArray SeenObjects; bool bIsDirectPtr = false; const FProperty* Property = nullptr; FKismetDebugUtilities::EWatchTextResult Result = FindDebuggingData(Blueprint, ActiveObject, WatchPin, Property, DataPtr, DeltaPtr, ParentObj, SeenObjects, &bIsDirectPtr); if (Result == FKismetDebugUtilities::EWatchTextResult::EWTR_Valid) { // If this came from an out parameter it isn't in a property container, so must be accessed directly if (bIsDirectPtr) { GetDebugInfoInternal(OutDebugInfo, Property, DataPtr); } else { GetDebugInfo_InContainer(0, OutDebugInfo, Property, DataPtr); } } return Result; } FKismetDebugUtilities::EWatchTextResult FKismetDebugUtilities::FindDebuggingData(UBlueprint* Blueprint, UObject* ActiveObject, const UEdGraphPin* WatchPin, const FProperty*& OutProperty, const void*& OutData, const void*& OutDelta, UObject*& OutParent, TArray& SeenObjects, bool* bOutIsDirectPtr /* = nullptr */) { FKismetDebugUtilitiesData& Data = FKismetDebugUtilitiesData::Get(); if (FProperty* Property = FKismetDebugUtilities::FindClassPropertyForPin(Blueprint, WatchPin)) { if (!Property->IsValidLowLevel()) { //@TODO: Temporary checks to attempt to determine intermittent unreproducable crashes in this function static bool bErrorOnce = true; if (bErrorOnce) { ensureMsgf(false, TEXT("Error: Invalid (but non-null) property associated with pin; cannot get variable value")); bErrorOnce = false; } return EWTR_NoProperty; } if (ActiveObject != nullptr) { if (!ActiveObject->IsValidLowLevel()) { //@TODO: Temporary checks to attempt to determine intermittent unreproducable crashes in this function static bool bErrorOnce = true; if (bErrorOnce) { ensureMsgf(false, TEXT("Error: Invalid (but non-null) active object being debugged; cannot get variable value for property %s"), *Property->GetPathName()); bErrorOnce = false; } return EWTR_NoDebugObject; } void* PropertyBase = nullptr; // Walk up the stack frame to see if we can find a function scope that contains the property as a local for (const FFrame* TestFrame = Data.StackFrameAtIntraframeDebugging; TestFrame != NULL; TestFrame = TestFrame->PreviousFrame) { if (Property->IsIn(TestFrame->Node)) { // output parameters need special handling for (FOutParmRec* OutParmRec = TestFrame->OutParms; OutParmRec != nullptr; OutParmRec = OutParmRec->NextOutParm) { if (OutParmRec->Property == Property) { if (WatchPin->Direction == EEdGraphPinDirection::EGPD_Input) { // try to use the output pin we're linked to // otherwise the output param won't show any data since the return node hasn't executed when we stop here if (WatchPin->LinkedTo.Num() == 1) { return FindDebuggingData(Blueprint, ActiveObject, WatchPin->LinkedTo[0], OutProperty, OutData, OutDelta, OutParent, SeenObjects, bOutIsDirectPtr); } else if (!WatchPin->LinkedTo.Num()) { // If this is an output pin with no links, then we have no debug data // so fallback to the local stack frame PropertyBase = TestFrame->Locals; } } if (PropertyBase == nullptr && bOutIsDirectPtr) { // Flag to caller that PropertyBase points directly at the data, not at the // base of a property container (so don't apply property's Offset_Internal) *bOutIsDirectPtr = true; PropertyBase = OutParmRec->PropAddr; } break; } } // Fallback to the local variables if we couldn't find one if (PropertyBase == nullptr) { PropertyBase = TestFrame->Locals; } break; } } // Try at member scope if it wasn't part of a current function scope UClass* PropertyClass = Property->GetOwner(); if (!PropertyBase && PropertyClass) { if (ActiveObject->GetClass()->IsChildOf(PropertyClass)) { PropertyBase = ActiveObject; } else if (AActor* Actor = Cast(ActiveObject)) { // Try and locate the propertybase in the actor components for (UActorComponent* ComponentIter : Actor->GetComponents()) { if (ComponentIter->GetClass()->IsChildOf(PropertyClass)) { PropertyBase = ComponentIter; break; } } } } // Try find the propertybase in the persistent ubergraph frame UFunction* OuterFunction = Property->GetOwner(); if (!PropertyBase && OuterFunction) { UBlueprintGeneratedClass* BPGC = Cast(Blueprint->GeneratedClass); if (BPGC && ActiveObject->IsA(BPGC)) { PropertyBase = BPGC->GetPersistentUberGraphFrame(ActiveObject, OuterFunction); } } // see if our WatchPin is on a animation node & if so try to get its property info const UAnimBlueprintGeneratedClass* AnimBlueprintGeneratedClass = Cast(Blueprint->GeneratedClass); // Use the root anim BP's debug data - derived anim BPs have empty debug data if(AnimBlueprintGeneratedClass) { if (UAnimBlueprint* AnimBlueprint = Cast(AnimBlueprintGeneratedClass->ClassGeneratedBy)) { if(UAnimBlueprint* RootAnimBP = UAnimBlueprint::FindRootAnimBlueprint(AnimBlueprint)) { AnimBlueprintGeneratedClass = RootAnimBP->GetAnimBlueprintGeneratedClass(); } } } if (!PropertyBase && AnimBlueprintGeneratedClass) { // are we linked to an anim graph node? UEdGraphPin* LinkedPin = nullptr; const FProperty* LinkedProperty = Property; const UAnimGraphNode_Base* Node = Cast(WatchPin->GetOuter()); if (Node == nullptr && WatchPin->LinkedTo.Num() > 0) { LinkedPin = WatchPin->LinkedTo[0]; // When we change Node we *must* change Property, so it's still a sub-element of that. LinkedProperty = FKismetDebugUtilities::FindClassPropertyForPin(Blueprint, LinkedPin); Node = Cast(LinkedPin->GetOuter()); } if (Node && LinkedProperty) { // In case the property was folded its value has to be retrieved from the Mutable data struct on the instance rather than from the Anim Node itself if (FProperty* const* FoldedPropertyPathPtr = AnimBlueprintGeneratedClass->AnimBlueprintDebugData.GraphPinToFoldedPropertyMap.Find(LinkedPin)) { const FProperty* FoldedProperty = *FoldedPropertyPathPtr; const UStruct* FoldedPropertyStruct = FoldedProperty ? FoldedProperty->GetOwnerStruct() : nullptr; if(FoldedPropertyStruct && FoldedPropertyStruct->IsChildOf(FAnimBlueprintMutableData::StaticStruct())) { const FAnimBlueprintMutableData* MutableData = AnimBlueprintGeneratedClass->GetMutableNodeData(Cast(ActiveObject)); if(bOutIsDirectPtr && MutableData) { const void* ValuePtr = FoldedProperty->ContainerPtrToValuePtr(MutableData); OutProperty = FoldedProperty; OutData = ValuePtr; OutDelta = ValuePtr; OutParent = nullptr; // Flag to caller that OutData points directly at the data, not at the // base of a property container (so don't apply property's Offset_Internal) *bOutIsDirectPtr = true; return EWTR_Valid; } } } else if (FStructProperty* NodeStructProperty = CastField(FKismetDebugUtilities::FindClassPropertyForNode(Blueprint, Node))) { for (const FStructPropertyPath& NodeProperty : AnimBlueprintGeneratedClass->GetAnimNodeProperties()) { if (NodeProperty.Get() == NodeStructProperty) { const void* NodePtr = NodeProperty->ContainerPtrToValuePtr(ActiveObject); OutProperty = LinkedProperty; OutData = NodePtr; OutDelta = NodePtr; OutParent = ActiveObject; return EWTR_Valid; } } } } } // If we still haven't found a result, try changing the active object to whatever is passed into the self pin. if (!PropertyBase) { UEdGraphNode* WatchNode = WatchPin->GetOwningNode(); if (WatchNode) { UEdGraphPin* SelfPin = WatchNode->FindPin(TEXT("self")); if (SelfPin && SelfPin != WatchPin) { const FProperty* SelfPinProperty = nullptr; const void* SelfPinData = nullptr; const void* SelfPinDelta = nullptr; UObject* SelfPinParent = nullptr; SeenObjects.AddUnique(ActiveObject); FKismetDebugUtilities::EWatchTextResult Result = FindDebuggingData(Blueprint, ActiveObject, SelfPin, SelfPinProperty, SelfPinData, SelfPinDelta, SelfPinParent, SeenObjects); const FObjectPropertyBase* SelfPinPropertyBase = CastField(SelfPinProperty); if (Result == EWTR_Valid && SelfPinPropertyBase != nullptr) { const void* PropertyValue = SelfPinProperty->ContainerPtrToValuePtr(SelfPinData); UObject* TempActiveObject = SelfPinPropertyBase->GetObjectPropertyValue(PropertyValue); if (TempActiveObject && TempActiveObject != ActiveObject) { if (!SeenObjects.Contains(TempActiveObject)) { return FindDebuggingData(Blueprint, TempActiveObject, WatchPin, OutProperty, OutData, OutDelta, OutParent, SeenObjects); } } } } } } // Now either print out the variable value, or that it was out-of-scope if (PropertyBase != nullptr) { OutProperty = Property; OutData = PropertyBase; OutDelta = PropertyBase; OutParent = ActiveObject; return EWTR_Valid; } else { return EWTR_NotInScope; } } else { return EWTR_NoDebugObject; } } else { return EWTR_NoProperty; } } void FKismetDebugUtilities::GetDebugInfo_InContainer(int32 Index, TSharedPtr &DebugInfo, const FProperty* Property, const void* Data) { GetDebugInfoInternal(DebugInfo, Property, Property->ContainerPtrToValuePtr(Data, Index)); } void FKismetDebugUtilities::GetDebugInfoInternal(TSharedPtr &DebugInfo, const FProperty* Property, const void* PropertyValue) { DebugInfo = FPropertyInstanceInfo::Make({Property, PropertyValue}, nullptr); } FPropertyInstanceInfo::FPropertyInstanceInfo(FPropertyInstance PropertyInstance, const TSharedPtr& InParent) : Name(FText::FromString(PropertyInstance.Property->GetName())), DisplayName(PropertyInstance.Property->GetDisplayNameText()), Type(UEdGraphSchema_K2::TypeToText(PropertyInstance.Property)), Property(PropertyInstance.Property), ValueAddress(PropertyInstance.Value), Parent(InParent) { const FProperty* ResolvedProperty = Property.Get(); check(ResolvedProperty); if (PropertyInstance.Value == nullptr) { return; } if (const FByteProperty* ByteProperty = CastField(ResolvedProperty)) { UEnum* Enum = ByteProperty->GetIntPropertyEnum(); if (Enum) { if (Enum->IsValidEnumValue(*(const uint8*)PropertyInstance.Value)) { Value = Enum->GetDisplayNameTextByValue(*(const uint8*)PropertyInstance.Value); } else { Value = FText::FromString(TEXT("(INVALID)")); } return; } // if there is no Enum we need to fall through and treat this as a FNumericProperty } if (const FNumericProperty* NumericProperty = CastField(ResolvedProperty)) { Value = FText::FromString(NumericProperty->GetNumericPropertyValueToString(PropertyInstance.Value)); return; } else if (const FBoolProperty* BoolProperty = CastField(ResolvedProperty)) { const FCoreTexts& CoreTexts = FCoreTexts::Get(); Value = BoolProperty->GetPropertyValue(PropertyInstance.Value) ? CoreTexts.True : CoreTexts.False; return; } else if (const FNameProperty* NameProperty = CastField(ResolvedProperty)) { Value = FText::FromName(*(FName*)PropertyInstance.Value); return; } else if (const FTextProperty* TextProperty = CastField(ResolvedProperty)) { Value = TextProperty->GetPropertyValue(PropertyInstance.Value); return; } else if (const FStrProperty* StringProperty = CastField(ResolvedProperty)) { Value = FText::FromString(StringProperty->GetPropertyValue(PropertyInstance.Value)); return; } else if (const FArrayProperty* ArrayProperty = CastField(ResolvedProperty)) { checkSlow(ArrayProperty->Inner); FScriptArrayHelper ArrayHelper(ArrayProperty, PropertyInstance.Value); Value = FText::Format(LOCTEXT("ArraySize", "Num={0}"), FText::AsNumber(ArrayHelper.Num())); return; } else if (const FStructProperty* StructProperty = CastField(ResolvedProperty)) { FString WatchText; StructProperty->ExportTextItem_Direct(WatchText, PropertyInstance.Value, PropertyInstance.Value, nullptr, PPF_PropertyWindow | PPF_BlueprintDebugView, nullptr); Value = FText::FromString(WatchText); return; } else if (const FEnumProperty* EnumProperty = CastField(ResolvedProperty)) { FNumericProperty* LocalUnderlyingProp = EnumProperty->GetUnderlyingProperty(); UEnum* Enum = EnumProperty->GetEnum(); int64 NumValue = LocalUnderlyingProp->GetSignedIntPropertyValue(PropertyInstance.Value); // if the value is the max value (the autogenerated *_MAX value), export as "INVALID", unless we're exporting text for copy/paste (for copy/paste, // the property text value must actually match an entry in the enum's names array) if (Enum) { if (Enum->IsValidEnumValue(NumValue)) { Value = Enum->GetDisplayNameTextByValue(NumValue); } else { Value = LOCTEXT("Invalid", "(INVALID)"); } } else { Value = FText::AsNumber(NumValue); } return; } else if (const FMapProperty* MapProperty = CastField(ResolvedProperty)) { FScriptMapHelper MapHelper(MapProperty, PropertyInstance.Value); Value = FText::Format(LOCTEXT("MapSize", "Num={0}"), FText::AsNumber(MapHelper.Num())); return; } else if (const FSetProperty* SetProperty = CastField(ResolvedProperty)) { FScriptSetHelper SetHelper(SetProperty, PropertyInstance.Value); Value = FText::Format(LOCTEXT("SetSize", "Num={0}"), FText::AsNumber(SetHelper.Num())); return; } else if (const FObjectPropertyBase* ObjectPropertyBase = CastField(ResolvedProperty)) { Object = ObjectPropertyBase->GetObjectPropertyValue(PropertyInstance.Value); if (Object.IsValid()) { Value = FText::FromString(Object->GetFullName()); } else { Value = FText::FromString(TEXT("None")); } return; } else if (const FDelegateProperty* DelegateProperty = CastField(ResolvedProperty)) { if (DelegateProperty->SignatureFunction) { Value = DelegateProperty->SignatureFunction->GetDisplayNameText(); } else { Value = LOCTEXT("NoFunc", "(No bound function)"); } return; } else if (const FMulticastDelegateProperty* MulticastDelegateProperty = CastField(ResolvedProperty)) { if (MulticastDelegateProperty->SignatureFunction) { Value = MulticastDelegateProperty->SignatureFunction->GetDisplayNameText(); } else { Value = LOCTEXT("NoFunc", "(No bound function)"); } return; } else if (const FInterfaceProperty* InterfaceProperty = CastField(ResolvedProperty)) { const FScriptInterface* InterfaceData = StaticCast(PropertyInstance.Value); Object = InterfaceData->GetObject(); if (Object.IsValid()) { Value = FText::FromString(Object->GetFullName()); } else { Value = FText::FromString(TEXT("None")); } return; } ensureMsgf(false, TEXT("Failed to identify property type. This function may need to be expanded to include it: %s"), *ResolvedProperty->GetClass()->GetName()); } TSharedPtr FPropertyInstanceInfo::Make(FPropertyInstance PropertyInstance, const TSharedPtr& Parent) { return MakeShared(PropertyInstance, Parent); } void FPropertyInstanceInfo::PopulateChildren(FPropertyInstance PropertyInstance) { check(PropertyInstance.Property); if (PropertyInstance.Value == nullptr) { return; } if (UObject* ResolvedObject = Object.Get()) { if (ResolvedObject->GetClass()->HasMetaDataHierarchical("DebugTreeLeaf")) { return; } } if (const FArrayProperty* ArrayProperty = CastField(Property.Get())) { checkSlow(ArrayProperty->Inner); FScriptArrayHelper ArrayHelper(ArrayProperty, PropertyInstance.Value); for (int32 i = 0; i < ArrayHelper.Num(); i++) { FPropertyInstance ChildProperty = { ArrayProperty->Inner, ArrayHelper.GetRawPtr(i) }; const TSharedPtr ChildInfo = Make(ChildProperty, AsShared()); // overwrite the display name with the array index for the current element ChildInfo->DisplayName = FText::Format(LOCTEXT("ArrayIndexName", "[{0}]"), FText::AsNumber(i)); ChildInfo->bIsInContainer = true; ChildInfo->ContainerIndex = i; Children.Add(ChildInfo); } } else if (const FStructProperty* StructProperty = CastField(Property.Get())) { for (TFieldIterator It(StructProperty->Struct); It; ++It) { FPropertyInstance ChildProperty = { *It, It->ContainerPtrToValuePtr(PropertyInstance.Value, 0) }; const TSharedPtr ChildInfo = Make(ChildProperty, AsShared()); Children.Add(ChildInfo); } } else if (const FMapProperty* MapProperty = CastField(Property.Get())) { FScriptMapHelper MapHelper(MapProperty, PropertyInstance.Value); for (FScriptMapHelper::FIterator Itr = MapHelper.CreateIterator(); Itr; ++Itr) { uint8* KeyData = MapHelper.GetKeyPtr(Itr); uint8* ValData = MapHelper.GetValuePtr(Itr); FPropertyInstance ChildProperty = { MapProperty->ValueProp, ValData }; const TSharedPtr ChildInfo = Make(ChildProperty, AsShared()); FString NameStr = TEXT("["); MapProperty->KeyProp->ExportTextItem_Direct( NameStr, KeyData, nullptr, nullptr, PPF_PropertyWindow | PPF_BlueprintDebugView | PPF_Delimited, nullptr ); NameStr += TEXT("] "); ChildInfo->DisplayName = FText::FromString(NameStr); ChildInfo->bIsInContainer = true; ChildInfo->ContainerIndex = Itr.GetLogicalIndex(); Children.Add(ChildInfo); } } else if (const FSetProperty* SetProperty = CastField(Property.Get())) { FScriptSetHelper SetHelper(SetProperty, PropertyInstance.Value); for (FScriptSetHelper::FIterator Itr = SetHelper.CreateIterator(); Itr; ++Itr) { uint8* PropData = SetHelper.GetElementPtr(Itr); FPropertyInstance ChildProperty = { SetProperty->ElementProp, PropData }; const TSharedPtr ChildInfo = Make(ChildProperty, AsShared()); ChildInfo->DisplayName = FText::Format(LOCTEXT("SetIndexName", "[{0}]"), FText::AsNumber(Itr.GetLogicalIndex())); ChildInfo->bIsInContainer = true; ChildInfo->ContainerIndex = Itr.GetLogicalIndex(); Children.Add(ChildInfo); } } else if (const FObjectPropertyBase* ObjectPropertyBase = CastField(Property.Get())) { if (UObject* Obj = ObjectPropertyBase->GetObjectPropertyValue(PropertyInstance.Value)) { for (TFieldIterator It(Obj->GetClass()); It; ++It) { if(It->HasAllPropertyFlags(CPF_BlueprintVisible)) { FPropertyInstance ChildProperty = { *It, It->ContainerPtrToValuePtr(Obj) }; const TSharedPtr ChildInfo = Make(ChildProperty, AsShared()); Children.Add(ChildInfo); } } } } } TSharedPtr FPropertyInstanceInfo::ResolvePathToProperty(const TArray& InPathToProperty) { const auto FindChildInPropertyInfo = [](FPropertyInstanceInfo* InPropertyInfo, const FName& InChildName) -> TSharedPtr* { if (InPropertyInfo) { const FString ChildNameStr = InChildName.ToString(); const FProperty* Prop = InPropertyInfo->Property.Get(); if (Prop->IsA() || Prop->IsA() || Prop->IsA()) { return InPropertyInfo->Children.FindByPredicate( [&ChildNameStr](const TSharedPtr& Child) { // Display name for Container Element Properties is just their index return Child->DisplayName.ToString() == ChildNameStr; } ); } else { return InPropertyInfo->Children.FindByPredicate( [&ChildNameStr](const TSharedPtr& Child) { return Child->Property->GetAuthoredName() == ChildNameStr; } ); } } return nullptr; }; TSharedPtr ThisDebugInfo = nullptr; for (const FName& ChildName : InPathToProperty) { TSharedPtr* FoundChild = ThisDebugInfo.IsValid() ? FindChildInPropertyInfo(ThisDebugInfo.Get(), ChildName) : FindChildInPropertyInfo(this, ChildName); if (FoundChild) { ThisDebugInfo = *FoundChild; } else { return nullptr; } } return ThisDebugInfo; } FString FPropertyInstanceInfo::GetWatchText() const { const FProperty* Prop = Property.Get(); if (Prop) { if (Prop->IsA() || Prop->IsA() || Prop->IsA()) { FString WatchText; for (const TSharedPtr& ContainerElement : Children) { WatchText.Append(FText::Format(LOCTEXT("WatchTextFmt", "{0} {1}\n"), ContainerElement->DisplayName, ContainerElement->Value).ToString()); } return WatchText; } } return Value.ToString(); } static bool IsAContainer(const FProperty* Property) { return Property->IsA() || Property->IsA() || Property->IsA() || Property->IsA() || Property->IsA(); } const TArray>& FPropertyInstanceInfo::GetChildren() { if (Children.IsEmpty() && IsAContainer(Property.Get())) { // Children haven't been generated yet. Generate them. const FPropertyInstance PropertyInstance = GetPropertyInstance(); if (PropertyInstance.Value != nullptr) { TMap> VisitedNodes; PopulateChildren(PropertyInstance); } } return Children; } FPropertyInstanceInfo::FPropertyInstance FPropertyInstanceInfo::GetPropertyInstance() const { // reiterate the property tree to rebuild data pointer in case it was invalidated TArray Path; const FPropertyInstanceInfo* Itr = this; while (const FPropertyInstanceInfo* ItrParent = Itr->Parent.Pin().Get()) { Path.Add(Itr); Itr = ItrParent; } Path.Add(Itr); Algo::Reverse(Path); const void* Data = Itr->ValueAddress; for (int I = 1; I < Path.Num() && Data != nullptr; ++I) { const FPropertyInstanceInfo* ParentInfo = Path[I - 1]; const FPropertyInstanceInfo* Info = Path[I]; if (const FArrayProperty* ArrayProperty = CastField(ParentInfo->Property.Get())) { FScriptArrayHelper ArrayHelper(ArrayProperty, Data); const int32 Index = Info->ContainerIndex; if (ArrayHelper.IsValidIndex(Index)) { Data = ArrayHelper.GetRawPtr(Index); } else { Data = nullptr; break; } } else if (const FSetProperty* SetProperty = CastField(ParentInfo->Property.Get())) { FScriptSetHelper SetHelper(SetProperty, Data); const int32 Index = Info->ContainerIndex; Data = SetHelper.FindNthElementPtr(Index); } else if (const FMapProperty* MapProperty = CastField(ParentInfo->Property.Get())) { FScriptMapHelper MapHelper(MapProperty, Data); const int32 Index = Info->ContainerIndex; Data = MapHelper.FindNthValuePtr(Index); } else if (const FStructProperty* StructProperty = CastField(ParentInfo->Property.Get())) { Data = Info->Property->ContainerPtrToValuePtr(Data); } else if (const FObjectPropertyBase* ObjectPropertyBase = CastField(ParentInfo->Property.Get())) { if (UObject* Obj = ObjectPropertyBase->GetObjectPropertyValue(Data)) { if (!Obj->IsA(Info->Property->GetOwner())) { // This node is invalid. (this can happen if it was removed from an array, map, or set) Data = nullptr; break; } Data = Info->Property->ContainerPtrToValuePtr(Obj); } } if (Data != Info->ValueAddress) { // This node is invalid. (this can happen if it was removed from an array, map, or set) Data = nullptr; break; } } return Data ? FPropertyInstance{Property.Get(), Data} : FPropertyInstance{nullptr, nullptr}; } FText FKismetDebugUtilities::GetAndClearLastExceptionMessage() { FKismetDebugUtilitiesData& Data = FKismetDebugUtilitiesData::Get(); const FText Result = Data.LastExceptionMessage; Data.LastExceptionMessage = FText(); return Result; } #undef LOCTEXT_NAMESPACE