1094 lines
		
	
	
		
			36 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			1094 lines
		
	
	
		
			36 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| #include "UnrealSharpEditor.h"
 | |
| #include "AssetToolsModule.h"
 | |
| #include "BlueprintCompilationManager.h"
 | |
| #include "BlueprintEditorLibrary.h"
 | |
| #include "CSUnrealSharpEditorCommands.h"
 | |
| #include "DirectoryWatcherModule.h"
 | |
| #include "CSStyle.h"
 | |
| #include "CSUnrealSharpEditorSettings.h"
 | |
| #include "DesktopPlatformModule.h"
 | |
| #include "IDirectoryWatcher.h"
 | |
| #include "IPluginBrowser.h"
 | |
| #include "ISettingsModule.h"
 | |
| #include "LevelEditor.h"
 | |
| #include "SourceCodeNavigation.h"
 | |
| #include "SubobjectDataSubsystem.h"
 | |
| #include "UnrealSharpRuntimeGlue.h"
 | |
| #include "AssetActions/CSAssetTypeAction_CSBlueprint.h"
 | |
| #include "Engine/AssetManager.h"
 | |
| #include "Engine/InheritableComponentHandler.h"
 | |
| #include "Features/IPluginsEditorFeature.h"
 | |
| #include "UnrealSharpCore/CSManager.h"
 | |
| #include "Framework/Notifications/NotificationManager.h"
 | |
| #include "Interfaces/IMainFrameModule.h"
 | |
| #include "Interfaces/IPluginManager.h"
 | |
| #include "Kismet2/BlueprintEditorUtils.h"
 | |
| #include "Kismet2/DebuggerCommands.h"
 | |
| #include "Logging/StructuredLog.h"
 | |
| #include "Misc/ScopedSlowTask.h"
 | |
| #include "Plugins/CSPluginTemplateDescription.h"
 | |
| #include "Slate/CSNewProjectWizard.h"
 | |
| #include "TypeGenerator/Register/CSGeneratedClassBuilder.h"
 | |
| #include "UnrealSharpProcHelper/CSProcHelper.h"
 | |
| #include "Widgets/Notifications/SNotificationList.h"
 | |
| #include "TypeGenerator/CSClass.h"
 | |
| #include "TypeGenerator/CSEnum.h"
 | |
| #include "TypeGenerator/CSScriptStruct.h"
 | |
| #include "UnrealSharpUtilities/UnrealSharpUtils.h"
 | |
| #include "Utils/CSClassUtilities.h"
 | |
| 
 | |
| #define LOCTEXT_NAMESPACE "FUnrealSharpEditorModule"
 | |
| 
 | |
| DEFINE_LOG_CATEGORY(LogUnrealSharpEditor);
 | |
| 
 | |
| FUnrealSharpEditorModule& FUnrealSharpEditorModule::Get()
 | |
