IntialCommit

This commit is contained in:
2025-07-14 00:29:30 -04:00
commit 32dacbe27b
91 changed files with 4368 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
[*.{cpp,h}]
# Naming convention rules (note: currently need to be ordered from more to less specific)
cpp_naming_rule.aactor_prefixed.symbols = aactor_class
cpp_naming_rule.aactor_prefixed.style = aactor_style
cpp_naming_rule.swidget_prefixed.symbols = swidget_class
cpp_naming_rule.swidget_prefixed.style = swidget_style
cpp_naming_rule.uobject_prefixed.symbols = uobject_class
cpp_naming_rule.uobject_prefixed.style = uobject_style
cpp_naming_rule.booleans_prefixed.symbols = boolean_vars
cpp_naming_rule.booleans_prefixed.style = boolean_style
cpp_naming_rule.structs_prefixed.symbols = structs
cpp_naming_rule.structs_prefixed.style = unreal_engine_structs
cpp_naming_rule.enums_prefixed.symbols = enums
cpp_naming_rule.enums_prefixed.style = unreal_engine_enums
cpp_naming_rule.templates_prefixed.symbols = templates
cpp_naming_rule.templates_prefixed.style = unreal_engine_templates
cpp_naming_rule.general_names.symbols = all_symbols
cpp_naming_rule.general_names.style = unreal_engine_default
# Naming convention symbols
cpp_naming_symbols.aactor_class.applicable_kinds = class
cpp_naming_symbols.aactor_class.applicable_type = AActor
cpp_naming_symbols.swidget_class.applicable_kinds = class
cpp_naming_symbols.swidget_class.applicable_type = SWidget
cpp_naming_symbols.uobject_class.applicable_kinds = class
cpp_naming_symbols.uobject_class.applicable_type = UObject
cpp_naming_symbols.boolean_vars.applicable_kinds = local,parameter,field
cpp_naming_symbols.boolean_vars.applicable_type = bool
cpp_naming_symbols.enums.applicable_kinds = enum
cpp_naming_symbols.templates.applicable_kinds = template_class
cpp_naming_symbols.structs.applicable_kinds = struct
cpp_naming_symbols.all_symbols.applicable_kinds = *
# Naming convention styles
cpp_naming_style.unreal_engine_default.capitalization = pascal_case
cpp_naming_style.unreal_engine_default.required_prefix =
cpp_naming_style.unreal_engine_default.required_suffix =
cpp_naming_style.unreal_engine_default.word_separator =
cpp_naming_style.unreal_engine_enums.capitalization = pascal_case
cpp_naming_style.unreal_engine_enums.required_prefix = E
cpp_naming_style.unreal_engine_enums.required_suffix =
cpp_naming_style.unreal_engine_enums.word_separator =
cpp_naming_style.unreal_engine_templates.capitalization = pascal_case
cpp_naming_style.unreal_engine_templates.required_prefix = T
cpp_naming_style.unreal_engine_templates.required_suffix =
cpp_naming_style.unreal_engine_templates.word_separator =
cpp_naming_style.unreal_engine_structs.capitalization = pascal_case
cpp_naming_style.unreal_engine_structs.required_prefix = F
cpp_naming_style.unreal_engine_structs.required_suffix =
cpp_naming_style.unreal_engine_structs.word_separator =
cpp_naming_style.uobject_style.capitalization = pascal_case
cpp_naming_style.uobject_style.required_prefix = U
cpp_naming_style.uobject_style.required_suffix =
cpp_naming_style.uobject_style.word_separator =
cpp_naming_style.aactor_style.capitalization = pascal_case
cpp_naming_style.aactor_style.required_prefix = A
cpp_naming_style.aactor_style.required_suffix =
cpp_naming_style.aactor_style.word_separator =
cpp_naming_style.swidget_style.capitalization = pascal_case
cpp_naming_style.swidget_style.required_prefix = S
cpp_naming_style.swidget_style.required_suffix =
cpp_naming_style.swidget_style.word_separator =
cpp_naming_style.boolean_style.capitalization = pascal_case
cpp_naming_style.boolean_style.required_prefix = b
cpp_naming_style.boolean_style.required_suffix =
cpp_naming_style.boolean_style.word_separator =

View File

@@ -0,0 +1,246 @@
// Copyright 2022 (c) Microsoft. All rights reserved.
#include "VisualStudioBlueprintDebuggerHelperModule.h"
#include <Modules/ModuleManager.h>
#include <UObject/Script.h>
#include <UObject/Stack.h>
#include <UObject/Object.h>
#include <Templates/Casts.h>
#include <Kismet2/KismetDebugUtilities.h>
#include <Containers/Array.h>
#include <UObject/Class.h>
#include <Engine/BlueprintGeneratedClass.h>
#include <Engine/Blueprint.h>
#include <Runtime/Launch/Resources/Version.h>
#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 4
#include <Blueprint/BlueprintExceptionInfo.h>
#endif
#include <Internationalization/Text.h>
#include <HAL/Platform.h>
#include <EdGraph/EdGraphNode.h>
#include <EdGraph/EdGraphPin.h>
#include <Templates/SharedPointer.h>
#include <Templates/Tuple.h>
#include <CoreGlobals.h>
#include <map>
IMPLEMENT_MODULE(FVisualStudioBlueprintDebuggerHelper, VisualStudioBlueprintDebuggerHelper);
DEFINE_LOG_CATEGORY(LogVisualStudioBlueprintDebuggerHelper);
#if ENGINE_MAJOR_VERSION >= 5
#define FCustomBlueprintPropertyInfo TSharedPtr<FPropertyInstanceInfo>
#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<TSharedPtr<FVSNodePinRuntimeInformation>> Properties;
int32 ScriptEntryTag;
const UEdGraphNode* Node;
};
struct FVSNodesRuntimeInformation
{
TArray<TSharedPtr<FVSNodeData>> Nodes;
};
struct FVSBlueprintRuntimeInformation
{
TArray<TTuple<UBlueprint*, TSharedPtr<FVSNodesRuntimeInformation>>> RunningBlueprints;
};
struct StackTraceHelper
{
int32 ScriptEntryTag;
FString NodeName;
};
// Keep exported so we can read it.
VISUALSTUDIOBLUEPRINTDEBUGGERHELPER_API FVSBlueprintRuntimeInformation BlueprintsRuntimeInformation;
VISUALSTUDIOBLUEPRINTDEBUGGERHELPER_API std::map<void*, StackTraceHelper> 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<UFunction>(Stack.Node);
if (!NodeFunction)
{
return;
}
UBlueprintGeneratedClass* BlueprintGeneratedClass = Cast<UBlueprintGeneratedClass>(NodeFunction->GetOuter());
if (!BlueprintGeneratedClass)
{
return;
}
UBlueprint* Blueprint = Cast<UBlueprint>(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<UBlueprint*, TSharedPtr<FVSNodesRuntimeInformation>>* ExistingNodesRuntimeInformationTuple = BlueprintsRuntimeInformation.RunningBlueprints.FindByPredicate([&Blueprint](const TTuple<UBlueprint*, TSharedPtr<FVSNodesRuntimeInformation>>& Tuple) {
return Tuple.Key == Blueprint;
});
TSharedPtr<FVSNodesRuntimeInformation> NodesRuntimeInformation;
if (!ExistingNodesRuntimeInformationTuple)
{
NodesRuntimeInformation = MakeShared<FVSNodesRuntimeInformation>();
BlueprintsRuntimeInformation.RunningBlueprints.Add(MakeTuple(Blueprint, NodesRuntimeInformation));
}
else
{
NodesRuntimeInformation = ExistingNodesRuntimeInformationTuple->Value;
}
TSharedPtr<FVSNodeData> CurrentNodeData;
if (NodesRuntimeInformation->Nodes.Num() == 0 || NodeStoppedAt != NodesRuntimeInformation->Nodes.Top()->Node)
{
CurrentNodeData = MakeShared<FVSNodeData>();
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<FVSNodePinRuntimeInformation>* Existing = CurrentNodeData->Properties.FindByPredicate([&GraphPin](TSharedPtr<FVSNodePinRuntimeInformation>& PinInfo) {
return PinInfo->Pin == GraphPin;
});
if (!Existing)
{
CurrentNodeData->Properties.Add(MakeShared<FVSNodePinRuntimeInformation>(GraphPin, PinInstanceInfo));
}
else
{
(*Existing)->Property = PinInstanceInfo;
}
}
}

View File

@@ -0,0 +1,29 @@
// Copyright 2022 (c) Microsoft. All rights reserved.
#pragma once
#include <CoreMinimal.h>
#include <Modules/ModuleInterface.h>
#include <Modules/ModuleManager.h>
#include <UObject/Script.h>
#include <UObject/Stack.h>
#include <UObject/Object.h>
#include <Logging/LogMacros.h>
#include <UObject/Class.h>
#include <HAL/Platform.h>
DECLARE_LOG_CATEGORY_EXTERN(LogVisualStudioBlueprintDebuggerHelper, Log, All);
class FVisualStudioBlueprintDebuggerHelper : public FDefaultModuleImpl
{
private:
void OnScriptException(const UObject* Owner, const struct FFrame& Stack, const FBlueprintExceptionInfo& ExceptionInfo);
void OnEnterScriptContext(const struct FBlueprintContextTracker& Context, const UObject* SourceObject, const UFunction* Function);
void OnExitScriptContext(const struct FBlueprintContextTracker& Context);
int32 CurrentScriptEntryTag;
public:
void StartupModule() override;
void ShutdownModule() override;
};

View File

@@ -0,0 +1,31 @@
// Copyright 2022 (c) Microsoft. All rights reserved.
using UnrealBuildTool;
public class VisualStudioBlueprintDebuggerHelper: ModuleRules
{
public VisualStudioBlueprintDebuggerHelper(ReadOnlyTargetRules Target) : base(Target)
{
OptimizeCode = CodeOptimization.Never;
PrivateDependencyModuleNames.AddRange(new string[] {
"Core",
"ApplicationCore",
"AssetRegistry",
"CoreUObject",
"Engine",
"Json",
"JsonUtilities",
"Kismet",
"UnrealEd",
"Slate",
"SlateCore",
"ToolMenus",
"EditorSubsystem",
"MainFrame",
"BlueprintGraph",
"VisualStudioDTE",
"EditorStyle",
"Projects"
});
}
}

View File

