// Copyright 2022 (c) Microsoft. All rights reserved. #include "VisualStudioBlueprintDebuggerHelperModule.h" #include #include #include #include #include #include #include #include #include #include #include #if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 4 #include #endif #include #include #include #include #include #include #include #include IMPLEMENT_MODULE(FVisualStudioBlueprintDebuggerHelper, VisualStudioBlueprintDebuggerHelper); DEFINE_LOG_CATEGORY(LogVisualStudioBlueprintDebuggerHelper); #if ENGINE_MAJOR_VERSION >= 5 #define FCustomBlueprintPropertyInfo TSharedPtr #else #define FCustomBlueprintPropertyInfo FDebugInfo #endif struct FVSNodePinRuntimeInformation { UEdGraphPin* Pin; FCustomBlueprintPropertyInfo Property; FVSNodePinRuntimeInformation(UEdGraphPin* InPin, FCustomBlueprintPropertyInfo InProperty) : Pin(InPin) , Property(InProperty) { } }; struct FVSNodeData { FText NodeName; TArray> Properties; int32 ScriptEntryTag; const UEdGraphNode* Node; }; struct FVSNodesRuntimeInformation { TArray> Nodes; }; struct FVSBlueprintRuntimeInformation { TArray>> RunningBlueprints; }; struct StackTraceHelper { int32 ScriptEntryTag; FString NodeName; }; // Keep exported so we can read it. VISUALSTUDIOBLUEPRINTDEBUGGERHELPER_API FVSBlueprintRuntimeInformation BlueprintsRuntimeInformation; VISUALSTUDIOBLUEPRINTDEBUGGERHELPER_API std::map StackFrameInformation; VISUALSTUDIOBLUEPRINTDEBUGGERHELPER_API const char* DebuggerHelperVersion = "1.0.0"; void FVisualStudioBlueprintDebuggerHelper::StartupModule() { CurrentScriptEntryTag = 0; FBlueprintContextTracker::OnEnterScriptContext.AddRaw( this, &FVisualStudioBlueprintDebuggerHelper::OnEnterScriptContext); FBlueprintContextTracker::OnExitScriptContext.AddRaw( this, &FVisualStudioBlueprintDebuggerHelper::OnExitScriptContext); FBlueprintCoreDelegates::OnScriptException.AddRaw( this, &FVisualStudioBlueprintDebuggerHelper::OnScriptException); } void FVisualStudioBlueprintDebuggerHelper::ShutdownModule() { FBlueprintCoreDelegates::OnScriptException.RemoveAll(this); FBlueprintContextTracker::OnExitScriptContext.RemoveAll(this); FBlueprintContextTracker::OnEnterScriptContext.RemoveAll(this); } void FVisualStudioBlueprintDebuggerHelper::OnEnterScriptContext( const struct FBlueprintContextTracker& Context, const UObject* SourceObject, const UFunction* Function) { if (!IsInGameThread()) { return; } CurrentScriptEntryTag = Context.GetScriptEntryTag(); } void FVisualStudioBlueprintDebuggerHelper::OnExitScriptContext(const struct FBlueprintContextTracker& Context) { if (!IsInGameThread()) { return; } for (auto ItRunningBlueprints = BlueprintsRuntimeInformation.RunningBlueprints.CreateIterator(); ItRunningBlueprints; ++ItRunningBlueprints) { auto& RunningBlueprint = ItRunningBlueprints->Value; for (auto ItNodeData = RunningBlueprint->Nodes.CreateIterator(); ItNodeData; ++ItNodeData) { if ((*ItNodeData)->ScriptEntryTag == Context.GetScriptEntryTag()) { ItNodeData.RemoveCurrent(); } } if (!RunningBlueprint->Nodes.Num()) { ItRunningBlueprints.RemoveCurrent(); } } for (auto ItStackFrameInfo = StackFrameInformation.begin(); ItStackFrameInfo != StackFrameInformation.end();) { if (ItStackFrameInfo->second.ScriptEntryTag == Context.GetScriptEntryTag()) { ItStackFrameInfo = StackFrameInformation.erase(ItStackFrameInfo); } else { ++ItStackFrameInfo; } } CurrentScriptEntryTag--; } void FVisualStudioBlueprintDebuggerHelper::OnScriptException( const UObject* Owner, const struct FFrame& Stack, const FBlueprintExceptionInfo& ExceptionInfo) { EBlueprintExceptionType::Type ExceptionType = ExceptionInfo.GetType(); if (ExceptionType != EBlueprintExceptionType::Type::Tracepoint && ExceptionType != EBlueprintExceptionType::Type::WireTracepoint && ExceptionType != EBlueprintExceptionType::Type::Breakpoint) { return; } UFunction* NodeFunction = Cast(Stack.Node); if (!NodeFunction) { return; } UBlueprintGeneratedClass* BlueprintGeneratedClass = Cast(NodeFunction->GetOuter()); if (!BlueprintGeneratedClass) { return; } UBlueprint* Blueprint = Cast(BlueprintGeneratedClass->ClassGeneratedBy); if (!Blueprint) { return; } const int32 BreakpointOffset = Stack.Code - Stack.Node->Script.GetData() - 1; const UEdGraphNode* NodeStoppedAt = FKismetDebugUtilities::FindSourceNodeForCodeLocation(Owner, Stack.Node, BreakpointOffset, /*bAllowImpreciseHit=*/ true); if (!NodeStoppedAt) { return; } StackFrameInformation[NodeFunction] = { CurrentScriptEntryTag, FString::Printf(TEXT("%s::%s"), *Blueprint->GetFriendlyName(), *NodeStoppedAt->GetNodeTitle(ENodeTitleType::Type::ListView).ToString()) }; TTuple>* ExistingNodesRuntimeInformationTuple = BlueprintsRuntimeInformation.RunningBlueprints.FindByPredicate([&Blueprint](const TTuple>& Tuple) { return Tuple.Key == Blueprint; }); TSharedPtr NodesRuntimeInformation; if (!ExistingNodesRuntimeInformationTuple) { NodesRuntimeInformation = MakeShared(); BlueprintsRuntimeInformation.RunningBlueprints.Add(MakeTuple(Blueprint, NodesRuntimeInformation)); } else { NodesRuntimeInformation = ExistingNodesRuntimeInformationTuple->Value; } TSharedPtr CurrentNodeData; if (NodesRuntimeInformation->Nodes.Num() == 0 || NodeStoppedAt != NodesRuntimeInformation->Nodes.Top()->Node) { CurrentNodeData = MakeShared(); CurrentNodeData->Node = NodeStoppedAt; CurrentNodeData->NodeName = NodeStoppedAt->GetNodeTitle(ENodeTitleType::Type::ListView); CurrentNodeData->ScriptEntryTag = CurrentScriptEntryTag; NodesRuntimeInformation->Nodes.Push(CurrentNodeData); } else { CurrentNodeData = NodesRuntimeInformation->Nodes.Top(); } FCustomBlueprintPropertyInfo PinInstanceInfo; for (auto GraphPin : NodeStoppedAt->Pins) { FKismetDebugUtilities::EWatchTextResult DebugResult = FKismetDebugUtilities::GetDebugInfo(PinInstanceInfo, Blueprint, (UObject*)Owner, GraphPin); if (DebugResult != FKismetDebugUtilities::EWTR_Valid) { continue; } TSharedPtr* Existing = CurrentNodeData->Properties.FindByPredicate([&GraphPin](TSharedPtr& PinInfo) { return PinInfo->Pin == GraphPin; }); if (!Existing) { CurrentNodeData->Properties.Add(MakeShared(GraphPin, PinInstanceInfo)); } else { (*Existing)->Property = PinInstanceInfo; } } }