| {
 | |
| 	return FModuleManager::LoadModuleChecked<FUnrealSharpEditorModule>("UnrealSharpEditor");
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::StartupModule()
 | |
| {
 | |
| 	IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
 | |
| 	AssetTools.RegisterAssetTypeActions(MakeShared<FCSAssetTypeAction_CSBlueprint>());
 | |
| 
 | |
| 	TArray<FString> ProjectPaths;
 | |
| 	FCSProcHelper::GetAllProjectPaths(ProjectPaths);
 | |
| 
 | |
| 	for (const FString& ProjectPath : ProjectPaths)
 | |
| 	{
 | |
| 		FString Path = FPaths::GetPath(ProjectPath);
 | |
| 		AddDirectoryToWatch(Path);
 | |
| 	}
 | |
| 
 | |
| 	Manager = &UCSManager::GetOrCreate();
 | |
| 	Manager->OnNewStructEvent().AddRaw(this, &FUnrealSharpEditorModule::OnStructRebuilt);
 | |
| 	Manager->OnNewClassEvent().AddRaw(this, &FUnrealSharpEditorModule::OnClassRebuilt);
 | |
| 	Manager->OnNewEnumEvent().AddRaw(this, &FUnrealSharpEditorModule::OnEnumRebuilt);
 | |
| 
 | |
| 	FEditorDelegates::ShutdownPIE.AddRaw(this, &FUnrealSharpEditorModule::OnPIEShutdown);
 | |
| 
 | |
| 	TickDelegate = FTickerDelegate::CreateRaw(this, &FUnrealSharpEditorModule::Tick);
 | |
| 	TickDelegateHandle = FTSTicker::GetCoreTicker().AddTicker(TickDelegate);
 | |
| 
 | |
| 	if (ProjectPaths.IsEmpty())
 | |
| 	{
 | |
| 		IMainFrameModule::Get().OnMainFrameCreationFinished().AddLambda([this](TSharedPtr<SWindow>, bool)
 | |
| 		{
 | |
| 			SuggestProjectSetup();
 | |
| 		});
 | |
| 	}
 | |
| 
 | |
| 	// Make managed types not available for edit in the editor
 | |
| 	{
 | |
| 		FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>(TEXT("AssetTools"));
 | |
| 		IAssetTools& AssetToolsRef = AssetToolsModule.Get();
 | |
| 
 | |
| 		Manager->ForEachManagedPackage([&AssetToolsRef](const UPackage* Package)
 | |
| 		{
 | |
| 			AssetToolsRef.GetWritableFolderPermissionList()->AddDenyListItem(Package->GetFName(), Package->GetFName());
 | |
| 		});
 | |
| 	}
 | |
| 
 | |
| 	FCSStyle::Initialize();
 | |
| 
 | |
| 	RegisterCommands();
 | |
| 	RegisterMenu();
 | |
|     RegisterPluginTemplates();
 | |
| 
 | |
| 	UCSManager& CSharpManager = UCSManager::Get();
 | |
| 	CSharpManager.LoadPluginAssemblyByName(TEXT("UnrealSharp.Editor"));
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::ShutdownModule()
 | |
| {
 | |
| 	FTSTicker::GetCoreTicker().RemoveTicker(TickDelegateHandle);
 | |
| 	UToolMenus::UnRegisterStartupCallback(this);
 | |
| 	UToolMenus::UnregisterOwner(this);
 | |
|     UnregisterPluginTemplates();
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::OnCSharpCodeModified(const TArray<FFileChangeData>& ChangedFiles)
 | |
| {
 | |
| 	if (IsHotReloading())
 | |
| 	{
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	const UCSUnrealSharpEditorSettings* Settings = GetDefault<UCSUnrealSharpEditorSettings>();
 | |
| 
 | |
| 	if (FPlayWorldCommandCallbacks::IsInPIE() && Settings->AutomaticHotReloading == OnScriptSave)
 | |
| 	{
 | |
| 		bHasQueuedHotReload = true;
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	for (const FFileChangeData& ChangedFile : ChangedFiles)
 | |
| 	{
 | |
| 		FString NormalizedFileName = ChangedFile.Filename;
 | |
| 		FPaths::NormalizeFilename(NormalizedFileName);
 | |
| 
 | |
| 		// Skip ProjectGlue files
 | |
| 		if (NormalizedFileName.Contains("Glue"))
 | |
| 		{
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		// Skip generated files in bin and obj folders
 | |
| 		if (NormalizedFileName.Contains(TEXT("/obj/")))
 | |
| 		{
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		if (Settings->AutomaticHotReloading == OnModuleChange && NormalizedFileName.EndsWith(".dll") &&
 | |
| 			NormalizedFileName.Contains(TEXT("/bin/")))
 | |
| 		{
 | |
| 			// A module changed, initiate the reload and return
 | |
| 			StartHotReload(false);
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		// Check if the file is a .cs file and not in the bin directory
 | |
| 		FString Extension = FPaths::GetExtension(NormalizedFileName);
 | |
| 		if (Extension != "cs" || NormalizedFileName.Contains(TEXT("/bin/")))
 | |
| 		{
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		// Return on the first .cs file we encounter so we can reload.
 | |
| 		if (Settings->AutomaticHotReloading != OnScriptSave)
 | |
| 		{
 | |
| 			HotReloadStatus = PendingReload;
 | |
| 		}
 | |
| 		else
 | |
| 		{
 | |
| 			StartHotReload(true);
 | |
| 		}
 | |
| 
 | |
| 		return;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::StartHotReload(bool bRebuild, bool bPromptPlayerWithNewProject)
 | |
| {
 | |
| 	if (HotReloadStatus == FailedToUnload)
 | |
| 	{
 | |
| 		// If we failed to unload an assembly, we can't hot reload until the editor is restarted.
 | |
| 		bHotReloadFailed = true;
 | |
| 		UE_LOGFMT(LogUnrealSharpEditor, Error, "Hot reload is disabled until the editor is restarted.");
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	TArray<FString> AllProjects;
 | |
| 	FCSProcHelper::GetAllProjectPaths(AllProjects);
 | |
| 
 | |
| 	if (AllProjects.IsEmpty())
 | |
| 	{
 | |
| 		if (bPromptPlayerWithNewProject)
 | |
| 		{
 | |
| 			SuggestProjectSetup();
 | |
| 		}
 | |
| 
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	HotReloadStatus = Active;
 | |
| 	double StartTime = FPlatformTime::Seconds();
 | |
| 
 | |
| 	FScopedSlowTask Progress(3, LOCTEXT("HotReload", "Reloading C#..."));
 | |
| 	Progress.MakeDialog();
 | |
| 
 | |
| 	FString SolutionPath = FCSProcHelper::GetPathToSolution();
 | |
| 	FString OutputPath = FCSProcHelper::GetUserAssemblyDirectory();
 | |
| 
 | |
| 	const UCSUnrealSharpEditorSettings* Settings = GetDefault<UCSUnrealSharpEditorSettings>();
 | |
| 	FString BuildConfiguration = Settings->GetBuildConfigurationString();
 | |
| 	ECSLoggerVerbosity LogVerbosity = Settings->LogVerbosity;
 | |
| 
 | |
| 	FString ExceptionMessage;
 | |
| 	if (!ManagedUnrealSharpEditorCallbacks.Build(*SolutionPath, *OutputPath, *BuildConfiguration, LogVerbosity, &ExceptionMessage, bRebuild))
 | |
| 	{
 | |
| 	 	HotReloadStatus = Inactive;
 | |
| 		bHotReloadFailed = true;
 | |
| 	 	FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(ExceptionMessage), FText::FromString(TEXT("Building C# Project Failed")));
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	UCSManager& CSharpManager = UCSManager::Get();
 | |
| 	bool bUnloadFailed = false;
 | |
| 
 | |
| 	TArray<FString> ProjectsByLoadOrder;
 | |
| 	FCSProcHelper::GetProjectNamesByLoadOrder(ProjectsByLoadOrder, true);
 | |
| 	
 | |
| 	// Unload all assemblies in reverse order to prevent unloading an assembly that is still being referenced.
 | |
| 	// For instance, most assemblies depend on ProjectGlue, so it must be unloaded last.
 | |
| 	// Good info: https://learn.microsoft.com/en-us/dotnet/standard/assembly/unloadability
 | |
| 	// Note: An assembly is only referenced if any of its types are referenced in code.
 | |
| 	// Otherwise optimized out, so ProjectGlue can be unloaded first if it's not used.
 | |
| 	for (int32 i = ProjectsByLoadOrder.Num() - 1; i >= 0; --i)
 | |
| 	{
 | |
| 		const FString& ProjectName = ProjectsByLoadOrder[i];
 | |
| 		UCSAssembly* Assembly = CSharpManager.FindAssembly(*ProjectName);
 | |
| 
 | |
| 		if (IsValid(Assembly) && !Assembly->UnloadAssembly())
 | |
| 		{
 | |
| 			UE_LOGFMT(LogUnrealSharpEditor, Error, "Failed to unload assembly: {0}", *ProjectName);
 | |
| 			bUnloadFailed = true;
 | |
| 			break;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if (bUnloadFailed)
 | |
| 	{
 | |
| 		HotReloadStatus = FailedToUnload;
 | |
| 		bHotReloadFailed = true;
 | |
| 
 | |
| 		FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("HotReloadFailure",
 | |
| 		                                              "One or more assemblies failed to unload. Hot reload will be disabled until the editor restarts.\n\n"
 | |
| 		                                              "Possible causes: Strong GC handles, running threads, etc."),
 | |
| 		                     FText::FromString(TEXT("Hot Reload Failed")));
 | |
| 
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	// Load all assemblies again in the correct order.
 | |
| 	for (const FString& ProjectName : ProjectsByLoadOrder)
 | |
| 	{
 | |
| 		UCSAssembly* Assembly = CSharpManager.FindAssembly(*ProjectName);
 | |
| 
 | |
| 		if (IsValid(Assembly))
 | |
| 		{
 | |
| 			Assembly->LoadAssembly();
 | |
| 		}
 | |
| 		else
 | |
| 		{
 | |
| 			// If the assembly is not loaded. It's a new project, and we need to load it.
 | |
| 			CSharpManager.LoadUserAssemblyByName(*ProjectName);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	Progress.EnterProgressFrame(1, LOCTEXT("HotReload", "Refreshing Affected Blueprints..."));
 | |
| 	RefreshAffectedBlueprints();
 | |
| 
 | |
| 	HotReloadStatus = Inactive;
 | |
| 	bHotReloadFailed = false;
 | |
| 
 | |
| 	UE_LOG(LogUnrealSharpEditor, Log, TEXT("Hot reload took %.2f seconds to execute"), FPlatformTime::Seconds() - StartTime);
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::InitializeUnrealSharpEditorCallbacks(FCSManagedUnrealSharpEditorCallbacks Callbacks)
 | |
| {
 | |
| 	ManagedUnrealSharpEditorCallbacks = Callbacks;
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::OnCreateNewProject()
 | |
| {
 | |
| 	OpenNewProjectDialog();
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::OnCompileManagedCode()
 | |
| {
 | |
| 	Get().StartHotReload();
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::OnReloadManagedCode()
 | |
| {
 | |
| 	Get().StartHotReload(false);
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::OnRegenerateSolution()
 | |
| {
 | |
| 	if (!FCSProcHelper::InvokeUnrealSharpBuildTool(BUILD_ACTION_GENERATE_SOLUTION))
 | |
| 	{
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	OpenSolution();
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::OnOpenSolution()
 | |
| {
 | |
| 	OpenSolution();
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::OnPackageProject()
 | |
| {
 | |
| 	PackageProject();
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::OnMergeManagedSlnAndNativeSln()
 | |
| {
 | |
| 	static FString NativeSolutionPath = FPaths::ProjectDir() / FApp::GetProjectName() + ".sln";
 | |
| 	static FString ManagedSolutionPath = FPaths::ConvertRelativePathToFull(FCSProcHelper::GetPathToSolution());
 | |
| 
 | |
| 	if (!FPaths::FileExists(NativeSolutionPath))
 | |
| 	{
 | |
| 		FString DialogText = FString::Printf(TEXT("Failed to load native solution %s"), *NativeSolutionPath);
 | |
| 		FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(DialogText));
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	if (!FPaths::FileExists(ManagedSolutionPath))
 | |
| 	{
 | |
| 		FString DialogText = FString::Printf(TEXT("Failed to load managed solution %s"), *ManagedSolutionPath);
 | |
| 		FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(DialogText));
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	TArray<FString> NativeSlnFileLines;
 | |
| 	FFileHelper::LoadFileToStringArray(NativeSlnFileLines, *NativeSolutionPath);
 | |
| 
 | |
| 	int32 LastEndProjectIdx = 0;
 | |
| 
 | |
| 	for (int32 idx = 0; idx < NativeSlnFileLines.Num(); ++idx)
 | |
| 	{
 | |
| 		FString Line = NativeSlnFileLines[idx];
 | |
| 		Line.ReplaceInline(TEXT("\n"), TEXT(""));
 | |
| 		if (Line == TEXT("EndProject"))
 | |
| 		{
 | |
| 			LastEndProjectIdx = idx;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	TArray<FString> ManagedSlnFileLines;
 | |
| 	FFileHelper::LoadFileToStringArray(ManagedSlnFileLines, *ManagedSolutionPath);
 | |
| 
 | |
| 	TArray<FString> ManagedProjectLines;
 | |
| 
 | |
| 	for (int32 idx = 0; idx < ManagedSlnFileLines.Num(); ++idx)
 | |
| 	{
 | |
| 		FString Line = ManagedSlnFileLines[idx];
 | |
| 		Line.ReplaceInline(TEXT("\n"), TEXT(""));
 | |
| 		if (Line.StartsWith(TEXT("Project(\"{")) || Line.StartsWith(TEXT("EndProject")))
 | |
| 		{
 | |
| 			ManagedProjectLines.Add(Line);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	for (int32 idx = 0; idx < ManagedProjectLines.Num(); ++idx)
 | |
| 	{
 | |
| 		FString Line = ManagedProjectLines[idx];
 | |
| 		if (Line.StartsWith(TEXT("Project(\"{")) && Line.Contains(TEXT(".csproj")))
 | |
| 		{
 | |
| 			TArray<FString> ProjectStrParts;
 | |
| 			Line.ParseIntoArray(ProjectStrParts, TEXT(", "));
 | |
| 			if(ProjectStrParts.Num() == 3 && ProjectStrParts[1].Contains(TEXT(".csproj")))
 | |
| 			{
 | |
| 				ProjectStrParts[1] = FString("\"Script\\") + ProjectStrParts[1].Mid(1);
 | |
| 				Line = FString::Join(ProjectStrParts, TEXT(", "));
 | |
| 			}
 | |
| 		}
 | |
| 		NativeSlnFileLines.Insert(Line, LastEndProjectIdx + 1 + idx);
 | |
| 	}
 | |
| 
 | |
| 	FString MixedSlnPath = NativeSolutionPath.LeftChop(4) + FString(".Mixed.sln");
 | |
| 	FFileHelper::SaveStringArrayToFile(NativeSlnFileLines, *MixedSlnPath);
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::OnOpenSettings()
 | |
| {
 | |
| 	FModuleManager::LoadModuleChecked<ISettingsModule>("Settings").ShowViewer(
 | |
| 		"Editor", "General", "CSUnrealSharpEditorSettings");
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::OnOpenDocumentation()
 | |
| {
 | |
| 	FPlatformProcess::LaunchURL(TEXT("https://www.unrealsharp.com"), nullptr, nullptr);
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::OnReportBug()
 | |
| {
 | |
| 	FPlatformProcess::LaunchURL(TEXT("https://github.com/UnrealSharp/UnrealSharp/issues"), nullptr, nullptr);
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::OnRefreshRuntimeGlue()
 | |
| {
 | |
| 	FUnrealSharpRuntimeGlueModule& RuntimeGlueModule = FModuleManager::LoadModuleChecked<FUnrealSharpRuntimeGlueModule>(
 | |
| 		"UnrealSharpRuntimeGlue");
 | |
| 	RuntimeGlueModule.ForceRefreshRuntimeGlue();
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::RepairComponents()
 | |
| {
 | |
| 	FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(
 | |
| 		AssetRegistryConstants::ModuleName);
 | |
| 	AssetRegistryModule.Get().SearchAllAssets(/*bSynchronousSearch =*/true);
 | |
| 
 | |
| 	TArray<FAssetData> OutAssetData;
 | |
| 	AssetRegistryModule.Get().GetAssetsByClass(UBlueprint::StaticClass()->GetClassPathName(), OutAssetData, true);
 | |
| 
 | |
| 	FScopedSlowTask Progress(OutAssetData.Num());
 | |
| 	Progress.MakeDialog();
 | |
| 
 | |
| 	USubobjectDataSubsystem* SubobjectDataSubsystem = GEngine->GetEngineSubsystem<USubobjectDataSubsystem>();
 | |
| 
 | |
| 	for (FAssetData const& Asset : OutAssetData)
 | |
| 	{
 | |
| 		const FString AssetPath = Asset.GetObjectPathString();
 | |
| 
 | |
| 		if (!AssetPath.Contains(TEXT("/Game/")))
 | |
| 		{
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		UBlueprint* LoadedBlueprint = Cast<
 | |
| 			UBlueprint>(StaticLoadObject(Asset.GetClass(), nullptr, *AssetPath, nullptr));
 | |
| 		UClass* GeneratedClass = LoadedBlueprint->GeneratedClass;
 | |
| 		UCSClass* ManagedClass = FCSClassUtilities::GetFirstManagedClass(GeneratedClass);
 | |
| 
 | |
| 		if (!ManagedClass)
 | |
| 		{
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		Progress.EnterProgressFrame(1, FText::FromString(FString::Printf(TEXT("Fixing up Blueprint: %s"), *AssetPath)));
 | |
| 
 | |
| 		AActor* ActorCDO = Cast<AActor>(GeneratedClass->GetDefaultObject(false));
 | |
| 		if (!ActorCDO)
 | |
| 		{
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		TArray<FSubobjectDataHandle> SubobjectData;
 | |
| 		SubobjectDataSubsystem->K2_GatherSubobjectDataForBlueprint(LoadedBlueprint, SubobjectData);
 | |
| 
 | |
| 		UInheritableComponentHandler* InheritableComponentHandler = LoadedBlueprint->
 | |
| 			GetInheritableComponentHandler(false);
 | |
| 
 | |
| 		if (!InheritableComponentHandler)
 | |
| 		{
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		TArray<UObject*> Subobjects;
 | |
| 		ActorCDO->GetDefaultSubobjects(Subobjects);
 | |
| 
 | |
| 		TArray<UObject*> MatchingInstances;
 | |
| 		GetObjectsOfClass(LoadedBlueprint->GeneratedClass, MatchingInstances, true, RF_ClassDefaultObject,
 | |
| 		                  EInternalObjectFlags::Garbage);
 | |
| 
 | |
| 		for (TFieldIterator<FObjectProperty> PropertyIt(ManagedClass, EFieldIteratorFlags::IncludeSuper); PropertyIt; ++
 | |
| 		     PropertyIt)
 | |
| 		{
 | |
| 			FObjectProperty* Property = *PropertyIt;
 | |
| 
 | |
| 			if (!FCSClassUtilities::IsManagedClass(Property->GetOwnerClass()))
 | |
| 			{
 | |
| 				break;
 | |
| 			}
 | |
| 
 | |
| 			UActorComponent* OldComponentArchetype = Cast<UActorComponent>(
 | |
| 				Property->GetObjectPropertyValue_InContainer(ActorCDO));
 | |
| 
 | |
| 			if (!OldComponentArchetype || !Subobjects.Contains(OldComponentArchetype))
 | |
| 			{
 | |
| 				continue;
 | |
| 			}
 | |
| 
 | |
| 			Property->SetObjectPropertyValue_InContainer(ActorCDO, nullptr);
 | |
| 
 | |
| 			FComponentKey ComponentKey = InheritableComponentHandler->FindKey(OldComponentArchetype->GetFName());
 | |
| 
 | |
| 			if (!ComponentKey.IsValid())
 | |
| 			{
 | |
| 				continue;
 | |
| 			}
 | |
| 
 | |
| 			UActorComponent* NewArchetype = InheritableComponentHandler->GetOverridenComponentTemplate(ComponentKey);
 | |
| 			CopyProperties(OldComponentArchetype, NewArchetype);
 | |
| 			FBlueprintEditorUtils::MarkBlueprintAsModified(LoadedBlueprint, Property);
 | |
| 
 | |
| 			for (UObject* Instance : MatchingInstances)
 | |
| 			{
 | |
| 				AActor* ActorInstance = static_cast<AActor*>(Instance);
 | |
| 				TArray<TObjectPtr<UActorComponent>>& Components = ActorInstance->BlueprintCreatedComponents;
 | |
| 
 | |
| 				for (TObjectPtr<UActorComponent>& Component : Components)
 | |
| 				{
 | |
| 					if (Component->GetName() == OldComponentArchetype->GetName())
 | |
| 					{
 | |
| 						CopyProperties(OldComponentArchetype, Component);
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		UBlueprintEditorLibrary::CompileBlueprint(LoadedBlueprint);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::CopyProperties(UActorComponent* Source, UActorComponent* Target)
 | |
| {
 | |
| 	UClass* SourceClass = Source->GetClass();
 | |
| 	UClass* TargetClass = Target->GetClass();
 | |
| 
 | |
| 	if (SourceClass != TargetClass)
 | |
| 	{
 | |
| 		UE_LOG(LogUnrealSharpEditor, Error, TEXT("Source and Target classes are not the same."));
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	for (TFieldIterator<FProperty> PropertyIt(SourceClass, EFieldIteratorFlags::IncludeSuper); PropertyIt; ++PropertyIt)
 | |
| 	{
 | |
| 		FProperty* Property = *PropertyIt;
 | |
| 
 | |
| 		if (!Property->HasAnyPropertyFlags(CPF_BlueprintVisible | CPF_Edit))
 | |
| 		{
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		FString Data;
 | |
| 		Property->ExportTextItem_InContainer(Data, Source, nullptr, nullptr, PPF_None);
 | |
| 		Property->ImportText_InContainer(*Data, Target, Target, 0);
 | |
| 	}
 | |
| 
 | |
| 	Target->PostLoad();
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::OnRepairComponents()
 | |
| {
 | |
| 	RepairComponents();
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::OnExploreArchiveDirectory(FString ArchiveDirectory)
 | |
| {
 | |
| 	FPlatformProcess::ExploreFolder(*ArchiveDirectory);
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::PackageProject()
 | |
| {
 | |
| 	FString ArchiveDirectory = SelectArchiveDirectory();
 | |
| 
 | |
| 	if (ArchiveDirectory.IsEmpty())
 | |
| 	{
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	FString ExecutablePath = ArchiveDirectory / FApp::GetProjectName() + ".exe";
 | |
| 	if (!FPaths::FileExists(ExecutablePath))
 | |
| 	{
 | |
| 		FString DialogText = FString::Printf(
 | |
| 			TEXT(
 | |
| 				"The executable for project '%s' could not be found in the directory: %s. Please select the root directory where you packaged your game."),
 | |
| 			FApp::GetProjectName(), *ArchiveDirectory);
 | |
| 		FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(DialogText));
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	FScopedSlowTask Progress(1, LOCTEXT("USharpPackaging", "Packaging Project..."));
 | |
| 	Progress.MakeDialog();
 | |
| 
 | |
| 	TMap<FString, FString> Arguments;
 | |
| 	Arguments.Add("ArchiveDirectory", FCSUnrealSharpUtils::MakeQuotedPath(ArchiveDirectory));
 | |
| 	Arguments.Add("BuildConfig", "Release");
 | |
| 	FCSProcHelper::InvokeUnrealSharpBuildTool(BUILD_ACTION_PACKAGE_PROJECT, Arguments);
 | |
| 
 | |
| 	FNotificationInfo Info(
 | |
| 		FText::FromString(
 | |
| 			FString::Printf(TEXT("Project '%s' has been packaged successfully."), FApp::GetProjectName())));
 | |
| 	Info.ExpireDuration = 15.0f;
 | |
| 	Info.bFireAndForget = true;
 | |
| 	Info.ButtonDetails.Add(FNotificationButtonInfo(
 | |
| 		LOCTEXT("USharpRunPackagedGame", "Run Packaged Game"),
 | |
| 		LOCTEXT("", ""),
 | |
| 		FSimpleDelegate::CreateStatic(&FUnrealSharpEditorModule::RunGame, ExecutablePath),
 | |
| 		SNotificationItem::CS_None));
 | |
| 
 | |
| 	Info.ButtonDetails.Add(FNotificationButtonInfo(
 | |
| 		LOCTEXT("USharpOpenPackagedGame", "Open Folder"),
 | |
| 		LOCTEXT("", ""),
 | |
| 		FSimpleDelegate::CreateStatic(&FUnrealSharpEditorModule::OnExploreArchiveDirectory, ArchiveDirectory),
 | |
| 		SNotificationItem::CS_None));
 | |
| 
 | |
| 	TSharedPtr<SNotificationItem> NotificationItem = FSlateNotificationManager::Get().AddNotification(Info);
 | |
| 	NotificationItem->SetCompletionState(SNotificationItem::CS_None);
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::RunGame(FString ExecutablePath)
 | |
| {
 | |
| 	FString OpenSolutionArgs = FString::Printf(TEXT("/c \"%s\""), *ExecutablePath);
 | |
| 	FPlatformProcess::ExecProcess(TEXT("cmd.exe"), *OpenSolutionArgs, nullptr, nullptr, nullptr);
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::OpenSolution()
 | |
| {
 | |
| 	FString SolutionPath = FPaths::ConvertRelativePathToFull(FCSProcHelper::GetPathToSolution());
 | |
| 
 | |
| 	if (!FPaths::FileExists(SolutionPath))
 | |
| 	{
 | |
| 		OnRegenerateSolution();
 | |
| 	}
 | |
| 
 | |
| 	FString ExceptionMessage;
 | |
| 	if (!ManagedUnrealSharpEditorCallbacks.OpenSolution(*SolutionPath, &ExceptionMessage))
 | |
| 	{
 | |
| 		FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(ExceptionMessage), FText::FromString(TEXT("Opening C# Project Failed")));
 | |
| 		return;
 | |
| 	}
 | |
| };
 | |
| 
 | |
| FString FUnrealSharpEditorModule::SelectArchiveDirectory()
 | |
| {
 | |
| 	IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get();
 | |
| 	if (!DesktopPlatform)
 | |
| 	{
 | |
| 		return FString();
 | |
| 	}
 | |
| 
 | |
| 	FString DestinationFolder;
 | |
| 	const void* ParentWindowHandle = FSlateApplication::Get().FindBestParentWindowHandleForDialogs(nullptr);
 | |
| 	const FString Title = LOCTEXT("USharpChooseArchiveRoot", "Find Archive Root").ToString();
 | |
| 
 | |
| 	if (DesktopPlatform->OpenDirectoryDialog(ParentWindowHandle, Title, FString(), DestinationFolder))
 | |
| 	{
 | |
| 		return FPaths::ConvertRelativePathToFull(DestinationFolder);
 | |
| 	}
 | |
| 
 | |
| 	return FString();
 | |
| }
 | |
| 
 | |
| TSharedRef<SWidget> FUnrealSharpEditorModule::GenerateUnrealSharpMenu()
 | |
| {
 | |
| 	const FCSUnrealSharpEditorCommands& CSCommands = FCSUnrealSharpEditorCommands::Get();
 | |
| 	FMenuBuilder MenuBuilder(true, UnrealSharpCommands);
 | |
| 
 | |
| 	// Build
 | |
| 	MenuBuilder.BeginSection("Build", LOCTEXT("Build", "Build"));
 | |
| 
 | |
| 	MenuBuilder.AddMenuEntry(CSCommands.CompileManagedCode, NAME_None, TAttribute<FText>(), TAttribute<FText>(),
 | |
| 	                         FSlateIcon(FAppStyle::Get().GetStyleSetName(), "LevelEditor.Recompile"));
 | |
| 
 | |
| 	MenuBuilder.AddMenuEntry(CSCommands.ReloadManagedCode, NAME_None, TAttribute<FText>(), TAttribute<FText>(),
 | |
| 	                         FSlateIcon(FAppStyle::Get().GetStyleSetName(), "LevelEditor.Recompile"));
 | |
| 
 | |
| 	MenuBuilder.EndSection();
 | |
| 
 | |
| 	// Project
 | |
| 	MenuBuilder.BeginSection("Project", LOCTEXT("Project", "Project"));
 | |
| 
 | |
| 	MenuBuilder.AddMenuEntry(CSCommands.CreateNewProject, NAME_None, TAttribute<FText>(), TAttribute<FText>(),
 | |
| 	                         FSourceCodeNavigation::GetOpenSourceCodeIDEIcon());
 | |
| 
 | |
| 	MenuBuilder.AddMenuEntry(CSCommands.OpenSolution, NAME_None, TAttribute<FText>(), TAttribute<FText>(),
 | |
| 	                         FSourceCodeNavigation::GetOpenSourceCodeIDEIcon());
 | |
| 
 | |
| 	MenuBuilder.AddMenuEntry(CSCommands.RegenerateSolution, NAME_None, TAttribute<FText>(), TAttribute<FText>(),
 | |
| 	                         FSourceCodeNavigation::GetOpenSourceCodeIDEIcon());
 | |
| 
 | |
| 	MenuBuilder.AddMenuEntry(CSCommands.MergeManagedSlnAndNativeSln, NAME_None, TAttribute<FText>(), TAttribute<FText>(),
 | |
| 							 FSourceCodeNavigation::GetOpenSourceCodeIDEIcon());
 | |
| 
 | |
| 	MenuBuilder.EndSection();
 | |
| 
 | |
| 	// Package
 | |
| 	MenuBuilder.BeginSection("Package", LOCTEXT("Package", "Package"));
 | |
| 
 | |
| 	MenuBuilder.AddMenuEntry(CSCommands.PackageProject, NAME_None, TAttribute<FText>(), TAttribute<FText>(),
 | |
| 	                         FSlateIcon(FAppStyle::Get().GetStyleSetName(), "LevelEditor.Recompile"));
 | |
| 
 | |
| 	MenuBuilder.EndSection();
 | |
| 
 | |
| 	// Plugin
 | |
| 	MenuBuilder.BeginSection("Plugin", LOCTEXT("Plugin", "Plugin"));
 | |
| 
 | |
| 	MenuBuilder.AddMenuEntry(CSCommands.OpenSettings, NAME_None, TAttribute<FText>(), TAttribute<FText>(),
 | |
| 	                         FSlateIcon(FAppStyle::Get().GetStyleSetName(), "EditorPreferences.TabIcon"));
 | |
| 
 | |
| 	MenuBuilder.AddMenuEntry(CSCommands.OpenDocumentation, NAME_None, TAttribute<FText>(), TAttribute<FText>(),
 | |
| 	                         FSlateIcon(FAppStyle::Get().GetStyleSetName(), "MainFrame.DocumentationHome"));
 | |
| 
 | |
| 	MenuBuilder.AddMenuEntry(CSCommands.ReportBug, NAME_None, TAttribute<FText>(), TAttribute<FText>(),
 | |
| 	                         FSlateIcon(FAppStyle::Get().GetStyleSetName(), "MainFrame.ReportABug"));
 | |
| 
 | |
| 	MenuBuilder.EndSection();
 | |
| 
 | |
| 	MenuBuilder.BeginSection("Glue", LOCTEXT("Glue", "Glue"));
 | |
| 
 | |
| 	MenuBuilder.AddMenuEntry(CSCommands.RefreshRuntimeGlue, NAME_None, TAttribute<FText>(), TAttribute<FText>(),
 | |
| 	                         FSlateIcon(FAppStyle::GetAppStyleSetName(), "SourceControl.Actions.Refresh"));
 | |
| 
 | |
| 	MenuBuilder.EndSection();
 | |
| 
 | |
| 	MenuBuilder.BeginSection("Tools", LOCTEXT("Tools", "Tools"));
 | |
| 
 | |
| 	MenuBuilder.AddMenuEntry(CSCommands.RepairComponents, NAME_None, TAttribute<FText>(), TAttribute<FText>(),
 | |
| 	                         FSlateIcon(FAppStyle::GetAppStyleSetName(), "SourceControl.Actions.Refresh"));
 | |
| 
 | |
| 	return MenuBuilder.MakeWidget();
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::OpenNewProjectDialog()
 | |
| {
 | |
| 	TSharedRef<SWindow> AddCodeWindow = SNew(SWindow)
 | |
| 		.Title(LOCTEXT("CreateNewProject", "New C# Project"))
 | |
| 		.SizingRule(ESizingRule::Autosized)
 | |
| 		.SupportsMinimize(false);
 | |
| 
 | |
| 	TSharedRef<SCSNewProjectDialog> NewProjectDialog = SNew(SCSNewProjectDialog);
 | |
| 	AddCodeWindow->SetContent(NewProjectDialog);
 | |
| 
 | |
| 	FSlateApplication::Get().AddWindow(AddCodeWindow);
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::SuggestProjectSetup()
 | |
| {
 | |
| 	FString DialogText = TEXT("No C# projects were found. Would you like to create a new C# project?");
 | |
| 	EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString(DialogText));
 | |
| 
 | |
| 	if (Result == EAppReturnType::No)
 | |
| 	{
 | |
| 		return;
 | |
| 	}
 | |
| 	
 | |
| 	OpenNewProjectDialog();
 | |
| }
 | |
| 
 | |
| bool FUnrealSharpEditorModule::Tick(float DeltaTime)
 | |
| {
 | |
| 	const UCSUnrealSharpEditorSettings* Settings = GetDefault<UCSUnrealSharpEditorSettings>();
 | |
| 	if (Settings->AutomaticHotReloading == OnEditorFocus && !IsHotReloading() && HasPendingHotReloadChanges() &&
 | |
| 		FApp::HasFocus())
 | |
| 	{
 | |
| 		StartHotReload();
 | |
| 	}
 | |
| 
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::RegisterCommands()
 | |
| {
 | |
| 	FCSUnrealSharpEditorCommands::Register();
 | |
| 	UnrealSharpCommands = MakeShareable(new FUICommandList);
 | |
| 	UnrealSharpCommands->MapAction(FCSUnrealSharpEditorCommands::Get().CreateNewProject,
 | |
| 	                               FExecuteAction::CreateStatic(&FUnrealSharpEditorModule::OnCreateNewProject));
 | |
| 	UnrealSharpCommands->MapAction(FCSUnrealSharpEditorCommands::Get().CompileManagedCode,
 | |
| 	                               FExecuteAction::CreateStatic(&FUnrealSharpEditorModule::OnCompileManagedCode));
 | |
| 	UnrealSharpCommands->MapAction(FCSUnrealSharpEditorCommands::Get().ReloadManagedCode,
 | |
| 	                               FExecuteAction::CreateStatic(&FUnrealSharpEditorModule::OnReloadManagedCode));
 | |
| 	UnrealSharpCommands->MapAction(FCSUnrealSharpEditorCommands::Get().RegenerateSolution,
 | |
| 	                               FExecuteAction::CreateRaw(this, &FUnrealSharpEditorModule::OnRegenerateSolution));
 | |
| 	UnrealSharpCommands->MapAction(FCSUnrealSharpEditorCommands::Get().OpenSolution,
 | |
| 	                               FExecuteAction::CreateRaw(this, &FUnrealSharpEditorModule::OnOpenSolution));
 | |
| 	UnrealSharpCommands->MapAction(FCSUnrealSharpEditorCommands::Get().MergeManagedSlnAndNativeSln,
 | |
| 								   FExecuteAction::CreateStatic(&FUnrealSharpEditorModule::OnMergeManagedSlnAndNativeSln));
 | |
| 	UnrealSharpCommands->MapAction(FCSUnrealSharpEditorCommands::Get().PackageProject,
 | |
| 	                               FExecuteAction::CreateStatic(&FUnrealSharpEditorModule::OnPackageProject));
 | |
| 	UnrealSharpCommands->MapAction(FCSUnrealSharpEditorCommands::Get().OpenSettings,
 | |
| 	                               FExecuteAction::CreateStatic(&FUnrealSharpEditorModule::OnOpenSettings));
 | |
| 	UnrealSharpCommands->MapAction(FCSUnrealSharpEditorCommands::Get().OpenDocumentation,
 | |
| 	                               FExecuteAction::CreateStatic(&FUnrealSharpEditorModule::OnOpenDocumentation));
 | |
| 	UnrealSharpCommands->MapAction(FCSUnrealSharpEditorCommands::Get().ReportBug,
 | |
| 	                               FExecuteAction::CreateStatic(&FUnrealSharpEditorModule::OnReportBug));
 | |
| 	UnrealSharpCommands->MapAction(FCSUnrealSharpEditorCommands::Get().RefreshRuntimeGlue,
 | |
| 							   FExecuteAction::CreateStatic(&FUnrealSharpEditorModule::OnRefreshRuntimeGlue));
 | |
| 	UnrealSharpCommands->MapAction(FCSUnrealSharpEditorCommands::Get().RepairComponents,
 | |
| 	                               FExecuteAction::CreateStatic(&FUnrealSharpEditorModule::OnRepairComponents));
 | |
| 
 | |
| 	const FLevelEditorModule& LevelEditorModule = FModuleManager::GetModuleChecked<FLevelEditorModule>("LevelEditor");
 | |
| 	const TSharedRef<FUICommandList> Commands = LevelEditorModule.GetGlobalLevelEditorActions();
 | |
| 	Commands->Append(UnrealSharpCommands.ToSharedRef());
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::RegisterMenu()
 | |
| {
 | |
| 	UToolMenu* ToolbarMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar.PlayToolBar");
 | |
| 	FToolMenuSection& Section = ToolbarMenu->FindOrAddSection("PluginTools");
 | |
| 
 | |
| 	FToolMenuEntry Entry = FToolMenuEntry::InitComboButton(
 | |
| 		"UnrealSharp",
 | |
| 		FUIAction(),
 | |
| 		FOnGetContent::CreateLambda([this]() { return GenerateUnrealSharpMenu(); }),
 | |
| 		LOCTEXT("UnrealSharp_Label", "UnrealSharp"),
 | |
| 		LOCTEXT("UnrealSharp_Tooltip", "List of all UnrealSharp actions"),
 | |
| 		TAttribute<FSlateIcon>::CreateLambda([this]()
 | |
| 		{
 | |
| 			return GetMenuIcon();
 | |
| 		}));
 | |
| 
 | |
| 	Section.AddEntry(Entry);
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::RegisterPluginTemplates()
 | |
| {
 | |
|     IPluginBrowser& PluginBrowser = IPluginBrowser::Get();
 | |
|     const FString PluginBaseDir = FPaths::ConvertRelativePathToFull(IPluginManager::Get().FindPlugin(UE_PLUGIN_NAME)->GetBaseDir());
 | |
| 
 | |
|     const FText BlankTemplateName = LOCTEXT("UnrealSharp_BlankLabel", "C++/C# Joint");
 | |
| 	const FText CSharpOnlyTemplateName = LOCTEXT("UnrealSharp_CSharpOnlyLabel", "C# Only");
 | |
| 
 | |
| 	const FText BlankDescription = LOCTEXT("UnrealSharp_BlankTemplateDesc", "Create a blank plugin with a minimal amount of C++ and C# code.");
 | |
| 	const FText CSharpOnlyDescription = LOCTEXT("UnrealSharp_CSharpOnlyTemplateDesc", "Create a blank plugin that can only contain content and C# scripts.");
 | |
| 	
 | |
|     const TSharedRef<FPluginTemplateDescription> BlankTemplate = MakeShared<FCSPluginTemplateDescription>(BlankTemplateName, BlankDescription,
 | |
|         PluginBaseDir / TEXT("Templates") / TEXT("Blank"), true, EHostType::Runtime, ELoadingPhase::Default, true);
 | |
| 	
 | |
|     const TSharedRef<FPluginTemplateDescription> CSharpOnlyTemplate = MakeShared<FCSPluginTemplateDescription>(CSharpOnlyTemplateName, CSharpOnlyDescription,
 | |
|         PluginBaseDir / TEXT("Templates") / TEXT("CSharpOnly"), true, EHostType::Runtime, ELoadingPhase::Default, false);
 | |
| 
 | |
|     PluginBrowser.RegisterPluginTemplate(BlankTemplate);
 | |
|     PluginBrowser.RegisterPluginTemplate(CSharpOnlyTemplate);
 | |
| 
 | |
|     PluginTemplates.Add(BlankTemplate);
 | |
|     PluginTemplates.Add(CSharpOnlyTemplate);
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::UnregisterPluginTemplates()
 | |
| {
 | |
|     IPluginBrowser& PluginBrowser = IPluginBrowser::Get();
 | |
|     for (const TSharedRef<FPluginTemplateDescription>& Template : PluginTemplates)
 | |
|     {
 | |
|         PluginBrowser.UnregisterPluginTemplate(Template);
 | |
|     }
 | |
| }
 | |
| 
 | |
| 
 | |
| void FUnrealSharpEditorModule::OnPIEShutdown(bool IsSimulating)
 | |
| {
 | |
| 	// Replicate UE behavior, which forces a garbage collection when exiting PIE.
 | |
| 	ManagedUnrealSharpEditorCallbacks.ForceManagedGC();
 | |
| 
 | |
| 	if (bHasQueuedHotReload)
 | |
| 	{
 | |
| 		bHasQueuedHotReload = false;
 | |
| 		StartHotReload();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::AddNewProject(const FString& ModuleName, const FString& ProjectParentFolder, const FString& ProjectRoot, const TMap<FString, FString>& ExtraArguments)
 | |
| {
 | |
| 	TMap<FString, FString> Arguments = ExtraArguments;
 | |
| 
 | |
| 	TMap<FString, FString> SolutionArguments;
 | |
| 	SolutionArguments.Add(TEXT("MODULENAME"), ModuleName);
 | |
| 
 | |
| 	FString ProjectFolder = FPaths::Combine(ProjectParentFolder, ModuleName);
 | |
| 	FString ModuleFilePath = FPaths::Combine(ProjectFolder, ModuleName + ".cs");
 | |
| 	
 | |
| 	FillTemplateFile(TEXT("Module"), SolutionArguments, ModuleFilePath);
 | |
| 
 | |
| 	Arguments.Add(TEXT("NewProjectName"), ModuleName);
 | |
| 	Arguments.Add(TEXT("NewProjectFolder"), FCSUnrealSharpUtils::MakeQuotedPath(FPaths::ConvertRelativePathToFull(ProjectParentFolder)));
 | |
| 	
 | |
| 	FString FullProjectRoot = FPaths::ConvertRelativePathToFull(ProjectRoot);
 | |
| 	Arguments.Add(TEXT("ProjectRoot"), FCSUnrealSharpUtils::MakeQuotedPath(FullProjectRoot));
 | |
| 
 | |
| 	if (!FCSProcHelper::InvokeUnrealSharpBuildTool(BUILD_ACTION_GENERATE_PROJECT, Arguments))
 | |
| 	{
 | |
| 		UE_LOGFMT(LogUnrealSharpEditor, Error, "Failed to generate project %s in %s", *ModuleName, *ProjectParentFolder);
 | |
| 		return;
 | |
| 	}
 | |
| 	
 | |
| 	OpenSolution();
 | |
| 	AddDirectoryToWatch(FPaths::Combine(FullProjectRoot, TEXT("Script")));
 | |
| 
 | |
| 	FString CsProjPath = FPaths::Combine(ProjectFolder, ModuleName + ".csproj");
 | |
| 
 | |
| 	if (!FPaths::FileExists(CsProjPath))
 | |
| 	{
 | |
| 		UE_LOGFMT(LogUnrealSharpEditor, Error, "Failed to find .csproj %s in %s", *ModuleName, *ProjectParentFolder);
 | |
| 		return;
 | |
| 	}
 | |
| 	
 | |
| 	GetManagedUnrealSharpEditorCallbacks().AddProjectToCollection(*CsProjPath);
 | |
| }
 | |
| 
 | |
| bool FUnrealSharpEditorModule::FillTemplateFile(const FString& TemplateName, TMap<FString, FString>& Replacements, const FString& Path)
 | |
| {
 | |
| 	const FString FullFileName = FCSProcHelper::GetPluginDirectory() / TEXT("Templates") / TemplateName + TEXT(".cs.template");
 | |
| 
 | |
| 	FString OutTemplate;
 | |
| 	if (!FFileHelper::LoadFileToString(OutTemplate, *FullFileName))
 | |
| 	{
 | |
| 		UE_LOG(LogUnrealSharpEditor, Error, TEXT("Failed to load template file %s"), *FullFileName);
 | |
| 		return false;
 | |
| 	}
 | |
| 
 | |
| 	for (const TPair<FString, FString>& Replacement : Replacements)
 | |
| 	{
 | |
| 		FString ReplacementKey = TEXT("%") + Replacement.Key + TEXT("%");
 | |
| 		OutTemplate = OutTemplate.Replace(*ReplacementKey, *Replacement.Value);
 | |
| 	}
 | |
| 
 | |
| 	if (!FFileHelper::SaveStringToFile(OutTemplate, *Path))
 | |
| 	{
 | |
| 		UE_LOG(LogUnrealSharpEditor, Error, TEXT("Failed to save %s when trying to create a template"), *Path);
 | |
| 		return false;
 | |
| 	}
 | |
| 
 | |
| 	return true;
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::OnStructRebuilt(UCSScriptStruct* NewStruct)
 | |
| {
 | |
| 	RebuiltStructs.Add(NewStruct);
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::OnClassRebuilt(UCSClass* NewClass)
 | |
| {
 | |
| 	RebuiltClasses.Add(NewClass);
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::OnEnumRebuilt(UCSEnum* NewEnum)
 | |
| {
 | |
| 	RebuiltEnums.Add(NewEnum);
 | |
| }
 | |
| 
 | |
| bool FUnrealSharpEditorModule::IsPinAffectedByReload(const FEdGraphPinType& PinType) const
 | |
| {
 | |
| 	UObject* PinSubCategoryObject = PinType.PinSubCategoryObject.Get();
 | |
| 	if (!IsValid(PinSubCategoryObject) || !Manager->IsManagedType(PinSubCategoryObject))
 | |
| 	{
 | |
| 		return false;
 | |
| 	}
 | |
| 
 | |
| 	auto IsPinTypeRebuilt = [this](UObject* PinSubCategoryObject) -> bool
 | |
| 	{
 | |
| 		if (UCSClass* Class = Cast<UCSClass>(PinSubCategoryObject))
 | |
| 		{
 | |
| 			return RebuiltClasses.Contains(Class);
 | |
| 		}
 | |
| 
 | |
| 		if (UCSEnum* Enum = Cast<UCSEnum>(PinSubCategoryObject))
 | |
| 		{
 | |
| 			return RebuiltEnums.Contains(Enum);
 | |
| 		}
 | |
| 
 | |
| 		if (UCSScriptStruct* Struct = Cast<UCSScriptStruct>(PinSubCategoryObject))
 | |
| 		{
 | |
| 			return RebuiltStructs.Contains(Struct);
 | |
| 		}
 | |
| 
 | |
| 		if (UCSEnum* Enum = Cast<UCSEnum>(PinSubCategoryObject))
 | |
| 		{
 | |
| 			return RebuiltEnums.Contains(Enum);
 | |
| 		}
 | |
| 
 | |
| 		return false;
 | |
| 	};
 | |
| 
 | |
| 	if (!IsPinTypeRebuilt(PinSubCategoryObject))
 | |
| 	{
 | |
| 		return false;
 | |
| 	}
 | |
| 
 | |
| 	if (PinType.IsMap() && PinType.PinValueType.TerminalSubCategoryObject.IsValid())
 | |
| 	{
 | |
| 		UObject* MapValueType = PinType.PinValueType.TerminalSubCategoryObject.Get();
 | |
| 		if (IsValid(MapValueType) && Manager->IsManagedType(MapValueType))
 | |
| 		{
 | |
| 			return IsPinTypeRebuilt(MapValueType);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return false;
 | |
| }
 | |
| 
 | |
| bool FUnrealSharpEditorModule::IsNodeAffectedByReload(UEdGraphNode* Node) const
 | |
| {
 | |
| 	if (UK2Node_EditablePinBase* EditableNode = Cast<UK2Node_EditablePinBase>(Node))
 | |
| 	{
 | |
| 		for (const TSharedPtr<FUserPinInfo>& Pin : EditableNode->UserDefinedPins)
 | |
| 		{
 | |
| 			if (IsPinAffectedByReload(Pin->PinType))
 | |
| 			{
 | |
| 				return true;
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		return false;
 | |
| 	}
 | |
| 
 | |
| 	for (UEdGraphPin* Pin : Node->Pins)
 | |
| 	{
 | |
| 		if (IsPinAffectedByReload(Pin->PinType))
 | |
| 		{
 | |
| 			return true;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return false;
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::AddDirectoryToWatch(const FString& Directory)
 | |
| {
 | |
| 	if (WatchingDirectories.Contains(Directory))
 | |
| 	{
 | |
| 		return;
 | |
| 	}
 | |
| 	
 | |
| 	if (!FPaths::DirectoryExists(Directory))
 | |
| 	{
 | |
| 		FPlatformFileManager::Get().GetPlatformFile().CreateDirectory(*Directory);
 | |
| 	}
 | |
| 
 | |
| 	FDirectoryWatcherModule& DirectoryWatcherModule = FModuleManager::LoadModuleChecked<FDirectoryWatcherModule>("DirectoryWatcher");
 | |
| 	
 | |
| 	FDelegateHandle Handle;
 | |
| 	DirectoryWatcherModule.Get()->RegisterDirectoryChangedCallback_Handle(
 | |
| 		Directory,
 | |
| 		IDirectoryWatcher::FDirectoryChanged::CreateRaw(this, &FUnrealSharpEditorModule::OnCSharpCodeModified),
 | |
| 		Handle);
 | |
| 
 | |
| 	WatchingDirectories.Add(Directory);
 | |
| }
 | |
| 
 | |
| void FUnrealSharpEditorModule::RefreshAffectedBlueprints()
 | |
| {
 | |
| 	if (RebuiltStructs.IsEmpty() && RebuiltClasses.IsEmpty() && RebuiltEnums.IsEmpty())
 | |
| 	{
 | |
| 		// Early out if nothing has changed its structure.
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	TArray<UBlueprint*> AffectedBlueprints;
 | |
| 	for (TObjectIterator<UBlueprint> BlueprintIt; BlueprintIt; ++BlueprintIt)
 | |
| 	{
 | |
| 		UBlueprint* Blueprint = *BlueprintIt;
 | |
| 		if (!IsValid(Blueprint->GeneratedClass) || FCSClassUtilities::IsManagedClass(Blueprint->GeneratedClass))
 | |
| 		{
 | |
| 			return;
 | |
| 		}
 | |
| 
 | |
| 		TArray<UK2Node*> AllNodes;
 | |
| 		FBlueprintEditorUtils::GetAllNodesOfClass<UK2Node>(Blueprint, AllNodes);
 | |
| 
 | |
| 		for (UK2Node* Node : AllNodes)
 | |
| 		{
 | |
| 			if (IsNodeAffectedByReload(Node))
 | |
| 			{
 | |
| 				Node->ReconstructNode();
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		AffectedBlueprints.Add(Blueprint);
 | |
| 	}
 | |
| 
 | |
| 	for (UBlueprint* Blueprint : AffectedBlueprints)
 | |
| 	{
 | |
| 		FKismetEditorUtilities::CompileBlueprint(Blueprint, EBlueprintCompileOptions::SkipGarbageCollection);
 | |
| 	}
 | |
| 
 | |
| 	RebuiltStructs.Reset();
 | |
| 	RebuiltClasses.Reset();
 | |
| 	RebuiltEnums.Reset();
 | |
| 
 | |
| 	CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
 | |
| }
 | |
| 
 | |
| FSlateIcon FUnrealSharpEditorModule::GetMenuIcon() const
 | |
| {
 | |
| 	if (HasHotReloadFailed())
 | |
| 	{
 | |
| 		return FSlateIcon(FCSStyle::GetStyleSetName(), "UnrealSharp.Toolbar.Fail");
 | |
| 	}
 | |
| 	if (HasPendingHotReloadChanges())
 | |
| 	{
 | |
| 		return FSlateIcon(FCSStyle::GetStyleSetName(), "UnrealSharp.Toolbar.Modified");
 | |
| 	}
 | |
| 
 | |
| 	return FSlateIcon(FCSStyle::GetStyleSetName(), "UnrealSharp.Toolbar");
 | |
| }
 | |
| 
 | |
| #undef LOCTEXT_NAMESPACE
 | |
| 
 | |
| IMPLEMENT_MODULE(FUnrealSharpEditorModule, UnrealSharpEditor)
 |