@@ -0,0 +1,113 @@
// Copyright 2022 (c) Microsoft. All rights reserved.
// Licensed under the MIT License.
#include "BlueprintAssetHelpers.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "Blueprint/BlueprintSupport.h"
#include "Engine/BlueprintCore.h"
#include "Engine/BlueprintGeneratedClass.h"
#include "Engine/Engine.h"
#include "Engine/StreamableManager.h"
#include "Misc/ScopeExit.h"
#include "VisualStudioTools.h"
namespace VisualStudioTools
{
namespace AssetHelpers
{
/*
* These helpers handle the usage of some APIs that were deprecated in 5.1
* but the replacements are not available in older versions.
* Might be overridden by the `Build.cs` rules
*/
#if FILTER_ASSETS_BY_CLASS_PATH
void SetBlueprintClassFilter(FARFilter& InOutFilter)
{
// UE5.1 deprecated the API to filter using class names
InOutFilter.ClassPaths.Add(UBlueprintCore::StaticClass()->GetClassPathName());
}
static FString GetObjectPathString(const FAssetData& InAssetData)
{
// UE5.1 deprecated 'FAssetData::ObjectPath' in favor of 'FAssetData::GetObjectPathString()'
return InAssetData.GetObjectPathString();
}
#else // FILTER_ASSETS_BY_CLASS_PATH
void SetBlueprintClassFilter(FARFilter& InOutFilter)
{
InOutFilter.ClassNames.Add(UBlueprintCore::StaticClass()->GetFName());
}
static FString GetObjectPathString(const FAssetData& InAssetData)
{
return InAssetData.ObjectPath.ToString();
}
#endif // FILTER_ASSETS_BY_CLASS_PATH
void ForEachAsset(
const TArray<FAssetData>& TargetAssets,
TFunctionRef<void(UBlueprintGeneratedClass*, const FAssetData& AssetData)> Callback)
{
// Show a simpler logging output.
// LogTimes are still useful to tell how long it takes to process each asset.
TGuardValue<bool> DisableLogVerbosity(GPrintLogVerbosity, false);
TGuardValue<bool> DisableLogCategory(GPrintLogCategory, false);
// We're about to load the assets which might trigger a ton of log messages
// Temporarily suppress them during this stage.
GEngine->Exec(nullptr, TEXT("log LogVisualStudioTools only"));
ON_SCOPE_EXIT
{
GEngine->Exec(nullptr, TEXT("log reset"));
};
FStreamableManager AssetLoader;
for (int32 Idx = 0; Idx < TargetAssets.Num(); Idx++)
{
const FAssetData AssetData = TargetAssets[Idx];
FSoftClassPath GenClassPath = AssetData.GetTagValueRef<FString>(FBlueprintTags::GeneratedClassPath);
UE_LOG(LogVisualStudioTools, Display, TEXT("Processing blueprints [%d/%d]: %s"), Idx + 1, TargetAssets.Num(), *GenClassPath.ToString());
TSharedPtr<FStreamableHandle> Handle = AssetLoader.RequestSyncLoad(GenClassPath);
ON_SCOPE_EXIT
{
// We're done, notify an unload.
Handle->ReleaseHandle();
};
if (!Handle.IsValid())
{
UE_LOG(LogVisualStudioTools, Warning, TEXT("Failed to get a streamable handle for Blueprint. Skipping. GenClassPath: %s"), *GenClassPath.ToString());
continue;
}
if (auto BlueprintGeneratedClass = Cast<UBlueprintGeneratedClass>(Handle->GetLoadedAsset()))
{
Callback(BlueprintGeneratedClass, AssetData);
}
else
{
// Log some extra information to help the user understand why the asset failed to load.
FString ObjectPathString = AssetHelpers::GetObjectPathString(AssetData);
FString Msg = !GenClassPath.ToString().Contains(ObjectPathString)
? FString::Printf(
TEXT("ObjectPath is not compatible with GenClassPath, consider re-saving it to avoid future issues. { ObjectPath: %s, GenClassPath: %s }"),
*ObjectPathString,
*GenClassPath.ToString())
: FString::Printf(TEXT("ClassPath: %s"), *GenClassPath.ToString());
UE_LOG(LogVisualStudioTools, Warning, TEXT("Failed to load Blueprint. Skipping. %s"), *Msg);
}
}
}
}
}

View File

@@ -0,0 +1,26 @@
// Copyright 2022 (c) Microsoft. All rights reserved.
// Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
class UBlueprintGeneratedClass;
namespace VisualStudioTools
{
namespace AssetHelpers
{
void SetBlueprintClassFilter(FARFilter& InOutFilter);
/**
* Loads each blueprint asset and invokes the callback with the resulting blueprint generated class.
* Each iteration will load the asset using a FStreamableHandle and verify that is a valid blueprint
* before invoking the callback.
*/
void ForEachAsset(
const TArray<FAssetData>& TargetAssets,
TFunctionRef<void(UBlueprintGeneratedClass*, const FAssetData& AssetData)> Callback);
} // namespace AssetHelpers
} // namespace VisualStudioTools

View File

@@ -0,0 +1,248 @@
// Copyright 2022 (c) Microsoft. All rights reserved.
// Licensed under the MIT License.
#include "BlueprintReferencesCommandlet.h"
#include "Algo/Find.h"
#include "Algo/Transform.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "BlueprintAssetHelpers.h"
#include "Engine/BlueprintGeneratedClass.h"
#include "FindInBlueprintManager.h"
#include "JsonObjectConverter.h"
#include "Misc/Paths.h"
#include "Misc/ScopeExit.h"
#include "Policies/CondensedJsonPrintPolicy.h"
#include "VisualStudioTools.h"
namespace VisualStudioTools
{
static FString StripClassPrefix(const FString& InClassName)
{
if (InClassName.IsEmpty())
{
return InClassName;
}
size_t PrefixSize = 0;
const TCHAR ClassPrefixChar = InClassName[0];
switch (ClassPrefixChar)
{
case TEXT('I'):
case TEXT('A'):
case TEXT('U'):
// If it is a class prefix, check for deprecated class prefix also
if (InClassName.Len() > 12 && FCString::Strncmp(&(InClassName[1]), TEXT("DEPRECATED_"), 11) == 0)
{
PrefixSize = 12;
}
else
{
PrefixSize = 1;
}
break;
case TEXT('F'):
case TEXT('T'):
// Struct prefixes are also fine.
PrefixSize = 1;
break;
default:
PrefixSize = 0;
break;
}
return InClassName.RightChop(PrefixSize);
}
/**
* Retrieves the asset data matching the given FindInBlueprints query.
*/
TArray<FAssetData> SearchForCandidateAssets(const FString& SearchQuery)
{
IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")).Get();
AssetRegistry.SearchAllAssets(true);
TArray<FSearchResult> OutItemsFound;
FStreamSearch StreamSearch(SearchQuery);
while (!StreamSearch.IsComplete())
{
FFindInBlueprintSearchManager::Get().Tick(0.0);
}
// Execute the search and get all the assets in the result.
StreamSearch.GetFilteredItems(OutItemsFound);
TArray<FAssetData> OutTargetAssets;
Algo::Transform(OutItemsFound, OutTargetAssets,
[&](const FSearchResult& Item)
{
// The DisplayText property of the result contains the blueprint's object path
// Use that to find the respective asset in the registry
#if FILTER_ASSETS_BY_CLASS_PATH
return AssetRegistry.GetAssetByObjectPath(FSoftObjectPath(*Item->DisplayText.ToString()));
#else
return AssetRegistry.GetAssetByObjectPath(*Item->DisplayText.ToString());
#endif // FILTER_ASSETS_BY_CLASS_PATH
});
return OutTargetAssets;
}
/**
* Loads each blueprint asset and filters the collection to items which use the
* target UFunction in their call graph, matching the native class and function names.
*/
TMap<FString, FAssetData> GetConfirmedAssets(
const FString& FunctionName, const FString& ClassNameWithoutPrefix, const TArray<FAssetData>& InAssets)
{
TMap<FString, FAssetData> OutResults;
AssetHelpers::ForEachAsset(InAssets,
[&](UBlueprintGeneratedClass* BlueprintClassName, const FAssetData AssetData)
{
auto MatchingFunction = Algo::FindByPredicate(BlueprintClassName->CalledFunctions,
[&](const UFunction* Fn)
{
return Fn->HasAnyFunctionFlags(EFunctionFlags::FUNC_Native)
&& Fn->GetName() == FunctionName
&& Fn->GetOwnerClass()->GetName() == ClassNameWithoutPrefix;
});
if (MatchingFunction != nullptr)
{
OutResults.Add(BlueprintClassName->GetName(), AssetData);
}
});
return OutResults;
}
using JsonWriter = TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>;
static void SerializeBlueprintReference(
TSharedRef<JsonWriter>& Json, const FString& BlueprintClassName, const FAssetData& Asset)
{
FString PackageFileName;
FString PackageFile;
FString PackageFilePath;
if (FPackageName::TryConvertLongPackageNameToFilename(Asset.GetPackage()->GetName(), PackageFileName) &&
FPackageName::FindPackageFileWithoutExtension(PackageFileName, PackageFile))
{
PackageFilePath = FPaths::ConvertRelativePathToFull(MoveTemp(PackageFile));
}
Json->WriteObjectStart();
Json->WriteValue(TEXT("name"), BlueprintClassName);
Json->WriteValue(TEXT("path"), PackageFilePath);
Json->WriteObjectEnd();
}
static void SerializeBlueprints(
TSharedRef<JsonWriter>& Json, const TMap<FString, FAssetData>& InAssets)
{
Json->WriteIdentifierPrefix(TEXT("blueprints"));
Json->WriteArrayStart();
for (auto& Item : InAssets)
{
const FString& BlueprintClassName = Item.Key;
const FAssetData& Asset = Item.Value;
SerializeBlueprintReference(Json, BlueprintClassName, Asset);
}
Json->WriteArrayEnd();
}
static void SerializeMetadata(
TSharedRef<JsonWriter>& Json, int TotalAssetCount)
{
Json->WriteIdentifierPrefix(TEXT("metadata"));
Json->WriteObjectStart();
{
Json->WriteValue(TEXT("asset_count"), TotalAssetCount);
}
Json->WriteObjectEnd();
}
static void SerializeResults(
const TMap<FString, FAssetData>& InAssets,
FArchive& OutArchive,
int TotalAssetCount)
{
TSharedRef<JsonWriter> Json = JsonWriter::Create(&OutArchive);
Json->WriteObjectStart();
SerializeBlueprints(Json, InAssets);
SerializeMetadata(Json, TotalAssetCount);
Json->WriteObjectEnd();
Json->Close();
}
} // namespace VisualStudioTools
static constexpr auto SymbolParamVal = TEXT("symbol");
UVsBlueprintReferencesCommandlet::UVsBlueprintReferencesCommandlet()
: Super()
{
HelpDescription = TEXT("Commandlet for generating data used by Blueprint support in Visual Studio.");
HelpParamNames.Add(SymbolParamVal);
HelpParamDescriptions.Add(TEXT("[Optional] Fully qualified symbol to search for in the blueprints."));
HelpUsage = TEXT("<Editor-Cmd.exe> <path_to_uproject> -run=VsBlueprintReferences -output=<path_to_output_file> -symbol=<ClassName::FunctionName> [-unattended -noshadercompile -nosound -nullrhi -nocpuprofilertrace -nocrashreports -nosplash]");
}
int32 UVsBlueprintReferencesCommandlet::Run(
TArray<FString>& Tokens,
TArray<FString>& Switches,
TMap<FString, FString>& ParamVals,
FArchive& OutArchive)
{
using namespace VisualStudioTools;
GIsRunning = true; // Required for the blueprint search to work.
FString* ReferencesSymbol = ParamVals.Find(SymbolParamVal);
if (ReferencesSymbol->IsEmpty())
{
UE_LOG(LogVisualStudioTools, Error, TEXT("Missing required symbol parameter."));
PrintHelp();
return -1;
}
FString FunctionName;
FString ClassNameNative;
if (!ReferencesSymbol->Split(TEXT("::"), &ClassNameNative, &FunctionName))
{
UE_LOG(LogVisualStudioTools, Error, TEXT("Reference parameter should be in the qualified 'NativeClassName::MethodName' format."));
PrintHelp();
return -1;
}
// Execute the search in two stages:
// 1. Use FindInBlueprints to get all candidate blueprints with calls to functions that match the requested symbol
// 2. Confirm the blueprints reference the requested function, by matching the target UFunction in their call graph.
// The first step acts as a filter to avoid loading too many blueprints to inspect their call graph.
// The second step is required because the FiB data does not always allow for searching with the function
// qualified with the owned class name, if the function is static.
FString ClassNameWithoutPrefix = StripClassPrefix(ClassNameNative);
// Create a FiB search query for function nodes where the native name matches the requested symbol
FString SearchValue = FString::Printf(TEXT("Nodes(\"Native Name\"=+%s & ClassName=K2Node_CallFunction)"), *FunctionName);
UE_LOG(LogVisualStudioTools, Display, TEXT("Blueprint search query: %s"), *SearchValue);
// Step 1: Execute the Fib search
TArray<FAssetData> TargetAssets = SearchForCandidateAssets(SearchValue);
// Step 2: Load the assets to confirm they are a match
TMap<FString, FAssetData> MatchAssets = GetConfirmedAssets(FunctionName, ClassNameWithoutPrefix, TargetAssets);
// Finally, write the results back to the output
SerializeResults(MatchAssets, OutArchive, TargetAssets.Num());
UE_LOG(LogVisualStudioTools, Display, TEXT("Found %d blueprints."), MatchAssets.Num());
return 0;
}

