#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("UnrealSharpEditor"); } void FUnrealSharpEditorModule::StartupModule() { IAssetTools& AssetTools = FModuleManager::LoadModuleChecked("AssetTools").Get(); AssetTools.RegisterAssetTypeActions(MakeShared()); TArray 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, bool) { SuggestProjectSetup(); }); } // Make managed types not available for edit in the editor { FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked(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& ChangedFiles) { if (IsHotReloading()) { return; } const UCSUnrealSharpEditorSettings* Settings = GetDefault(); 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 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(); 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 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 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 ManagedSlnFileLines; FFileHelper::LoadFileToStringArray(ManagedSlnFileLines, *ManagedSolutionPath); TArray 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 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("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( "UnrealSharpRuntimeGlue"); RuntimeGlueModule.ForceRefreshRuntimeGlue(); } void FUnrealSharpEditorModule::RepairComponents() { FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( AssetRegistryConstants::ModuleName); AssetRegistryModule.Get().SearchAllAssets(/*bSynchronousSearch =*/true); TArray OutAssetData; AssetRegistryModule.Get().GetAssetsByClass(UBlueprint::StaticClass()->GetClassPathName(), OutAssetData, true); FScopedSlowTask Progress(OutAssetData.Num()); Progress.MakeDialog(); USubobjectDataSubsystem* SubobjectDataSubsystem = GEngine->GetEngineSubsystem(); 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(GeneratedClass->GetDefaultObject(false)); if (!ActorCDO) { continue; } TArray SubobjectData; SubobjectDataSubsystem->K2_GatherSubobjectDataForBlueprint(LoadedBlueprint, SubobjectData); UInheritableComponentHandler* InheritableComponentHandler = LoadedBlueprint-> GetInheritableComponentHandler(false); if (!InheritableComponentHandler) { continue; } TArray Subobjects; ActorCDO->GetDefaultSubobjects(Subobjects); TArray MatchingInstances; GetObjectsOfClass(LoadedBlueprint->GeneratedClass, MatchingInstances, true, RF_ClassDefaultObject, EInternalObjectFlags::Garbage); for (TFieldIterator PropertyIt(ManagedClass, EFieldIteratorFlags::IncludeSuper); PropertyIt; ++ PropertyIt) { FObjectProperty* Property = *PropertyIt; if (!FCSClassUtilities::IsManagedClass(Property->GetOwnerClass())) { break; } UActorComponent* OldComponentArchetype = Cast( 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(Instance); TArray>& Components = ActorInstance->BlueprintCreatedComponents; for (TObjectPtr& 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 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 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 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 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(), TAttribute(), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "LevelEditor.Recompile")); MenuBuilder.AddMenuEntry(CSCommands.ReloadManagedCode, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "LevelEditor.Recompile")); MenuBuilder.EndSection(); // Project MenuBuilder.BeginSection("Project", LOCTEXT("Project", "Project")); MenuBuilder.AddMenuEntry(CSCommands.CreateNewProject, NAME_None, TAttribute(), TAttribute(), FSourceCodeNavigation::GetOpenSourceCodeIDEIcon()); MenuBuilder.AddMenuEntry(CSCommands.OpenSolution, NAME_None, TAttribute(), TAttribute(), FSourceCodeNavigation::GetOpenSourceCodeIDEIcon()); MenuBuilder.AddMenuEntry(CSCommands.RegenerateSolution, NAME_None, TAttribute(), TAttribute(), FSourceCodeNavigation::GetOpenSourceCodeIDEIcon()); MenuBuilder.AddMenuEntry(CSCommands.MergeManagedSlnAndNativeSln, NAME_None, TAttribute(), TAttribute(), FSourceCodeNavigation::GetOpenSourceCodeIDEIcon()); MenuBuilder.EndSection(); // Package MenuBuilder.BeginSection("Package", LOCTEXT("Package", "Package")); MenuBuilder.AddMenuEntry(CSCommands.PackageProject, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "LevelEditor.Recompile")); MenuBuilder.EndSection(); // Plugin MenuBuilder.BeginSection("Plugin", LOCTEXT("Plugin", "Plugin")); MenuBuilder.AddMenuEntry(CSCommands.OpenSettings, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "EditorPreferences.TabIcon")); MenuBuilder.AddMenuEntry(CSCommands.OpenDocumentation, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "MainFrame.DocumentationHome")); MenuBuilder.AddMenuEntry(CSCommands.ReportBug, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FAppStyle::Get().GetStyleSetName(), "MainFrame.ReportABug")); MenuBuilder.EndSection(); MenuBuilder.BeginSection("Glue", LOCTEXT("Glue", "Glue")); MenuBuilder.AddMenuEntry(CSCommands.RefreshRuntimeGlue, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FAppStyle::GetAppStyleSetName(), "SourceControl.Actions.Refresh")); MenuBuilder.EndSection(); MenuBuilder.BeginSection("Tools", LOCTEXT("Tools", "Tools")); MenuBuilder.AddMenuEntry(CSCommands.RepairComponents, NAME_None, TAttribute(), TAttribute(), FSlateIcon(FAppStyle::GetAppStyleSetName(), "SourceControl.Actions.Refresh")); return MenuBuilder.MakeWidget(); } void FUnrealSharpEditorModule::OpenNewProjectDialog() { TSharedRef AddCodeWindow = SNew(SWindow) .Title(LOCTEXT("CreateNewProject", "New C# Project")) .SizingRule(ESizingRule::Autosized) .SupportsMinimize(false); TSharedRef 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(); 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("LevelEditor"); const TSharedRef 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::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 BlankTemplate = MakeShared(BlankTemplateName, BlankDescription, PluginBaseDir / TEXT("Templates") / TEXT("Blank"), true, EHostType::Runtime, ELoadingPhase::Default, true); const TSharedRef CSharpOnlyTemplate = MakeShared(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& 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& ExtraArguments) { TMap Arguments = ExtraArguments; TMap 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& 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& 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(PinSubCategoryObject)) { return RebuiltClasses.Contains(Class); } if (UCSEnum* Enum = Cast(PinSubCategoryObject)) { return RebuiltEnums.Contains(Enum); } if (UCSScriptStruct* Struct = Cast(PinSubCategoryObject)) { return RebuiltStructs.Contains(Struct); } if (UCSEnum* Enum = Cast(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(Node)) { for (const TSharedPtr& 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("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 AffectedBlueprints; for (TObjectIterator BlueprintIt; BlueprintIt; ++BlueprintIt) { UBlueprint* Blueprint = *BlueprintIt; if (!IsValid(Blueprint->GeneratedClass) || FCSClassUtilities::IsManagedClass(Blueprint->GeneratedClass)) { return; } TArray AllNodes; FBlueprintEditorUtils::GetAllNodesOfClass(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)