IntialCommit
This commit is contained in:
91
Plugins/VisualStudioTools/Source/.editorconfig
Normal file
91
Plugins/VisualStudioTools/Source/.editorconfig
Normal 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 =
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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"
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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)
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright 2022 (c) Microsoft. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogVisualStudioTools, Log, All);
|
||||
@@ -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"
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user