View File

@@ -0,0 +1,25 @@
// Copyright 2022 (c) Microsoft. All rights reserved.
// Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "VisualStudioToolsCommandletBase.h"
#include "BlueprintReferencesCommandlet.generated.h"
UCLASS()
class UVsBlueprintReferencesCommandlet
: public UVisualStudioToolsCommandletBase
{
GENERATED_BODY()
public:
UVsBlueprintReferencesCommandlet();
int32 Run(
TArray<FString>& Tokens,
TArray<FString>& Switches,
TMap<FString, FString>& ParamVals,
FArchive& OutArchive) override;
};

View File

@@ -0,0 +1,63 @@
#pragma once
#include "VisualStudioDTE.h"
#include <utility>
class FSmartBSTR
{
public:
FSmartBSTR() : data(nullptr)
{
}
FSmartBSTR(const FSmartBSTR& Other)
{
if (Other.data) data = SysAllocString(Other.data);
else data = nullptr;
}
FSmartBSTR(FSmartBSTR&& Other)
{
data = std::exchange(Other.data, nullptr);
}
FSmartBSTR(const FString& Other)
{
data = SysAllocString(*Other);
}
FSmartBSTR(const OLECHAR *Ptr)
{
if (Ptr) data = SysAllocString(Ptr);
else data = nullptr;
}
~FSmartBSTR()
{
if (data) SysFreeString(data);
}
FSmartBSTR& operator=(const FSmartBSTR& Other)
{
if (this == &Other) return *this;
if (data) SysFreeString(data);
if (Other.data) data = SysAllocString(Other.data);
else data = nullptr;
return *this;
}
FSmartBSTR& operator=(FSmartBSTR&& Other)
{
if (data) SysFreeString(data);
data = std::exchange(Other.data, nullptr);
return *this;
}
BSTR operator*() const
{
return data;
}
private:
BSTR data;
};

View File

@@ -0,0 +1,118 @@
// Copyright 2022 (c) Microsoft. All rights reserved.
#include "VSServerCommandlet.h"
#include "VSTestAdapterCommandlet.h"
#include "Windows/AllowWindowsPlatformTypes.h"
#include "HAL/PlatformNamedPipe.h"
#include "Runtime/Core/Public/Async/TaskGraphInterfaces.h"
#include "Runtime/Core/Public/Containers/Ticker.h"
#include "Runtime/Engine/Classes/Engine/World.h"
#include "Runtime/Engine/Public/TimerManager.h"
#include "Runtime/Launch/Resources/Version.h"
#include "Runtime\CoreUObject\Public\UObject\UObjectGlobals.h"
#include <chrono>
#include <codecvt>
#include <fstream>
#include <string>
#include <thread>
#include <windows.h>
#include "Windows/HideWindowsPlatformTypes.h"
#include "VisualStudioTools.h"
static constexpr auto NamedPipeParam = TEXT("NamedPipe");
static constexpr auto KillServerParam = TEXT("KillVSServer");
UVSServerCommandlet::UVSServerCommandlet()
{
HelpDescription = TEXT("Commandlet for Unreal Engine server mode.");
HelpUsage = TEXT("<Editor-Cmd.exe> <path_to_uproject> -run=VSServer [-stdout -multiprocess -silent -unattended -AllowStdOutLogVerbosity -NoShaderCompile]");
HelpParamNames.Add(NamedPipeParam);
HelpParamDescriptions.Add(TEXT("[Required] The name of the named pipe used to communicate with Visual Studio."));
HelpParamNames.Add(KillServerParam);
HelpParamDescriptions.Add(TEXT("[Optional] Quit the server mode commandlet immediately."));
}
void UVSServerCommandlet::ExecuteSubCommandlet(FString ueServerNamedPipe)
{
char buffer[1024];
DWORD dwRead;
std::string result = "0";
// Open the named pipe.
std::wstring pipeName = L"\\\\.\\pipe\\";
pipeName.append(ueServerNamedPipe.GetCharArray().GetData());
HANDLE HPipe = CreateFile(pipeName.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if (HPipe != INVALID_HANDLE_VALUE)
{
ConnectNamedPipe(HPipe, NULL);
DWORD dwState;
BOOL bSuccess = GetNamedPipeHandleState(HPipe, &dwState, NULL, NULL, NULL, NULL, 0);
if (bSuccess)
{
// Read data from the named pipe.
ReadFile(HPipe, buffer, sizeof(buffer) - 1, &dwRead, NULL);
buffer[dwRead] = '\0';
std::string strSubCommandletParams(buffer, dwRead);
FString SubCommandletParams = FString(strSubCommandletParams.c_str());
// Determine which sub-commandlet to invoke, and write back result response.
if (SubCommandletParams.Contains("VSTestAdapter"))
{
UVSTestAdapterCommandlet *Commandlet = NewObject<UVSTestAdapterCommandlet>();
try
{
int32 subCommandletResult = Commandlet->Main(SubCommandletParams);
}
catch (const std::exception &ex)
{
UE_LOG(LogVisualStudioTools, Display, TEXT("Exception invoking VSTestAdapter commandlet: %s"), UTF8_TO_TCHAR(ex.what()));
result = "0";
}
}
else if (SubCommandletParams.Contains("KillVSServer"))
{
// When KillVSServer is passed in, then kill the Unreal Editor process to end server mode.
exit(0);
}
else
{
// If cannot find which sub-commandlet to run, then return error.
result = "1";
}
WriteFile(HPipe, result.c_str(), result.size(), &dwRead, NULL);
}
}
}
int32 UVSServerCommandlet::Main(const FString &ServerParams)
{
TArray<FString> Tokens;
TArray<FString> Switches;
TMap<FString, FString> ParamVals;
ParseCommandLine(*ServerParams, Tokens, Switches, ParamVals);
if (ParamVals.Contains(NamedPipeParam))
{
FString ueServerNamedPipe = ParamVals[NamedPipeParam];
// Infinite loop that listens to requests every second.
while (true)
{
std::this_thread::sleep_for(std::chrono::seconds(1));
ExecuteSubCommandlet(ueServerNamedPipe);
}
}
else
{
UE_LOG(LogVisualStudioTools, Display, TEXT("Missing named pipe parameter."));
}
return 1;
}

View File

@@ -0,0 +1,29 @@
// Copyright 2022 (c) Microsoft. All rights reserved.
// Licensed under the MIT License.
#pragma once
#include "Commandlets/Commandlet.h"
#include <string>
#include <Runtime/Core/Public/Misc/AutomationTest.h>
#include <Runtime/CoreUObject/Public/UObject/ObjectMacros.h>
#include <Runtime/Engine/Classes/Commandlets/Commandlet.h>
#include "VSServerCommandlet.generated.h"
UCLASS()
class UVSServerCommandlet
: public UCommandlet
{
GENERATED_BODY()
public:
UVSServerCommandlet();
public:
virtual int32 Main(const FString& Params) override;
private:
void ExecuteSubCommandlet(FString ueServerNamedPipe);
};

View File

@@ -0,0 +1,288 @@
// Copyright 2022 (c) Microsoft. All rights reserved.
#include "VSTestAdapterCommandlet.h"
#include "Runtime/Core/Public/Async/TaskGraphInterfaces.h"
#include "Runtime/Core/Public/Containers/Ticker.h"
#include "Runtime/Launch/Resources/Version.h"
#include <string>
#include <fstream>
#include "VisualStudioTools.h"
static constexpr auto FiltersParam = TEXT("filters");
static constexpr auto ListTestsParam = TEXT("listtests");
static constexpr auto RunTestsParam = TEXT("runtests");
static constexpr auto TestResultsFileParam = TEXT("testresultfile");
static constexpr auto HelpParam = TEXT("help");
static void GetAllTests(TArray<FAutomationTestInfo>& OutTestList)
{
FAutomationTestFramework& Framework = FAutomationTestFramework::GetInstance();
Framework.GetValidTestNames(OutTestList);
}
static void ReadTestsFromFile(const FString& InFile, TArray<FAutomationTestInfo>& OutTestList)
{
TSet<FString> TestCommands;
// Wrapping in an inner scope to ensure automatic destruction of InStream object without explicitly calling .close().
{
std::wifstream InStream(*InFile);
if (!InStream.good())
{
UE_LOG(LogVisualStudioTools, Error, TEXT("Failed to open file at path: %s"), *InFile);
return;
}
std::wstring Line;
while (std::getline(InStream, Line))
{
if (Line.length() > 0)
{
TestCommands.Add(FString(Line.c_str()));
}
}
}
GetAllTests(OutTestList);
for (int32 Idx = OutTestList.Num() - 1; Idx >= 0; Idx--)
{
if (!TestCommands.Contains(OutTestList[Idx].GetTestName()))
{
OutTestList.RemoveAt(Idx);
}
}
}
static int32 ListTests(const FString& TargetFile)
{
std::wofstream OutFile(*TargetFile);
if (!OutFile.good())
{
UE_LOG(LogVisualStudioTools, Error, TEXT("Failed to open file at path: %s"), *TargetFile);
return 1;
}
FAutomationTestFramework& Framework = FAutomationTestFramework::GetInstance();
TArray<FAutomationTestInfo> TestInfos;
GetAllTests(TestInfos);
for (const auto& TestInfo : TestInfos)
{
const FString TestCommand = TestInfo.GetTestName();
const FString DisplayName = TestInfo.GetDisplayName();
const FString SourceFile = TestInfo.GetSourceFile();
const int32 Line = TestInfo.GetSourceFileLine();
OutFile << *TestCommand << TEXT("|") << *DisplayName << TEXT("|") << Line << TEXT("|") << *SourceFile << std::endl;
}
UE_LOG(LogVisualStudioTools, Display, TEXT("Found %d tests"), TestInfos.Num());
OutFile.close();
return 0;
}
static int32 RunTests(const FString& TestListFile, const FString& ResultsFile)
{
std::wofstream OutFile(*ResultsFile);
if (!OutFile.good())
{
UE_LOG(LogVisualStudioTools, Error, TEXT("Failed to open file at path: %s"), *ResultsFile);
return 1;
}
TArray<FAutomationTestInfo> TestInfos;
if (TestListFile.Equals(TEXT("All"), ESearchCase::IgnoreCase))
{
GetAllTests(TestInfos);
}
else
{
ReadTestsFromFile(TestListFile, TestInfos);
}
bool AllSuccessful = true;
FAutomationTestFramework& Framework = FAutomationTestFramework::GetInstance();
for (const FAutomationTestInfo& TestInfo : TestInfos)
{
const FString TestCommand = TestInfo.GetTestName();
const FString DisplayName = TestInfo.GetDisplayName();
UE_LOG(LogVisualStudioTools, Log, TEXT("Running %s"), *DisplayName);
const int32 RoleIndex = 0; // always default to "local" role index. Only used for multi-participant tests
Framework.StartTestByName(TestCommand, RoleIndex);
FDateTime Last = FDateTime::UtcNow();
while (!Framework.ExecuteLatentCommands())
{
// Because we are not 'ticked' by the Engine we need to pump the TaskGraph
FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GameThread);
const FDateTime Now = FDateTime::UtcNow();
const float Delta = static_cast<float>((Now - Last).GetTotalSeconds());
// .. and the core FTicker
#if ENGINE_MAJOR_VERSION >= 5
FTSTicker::GetCoreTicker().Tick(Delta);
#else
FTicker::GetCoreTicker().Tick(Delta);
#endif
Last = Now;
}
FAutomationTestExecutionInfo ExecutionInfo;
const bool CurrentTestSuccessful = Framework.StopTest(ExecutionInfo) && ExecutionInfo.GetErrorTotal() == 0;
AllSuccessful = AllSuccessful && CurrentTestSuccessful;
const FString Result = CurrentTestSuccessful ? TEXT("OK") : TEXT("FAIL");
// [RUNTEST] is part of the protocol, so do not remove.
OutFile << TEXT("[RUNTEST]") << *TestCommand << TEXT("|") << *DisplayName << TEXT("|") << *Result << TEXT("|") << ExecutionInfo.Duration << std::endl;
if (!CurrentTestSuccessful)
{
for (const auto& Entry : ExecutionInfo.GetEntries())
{
if (Entry.Event.Type == EAutomationEventType::Error)
{
OutFile << *Entry.Event.Message << std::endl;
UE_LOG(LogVisualStudioTools, Error, TEXT("%s"), *Entry.Event.Message);
}
}
UE_LOG(LogVisualStudioTools, Log, TEXT("Failed %s"), *DisplayName);
}
OutFile.flush();
}
return AllSuccessful ? 0 : 1;
}
UVSTestAdapterCommandlet::UVSTestAdapterCommandlet()
{
HelpDescription = TEXT("Commandlet for generating data used by Blueprint support in Visual Studio.");
HelpUsage = TEXT("<Editor-Cmd.exe> <path_to_uproject> -run=VSTestAdapter [-stdout -multiprocess -silent -unattended -AllowStdOutLogVerbosity -NoShaderCompile]");
HelpParamNames.Add(ListTestsParam);
HelpParamDescriptions.Add(TEXT("[Required] The file path to write the test cases retrieved from FAutomationTestFramework"));
HelpParamNames.Add(RunTestsParam);
HelpParamDescriptions.Add(TEXT("[Required] The test cases that will be sent to FAutomationTestFramework to run."));
HelpParamNames.Add(TestResultsFileParam);
HelpParamDescriptions.Add(TEXT("[Required] The output file from running test cases that we parse to retrieve test case results."));
HelpParamNames.Add(FiltersParam);
HelpParamDescriptions.Add(TEXT("[Optional] List of test filters to enable separated by '+'. Default is 'application+smoke+product+perf+stress+negative'"));
HelpParamNames.Add(HelpParam);
HelpParamDescriptions.Add(TEXT("[Optional] Print this help message and quit the commandlet immediately."));
}
void UVSTestAdapterCommandlet::PrintHelp() const
{
UE_LOG(LogVisualStudioTools, Display, TEXT("%s"), *HelpDescription);
UE_LOG(LogVisualStudioTools, Display, TEXT("Usage: %s"), *HelpUsage);
UE_LOG(LogVisualStudioTools, Display, TEXT("Parameters:"));
for (int32 Idx = 0; Idx < HelpParamNames.Num(); ++Idx)
{
UE_LOG(LogVisualStudioTools, Display, TEXT("\t-%s: %s"), *HelpParamNames[Idx], *HelpParamDescriptions[Idx]);
}
}
int32 UVSTestAdapterCommandlet::Main(const FString& Params)
{
TArray<FString> Tokens;
TArray<FString> Switches;
TMap<FString, FString> ParamVals;
// Functionality for Unreal Engine Test Adapter.
ParseCommandLine(*Params, Tokens, Switches, ParamVals);
if (ParamVals.Contains(HelpParam))
{
PrintHelp();
return 0;
}
// Default to all the test filters.
auto filter = EAutomationTestFlags::ProductFilter | EAutomationTestFlags::SmokeFilter | EAutomationTestFlags::PerfFilter | EAutomationTestFlags::EngineFilter;
if (ParamVals.Contains(FiltersParam))
{
FString filters = ParamVals[FiltersParam];
if (filters.Contains("smoke"))
{
filter |= EAutomationTestFlags::SmokeFilter;
}
else
{
filter &= ~EAutomationTestFlags::SmokeFilter;
}
if (filters.Contains("engine"))
{
filter |= EAutomationTestFlags::EngineFilter;
}
else
{
filter &= ~EAutomationTestFlags::EngineFilter;
}
if (filters.Contains("product"))
{
filter |= EAutomationTestFlags::ProductFilter;
}
else
{
filter &= ~EAutomationTestFlags::ProductFilter;
}
if (filters.Contains("perf"))
{
filter |= EAutomationTestFlags::PerfFilter;
}
else
{
filter &= ~EAutomationTestFlags::PerfFilter;
}
if (filters.Contains("stress"))
{
filter |= EAutomationTestFlags::StressFilter;
}
else
{
filter &= ~EAutomationTestFlags::StressFilter;
}
if (filters.Contains("negative"))
{
filter |= EAutomationTestFlags::NegativeFilter;
}
else
{
filter &= ~EAutomationTestFlags::NegativeFilter;
}
}
FAutomationTestFramework::GetInstance().SetRequestedTestFilter(filter);
if (ParamVals.Contains(ListTestsParam))
{
return ListTests(ParamVals[ListTestsParam]);
}
else if (ParamVals.Contains(RunTestsParam) && ParamVals.Contains(TestResultsFileParam))
{
return RunTests(ParamVals[RunTestsParam], ParamVals[TestResultsFileParam]);
}
PrintHelp();
return 1;
}

View File

@@ -0,0 +1,28 @@
// Copyright 2022 (c) Microsoft. All rights reserved.
// Licensed under the MIT License.
#pragma once
#include "Commandlets/Commandlet.h"
#include <Runtime/Core/Public/Misc/AutomationTest.h>
#include <Runtime/CoreUObject/Public/UObject/ObjectMacros.h>
#include <Runtime/Engine/Classes/Commandlets/Commandlet.h>
#include "VSTestAdapterCommandlet.generated.h"
UCLASS()
class UVSTestAdapterCommandlet
: public UCommandlet
{
GENERATED_BODY()
public:
UVSTestAdapterCommandlet();
public:
virtual int32 Main(const FString &Params) override;
private:
void PrintHelp() const;
};

View File

@@ -0,0 +1,19 @@
// Copyright 2022 (c) Microsoft. All rights reserved.
// Licensed under the MIT License.
#include "VisualStudioTools.h"
#include "Modules/ModuleInterface.h"
#include "Modules/ModuleManager.h"
DEFINE_LOG_CATEGORY(LogVisualStudioTools);
class FVisualStudioToolsModule : public IModuleInterface
{
public:
/** IModuleInterface implementation */
virtual void StartupModule() override {}
virtual void ShutdownModule() override {}
};
IMPLEMENT_MODULE(FVisualStudioToolsModule, VisualStudioTools)

View File

@@ -0,0 +1,576 @@
#include "VisualStudioToolsBlueprintBreakpointExtension.h"
#include "FSmartBSTR.h"
#include <Modules/ModuleManager.h>
#include <Framework/MultiBox/MultiBoxBuilder.h>
#include <BlueprintGraphClasses.h>
#include <EditorSubsystem.h>
#include <unknwn.h>
#include <Windows/WindowsPlatformStackWalk.h>
#include <Subsystems/Subsystem.h>
#include <Windows/WindowsPlatformMisc.h>
#include <Windows/WindowsPlatformProcess.h>
#include <SourceCodeNavigation.h>
#include <GraphEditorModule.h>
#include <Containers/Array.h>
#include <EdGraph/EdGraph.h>
#include <EdGraph/EdGraphNode.h>
#include <EdGraph/EdGraphPin.h>
#include <Framework/Commands/UIAction.h>
#include <Widgets/Notifications/SNotificationList.h>
#include <Framework/Notifications/NotificationManager.h>
#include <Misc/FileHelper.h>
#include <Interfaces/IProjectManager.h>
#include <Misc/UProjectInfo.h>
#include <ProjectDescriptor.h>
#include <Misc/App.h>
#include <Runtime/Launch/Resources/Version.h>
#include <EditorStyleSet.h>
#if ENGINE_MAJOR_VERSION < 5
#include <DbgHelp.h>
#include <Psapi.h>
#endif
DEFINE_LOG_CATEGORY(LogUVisualStudioToolsBlueprintBreakpointExtension);
static const FName GraphEditorModuleName(TEXT("GraphEditor"));
void UVisualStudioToolsBlueprintBreakpointExtension::Initialize(FSubsystemCollectionBase& Collection)
{
FGraphEditorModule& GraphEditorModule = FModuleManager::LoadModuleChecked<FGraphEditorModule>(GraphEditorModuleName);
GraphEditorModule.GetAllGraphEditorContextMenuExtender().Add(
FGraphEditorModule::FGraphEditorMenuExtender_SelectedNode::CreateUObject(this, &ThisClass::HandleOnExtendGraphEditorContextMenu));
}
void UVisualStudioToolsBlueprintBreakpointExtension::Deinitialize()
{
FGraphEditorModule* GraphEditorModule = FModuleManager::GetModulePtr<FGraphEditorModule>(GraphEditorModuleName);
if (!GraphEditorModule)
{
return;
}
GraphEditorModule->GetAllGraphEditorContextMenuExtender().RemoveAll(
[](const FGraphEditorModule::FGraphEditorMenuExtender_SelectedNode& Delegate) {
FName LocalFunction = GET_FUNCTION_NAME_CHECKED(ThisClass, HandleOnExtendGraphEditorContextMenu);
return Delegate.TryGetBoundFunctionName() == LocalFunction;
});
}
TSharedRef<FExtender> UVisualStudioToolsBlueprintBreakpointExtension::HandleOnExtendGraphEditorContextMenu(
const TSharedRef<FUICommandList> CommandList,
const UEdGraph* Graph,
const UEdGraphNode* Node,
const UEdGraphPin* Pin,
bool /* bIsConst */)
{
TSharedRef<FExtender> Extender = MakeShared<FExtender>();
if (!CanAddVisualStudioBreakpoint(Node, nullptr, nullptr))
{
return Extender;
}
const FName ExtensionHook(TEXT("EdGraphSchemaNodeActions"));
Extender->AddMenuExtension(
ExtensionHook,
EExtensionHook::After,
CommandList,
FMenuExtensionDelegate::CreateUObject(this, &ThisClass::AddVisualStudioBlueprintBreakpointMenuOption, Node));
return Extender;
}
void UVisualStudioToolsBlueprintBreakpointExtension::AddVisualStudioBlueprintBreakpointMenuOption(FMenuBuilder& MenuBuilder, const UEdGraphNode *Node)
{
MenuBuilder.BeginSection(TEXT("VisualStudioTools"), FText::FromString("Visual Studio Tools"));
MenuBuilder.AddMenuEntry(
FText::FromString("Set breakpoint in Visual Studio"),
FText::FromString("This will set a breakpoint in Visual Studio so the native debugger can break the execution"),
FSlateIcon(),
FUIAction(FExecuteAction::CreateUObject(this, &ThisClass::AddVisualStudioBreakpoint, Node)));
MenuBuilder.EndSection();
}
FString UVisualStudioToolsBlueprintBreakpointExtension::GetProjectPath(const FString &ProjectDir)
{
FString ProjectPath;
if (!FFileHelper::LoadFileToString(ProjectPath, *(FPaths::EngineIntermediateDir() / TEXT("ProjectFiles") / TEXT("PrimaryProjectPath.txt"))))
{
const FProjectDescriptor* CurrentProject = IProjectManager::Get().GetCurrentProject();
if ((CurrentProject == nullptr || CurrentProject->Modules.Num() == 0) || !FUProjectDictionary::GetDefault().IsForeignProject(ProjectDir))
{
ProjectPath = FPaths::RootDir() / TEXT("UE5");
}
else
{
const FString BaseName = FApp::HasProjectName() ? FApp::GetProjectName() : FPaths::GetBaseFilename(ProjectDir);
ProjectPath = ProjectDir / BaseName;
}
}
ProjectPath = ProjectPath + TEXT(".sln");
FPaths::NormalizeFilename(ProjectPath);
return ProjectPath;
}
bool UVisualStudioToolsBlueprintBreakpointExtension::GetRunningVisualStudioDTE(TComPtr<EnvDTE::_DTE>& OutDTE)
{
IRunningObjectTable* RunningObjectTable;
bool bResult = false;
FString ProjectDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir());
FPaths::NormalizeDirectoryName(ProjectDir);
FString SolutionPath = GetProjectPath(ProjectDir);
if (SUCCEEDED(GetRunningObjectTable(0, &RunningObjectTable)) && RunningObjectTable)
{
IEnumMoniker* MonikersTable;
if (SUCCEEDED(RunningObjectTable->EnumRunning(&MonikersTable)))
{
MonikersTable->Reset();
// Look for all visual studio instances in the ROT
IMoniker* CurrentMoniker;
while (MonikersTable->Next(1, &CurrentMoniker, NULL) == S_OK)
{
IBindCtx* BindContext;
LPOLESTR OutName;
if (SUCCEEDED(CreateBindCtx(0, &BindContext)) && SUCCEEDED(CurrentMoniker->GetDisplayName(BindContext, NULL, &OutName)))
{
TComPtr<IUnknown> ComObject;
if (SUCCEEDED(RunningObjectTable->GetObject(CurrentMoniker, &ComObject)))
{
TComPtr<EnvDTE::_DTE> TempDTE;
if (SUCCEEDED(TempDTE.FromQueryInterface(__uuidof(EnvDTE::_DTE), ComObject)))
{
TComPtr<EnvDTE::_Solution> Solution;
BSTR OutPath = nullptr;
if (SUCCEEDED(TempDTE->get_Solution(&Solution)) &&
SUCCEEDED(Solution->get_FullName(&OutPath)))
{
FString Filename(OutPath);
FPaths::NormalizeFilename(Filename);
if (Filename == SolutionPath || Filename == ProjectDir)
{
OutDTE = TempDTE;
bResult = true;
}
}
else
{
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Error, TEXT("Could not get solution from DTE"));
}
SysFreeString(OutPath);
}
}
}
else
{
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Error, TEXT("Could not get display name for moniker"));
}
BindContext->Release();
CurrentMoniker->Release();
if (bResult) break;
}
MonikersTable->Release();
RunningObjectTable->Release();
}
else
{
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Error, TEXT("Could not enumerate Running Object Table"));
}
}
else
{
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Error, TEXT("Could not get Running Object Table"));
}
return bResult;
}
bool UVisualStudioToolsBlueprintBreakpointExtension::CanAddVisualStudioBreakpoint(const UEdGraphNode* Node, UClass **OutOwnerClass, UFunction **OutFunction)
{
const UK2Node_CallFunction* K2Node = Cast<const UK2Node_CallFunction>(Node);
if (!K2Node)
{
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Log, TEXT("Node is not a UK2Node_CallFunction"));
return false;
}
UFunction* Function = K2Node->GetTargetFunction();
if (!Function || !Function->IsNative())
{
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Log, TEXT("Function is not native"));
return false;
}
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Log, TEXT("Trying to get function definition for %s"), *Function->GetName());
UClass* OwnerClass = Function->GetOwnerClass();
if (!OwnerClass->HasAllClassFlags(CLASS_Native))
{
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Log, TEXT("Owning class is not native"));
return false;
}
if (OutOwnerClass) *OutOwnerClass = OwnerClass;
if (OutFunction) *OutFunction = Function;
return true;
}
#if ENGINE_MAJOR_VERSION < 5
#define PRINT_PLATFORM_ERROR_MSG(_TXT) \
do { \
TCHAR _ErrorBuffer[MAX_SPRINTF] = { 0 }; \
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Error, TEXT("" #_TXT ": [%s]"), \
FPlatformMisc::GetSystemErrorMessage(_ErrorBuffer, MAX_SPRINTF, 0)); \
} while (0)
bool UVisualStudioToolsBlueprintBreakpointExtension::PreloadModule(HANDLE ProcessHandle, HMODULE ModuleHandle, const FString& RemoteStorage)
{
int32 ErrorCode = 0;
MODULEINFO ModuleInfo = { 0 };
WCHAR ModuleName[FProgramCounterSymbolInfo::MAX_NAME_LENGTH] = { 0 };
WCHAR ImageName[FProgramCounterSymbolInfo::MAX_NAME_LENGTH] = { 0 };
#if PLATFORM_64BITS
static_assert(sizeof(MODULEINFO) == 24, "Broken alignment for 64bit Windows include.");
#else
static_assert(sizeof(MODULEINFO) == 12, "Broken alignment for 32bit Windows include.");
#endif
if (!GetModuleInformation(ProcessHandle, ModuleHandle, &ModuleInfo, sizeof(ModuleInfo)))
{
PRINT_PLATFORM_ERROR_MSG("Could not read GetModuleInformation");
return false;
}
IMAGEHLP_MODULE64 ImageHelpModule = { 0 };
ImageHelpModule.SizeOfStruct = sizeof(ImageHelpModule);
if (!SymGetModuleInfo64(ProcessHandle, (DWORD64)ModuleInfo.EntryPoint, &ImageHelpModule))
{
PRINT_PLATFORM_ERROR_MSG("Could not SymGetModuleInfo64 from module");
return false;
}
if (ImageHelpModule.SymType != SymDeferred && ImageHelpModule.SymType != SymNone)
{
return true;
}
if (!GetModuleFileNameExW(ProcessHandle, ModuleHandle, ImageName, 1024))
{
PRINT_PLATFORM_ERROR_MSG("Could not GetModuleFileNameExW");
return false;
}
if (!GetModuleBaseNameW(ProcessHandle, ModuleHandle, ModuleName, 1024))
{
PRINT_PLATFORM_ERROR_MSG("Could not GetModuleBaseNameW");
return false;
}
WCHAR SearchPath[MAX_PATH] = { 0 };
WCHAR* FileName = NULL;
const auto Result = GetFullPathNameW(ImageName, MAX_PATH, SearchPath, &FileName);
FString SearchPathList;
if (Result != 0 && Result < MAX_PATH)
{
*FileName = 0;
SearchPathList = SearchPath;
}
if (!RemoteStorage.IsEmpty())
{
if (!SearchPathList.IsEmpty())
{
SearchPathList.AppendChar(TEXT(';'));
}
SearchPathList.Append(RemoteStorage);
}
if (!SymSetSearchPathW(ProcessHandle, *SearchPathList))
{
PRINT_PLATFORM_ERROR_MSG("Could not SymSetSearchPathW");
return false;
}
const DWORD64 BaseAddress = SymLoadModuleExW(
ProcessHandle,
ModuleHandle,
ImageName,
ModuleName,
(DWORD64)ModuleInfo.lpBaseOfDll,
ModuleInfo.SizeOfImage,
NULL,
0);
if (!BaseAddress)
{
PRINT_PLATFORM_ERROR_MSG("Could not load the module");
return false;
}
return true;
}
bool UVisualStudioToolsBlueprintBreakpointExtension::GetFunctionDefinitionLocation(const FString& FunctionSymbolName, const FString& FunctionModuleName, FString& SourceFilePath, uint32& SourceLineNumber)
{
const HANDLE ProcessHandle = GetCurrentProcess();
HMODULE ModuleHandle = GetModuleHandle(*FunctionModuleName);
if (!ModuleHandle || !PreloadModule(ProcessHandle, ModuleHandle, FPlatformStackWalk::GetDownstreamStorage()))
{
return false;
}
ANSICHAR SymbolInfoBuffer[sizeof(IMAGEHLP_SYMBOL64) + MAX_SYM_NAME];
PIMAGEHLP_SYMBOL64 SymbolInfoPtr = reinterpret_cast<IMAGEHLP_SYMBOL64*>(SymbolInfoBuffer);
SymbolInfoPtr->SizeOfStruct = sizeof(SymbolInfoBuffer);
SymbolInfoPtr->MaxNameLength = MAX_SYM_NAME;
FString FullyQualifiedSymbolName = FunctionSymbolName;
if (!FunctionModuleName.IsEmpty())
{
FullyQualifiedSymbolName = FString::Printf(TEXT("%s!%s"), *FunctionModuleName, *FunctionSymbolName);
}
if (!SymGetSymFromName64(ProcessHandle, TCHAR_TO_ANSI(*FullyQualifiedSymbolName), SymbolInfoPtr))
{
PRINT_PLATFORM_ERROR_MSG("Could not load module symbol information");
return false;
}
IMAGEHLP_LINE64 FileAndLineInfo;
FileAndLineInfo.SizeOfStruct = sizeof(FileAndLineInfo);
uint32 SourceColumnNumber = 0;
if (!SymGetLineFromAddr64(ProcessHandle, SymbolInfoPtr->Address, (::DWORD*)&SourceColumnNumber, &FileAndLineInfo))
{
PRINT_PLATFORM_ERROR_MSG("Could not query module file and line number");
return false;
}
SourceLineNumber = FileAndLineInfo.LineNumber;
SourceFilePath = FString((const ANSICHAR*)(FileAndLineInfo.FileName));
return true;
}
#endif
bool UVisualStudioToolsBlueprintBreakpointExtension::GetFunctionDefinitionLocation(const UEdGraphNode* Node, FString& SourceFilePath, FString& SymbolName, uint32& SourceLineNumber)
{
UClass* OwningClass;
UFunction* Function;
if (!CanAddVisualStudioBreakpoint(Node, &OwningClass, &Function))
{
return false;
}
FString ModuleName;
// Find module name for class
if (!FSourceCodeNavigation::FindClassModuleName(OwningClass, ModuleName))
{
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Error, TEXT("Failed to find module name for class"));
return false;
}
SymbolName = FString::Printf(
TEXT("%s%s::%s"),
OwningClass->GetPrefixCPP(),
*OwningClass->GetName(),
*Function->GetName());
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Log, TEXT("Symbol %s is defined in module %s"), *SymbolName, *ModuleName);
#if ENGINE_MAJOR_VERSION >= 5
uint32 SourceColumnNumber = 0;
return FPlatformStackWalk::GetFunctionDefinitionLocation(
SymbolName,
ModuleName,
SourceFilePath,
SourceLineNumber,
SourceColumnNumber);
#else
return GetFunctionDefinitionLocation(SymbolName, ModuleName, SourceFilePath, SourceLineNumber);
#endif
}
bool UVisualStudioToolsBlueprintBreakpointExtension::GetProcessById(const TComPtr<EnvDTE::Processes>& Processes, DWORD CurrentProcessId, TComPtr<EnvDTE::Process>& OutProcess)
{
long Count = 0;
if (FAILED(Processes->get_Count(&Count)))
{
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Error, TEXT("Could not get the process count"));
return false;
}
TComPtr<EnvDTE::Process> Process;
for (long i = 1; i <= Count; i++)
{
VARIANT Index;
Index.vt = VT_I4;
Index.lVal = i;
if (SUCCEEDED(Processes->Item(Index, &Process)))
{
long PID = 0;
if (SUCCEEDED(Process->get_ProcessID(&PID)) && CurrentProcessId == PID)
{
OutProcess = Process;
return true;
}
Process.Reset();
}
}
return true;
}
void UVisualStudioToolsBlueprintBreakpointExtension::AttachDebuggerIfNecessary(const TComPtr<EnvDTE::Debugger>& Debugger)
{
TComPtr<EnvDTE::Processes> Processes;
if (FAILED(Debugger->get_DebuggedProcesses(&Processes)))
{
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Error, TEXT("Failed to get debugging proccess"));
return;
}
TComPtr<EnvDTE::Process> Process;
DWORD CurrentProcessId = GetCurrentProcessId();
if (!GetProcessById(Processes, CurrentProcessId, Process))
{
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Error, TEXT("Failed to check if UE is already in debug mode"));
return;
}
// currently debugging this process
if (Process.Get() != nullptr)
{
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Log, TEXT("Already debugging UE."));
return;
}
Processes.Reset();
if (FAILED(Debugger->get_LocalProcesses(&Processes)))
{
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Error, TEXT("Failed to attach to process"));
return;
}
Process.Reset();
if (!GetProcessById(Processes, CurrentProcessId, Process))
{
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Error, TEXT("Failed to get all process"));
return;
}
if (Process.Get() == nullptr)
{
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Log, TEXT("No UE proccess running."));
return;
}
if (FAILED(Process->Attach()))
{
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Error, TEXT("Failed to attach to process"));
}
}
bool UVisualStudioToolsBlueprintBreakpointExtension::SetVisualStudioBreakpoint(const UEdGraphNode* Node, const FString& SourceFilePath, const FString& SymbolName, uint32 SourceLineNumber)
{
TComPtr<EnvDTE::_DTE> DTE;
bool bBreakpointAdded = false;
if (!GetRunningVisualStudioDTE(DTE))
{
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Error, TEXT("Failed to access Visual Studio via DTE"));
return bBreakpointAdded;
}
TComPtr<EnvDTE::Debugger> Debugger;
TComPtr<EnvDTE::Breakpoints> Breakpoints;
if (SUCCEEDED(DTE->get_Debugger(&Debugger)) && SUCCEEDED(Debugger->get_Breakpoints(&Breakpoints)))
{
FSmartBSTR BSTREmptyStr;
FSmartBSTR BSTRFilePath(SourceFilePath);
HRESULT Result = Breakpoints->Add(
*BSTREmptyStr,
*BSTRFilePath,
SourceLineNumber,
1,
*BSTREmptyStr,
EnvDTE::dbgBreakpointConditionType::dbgBreakpointConditionTypeWhenTrue,
*BSTREmptyStr,
*BSTREmptyStr,
0,
*BSTREmptyStr,
0,
EnvDTE::dbgHitCountType::dbgHitCountTypeNone,
&Breakpoints);
if (FAILED(Result))
{
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Error, TEXT("Failed to add breakpoint"));
}
else
{
bBreakpointAdded = true;
AttachDebuggerIfNecessary(Debugger);
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Log, TEXT("Breakpoint set for %s"), *SymbolName);
}
}
else
{
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Error, TEXT("Failed to get debugger or breakpoints"));
}
return bBreakpointAdded;
}
void UVisualStudioToolsBlueprintBreakpointExtension::AddVisualStudioBreakpoint(const UEdGraphNode* Node)
{
FWindowsPlatformMisc::CoInitialize();
FPlatformStackWalk::InitStackWalking();
FString SourceFilePath;
FString SymbolName;
uint32 SourceLineNumber;
bool bBreakpointAdded = false;
if (GetFunctionDefinitionLocation(Node, SourceFilePath, SymbolName, SourceLineNumber))
{
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Log, TEXT("Method defined in %s at line %d"), *SourceFilePath, SourceLineNumber);
bBreakpointAdded = SetVisualStudioBreakpoint(Node, SourceFilePath, SymbolName, SourceLineNumber);
}
else
{
UE_LOG(LogUVisualStudioToolsBlueprintBreakpointExtension, Error, TEXT("Failed to get function definition location"));
}
ShowOperationResultNotification(bBreakpointAdded, SymbolName);
FWindowsPlatformMisc::CoUninitialize();
}
void UVisualStudioToolsBlueprintBreakpointExtension::ShowOperationResultNotification(bool bBreakpointAdded, const FString &SymbolName)
{
FNotificationInfo Info(bBreakpointAdded ? FText::FromString(FString::Printf(TEXT("Breakpoint added at %s"), *SymbolName)) : FText::FromString("Could not add Breakpoint in Visual Studio"));
#if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1
Info.Image = FAppStyle::GetBrush(TEXT("LevelEditor.RecompileGameCode"));
#else
Info.Image = FEditorStyle::GetBrush(TEXT("LevelEditor.RecompileGameCode"));
#endif
Info.FadeInDuration = 0.1f;
Info.FadeOutDuration = 0.5f;
Info.ExpireDuration = 3.0f;
Info.bUseThrobber = false;
Info.bUseSuccessFailIcons = true;
Info.bUseLargeFont = true;
Info.bFireAndForget = false;
Info.bAllowThrottleWhenFrameRateIsLow = false;
Info.WidthOverride = 400.0f;
TSharedPtr<SNotificationItem> NotificationItem = FSlateNotificationManager::Get().AddNotification(Info);
NotificationItem->SetCompletionState(bBreakpointAdded ? SNotificationItem::CS_Success : SNotificationItem::CS_Fail);
NotificationItem->ExpireAndFadeout();
}

View File

@@ -0,0 +1,64 @@
#pragma once
#include <CoreMinimal.h>
#include <EditorSubsystem.h>
#include <EdGraph/EdGraph.h>
#include <EdGraph/EdGraphNode.h>
#include <EdGraph/EdGraphPin.h>
#include <GraphEditorModule.h>
#include <VisualStudioDTE.h>
#include <Microsoft/COMPointer.h>
#include <Runtime/Launch/Resources/Version.h>
#include "VisualStudioToolsBlueprintBreakpointExtension.generated.h"
DECLARE_LOG_CATEGORY_EXTERN(LogUVisualStudioToolsBlueprintBreakpointExtension, Log, All);
UCLASS()
class UVisualStudioToolsBlueprintBreakpointExtension : public UEditorSubsystem
{
GENERATED_BODY()
public:
DECLARE_MULTICAST_DELEGATE_ThreeParams(FOnNodeMenuExtensionHookRequestDelegate, const UEdGraphNode*, const UEdGraph*, TSet<FName>&);
void Initialize(FSubsystemCollectionBase& Collection) override;
void Deinitialize() override;
FOnNodeMenuExtensionHookRequestDelegate& OnNodeMenuExtensionHookRequest() { return OnNodeMenuExtensionHookRequestDelegate; }
private:
FOnNodeMenuExtensionHookRequestDelegate OnNodeMenuExtensionHookRequestDelegate;
TSharedRef<FExtender> HandleOnExtendGraphEditorContextMenu(
const TSharedRef<FUICommandList> CommandList,
const UEdGraph* Graph,
const UEdGraphNode* Node,
const UEdGraphPin* Pin,
bool bIsConst);
void AddVisualStudioBlueprintBreakpointMenuOption(FMenuBuilder& MenuBuilder, const UEdGraphNode* node);
void AddVisualStudioBreakpoint(const UEdGraphNode* Node);
bool GetFunctionDefinitionLocation(const UEdGraphNode* Node, FString& SourceFilePath, FString& SymbolName, uint32& SourceLineNumber);
bool SetVisualStudioBreakpoint(const UEdGraphNode* Node, const FString& SourceFilePath, const FString& SymbolName, uint32 SourceLineNumber);
bool CanAddVisualStudioBreakpoint(const UEdGraphNode* Node, UClass** OutOwnerClass, UFunction** OutFunction);
void ShowOperationResultNotification(bool bBreakpointAdded, const FString& SymbolName);
FString GetProjectPath(const FString& ProjectDir);
bool GetRunningVisualStudioDTE(TComPtr<EnvDTE::_DTE>& OutDTE);
void AttachDebuggerIfNecessary(const TComPtr<EnvDTE::Debugger>& Debugger);
bool GetProcessById(const TComPtr<EnvDTE::Processes>& Processes, DWORD CurrentProcessId, TComPtr<EnvDTE::Process>& OutProcess);
#if ENGINE_MAJOR_VERSION < 5
bool PreloadModule(HANDLE ProcessHandle, HMODULE ModuleHandle, const FString& RemoteStorage);
bool GetFunctionDefinitionLocation(const FString& FunctionSymbolName, const FString& FunctionModuleName, FString& SourceFilePath, uint32& SourceLineNumber);
#endif
};

View File

@@ -0,0 +1,492 @@
// Copyright 2022 (c) Microsoft. All rights reserved.
#include "VisualStudioToolsCommandlet.h"
#include "Algo/Transform.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "Blueprint/BlueprintSupport.h"
#include "BlueprintAssetHelpers.h"
#include "Engine/BlueprintGeneratedClass.h"
#include "JsonObjectConverter.h"
#include "Misc/Paths.h"
#include "Misc/ScopeExit.h"
#include "Policies/CondensedJsonPrintPolicy.h"
#include "SourceCodeNavigation.h"
#include "UObject/CoreRedirects.h"
#include "UObject/UObjectIterator.h"
#include "VisualStudioTools.h"
namespace VisualStudioTools
{
static const FName CategoryFName = TEXT("Category");
static const FName ModuleNameFName = TEXT("ModuleName");
static TArray<FProperty*> GetChangedPropertiesList(
UStruct* InStruct, const uint8* DataPtr, const uint8* DefaultDataPtr)
{
TArray<FProperty*> Result;
const UClass* OwnerClass = Cast<UClass>(InStruct);
// Walk only in the properties defined in the current class, the super classes are processed individually
for (TFieldIterator<FProperty> It(OwnerClass, EFieldIteratorFlags::ExcludeSuper); It; ++It)
{
FProperty* Property = *It;
for (int32 Idx = 0; Idx < Property->ArrayDim; Idx++)
{
const uint8* PropertyValue = Property->ContainerPtrToValuePtr<uint8>(DataPtr, Idx);
const uint8* DefaultPropertyValue = Property->ContainerPtrToValuePtrForDefaults<uint8>(InStruct, DefaultDataPtr, Idx);
if (!Property->Identical(PropertyValue, DefaultPropertyValue))
{
Result.Add(Property);
break;
}
}
}
return Result;
}
static bool FindBlueprintNativeParents(
const UClass* BlueprintGeneratedClass, TFunctionRef<void(UClass*)> Callback)
{
bool bAnyNativeParent = false;
for (UClass* Super = BlueprintGeneratedClass->GetSuperClass(); Super; Super = Super->GetSuperClass())
{
// Ignore the root `UObject` class and non-native parents.
if (Super->HasAnyClassFlags(CLASS_Native) && Super->GetFName() != NAME_Object)
{
bAnyNativeParent = true;
Callback(Super);
}
}
return bAnyNativeParent;
}
struct FPropertyEntry
{
FProperty* Property;
TArray<int32> Blueprints;
};
struct FFunctionEntry
{
UFunction* Function;
TArray<int32> Blueprints;
};
struct FClassEntry
{
const UClass* Class;
TArray<int32> Blueprints;
TMap<FString, FPropertyEntry> Properties;
TMap<FString, FFunctionEntry> Functions;
};
using ClassMap = TMap<FString, FClassEntry>;
struct FAssetIndex
{
TSet<FString> AssetPathCache;
ClassMap Classes;
TArray<const UClass*> Blueprints;
void ProcessBlueprint(const UBlueprintGeneratedClass* BlueprintGeneratedClass)
{
if (BlueprintGeneratedClass == nullptr)
{
return;
}
int32 BlueprintIndex = Blueprints.Num();
bool bHasAnyParent = FindBlueprintNativeParents(BlueprintGeneratedClass, [&](UClass* Parent)
{
FString ParentName = Parent->GetFName().ToString();
if (!Classes.Contains(ParentName))
{
Classes.Add(ParentName).Class = Parent;
}
FClassEntry& ClassEntry = Classes[ParentName];
ClassEntry.Blueprints.Add(BlueprintIndex);
// Retrieve the properties from the parent class that changed in the Blueprint class, by comparing their CDOs.
UObject* GeneratedClassDefault = BlueprintGeneratedClass->ClassDefaultObject;
UObject* SuperClassDefault = Parent->GetDefaultObject(false);
TArray<FProperty*> ChangedProperties = GetChangedPropertiesList(Parent, (uint8*)GeneratedClassDefault, (uint8*)SuperClassDefault);
for (FProperty* Property : ChangedProperties)
{
FString PropertyName = Property->GetFName().ToString();
if (!ClassEntry.Properties.Contains(PropertyName))
{
ClassEntry.Properties.Add(PropertyName).Property = Property;
}
FPropertyEntry& PropEntry = ClassEntry.Properties[PropertyName];
PropEntry.Blueprints.Add(BlueprintIndex);
}
// Iterate over the functions originally from the parent class
// and check if they are implemented in the BP class as well.
for (TFieldIterator<UFunction> It(Parent, EFieldIteratorFlags::ExcludeSuper); It; ++It)
{
UFunction* Fn = BlueprintGeneratedClass->FindFunctionByName((*It)->GetFName(), EIncludeSuperFlag::ExcludeSuper);
// If the function not present in the BP class directly, it means it was implemented. Otherwise, ignore.
if (!Fn)
{
continue;
}
FString FnName = Fn->GetFName().ToString();
if (!ClassEntry.Functions.Contains(FnName))
{
ClassEntry.Functions.Add(FnName).Function = Fn;
}
FFunctionEntry& FuncEntry = ClassEntry.Functions[FnName];
FuncEntry.Blueprints.Add(BlueprintIndex);
}
});
if (bHasAnyParent)
{
check(Blueprints.Add(BlueprintGeneratedClass) == BlueprintIndex);
}
return;
}
};
using JsonWriter = TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>;
static bool ShouldSerializePropertyValue(FProperty* Property)
{
if (Property->ArrayDim > 1) // Skip properties that are not scalars
{
return false;
}
if (FEnumProperty* EnumProperty = CastField<FEnumProperty>(Property))
{
return true;
}
if (FNumericProperty* NumericProperty = CastField<FNumericProperty>(Property))
{
UEnum* EnumDef = NumericProperty->GetIntPropertyEnum();
if (EnumDef != NULL)
{
return true;
}
if (NumericProperty->IsFloatingPoint())
{
return true;
}
if (NumericProperty->IsInteger())
{
return true;
}
}
if (FBoolProperty* BoolProperty = CastField<FBoolProperty>(Property))
{
return true;
}
if (FStrProperty* StringProperty = CastField<FStrProperty>(Property))
{
return true;
}
return false;
}
static void SerializeBlueprints(TSharedRef<JsonWriter>& Json, TArray<const UClass*> Items)
{
Json->WriteArrayStart();
for (const UClass* Blueprint : Items)
{
Json->WriteObjectStart();
Json->WriteValue(TEXT("name"), Blueprint->GetName());
Json->WriteValue(TEXT("path"), Blueprint->GetPathName());
Json->WriteObjectEnd();
}
Json->WriteArrayEnd();
}
static void SerializeProperties(TSharedRef<JsonWriter>& Json, FClassEntry& Entry, TArray<const UClass*>& Blueprints)
{
Json->WriteArrayStart();
for (auto& Item : Entry.Properties)
{
auto& PropName = Item.Key;
auto& PropEntry = Item.Value;
FProperty* Property = PropEntry.Property;
Json->WriteObjectStart();
Json->WriteValue(TEXT("name"), PropName);
Json->WriteIdentifierPrefix(TEXT("metadata"));
{
Json->WriteObjectStart();
if (Property->HasMetaData(CategoryFName))
{
Json->WriteValue(TEXT("categories"), Property->GetMetaData(CategoryFName));
}
Json->WriteObjectEnd();
}
Json->WriteIdentifierPrefix(TEXT("values"));
{
Json->WriteArrayStart();
for (auto& BlueprintEntry : PropEntry.Blueprints)
{
Json->WriteObjectStart();
Json->WriteValue(TEXT("blueprint"), BlueprintEntry);
UObject* GeneratedClassDefault = Blueprints[BlueprintEntry]->ClassDefaultObject;
const uint8* PropData = PropEntry.Property->ContainerPtrToValuePtr<uint8>(GeneratedClassDefault);
if (ShouldSerializePropertyValue(PropEntry.Property))
{
TSharedPtr<FJsonValue> JsonValue = FJsonObjectConverter::UPropertyToJsonValue(Property, PropData);
FJsonSerializer::Serialize(JsonValue.ToSharedRef(), TEXT("value"), Json);
}
Json->WriteObjectEnd();
}
Json->WriteArrayEnd();
}
Json->WriteObjectEnd();
}
Json->WriteArrayEnd();
}
static void SerializeFunctions(TSharedRef<JsonWriter>& Json, FClassEntry& Entry)
{
Json->WriteArrayStart();
for (auto& Item : Entry.Functions)
{
auto& Name = Item.Key;
auto& FnEntry = Item.Value;
Json->WriteObjectStart();
Json->WriteValue(TEXT("name"), Name);
Json->WriteValue(TEXT("blueprints"), FnEntry.Blueprints);
Json->WriteObjectEnd();
}
Json->WriteArrayEnd();
}
static void SerializeClasses(TSharedRef<JsonWriter>& Json, ClassMap& Items, TArray<const UClass*> Blueprints)
{
Json->WriteArrayStart();
for (auto& Item : Items)
{
auto& ClassName = Item.Key;
auto& Entry = Item.Value;
Json->WriteObjectStart();
Json->WriteValue(TEXT("name"), FString::Printf(TEXT("%s%s"), Entry.Class->GetPrefixCPP(), *Entry.Class->GetName()));
Json->WriteValue(TEXT("blueprints"), Entry.Blueprints);
Json->WriteIdentifierPrefix(TEXT("properties"));
SerializeProperties(Json, Entry, Blueprints);
Json->WriteIdentifierPrefix(TEXT("functions"));
SerializeFunctions(Json, Entry);
Json->WriteObjectEnd();
}
Json->WriteArrayEnd();
}
static void SerializeToIndex(FAssetIndex Index, FArchive& IndexFile)
{
TSharedRef<JsonWriter> Json = JsonWriter::Create(&IndexFile);
Json->WriteObjectStart();
Json->WriteIdentifierPrefix(TEXT("blueprints"));
SerializeBlueprints(Json, Index.Blueprints);
Json->WriteIdentifierPrefix(TEXT("classes"));
SerializeClasses(Json, Index.Classes, Index.Blueprints);
Json->WriteObjectEnd();
Json->Close();
}
static TArray<FString> GetModulesByPath(const FString& InDir)
{
TArray<FString> OutResult;
Algo::TransformIf(
FSourceCodeNavigation::GetSourceFileDatabase().GetModuleNames(),
OutResult,
[&](const FString& Module) {
return FPaths::IsUnderDirectory(Module, InDir);
},
[](const FString& Module) {
#if 0
// Old version assumes that each module is in a folder with the same name as the module
return FPaths::GetBaseFilename(FPaths::GetPath(*Module));
#else
// New version assumes that each module is in a file with the name Module.Build.cs
FString TempString = FPaths::GetBaseFilename(*Module);
TempString.RemoveFromEnd(TEXT(".Build"));
return TempString;
#endif
});
return OutResult;
}
static void GetNativeClassesByPath(const FString& InDir, TArray<TWeakObjectPtr<UClass>>& OutClasses)
{
TArray<FString> Modules = GetModulesByPath(InDir);
for (TObjectIterator<UClass> ClassIt; ClassIt; ++ClassIt)
{
UClass* TestClass = *ClassIt;
if (!TestClass->HasAnyClassFlags(CLASS_Native))
{
continue;
}
FAssetData ClassAssetData(TestClass);
FString ModuleName = ClassAssetData.GetTagValueRef<FString>(ModuleNameFName);
if (!ModuleName.IsEmpty() && Modules.Contains(ModuleName))
{
OutClasses.Add(TestClass);
}
}
}
static void RunAssetScan(
FAssetIndex& Index,
const TArray<TWeakObjectPtr<UClass>>& FilterBaseClasses)
{
FARFilter Filter;
Filter.bRecursivePaths = true;
Filter.bRecursiveClasses = true;
AssetHelpers::SetBlueprintClassFilter(Filter);
// Add all base classes to the tag filter for native parent
Algo::Transform(FilterBaseClasses, Filter.TagsAndValues, [](const TWeakObjectPtr<UClass>& Class) {
return MakeTuple(
FBlueprintTags::NativeParentClassPath,
FObjectPropertyBase::GetExportPath(Class.Get(), nullptr /*Parent*/, nullptr /*ExportRootScope*/, 0 /*PortFlags*/));
});
// Take account of any core redirects for the blueprint classes we want to scan.
for (const auto& BaseClass : FilterBaseClasses)
{
if (BaseClass.IsValid())
{
TArray<FCoreRedirectObjectName> PreviousNames;
if (FCoreRedirects::FindPreviousNames(ECoreRedirectFlags::Type_Class, BaseClass->GetPathName(), PreviousNames))
{
for (const auto& PreviousName : PreviousNames)
{
// FString PreviousString = FObjectPropertyBase::GetExportPath(BaseClass->GetClass()->GetClassPathName(), PreviousName.ToString()); // Alternative way to add /Script/CoreUObject.Class'' wrapper - but not sure it makes sense to use the new class when referencing a previous name
FString PreviousString = "/Script/CoreUObject.Class'" + PreviousName.ToString() + "'";
Filter.TagsAndValues.Add(FBlueprintTags::NativeParentClassPath, PreviousString);
}
}
}
}
IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")).Get();
TArray<FAssetData> TargetAssets;
AssetRegistry.GetAssets(Filter, TargetAssets);
AssetHelpers::ForEachAsset(TargetAssets,
[&](UBlueprintGeneratedClass* BlueprintGeneratedClass, const FAssetData& /*AssetData*/)
{
Index.ProcessBlueprint(BlueprintGeneratedClass);
});
}
} // namespace VS
static constexpr auto FilterSwitch = TEXT("filter");
static constexpr auto FullSwitch = TEXT("full");
UVisualStudioToolsCommandlet::UVisualStudioToolsCommandlet()
: Super()
{
HelpDescription = TEXT("Commandlet for generating data used by Blueprint support in Visual Studio.");
HelpParamNames.Add(FilterSwitch);
HelpParamDescriptions.Add(TEXT("[Optional] Scan only blueprints derived from native classes under the provided path. Defaults to `FPaths::ProjectDir`. Incompatible with `-full`."));
HelpParamNames.Add(FullSwitch);
HelpParamDescriptions.Add(TEXT("[Optional] Scan blueprints derived from native classes from ALL modules, include the Engine. This can be _very slow_ for large projects. Incompatible with `-filter`."));
HelpUsage = TEXT("<Editor-Cmd.exe> <path_to_uproject> -run=VisualStudioTools -output=<path_to_output_file> [-filter=<subdir_native_classes>|-full] [-unattended -noshadercompile -nosound -nullrhi -nocpuprofilertrace -nocrashreports -nosplash]");
}
int32 UVisualStudioToolsCommandlet::Run(
TArray<FString>& Tokens,
TArray<FString>& Switches,
TMap<FString, FString>& ParamVals,
FArchive& OutArchive)
{
using namespace VisualStudioTools;
FString* Filter = ParamVals.Find(FilterSwitch);
const bool bFullScan = Switches.Contains(FullSwitch);
if (Filter != nullptr && bFullScan)
{
UE_LOG(LogVisualStudioTools, Error, TEXT("Incompatible scan options."));
PrintHelp();
return -1;
}
TArray<TWeakObjectPtr<UClass>> FilterBaseClasses;
if (!bFullScan)
{
if (Filter)
{
FPaths::NormalizeDirectoryName(*Filter);
GetNativeClassesByPath(*Filter, FilterBaseClasses);
}
else
{
GetNativeClassesByPath(FPaths::ProjectDir(), FilterBaseClasses);
}
}
else
{
for (TObjectIterator<UClass> ClassIt; ClassIt; ++ClassIt)
{
UClass* TestClass = *ClassIt;
if (!TestClass->HasAnyClassFlags(CLASS_Native))
{
continue;
}
FilterBaseClasses.Add(TestClass);
}
}
FAssetIndex Index;
RunAssetScan(Index, FilterBaseClasses);
SerializeToIndex(Index, OutArchive);
UE_LOG(LogVisualStudioTools, Display, TEXT("Found %d blueprints."), Index.Blueprints.Num());
return 0;
}

View File

@@ -0,0 +1,24 @@
// Copyright 2022 (c) Microsoft. All rights reserved.
// Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "VisualStudioToolsCommandletBase.h"
#include "VisualStudioToolsCommandlet.generated.h"
UCLASS()
class UVisualStudioToolsCommandlet
: public UVisualStudioToolsCommandletBase
{
GENERATED_BODY()
public:
UVisualStudioToolsCommandlet();
int32 Run(
TArray<FString>& Tokens,
TArray<FString>& Switches,
TMap<FString, FString>& ParamVals,
FArchive& OutArchive) override;
};

View File

@@ -0,0 +1,88 @@
// Copyright 2022 (c) Microsoft. All rights reserved.
// Licensed under the MIT License.
#include "VisualStudioToolsCommandletBase.h"
#include "Windows/AllowWindowsPlatformTypes.h"
#include "HAL/FileManager.h"
#include "Misc/Paths.h"
#include "VisualStudioTools.h"
#include "Windows/HideWindowsPlatformTypes.h"
static constexpr auto HelpSwitch = TEXT("help");
static constexpr auto OutputSwitch = TEXT("output");
UVisualStudioToolsCommandletBase::UVisualStudioToolsCommandletBase()
{
IsClient = false;
IsEditor = true;
IsServer = false;
LogToConsole = false;
ShowErrorCount = false;
HelpParamNames.Add(OutputSwitch);
HelpParamDescriptions.Add(TEXT("[Required] The file path to write the command output."));
HelpParamNames.Add(HelpSwitch);
HelpParamDescriptions.Add(TEXT("[Optional] Print this help message and quit the commandlet immediately."));
}
void UVisualStudioToolsCommandletBase::PrintHelp() const
{
UE_LOG(LogVisualStudioTools, Display, TEXT("%s"), *HelpDescription);
UE_LOG(LogVisualStudioTools, Display, TEXT("Usage: %s"), *HelpUsage);
UE_LOG(LogVisualStudioTools, Display, TEXT("Parameters:"));
for (int32 i = 0; i < HelpParamNames.Num(); ++i)
{
UE_LOG(LogVisualStudioTools, Display, TEXT("\t-%s: %s"), *HelpParamNames[i], *HelpParamDescriptions[i]);
}
}
int32 UVisualStudioToolsCommandletBase::Main(const FString& Params)
{
TArray<FString> Tokens;
TArray<FString> Switches;
TMap<FString, FString> ParamVals;
ParseCommandLine(*Params, Tokens, Switches, ParamVals);
if (Switches.Contains(HelpSwitch))
{
PrintHelp();
return 0;
}
UE_LOG(LogVisualStudioTools, Display, TEXT("Init VS Tools cmdlet."));
if (!FPaths::IsProjectFilePathSet())
{
UE_LOG(LogVisualStudioTools, Error, TEXT("You must invoke this commandlet with a project file."));
return -1;
}
FString FullPath = ParamVals.FindRef(OutputSwitch);
if (FullPath.IsEmpty() && !FParse::Value(*Params, TEXT("output "), FullPath))
{
// VS:1678426 - Initial version was using `-output "path-to-file"` (POSIX style).
// However, that does not support paths with spaces, even when surrounded with
// quotes because `FParse::Value` only handles that case when there's no space
// between the parameter name and quoted value.
// For back-compatibility reasons, parse that style by including the space in
// the parameter token like it's usually done for the `=` sign.
UE_LOG(LogVisualStudioTools, Error, TEXT("Missing file output parameter."));
PrintHelp();
return -1;
}
TUniquePtr<FArchive> OutArchive{ IFileManager::Get().CreateFileWriter(*FullPath) };
if (!OutArchive)
{
UE_LOG(LogVisualStudioTools, Error, TEXT("Failed to create index with path: %s."), *FullPath);
return -1;
}
return this->Run(Tokens, Switches, ParamVals, *OutArchive);
}

View File

@@ -0,0 +1,29 @@
// Copyright 2022 (c) Microsoft. All rights reserved.
// Licensed under the MIT License.
#pragma once
#include "Commandlets/Commandlet.h"
#include "VisualStudioToolsCommandletBase.generated.h"
UCLASS(Abstract)
class UVisualStudioToolsCommandletBase
: public UCommandlet
{
GENERATED_BODY()
public:
int32 Main(const FString& Params) override;
protected:
UVisualStudioToolsCommandletBase();
void PrintHelp() const;
virtual int32 Run(
TArray<FString>& Tokens,
TArray<FString>& Switches,
TMap<FString, FString>& ParamVals,
FArchive& OutArchive) PURE_VIRTUAL(UVisualStudioToolsCommandletBase::Run, return 0;);
};

View File

@@ -0,0 +1,7 @@
// Copyright 2022 (c) Microsoft. All rights reserved.
#pragma once
#include "CoreMinimal.h"
DECLARE_LOG_CATEGORY_EXTERN(LogVisualStudioTools, Log, All);

View File

@@ -0,0 +1,77 @@
// Copyright 2022 (c) Microsoft. All rights reserved.
using UnrealBuildTool;
public class VisualStudioTools : ModuleRules
{
public VisualStudioTools(ReadOnlyTargetRules Target) : base(Target)
{
bool bIsCustomDevBuild = System.Environment.GetEnvironmentVariable("VSTUE_IsCustomDevBuild") == "1";
if (bIsCustomDevBuild)
{
// Get correct header suggestions in the IDE and validate
// the plugin build without having to affect for the whole target,
// which is expensive in source-builds of the engine.
PCHUsage = ModuleRules.PCHUsageMode.NoPCHs;
bUseUnity = false;
// When debugging the commandlet code, disable optimizations to get
// proper local variable inspection and less inlined stack frames
OptimizeCode = CodeOptimization.Never;
// Enable more restrict warnings during compilation in UE5.
// Required by tasks in the compliance pipeline.
if (Target.Version.MajorVersion >= 5)
{
UnsafeTypeCastWarningLevel = WarningLevel.Error;
}
}
else
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
}
// To support UE5.1+, the code is using the new FTopLevelAssetPath API
// with a detection of support via version numbers.
// If the check is producing a false positive/negative in your version of the engine
// you can change the block below and force the check as enabled/disabled.
if ((Target.Version.MajorVersion == 5 && Target.Version.MinorVersion >= 1) || Target.Version.MajorVersion > 5)
{
PrivateDefinitions.Add("FILTER_ASSETS_BY_CLASS_PATH=1");
}
else
{
PrivateDefinitions.Add("FILTER_ASSETS_BY_CLASS_PATH=0");
}
PublicDependencyModuleNames.AddRange(
new[]
{
"Core",
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"ApplicationCore",
"AssetRegistry",
"CoreUObject",
"Engine",
"Json",
"JsonUtilities",
"Kismet",
"UnrealEd",
"Slate",
"SlateCore",
"ToolMenus",
"EditorSubsystem",
"MainFrame",
"BlueprintGraph",
"VisualStudioDTE",
"EditorStyle",
"Projects"
}
);
}
}