// Copyright Epic Games, Inc. All Rights Reserved. #include "GameProjectUtils.h" #include "Misc/Guid.h" #include "UObject/Class.h" #include "FeaturePackContentSource.h" #include "TemplateProjectDefs.h" #include "HAL/PlatformFileManager.h" #include "Misc/MessageDialog.h" #include "HAL/FileManager.h" #include "Misc/CommandLine.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" #include "Misc/ConfigCacheIni.h" #include "Misc/ScopedSlowTask.h" #include "Misc/App.h" #include "Misc/EngineVersion.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/SWindow.h" #include "Framework/Application/SlateApplication.h" #include "ClassTemplateEditorSubsystem.h" #include "Components/ActorComponent.h" #include "Components/SceneComponent.h" #include "GameFramework/Actor.h" #include "GameFramework/Pawn.h" #include "Editor/EditorPerProjectUserSettings.h" #include "ISourceControlOperation.h" #include "SourceControlOperations.h" #include "ISourceControlProvider.h" #include "ISourceControlModule.h" #include "GeneralProjectSettings.h" #include "GameFramework/Character.h" #include "Misc/FeedbackContext.h" #include "UObject/UObjectHash.h" #include "UObject/UObjectIterator.h" #include "GameFramework/GameModeBase.h" #include "UnrealEdMisc.h" #include "PluginDescriptor.h" #include "Interfaces/IPluginManager.h" #include "ProjectDescriptor.h" #include "Interfaces/IProjectManager.h" #include "GameProjectGenerationLog.h" #include "DefaultTemplateProjectDefs.h" #include "SNewClassDialog.h" #include "FeaturedClasses.inl" #include "TemplateCategory.h" #include "Features/IModularFeatures.h" #include "Interfaces/IMainFrameModule.h" #include "AnalyticsEventAttribute.h" #include "Interfaces/IAnalyticsProvider.h" #include "EngineAnalytics.h" #include "DesktopPlatformModule.h" #include "Interfaces/ITargetPlatform.h" #include "Interfaces/ITargetPlatformManagerModule.h" #include "Styling/SlateIconFinder.h" #include "SourceCodeNavigation.h" #include "Misc/UProjectInfo.h" #include "Framework/Notifications/NotificationManager.h" #include "Widgets/Notifications/SNotificationList.h" #include "Misc/HotReloadInterface.h" #include "Dialogs/SOutputLogDialog.h" #include "IAudioExtensionPlugin.h" #include "AudioPluginUtilities.h" #include "Sound/AudioSettings.h" #include "PlatformInfo.h" #include "Blueprint/BlueprintSupport.h" #include "Settings/ProjectPackagingSettings.h" #include "Async/ParallelFor.h" #if WITH_LIVE_CODING #include "ILiveCodingModule.h" #endif #define LOCTEXT_NAMESPACE "GameProjectUtils" #define MAX_PROJECT_PATH_BUFFER_SPACE 130 // Leave a reasonable buffer of additional characters to account for files created in the content directory during or after project generation #define MAX_PROJECT_NAME_LENGTH 20 // Enforce a reasonable project name length so the path is not too long for FPlatformMisc::GetMaxPathLength() #define MAX_CLASS_NAME_LENGTH 32 // Enforce a reasonable class name length so the path is not too long for FPlatformMisc::GetMaxPathLength() TWeakPtr GameProjectUtils::UpdateGameProjectNotification = NULL; TWeakPtr GameProjectUtils::WarningProjectNameNotification = NULL; constexpr const TCHAR GameProjectUtils::IncludePathFormatString[]; namespace { /** Get the configuration values for enabling Lumen by default. */ void AddLumenConfigValues(const FProjectInformation& InProjectInfo, TArray& ConfigValues, bool bDisableStaticLighting = true) { // In some cases we may want to leave static lighting enabled (e.g. VRTemplate needs it as of UE 5.5) if (bDisableStaticLighting) { // Disable static lighting support ConfigValues.Emplace(TEXT("DefaultEngine.ini"), TEXT("/Script/Engine.RendererSettings"), TEXT("r.AllowStaticLighting"), TEXT("0"), true /* ShouldReplaceExistingValue */); } // Required for Lumen's Software Ray Tracing support ConfigValues.Emplace(TEXT("DefaultEngine.ini"), TEXT("/Script/Engine.RendererSettings"), TEXT("r.GenerateMeshDistanceFields"), TEXT("True"), true /* ShouldReplaceExistingValue */); // Enable Lumen Global Illumination by default ConfigValues.Emplace(TEXT("DefaultEngine.ini"), TEXT("/Script/Engine.RendererSettings"), TEXT("r.DynamicGlobalIlluminationMethod"), TEXT("1"), true /* ShouldReplaceExistingValue */); // Enable Lumen Reflections by default ConfigValues.Emplace(TEXT("DefaultEngine.ini"), TEXT("/Script/Engine.RendererSettings"), TEXT("r.ReflectionMethod"), TEXT("1"), true /* ShouldReplaceExistingValue */); } void AddNewProjectDefaultShadowConfigValues(const FProjectInformation& InProjectInfo, TArray& ConfigValues) { // Enable support for virtual shadow maps by default for new projects ConfigValues.Emplace(TEXT("DefaultEngine.ini"), TEXT("/Script/Engine.RendererSettings"), TEXT("r.Shadow.Virtual.Enable"), TEXT("1"), true /* ShouldReplaceExistingValue */); } void AddPostProcessingConfigValues(const FProjectInformation& InProjectInfo, TArray& ConfigValues) { // Enable support for ExtendDefaultLuminanceRange by default for new projects ConfigValues.Emplace(TEXT("DefaultEngine.ini"), TEXT("/Script/Engine.RendererSettings"), TEXT("r.DefaultFeature.AutoExposure.ExtendDefaultLuminanceRange"), TEXT("True"), false /* ShouldReplaceExistingValue */); // Enable Local Exposure by default for new projects ConfigValues.Emplace(TEXT("DefaultEngine.ini"), TEXT("/Script/Engine.RendererSettings"), TEXT("r.DefaultFeature.LocalExposure.HighlightContrastScale"), TEXT("0.8"), false /* ShouldReplaceExistingValue */); // Enable Local Exposure by default for new projects ConfigValues.Emplace(TEXT("DefaultEngine.ini"), TEXT("/Script/Engine.RendererSettings"), TEXT("r.DefaultFeature.LocalExposure.ShadowContrastScale"), TEXT("0.8"), false /* ShouldReplaceExistingValue */); } /** Get the configuration values for raytracing if enabled. */ void AddRaytracingConfigValues(const FProjectInformation& InProjectInfo, TArray& ConfigValues) { ConfigValues.Emplace(TEXT("DefaultEngine.ini"), TEXT("/Script/WindowsTargetPlatform.WindowsTargetSettings"), TEXT("DefaultGraphicsRHI"), TEXT("DefaultGraphicsRHI_DX12"), false /* ShouldReplaceExistingValue */); ConfigValues.Emplace(TEXT("DefaultEngine.ini"), TEXT("/Script/Engine.RendererSettings"), TEXT("r.SkinCache.CompileShaders"), TEXT("True"), false /* ShouldReplaceExistingValue */); ConfigValues.Emplace(TEXT("DefaultEngine.ini"), TEXT("/Script/Engine.RendererSettings"), TEXT("r.RayTracing.RayTracingProxies.ProjectEnabled"), TEXT("True"), false /* ShouldReplaceExistingValue */); } /** Get the configuration values for enabling WorldPartition by default. */ void AddWorldPartitionConfigValues(const FProjectInformation& InProjectInfo, TArray& ConfigValues) { if (InProjectInfo.bIsBlankTemplate) { ConfigValues.Emplace(TEXT("DefaultEngine.ini"), TEXT("/Script/WorldPartitionEditor.WorldPartitionEditorSettings"), TEXT("CommandletClass"), TEXT("Class'/Script/UnrealEd.WorldPartitionConvertCommandlet'"), true /* ShouldReplaceExistingValue */); } } void AddUserInterfaceEngineConfigValues(const FProjectInformation& InProjectInfo, TArray& ConfigValues) { if (InProjectInfo.bIsBlankTemplate) { ConfigValues.Emplace(TEXT("DefaultEngine.ini"), TEXT("/Script/Engine.UserInterfaceSettings"), TEXT("bAuthorizeAutomaticWidgetVariableCreation"), TEXT("False"), true /* ShouldReplaceExistingValue */); ConfigValues.Emplace(TEXT("DefaultEngine.ini"), TEXT("/Script/Engine.UserInterfaceSettings"), TEXT("FontDPIPreset"), TEXT("Standard"), true /* ShouldReplaceExistingValue */); ConfigValues.Emplace(TEXT("DefaultEngine.ini"), TEXT("/Script/Engine.UserInterfaceSettings"), TEXT("FontDPI"), TEXT("72"), true /* ShouldReplaceExistingValue */); } } void AddUserInterfaceGameConfigValues(const FProjectInformation& InProjectInfo, TArray& ConfigValues) { if (InProjectInfo.bIsBlankTemplate) { ConfigValues.Emplace(TEXT("DefaultGame.ini"), TEXT("/Script/CommonUI.CommonUISettings"), TEXT("CommonButtonAcceptKeyHandling"), TEXT("TriggerClick"), true /* ShouldReplaceExistingValue */); } } } // namespace <> FText FNewClassInfo::GetClassName() const { switch(ClassType) { case EClassType::UObject: return BaseClass ? BaseClass->GetDisplayNameText() : FText::GetEmpty(); case EClassType::EmptyCpp: return LOCTEXT("NoParentClass", "None"); case EClassType::SlateWidget: return LOCTEXT("SlateWidgetParentClass", "Slate Widget"); case EClassType::SlateWidgetStyle: return LOCTEXT("SlateWidgetStyleParentClass", "Slate Widget Style"); case EClassType::UInterface: return LOCTEXT("UInterfaceParentClass", "Unreal Interface"); default: break; } return FText::GetEmpty(); } FText FNewClassInfo::GetClassDescription(const bool bFullDescription/* = true*/) const { switch(ClassType) { case EClassType::UObject: { if(BaseClass) { FString ClassDescription = BaseClass->GetToolTipText(/*bShortTooltip=*/!bFullDescription).ToString(); if(!bFullDescription) { int32 FullStopIndex = 0; if(ClassDescription.FindChar('.', FullStopIndex)) { // Only show the first sentence so as not to clutter up the UI with a detailed description of implementation details ClassDescription.LeftInline(FullStopIndex + 1, EAllowShrinking::No); } // Strip out any new-lines in the description ClassDescription.ReplaceInline(TEXT("\n"), TEXT(" "), ESearchCase::CaseSensitive); } return FText::FromString(ClassDescription); } } break; case EClassType::EmptyCpp: return LOCTEXT("EmptyClassDescription", "An empty C++ class with a default constructor and destructor."); case EClassType::SlateWidget: return LOCTEXT("SlateWidgetClassDescription", "A custom Slate widget, deriving from SCompoundWidget."); case EClassType::SlateWidgetStyle: return LOCTEXT("SlateWidgetStyleClassDescription", "A custom Slate widget style, deriving from FSlateWidgetStyle, along with its associated UObject wrapper class."); case EClassType::UInterface: return LOCTEXT("UInterfaceClassDescription", "A UObject Interface class, to be implemented by other UObject-based classes."); default: break; } return FText::GetEmpty(); } const FSlateBrush* FNewClassInfo::GetClassIcon() const { // Safe to do even if BaseClass is null, since FindIconForClass will return the default icon return FSlateIconFinder::FindIconBrushForClass(BaseClass); } FString FNewClassInfo::GetClassPrefixCPP() const { switch(ClassType) { case EClassType::UObject: return BaseClass ? BaseClass->GetPrefixCPP() : TEXT("U"); case EClassType::EmptyCpp: return TEXT("F"); case EClassType::SlateWidget: return TEXT("S"); case EClassType::SlateWidgetStyle: return TEXT("F"); case EClassType::UInterface: return TEXT("U"); default: break; } return TEXT(""); } FString FNewClassInfo::GetClassNameCPP() const { switch(ClassType) { case EClassType::UObject: return BaseClass ? BaseClass->GetName() : TEXT(""); case EClassType::EmptyCpp: return TEXT(""); case EClassType::SlateWidget: return TEXT("CompoundWidget"); case EClassType::SlateWidgetStyle: return TEXT("SlateWidgetStyle"); case EClassType::UInterface: return TEXT("Interface"); default: break; } return TEXT(""); } FString FNewClassInfo::GetCleanClassName(const FString& ClassName) const { FString CleanClassName = ClassName; switch(ClassType) { case EClassType::SlateWidgetStyle: { // Slate widget style classes always take the form FMyThingWidget, and UMyThingWidgetStyle // if our class ends with either Widget or WidgetStyle, we need to strip those out to avoid silly looking duplicates if(CleanClassName.EndsWith(TEXT("Style"))) { CleanClassName.LeftChopInline(5, EAllowShrinking::No); // 5 for "Style" } if(CleanClassName.EndsWith(TEXT("Widget"))) { CleanClassName.LeftChopInline(6, EAllowShrinking::No); // 6 for "Widget" } } break; default: break; } return CleanClassName; } FString FNewClassInfo::GetFinalClassName(const FString& ClassName) const { const FString CleanClassName = GetCleanClassName(ClassName); switch(ClassType) { case EClassType::SlateWidgetStyle: return FString::Printf(TEXT("%sWidgetStyle"), *CleanClassName); default: break; } return CleanClassName; } bool FNewClassInfo::GetIncludePath(FString& OutIncludePath) const { switch(ClassType) { case EClassType::UObject: if(BaseClass && BaseClass->HasMetaData(TEXT("IncludePath"))) { OutIncludePath = BaseClass->GetMetaData(TEXT("IncludePath")); return true; } break; case EClassType::SlateWidget: OutIncludePath = "Widgets/SCompoundWidget.h"; return true; case EClassType::SlateWidgetStyle: OutIncludePath = "Styling/SlateWidgetStyle.h"; return true; default: break; } return false; } FString FNewClassInfo::GetBaseClassHeaderFilename() const { FString IncludePath; switch (ClassType) { case EClassType::UObject: if (BaseClass) { FString ClassHeaderPath; if (FSourceCodeNavigation::FindClassHeaderPath(BaseClass, ClassHeaderPath) && IFileManager::Get().FileSize(*ClassHeaderPath) != INDEX_NONE) { return ClassHeaderPath; } } break; case EClassType::SlateWidget: case EClassType::SlateWidgetStyle: GetIncludePath(IncludePath); return FPaths::EngineDir() / TEXT("Source") / TEXT("Runtime") / TEXT("SlateCore") / TEXT("Public") / IncludePath; default: return FString(); } return FString(); } FString FNewClassInfo::GetHeaderFilename(const FString& ClassName) const { const FString HeaderFilename = GetFinalClassName(ClassName) + TEXT(".h"); switch(ClassType) { case EClassType::SlateWidget: return TEXT("S") + HeaderFilename; default: break; } return HeaderFilename; } FString FNewClassInfo::GetSourceFilename(const FString& ClassName) const { const FString SourceFilename = GetFinalClassName(ClassName) + TEXT(".cpp"); switch(ClassType) { case EClassType::SlateWidget: return TEXT("S") + SourceFilename; default: break; } return SourceFilename; } FString FNewClassInfo::GetHeaderTemplateFilename() const { switch(ClassType) { case EClassType::UObject: { if (BaseClass != nullptr) { if ((BaseClass == UActorComponent::StaticClass()) || (BaseClass == USceneComponent::StaticClass())) { return TEXT("ActorComponentClass.h.template"); } else if (BaseClass == AActor::StaticClass()) { return TEXT("ActorClass.h.template"); } else if (BaseClass == APawn::StaticClass()) { return TEXT("PawnClass.h.template"); } else if (BaseClass == ACharacter::StaticClass()) { return TEXT("CharacterClass.h.template"); } } // Some other non-actor, non-component UObject class return TEXT( "UObjectClass.h.template" ); } case EClassType::EmptyCpp: return TEXT("EmptyClass.h.template"); case EClassType::SlateWidget: return TEXT("SlateWidget.h.template"); case EClassType::SlateWidgetStyle: return TEXT("SlateWidgetStyle.h.template"); case EClassType::UInterface: return TEXT("InterfaceClass.h.template"); default: break; } return TEXT(""); } FString FNewClassInfo::GetSourceTemplateFilename() const { switch(ClassType) { case EClassType::UObject: if (BaseClass != nullptr) { if ((BaseClass == UActorComponent::StaticClass()) || (BaseClass == USceneComponent::StaticClass())) { return TEXT("ActorComponentClass.cpp.template"); } else if (BaseClass == AActor::StaticClass()) { return TEXT("ActorClass.cpp.template"); } else if (BaseClass == APawn::StaticClass()) { return TEXT("PawnClass.cpp.template"); } else if (BaseClass == ACharacter::StaticClass()) { return TEXT("CharacterClass.cpp.template"); } } // Some other non-actor, non-component UObject class return TEXT( "UObjectClass.cpp.template" ); case EClassType::EmptyCpp: return TEXT("EmptyClass.cpp.template"); case EClassType::SlateWidget: return TEXT("SlateWidget.cpp.template"); case EClassType::SlateWidgetStyle: return TEXT("SlateWidgetStyle.cpp.template"); case EClassType::UInterface: return TEXT("InterfaceClass.cpp.template"); default: break; } return TEXT(""); } bool GameProjectUtils::IsValidProjectFileForCreation(const FString& ProjectFile, FText& OutFailReason) { const FString BaseProjectFile = FPaths::GetBaseFilename(ProjectFile); if ( FPaths::GetPath(ProjectFile).IsEmpty() ) { OutFailReason = LOCTEXT( "NoProjectPath", "You must specify a path." ); return false; } if ( BaseProjectFile.IsEmpty() ) { OutFailReason = LOCTEXT( "NoProjectName", "You must specify a project name." ); return false; } if ( BaseProjectFile.Contains(TEXT(" ")) ) { OutFailReason = LOCTEXT( "ProjectNameContainsSpace", "Project names may not contain a space." ); return false; } if ( !FChar::IsAlpha(BaseProjectFile[0]) ) { OutFailReason = LOCTEXT( "ProjectNameMustBeginWithACharacter", "Project names must begin with an alphabetic character." ); return false; } if ( BaseProjectFile.Len() > MAX_PROJECT_NAME_LENGTH ) { FFormatNamedArguments Args; Args.Add( TEXT("MaxProjectNameLength"), MAX_PROJECT_NAME_LENGTH ); OutFailReason = FText::Format( LOCTEXT( "ProjectNameTooLong", "Project names must not be longer than {MaxProjectNameLength} characters." ), Args ); return false; } const int32 MaxProjectPathLength = FPlatformMisc::GetMaxPathLength() - MAX_PROJECT_PATH_BUFFER_SPACE; if ( FPaths::GetBaseFilename(ProjectFile, false).Len() > MaxProjectPathLength ) { FFormatNamedArguments Args; Args.Add( TEXT("MaxProjectPathLength"), MaxProjectPathLength ); OutFailReason = FText::Format( LOCTEXT( "ProjectPathTooLong", "A project's path must not be longer than {MaxProjectPathLength} characters." ), Args ); return false; } if ( FPaths::GetExtension(ProjectFile) != FProjectDescriptor::GetExtension() ) { FFormatNamedArguments Args; Args.Add( TEXT("ProjectFileExtension"), FText::FromString( FProjectDescriptor::GetExtension() ) ); OutFailReason = FText::Format( LOCTEXT( "InvalidProjectFileExtension", "File extension is not {ProjectFileExtension}" ), Args ); return false; } FString IllegalNameCharacters; if ( !NameContainsOnlyLegalCharacters(BaseProjectFile, IllegalNameCharacters) ) { FFormatNamedArguments Args; Args.Add( TEXT("IllegalNameCharacters"), FText::FromString( IllegalNameCharacters ) ); OutFailReason = FText::Format( LOCTEXT( "ProjectNameContainsIllegalCharacters", "Project names may not contain the following characters: {IllegalNameCharacters}" ), Args ); return false; } if (NameMatchesPlatformModuleName(BaseProjectFile)) { FFormatNamedArguments Args; Args.Add(TEXT("PlatformModuleName"), FText::FromString(BaseProjectFile)); OutFailReason = FText::Format(LOCTEXT("ProjectNameConflictsWithPlatformModuleName", "Project name conflicts with a platform name: {PlatformModuleName}"), Args); return false; } if ( !FPaths::ValidatePath(FPaths::GetPath(ProjectFile), &OutFailReason) ) { return false; } if ( ProjectFileExists(ProjectFile) ) { OutFailReason = LOCTEXT( "ProjectFileAlreadyExists", "This project file already exists." ); return false; } if ( FPaths::ConvertRelativePathToFull(FPaths::GetPath(ProjectFile)).StartsWith( FPaths::ConvertRelativePathToFull(FPaths::EngineDir())) ) { OutFailReason = LOCTEXT( "ProjectFileCannotBeUnderEngineFolder", "Project cannot be saved under the Engine folder. Please choose a different directory." ); return false; } if ( AnyProjectFilesExistInFolder(FPaths::GetPath(ProjectFile)) ) { FFormatNamedArguments Args; Args.Add( TEXT("ProjectFileExtension"), FText::FromString( FProjectDescriptor::GetExtension() ) ); OutFailReason = FText::Format( LOCTEXT( "AProjectFileAlreadyExistsAtLoction", "Another .{ProjectFileExtension} file already exists in the specified folder" ), Args ); return false; } // Don't allow any files within target directory so we can safely delete everything on failure TArray ExistingFiles; IFileManager::Get().FindFiles(ExistingFiles, *(FPaths::GetPath(ProjectFile) / TEXT("*")), true, true); if (ExistingFiles.Num() > 0) { OutFailReason = LOCTEXT("ProjectFileCannotBeWithExistingFiles", "Project cannot be saved in a folder with existing files. Please choose a different directory/project name."); return false; } return true; } bool GameProjectUtils::OpenProject(const FString& ProjectFile, FText& OutFailReason) { if ( ProjectFile.IsEmpty() ) { OutFailReason = LOCTEXT( "NoProjectFileSpecified", "You must specify a project file." ); return false; } const FString BaseProjectFile = FPaths::GetBaseFilename(ProjectFile); if ( BaseProjectFile.Contains(TEXT(" ")) ) { OutFailReason = LOCTEXT( "ProjectNameContainsSpace", "Project names may not contain a space." ); return false; } if ( !FChar::IsAlpha(BaseProjectFile[0]) ) { OutFailReason = LOCTEXT( "ProjectNameMustBeginWithACharacter", "Project names must begin with an alphabetic character." ); return false; } const int32 MaxProjectPathLength = FPlatformMisc::GetMaxPathLength() - MAX_PROJECT_PATH_BUFFER_SPACE; if ( FPaths::GetBaseFilename(ProjectFile, false).Len() > MaxProjectPathLength ) { FFormatNamedArguments Args; Args.Add( TEXT("MaxProjectPathLength"), MaxProjectPathLength ); OutFailReason = FText::Format( LOCTEXT( "ProjectPathTooLong", "A project's path must not be longer than {MaxProjectPathLength} characters." ), Args ); return false; } if ( FPaths::GetExtension(ProjectFile) != FProjectDescriptor::GetExtension() ) { FFormatNamedArguments Args; Args.Add( TEXT("ProjectFileExtension"), FText::FromString( FProjectDescriptor::GetExtension() ) ); OutFailReason = FText::Format( LOCTEXT( "InvalidProjectFileExtension", "File extension is not {ProjectFileExtension}" ), Args ); return false; } FString IllegalNameCharacters; if ( !NameContainsOnlyLegalCharacters(BaseProjectFile, IllegalNameCharacters) ) { FFormatNamedArguments Args; Args.Add( TEXT("IllegalNameCharacters"), FText::FromString( IllegalNameCharacters ) ); OutFailReason = FText::Format( LOCTEXT( "ProjectNameContainsIllegalCharacters", "Project names may not contain the following characters: {IllegalNameCharacters}" ), Args ); return false; } if ( !FPaths::ValidatePath(FPaths::GetPath(ProjectFile), &OutFailReason) ) { return false; } if ( !ProjectFileExists(ProjectFile) ) { FFormatNamedArguments Args; Args.Add( TEXT("ProjectFile"), FText::FromString( ProjectFile ) ); OutFailReason = FText::Format( LOCTEXT( "ProjectFileDoesNotExist", "{ProjectFile} does not exist." ), Args ); return false; } FUnrealEdMisc::Get().SwitchProject(ProjectFile, false); return true; } bool GameProjectUtils::OpenCodeIDE(const FString& ProjectFile, FText& OutFailReason) { if ( ProjectFile.IsEmpty() ) { OutFailReason = LOCTEXT( "NoProjectFileSpecified", "You must specify a project file." ); return false; } // Check whether this project is a foreign project. Don't use the cached project dictionary; we may have just created a new project. FString SolutionFolder; FString SolutionFilenameWithoutExtension; if( FUProjectDictionary(FPaths::RootDir()).IsForeignProject(ProjectFile) ) { SolutionFolder = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*FPaths::GetPath(ProjectFile)); SolutionFilenameWithoutExtension = FPaths::GetBaseFilename(ProjectFile); } else { SolutionFolder = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*FPaths::RootDir()); SolutionFilenameWithoutExtension = TEXT("UE5"); } if (!FSourceCodeNavigation::OpenProjectSolution(FPaths::Combine(SolutionFolder, SolutionFilenameWithoutExtension))) { FFormatNamedArguments Args; Args.Add(TEXT("AccessorName"), FSourceCodeNavigation::GetSelectedSourceCodeIDE()); OutFailReason = FText::Format(LOCTEXT("OpenCodeIDE_FailedToOpen", "Failed to open selected source code accessor '{AccessorName}'"), Args); return false; } return true; } bool GameProjectUtils::CreateProject(const FProjectInformation& InProjectInfo, FText& OutFailReason, FText& OutFailLog, TArray* OutCreatedFiles) { if ( !IsValidProjectFileForCreation(InProjectInfo.ProjectFilename, OutFailReason) ) { return false; } FScopedSlowTask SlowTask(0, LOCTEXT( "CreatingProjectStatus", "Creating project..." )); SlowTask.MakeDialog(); TOptional ProjectID; FString TemplateName; if ( InProjectInfo.TemplateFile.IsEmpty() ) { ProjectID = GenerateProjectFromScratch(InProjectInfo, OutFailReason, OutFailLog); TemplateName = InProjectInfo.bShouldGenerateCode ? TEXT("Basic Code") : TEXT("Blank"); } else { ProjectID = CreateProjectFromTemplate(InProjectInfo, OutFailReason, OutFailLog, OutCreatedFiles); TemplateName = FPaths::GetBaseFilename(InProjectInfo.TemplateFile); } bool bProjectCreationSuccessful = ProjectID.IsSet(); if (!bProjectCreationSuccessful && CleanupIsEnabled()) { // Delete the new project folder const FString NewProjectFolder = FPaths::GetPath(InProjectInfo.ProjectFilename); IFileManager::Get().DeleteDirectory(*NewProjectFolder, /*RequireExists=*/false, /*Tree=*/true); if( OutCreatedFiles != nullptr ) { OutCreatedFiles->Empty(); } } if( FEngineAnalytics::IsAvailable() ) { TArray EventAttributes; EventAttributes.Add(FAnalyticsEventAttribute(TEXT("Template"), TemplateName)); EventAttributes.Add(FAnalyticsEventAttribute(TEXT("Category"), InProjectInfo.TemplateCategory.ToString())); EventAttributes.Add(FAnalyticsEventAttribute(TEXT("ProjectType"), InProjectInfo.bShouldGenerateCode ? TEXT("C++ Code") : TEXT("Content Only"))); EventAttributes.Add(FAnalyticsEventAttribute(TEXT("Outcome"), bProjectCreationSuccessful ? TEXT("Successful") : TEXT("Failed"))); EventAttributes.Add(FAnalyticsEventAttribute(TEXT("ProjectID"), *(bProjectCreationSuccessful ? ProjectID.GetValue().ToString() : FString()))); if (InProjectInfo.TargetedHardware.IsSet()) { UEnum* HardwareClassEnum = StaticEnum(); if (HardwareClassEnum != nullptr) { EventAttributes.Add(FAnalyticsEventAttribute(TEXT("HardwareClass"), HardwareClassEnum->GetNameStringByValue(static_cast(InProjectInfo.TargetedHardware.GetValue())))); } } if (InProjectInfo.DefaultGraphicsPerformance.IsSet()) { UEnum* GraphicsPresetEnum = StaticEnum(); if (GraphicsPresetEnum != nullptr) { EventAttributes.Add(FAnalyticsEventAttribute(TEXT("GraphicsPreset"), GraphicsPresetEnum->GetNameStringByValue(static_cast(InProjectInfo.DefaultGraphicsPerformance.GetValue())))); } } FEngineAnalytics::GetProvider().RecordEvent(TEXT("Editor.NewProject.ProjectCreated"), EventAttributes); } return bProjectCreationSuccessful; } void GameProjectUtils::CheckForOutOfDateGameProjectFile() { if (FPaths::IsProjectFilePathSet() && !IProjectManager::Get().IsSuppressingProjectFileWrite()) { if (IProjectManager::Get().IsCurrentProjectDirty()) { FText FailMessage; TryMakeProjectFileWriteable(FPaths::GetProjectFilePath()); if (!IProjectManager::Get().SaveCurrentProjectToDisk(FailMessage)) { FMessageDialog::Open(EAppMsgType::Ok, FailMessage); } } // Check if the project file is an older version FProjectStatus ProjectStatus; bool bRequiresUpdate = false; if (IProjectManager::Get().QueryStatusForCurrentProject(ProjectStatus)) { if ( ProjectStatus.bRequiresUpdate ) { bRequiresUpdate = true; UE_LOG(LogGameProjectGeneration, Log, TEXT("Project %s requires update. Determined from QueryStatusForCurrentProject"), *ProjectStatus.Name); } } // Check if there are any referenced plugins that do not have a matching supported plugins list const FProjectDescriptor* Project = IProjectManager::Get().GetCurrentProject(); TArray NewPluginReferences = Project->Plugins; for(FPluginReferenceDescriptor& Reference: NewPluginReferences) { if(Reference.bEnabled) { TSharedPtr Plugin = IPluginManager::Get().FindPlugin(Reference.Name); if(Plugin.IsValid()) { const FPluginDescriptor& Descriptor = Plugin->GetDescriptor(); if(Reference.MarketplaceURL != Descriptor.MarketplaceURL) { UE_LOG(LogGameProjectGeneration, Log, TEXT("Project %s requires update. Plugin %s MarketplaceURL value in project descriptor (%s) differs from value in plugin descriptor (%s) "), *ProjectStatus.Name, *Plugin->GetName(), *Reference.MarketplaceURL, *Descriptor.MarketplaceURL); Reference.MarketplaceURL = Descriptor.MarketplaceURL; bRequiresUpdate = true; } if(Reference.SupportedTargetPlatforms != Descriptor.SupportedTargetPlatforms) { auto CombineStrings = [](const TArray& Strings) -> FString { FStringBuilderBase Output; const int32 LastIndex = Strings.Num() - 1; for (int32 I = 0; I <= LastIndex; ++I) { Output += Strings[I]; if (I != LastIndex) { Output += TEXT(", "); } } return Output.ToString(); }; UE_LOG(LogGameProjectGeneration, Log, TEXT("Project %s requires update. Plugin %s SupportedTargetPlatforms value in project descriptor (%s) differs from value in plugin descriptor (%s) "), *ProjectStatus.Name, *Plugin->GetName(), *CombineStrings(Reference.SupportedTargetPlatforms), *CombineStrings(Descriptor.SupportedTargetPlatforms)); Reference.SupportedTargetPlatforms = Descriptor.SupportedTargetPlatforms; bRequiresUpdate = true; } } } } // If we have updates pending, show the prompt if (bRequiresUpdate) { FProjectDescriptorModifier ModifyProject = FProjectDescriptorModifier::CreateLambda( [NewPluginReferences](FProjectDescriptor& Descriptor) { Descriptor.Plugins = NewPluginReferences; return true; }); FSimpleDelegate OnUpdateProjectConfirm = FSimpleDelegate::CreateLambda( [ModifyProject]() { UpdateProject_Impl(&ModifyProject); }); const FText UpdateProjectText = LOCTEXT("UpdateProjectFilePrompt", "Project file is out of date. Would you like to update it?"); const FText UpdateProjectConfirmText = LOCTEXT("UpdateProjectFileConfirm", "Update"); const FText UpdateProjectCancelText = LOCTEXT("UpdateProjectFileCancel", "Not Now"); /** Utility functions for notifications */ struct FSuppressDialogOptions { static bool ShouldSuppressModal() { bool bSuppressNotification = false; GConfig->GetBool(TEXT("GameProjectUtils"), TEXT("SuppressUpdateProjectFilePromptNotification"), bSuppressNotification, GEditorPerProjectIni); return bSuppressNotification; } static ECheckBoxState GetDontAskAgainCheckBoxState() { // Check the config for any preferences return ShouldSuppressModal() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } static void OnDontAskAgainCheckBoxStateChanged(ECheckBoxState NewState) { // If the user selects to not show this again, set that in the config so we know about it in between sessions const bool bSuppressNotification = (NewState == ECheckBoxState::Checked); GConfig->SetBool(TEXT("GameProjectUtils"), TEXT("SuppressUpdateProjectFilePromptNotification"), bSuppressNotification, GEditorPerProjectIni); } }; // If the user has specified to supress this pop up, then just early out and exit if (FSuppressDialogOptions::ShouldSuppressModal()) { return; } FNotificationInfo Info(UpdateProjectText); Info.ExpireDuration = 10.0f; Info.bFireAndForget = true; Info.bUseLargeFont = false; Info.bUseThrobber = false; Info.bUseSuccessFailIcons = false; Info.ButtonDetails.Add(FNotificationButtonInfo(UpdateProjectConfirmText, FText(), OnUpdateProjectConfirm)); Info.ButtonDetails.Add(FNotificationButtonInfo(UpdateProjectCancelText, FText(), FSimpleDelegate::CreateStatic(&GameProjectUtils::OnUpdateProjectCancel))); // Add a "Don't show this again" option Info.CheckBoxState = TAttribute::Create(&FSuppressDialogOptions::GetDontAskAgainCheckBoxState); Info.CheckBoxStateChanged = FOnCheckStateChanged::CreateStatic(&FSuppressDialogOptions::OnDontAskAgainCheckBoxStateChanged); Info.CheckBoxText = LOCTEXT("DefaultCheckBoxMessage", "Don't show this again"); if (UpdateGameProjectNotification.IsValid()) { UpdateGameProjectNotification.Pin()->ExpireAndFadeout(); UpdateGameProjectNotification.Reset(); } UpdateGameProjectNotification = FSlateNotificationManager::Get().AddNotification(Info); if (UpdateGameProjectNotification.IsValid()) { UpdateGameProjectNotification.Pin()->SetCompletionState(SNotificationItem::CS_Pending); } } } } void GameProjectUtils::CheckAndWarnProjectFilenameValid() { const FString& LoadedProjectFilePath = FPaths::IsProjectFilePathSet() ? FPaths::GetProjectFilePath() : FString(); if ( !LoadedProjectFilePath.IsEmpty() ) { const FString BaseProjectFile = FPaths::GetBaseFilename(LoadedProjectFilePath); if ( BaseProjectFile.Len() > MAX_PROJECT_NAME_LENGTH ) { FFormatNamedArguments Args; Args.Add( TEXT("MaxProjectNameLength"), MAX_PROJECT_NAME_LENGTH ); const FText WarningReason = FText::Format( LOCTEXT( "WarnProjectNameTooLong", "Project names must not be longer than {MaxProjectNameLength} characters.\nYou might have problems saving or modifying a project with a longer name." ), Args ); const FText WarningReasonOkText = LOCTEXT("WarningReasonOkText", "Ok"); FNotificationInfo Info(WarningReason); Info.bFireAndForget = false; Info.bUseLargeFont = false; Info.bUseThrobber = false; Info.bUseSuccessFailIcons = false; Info.FadeOutDuration = 3.f; Info.ButtonDetails.Add(FNotificationButtonInfo(WarningReasonOkText, FText(), FSimpleDelegate::CreateStatic(&GameProjectUtils::OnWarningReasonOk))); if (WarningProjectNameNotification.IsValid()) { WarningProjectNameNotification.Pin()->ExpireAndFadeout(); WarningProjectNameNotification.Reset(); } WarningProjectNameNotification = FSlateNotificationManager::Get().AddNotification(Info); if (WarningProjectNameNotification.IsValid()) { WarningProjectNameNotification.Pin()->SetCompletionState(SNotificationItem::CS_Pending); } } } } void GameProjectUtils::OnWarningReasonOk() { if ( WarningProjectNameNotification.IsValid() ) { WarningProjectNameNotification.Pin()->SetCompletionState(SNotificationItem::CS_None); WarningProjectNameNotification.Pin()->ExpireAndFadeout(); WarningProjectNameNotification.Reset(); } } bool GameProjectUtils::UpdateStartupModuleNames(FProjectDescriptor& Descriptor, const TArray* StartupModuleNames) { if (StartupModuleNames == nullptr) { return false; } // Replace the modules names, if specified Descriptor.Modules.Empty(); for (int32 Idx = 0; Idx < StartupModuleNames->Num(); Idx++) { Descriptor.Modules.Add(FModuleDescriptor(*(*StartupModuleNames)[Idx])); } ResetCurrentProjectModulesCache(); return true; } bool GameProjectUtils::UpdateRequiredAdditionalDependencies(FProjectDescriptor& Descriptor, TArray& RequiredDependencies, const FString& ModuleName) { bool bNeedsUpdate = false; for (auto& ModuleDesc : Descriptor.Modules) { if (ModuleDesc.Name != *ModuleName) { continue; } for (const auto& RequiredDep : RequiredDependencies) { if (!ModuleDesc.AdditionalDependencies.Contains(RequiredDep)) { ModuleDesc.AdditionalDependencies.Add(RequiredDep); bNeedsUpdate = true; } } } return bNeedsUpdate; } bool GameProjectUtils::UpdateGameProject(const FString& ProjectFile, const FString& EngineIdentifier, FText& OutFailReason) { return UpdateGameProjectFile(ProjectFile, EngineIdentifier, OutFailReason); } void GameProjectUtils::OpenAddToProjectDialog(const FAddToProjectConfig& Config, EClassDomain InDomain) { // If we've been given a class then we only show the second page of the dialog, so we can make the window smaller as that page doesn't have as much content const FVector2D WindowSize = (Config._ParentClass) ? (InDomain == EClassDomain::Blueprint) ? FVector2D(940, 480) : FVector2D(940, 380) : FVector2D(940, 540); FText WindowTitle = Config._WindowTitle; if (WindowTitle.IsEmpty()) { WindowTitle = InDomain == EClassDomain::Native ? LOCTEXT("AddCodeWindowHeader_Native", "Add C++ Class") : LOCTEXT("AddCodeWindowHeader_Blueprint", "Add Blueprint Class"); } TSharedRef AddCodeWindow = SNew(SWindow) .Title( WindowTitle ) .ClientSize( WindowSize ) .SizingRule( ESizingRule::FixedSize ) .SupportsMinimize(false) .SupportsMaximize(false); TSharedRef NewClassDialog = SNew(SNewClassDialog) .ParentWindow(AddCodeWindow) .Class(Config._ParentClass) .ClassViewerFilter(Config._AllowableParents) .ClassDomain(InDomain) .FeaturedClasses(Config._FeaturedClasses) .InitialPath(Config._InitialPath) .OnAddedToProject( Config._OnAddedToProject ) .DefaultClassPrefix( Config._DefaultClassPrefix ) .DefaultClassName( Config._DefaultClassName ) .SyncContentBrowserToNewClass(Config._SyncContentBrowserToNewClass); AddCodeWindow->SetContent( NewClassDialog ); TSharedPtr ParentWindow = Config._ParentWindow; if (!ParentWindow.IsValid()) { static const FName MainFrameModuleName = "MainFrame"; IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked(MainFrameModuleName); ParentWindow = MainFrameModule.GetParentWindow(); } if (Config._bModal) { FSlateApplication::Get().AddModalWindow(AddCodeWindow, ParentWindow); } else if (ParentWindow.IsValid()) { FSlateApplication::Get().AddWindowAsNativeChild(AddCodeWindow, ParentWindow.ToSharedRef()); } else { FSlateApplication::Get().AddWindow(AddCodeWindow); } } bool GameProjectUtils::IsValidClassNameForCreation(const FString& NewClassName, FText& OutFailReason) { if ( NewClassName.IsEmpty() ) { OutFailReason = LOCTEXT( "NoClassName", "You must specify a class name." ); return false; } if ( NewClassName.Contains(TEXT(" ")) ) { OutFailReason = LOCTEXT( "ClassNameContainsSpace", "Your class name may not contain a space." ); return false; } if ( !FChar::IsAlpha(NewClassName[0]) ) { OutFailReason = LOCTEXT( "ClassNameMustBeginWithACharacter", "Your class name must begin with an alphabetic character." ); return false; } if ( NewClassName.Len() > MAX_CLASS_NAME_LENGTH ) { OutFailReason = FText::Format( LOCTEXT( "ClassNameTooLong", "The class name must not be longer than {0} characters." ), FText::AsNumber(MAX_CLASS_NAME_LENGTH) ); return false; } FString IllegalNameCharacters; if ( !NameContainsOnlyLegalCharacters(NewClassName, IllegalNameCharacters) ) { FFormatNamedArguments Args; Args.Add( TEXT("IllegalNameCharacters"), FText::FromString( IllegalNameCharacters ) ); OutFailReason = FText::Format( LOCTEXT( "ClassNameContainsIllegalCharacters", "The class name may not contain the following characters: '{IllegalNameCharacters}'" ), Args ); return false; } return true; } bool GameProjectUtils::IsValidClassNameForCreation(const FString& NewClassName, const FModuleContextInfo& ModuleInfo, const TSet& DisallowedHeaderNames, FText& OutFailReason) { if (!IsValidClassNameForCreation(NewClassName, OutFailReason)) { return false; } // Look for a duplicate class in memory for ( TObjectIterator ClassIt; ClassIt; ++ClassIt ) { if ( ClassIt->GetName() == NewClassName ) { FFormatNamedArguments Args; Args.Add( TEXT("NewClassName"), FText::FromString( NewClassName ) ); OutFailReason = FText::Format( LOCTEXT("ClassNameAlreadyExists", "The name {NewClassName} is already used by another class."), Args ); return false; } } // Look for a duplicate class on disk in their project { FString UnusedFoundPath; if ( FindSourceFileInProject(NewClassName + ".h", ModuleInfo.ModuleSourcePath, UnusedFoundPath) ) { FFormatNamedArguments Args; Args.Add( TEXT("NewClassName"), FText::FromString( NewClassName ) ); OutFailReason = FText::Format( LOCTEXT("ClassNameAlreadyExists", "The name {NewClassName} is already used by another class."), Args ); return false; } } // See if header name clashes with an engine header { FString UnusedFoundPath; if (DisallowedHeaderNames.Contains(NewClassName)) { FFormatNamedArguments Args; Args.Add(TEXT("NewHeaderName"), FText::FromString(NewClassName + ".h")); OutFailReason = FText::Format(LOCTEXT("HeaderNameAlreadyExists", "The file {NewHeaderName} already exists elsewhere in the engine."), Args); return false; } } return true; } bool GameProjectUtils::IsValidBaseClassForCreation(const UClass* InClass, const FModuleContextInfo& InModuleInfo) { auto DoesClassNeedAPIExport = [&InModuleInfo](const FString& InClassModuleName) -> bool { return InModuleInfo.ModuleName != InClassModuleName; }; return IsValidBaseClassForCreation_Internal(InClass, FDoesClassNeedAPIExportCallback::CreateLambda(DoesClassNeedAPIExport)); } bool GameProjectUtils::IsValidBaseClassForCreation(const UClass* InClass, const TArray& InModuleInfoArray) { auto DoesClassNeedAPIExport = [&InModuleInfoArray](const FString& InClassModuleName) -> bool { for(const FModuleContextInfo& ModuleInfo : InModuleInfoArray) { if(ModuleInfo.ModuleName == InClassModuleName) { return false; } } return true; }; return IsValidBaseClassForCreation_Internal(InClass, FDoesClassNeedAPIExportCallback::CreateLambda(DoesClassNeedAPIExport)); } bool GameProjectUtils::IsValidBaseClassForCreation_Internal(const UClass* InClass, const FDoesClassNeedAPIExportCallback& InDoesClassNeedAPIExport) { // You may not make native classes based on blueprint generated classes const bool bIsBlueprintClass = (InClass->ClassGeneratedBy != nullptr); // UObject is special cased to be extensible since it would otherwise not be since it doesn't pass the API check (intrinsic class). const bool bIsExplicitlyUObject = (InClass == UObject::StaticClass()); // You need API if you are not UObject itself, and you're in a module that was validated as needing API export const FString ClassModuleName = InClass->GetOutermost()->GetName().RightChop( FString(TEXT("/Script/")).Len() ); const bool bNeedsAPI = !bIsExplicitlyUObject && InDoesClassNeedAPIExport.Execute(ClassModuleName); // You may not make a class that is not DLL exported. // MinimalAPI classes aren't compatible with the DLL export macro, but can still be used as a valid base const bool bHasAPI = InClass->HasAnyClassFlags(CLASS_RequiredAPI) || InClass->HasAnyClassFlags(CLASS_MinimalAPI); // @todo should we support interfaces? const bool bIsInterface = InClass->IsChildOf(UInterface::StaticClass()); return !bIsBlueprintClass && (!bNeedsAPI || bHasAPI) && !bIsInterface; } GameProjectUtils::EAddCodeToProjectResult GameProjectUtils::AddCodeToProject(const FString& NewClassName, const FString& NewClassPath, const FModuleContextInfo& ModuleInfo, const FNewClassInfo ParentClassInfo, const TSet& DisallowedHeaderNames, FString& OutHeaderFilePath, FString& OutCppFilePath, FText& OutFailReason) { EReloadStatus OutReloadStatus; return AddCodeToProject(NewClassName, NewClassPath, ModuleInfo, ParentClassInfo, DisallowedHeaderNames, OutHeaderFilePath, OutCppFilePath, OutFailReason, OutReloadStatus); } GameProjectUtils::EAddCodeToProjectResult GameProjectUtils::AddCodeToProject(const FString& NewClassName, const FString& NewClassPath, const FModuleContextInfo& ModuleInfo, const FNewClassInfo ParentClassInfo, const TSet& DisallowedHeaderNames, FString& OutHeaderFilePath, FString& OutCppFilePath, FText& OutFailReason, EReloadStatus& OutReloadStatus) { const EAddCodeToProjectResult Result = AddCodeToProject_Internal(NewClassName, NewClassPath, ModuleInfo, ParentClassInfo, DisallowedHeaderNames, OutHeaderFilePath, OutCppFilePath, OutFailReason, OutReloadStatus); if( FEngineAnalytics::IsAvailable() ) { const FString ParentClassName = ParentClassInfo.GetClassNameCPP(); TArray EventAttributes; EventAttributes.Add(FAnalyticsEventAttribute(TEXT("ParentClass"), ParentClassName.IsEmpty() ? TEXT("None") : ParentClassName)); EventAttributes.Add(FAnalyticsEventAttribute(TEXT("Outcome"), Result == EAddCodeToProjectResult::Succeeded ? TEXT("Successful") : TEXT("Failed"))); EventAttributes.Add(FAnalyticsEventAttribute(TEXT("FailureReason"), OutFailReason.ToString())); EventAttributes.Emplace(TEXT("Enterprise"), IProjectManager::Get().IsEnterpriseProject()); FEngineAnalytics::GetProvider().RecordEvent( TEXT( "Editor.AddCodeToProject.CodeAdded" ), EventAttributes ); } return Result; } UTemplateCategories* GameProjectUtils::LoadTemplateCategories(const FString& RootDir) { UTemplateCategories* TemplateCategories = nullptr; FString TemplateCategoriesIniFilename = RootDir / TEXT("TemplateCategories.ini"); if (FPlatformFileManager::Get().GetPlatformFile().FileExists(*TemplateCategoriesIniFilename)) { TemplateCategories = NewObject(); TemplateCategories->LoadConfig(UTemplateCategories::StaticClass(), *TemplateCategoriesIniFilename); for (FTemplateCategoryDef& Category : TemplateCategories->Categories) { // attempt to resolve the icon relative to the root directory FString TemplateCategoryIcon = RootDir / Category.Icon; if (FPlatformFileManager::Get().GetPlatformFile().FileExists(*TemplateCategoryIcon)) { Category.Icon = TemplateCategoryIcon; } } } return TemplateCategories; } UTemplateProjectDefs* GameProjectUtils::LoadTemplateDefs(const FString& ProjectDirectory) { UTemplateProjectDefs* TemplateDefs = nullptr; const FString TemplateDefsIniFilename = FConfigCacheIni::NormalizeConfigIniPath(ProjectDirectory / TEXT("Config") / GetTemplateDefsFilename()); if ( FPlatformFileManager::Get().GetPlatformFile().FileExists(*TemplateDefsIniFilename) ) { UClass* ClassToConstruct = UDefaultTemplateProjectDefs::StaticClass(); // see if template uses a custom project defs object FString ClassName; const bool bFoundValue = GConfig->GetString(*UTemplateProjectDefs::StaticClass()->GetPathName(), TEXT("TemplateProjectDefsClass"), ClassName, TemplateDefsIniFilename); if (bFoundValue && ClassName.Len() > 0) { UClass* OverrideClass = UClass::TryFindTypeSlow(ClassName); if (nullptr != OverrideClass) { ClassToConstruct = OverrideClass; } else { UE_LOG(LogGameProjectGeneration, Error, TEXT("Failed to find template project defs class '%s', using default."), *ClassName); } } TemplateDefs = NewObject(GetTransientPackage(), ClassToConstruct); TemplateDefs->LoadConfig(UTemplateProjectDefs::StaticClass(), *TemplateDefsIniFilename); if (TemplateDefs->HiddenSettings.Num() > 1 && TemplateDefs->HiddenSettings.Contains(ETemplateSetting::All)) { UE_LOG(LogGameProjectGeneration, Warning, TEXT("Template '%s' contains 'All' in HiddenSettings in addition to other entries. This is a mistake, and means that all settings will be hidden."), *ProjectDirectory); } if (TemplateDefs->Variants.Num() > 0) // We make sure the Variants are valid { TSet VariantNames; for (TArray::TIterator It = TemplateDefs->Variants.CreateIterator(); It; ++It) { const FTemplateVariant& Variant = *It; if (VariantNames.Contains(Variant.Name)) { UE_LOG(LogGameProjectGeneration, Warning, TEXT("Template '%s' contains multiple Variants with the name '%s', only the first one will be kept."), *ProjectDirectory, *Variant.Name.ToString()); } else if (Variant.Name.IsNone()) // NAME_None is used to represent no Variant selected { UE_LOG(LogGameProjectGeneration, Error, TEXT("Template '%s' contains a Variant with the name '%s' which is not allowed, it will be discarded."), *ProjectDirectory, *Variant.Name.ToString()); } else { VariantNames.Add(Variant.Name); continue; } It.RemoveCurrent(); } } } return TemplateDefs; } TOptional GameProjectUtils::GenerateProjectFromScratch(const FProjectInformation& InProjectInfo, FText& OutFailReason, FText& OutFailLog) { FScopedSlowTask SlowTask(5); const FString NewProjectFolder = FPaths::GetPath(InProjectInfo.ProjectFilename); const FString NewProjectName = FPaths::GetBaseFilename(InProjectInfo.ProjectFilename); TArray CreatedFiles; SlowTask.EnterProgressFrame(); ResetCurrentProjectModulesCache(); FGuid ProjectID; // Generate config files if (!GenerateConfigFiles(InProjectInfo, CreatedFiles, OutFailReason, ProjectID)) { return TOptional(); } // Make the Content folder const FString ContentFolder = NewProjectFolder / TEXT("Content"); if ( !IFileManager::Get().MakeDirectory(*ContentFolder) ) { FFormatNamedArguments Args; Args.Add( TEXT("ContentFolder"), FText::FromString( ContentFolder ) ); OutFailReason = FText::Format( LOCTEXT("FailedToCreateContentFolder", "Failed to create the content folder {ContentFolder}"), Args ); return TOptional(); } SlowTask.EnterProgressFrame(); TArray StartupModuleNames; if ( InProjectInfo.bShouldGenerateCode ) { FScopedSlowTask LocalScope(2); LocalScope.EnterProgressFrame(); // Generate basic source code files if ( !GenerateBasicSourceCode(NewProjectFolder / TEXT("Source"), NewProjectName, NewProjectFolder, StartupModuleNames, CreatedFiles, OutFailReason) ) { return TOptional(); } LocalScope.EnterProgressFrame(); // Generate game framework source code files if ( !GenerateGameFrameworkSourceCode(NewProjectFolder / TEXT("Source"), NewProjectName, CreatedFiles, OutFailReason) ) { return TOptional(); } } SlowTask.EnterProgressFrame(); // Generate the project file { // Set up the descriptor FProjectDescriptor Project; for(int32 Idx = 0; Idx < StartupModuleNames.Num(); Idx++) { Project.Modules.Add(FModuleDescriptor(*StartupModuleNames[Idx])); } //===================================================================== // Explicitly enable Modeling Mode plugin in Blank Template, // with AllowList=Editor flags. In 5.0 the Modeling Mode plugin // cannot be enabledByDefault in the .uplugin file due to // dependent Runtime modules that should not be included in all // game builds. So, In 5.0 the plugin is explicitly enabled here // for Blank projects. The uplugin-level issue is expected to // be resolved in 5.1, at which point this code block will be deleted //===================================================================== TSharedPtr ModelingModePlugin = IPluginManager::Get().FindPlugin(TEXT("ModelingToolsEditorMode")); if ( ModelingModePlugin.IsValid() ) { FPluginReferenceDescriptor ModelingModeDescriptor(ModelingModePlugin->GetName(), true); ModelingModeDescriptor.TargetAllowList.Add(EBuildTargetType::Editor); Project.Plugins.Add(ModelingModeDescriptor); } Project.bIsEnterpriseProject = InProjectInfo.bIsEnterpriseProject; // Try to save it FText LocalFailReason; if(!Project.Save(InProjectInfo.ProjectFilename, LocalFailReason)) { OutFailReason = LocalFailReason; return TOptional(); } CreatedFiles.Add(InProjectInfo.ProjectFilename); // Set the engine identifier for it. Do this after saving, so it can be correctly detected as foreign or non-foreign. if(!SetEngineAssociationForForeignProject(InProjectInfo.ProjectFilename, OutFailReason)) { return TOptional(); } } SlowTask.EnterProgressFrame(); if ( InProjectInfo.bShouldGenerateCode ) { // Generate project files if ( !GenerateCodeProjectFiles(InProjectInfo.ProjectFilename, OutFailReason, OutFailLog) ) { return TOptional(); } } SlowTask.EnterProgressFrame(); UE_LOG(LogGameProjectGeneration, Log, TEXT("Created new project with %d files (plus project files)"), CreatedFiles.Num()); return ProjectID; } static bool SaveConfigValues(const FProjectInformation& InProjectInfo, const TArray& ConfigValues, FText& OutFailReason) { const FString ProjectConfigPath = FPaths::GetPath(InProjectInfo.ProjectFilename) / TEXT("Config"); // Fix all specified config values for (const FTemplateConfigValue& ConfigValue : ConfigValues) { const FString IniFilename = ProjectConfigPath / ConfigValue.ConfigFile; bool bSuccessfullyProcessed = false; TArray FileLines; if (FFileHelper::LoadANSITextFileToStrings(*IniFilename, &IFileManager::Get(), FileLines)) { FString FileOutput; const FString TargetSection = ConfigValue.ConfigSection; FString CurSection; bool bFoundTargetKey = false; for (const FString& LineIn : FileLines) { FString Line = LineIn; Line.TrimStartAndEndInline(); bool bShouldExcludeLineFromOutput = false; // If we not yet found the target key parse each line looking for it if (!bFoundTargetKey) { if (Line.Len() == 0) { // Check for an empty line. No work needs to be done on these lines } else if (Line.StartsWith(TEXT(";"))) { // Comment lines start with ";". Skip these lines entirely. } else if (Line.StartsWith(TEXT("["))) { // If this is a section line, update the section // If we are entering a new section and we have not yet found our key in the target section, add it to the end of the section if (CurSection == TargetSection) { FileOutput += ConfigValue.ConfigKey + TEXT("=") + ConfigValue.ConfigValue + LINE_TERMINATOR + LINE_TERMINATOR; bFoundTargetKey = true; } // Update the current section CurSection = Line.Mid(1, Line.Len() - 2); } // This is possibly an actual key/value pair else if (CurSection == TargetSection) { // Key value pairs contain an equals sign const int32 EqualsIdx = Line.Find(TEXT("=")); if (EqualsIdx != INDEX_NONE) { // Determine the key and see if it is the target key const FString Key = Line.Left(EqualsIdx); if (Key == ConfigValue.ConfigKey) { // Found the target key, add it to the output and skip the current line if the target value is supposed to replace FileOutput += ConfigValue.ConfigKey + TEXT("=") + ConfigValue.ConfigValue + LINE_TERMINATOR; bShouldExcludeLineFromOutput = ConfigValue.bShouldReplaceExistingValue; bFoundTargetKey = true; } } } } // Unless we replaced the key, add this line to the output if (!bShouldExcludeLineFromOutput) { FileOutput += Line; if (&LineIn != &FileLines.Last()) { // Add a line terminator on every line except the last FileOutput += LINE_TERMINATOR; } } } // If the key did not exist, add it here if (!bFoundTargetKey) { // If we did not end in the correct section, add the section to the bottom of the file if (CurSection != TargetSection) { FileOutput += LINE_TERMINATOR; FileOutput += FString::Printf(TEXT("[%s]"), *TargetSection) + LINE_TERMINATOR; } // Add the key/value here FileOutput += ConfigValue.ConfigKey + TEXT("=") + ConfigValue.ConfigValue + LINE_TERMINATOR; } if (!FFileHelper::SaveStringToFile(FileOutput, *IniFilename)) { FFormatNamedArguments Args; Args.Add(TEXT("ConfigFile"), FText::FromString(IniFilename)); OutFailReason = LOCTEXT("FailedToFixUpConfigFile", "Failed to process file {ConfigFile}."); return false; } } } return true; } static FString GetReplacePlaceholder(int Idx) { return FString::Printf(TEXT("{{{REPLACE:%d}}}"), Idx); } TOptional GameProjectUtils::CreateProjectFromTemplate(const FProjectInformation& InProjectInfo, FText& OutFailReason, FText& OutFailLog, TArray* OutCreatedFiles) { FScopedSlowTask SlowTask(10); const FString ProjectName = FPaths::GetBaseFilename(InProjectInfo.ProjectFilename); const FString TemplateName = FPaths::GetBaseFilename(InProjectInfo.TemplateFile); const FString SrcFolder = FPaths::GetPath(InProjectInfo.TemplateFile); const FString DestFolder = FPaths::GetPath(InProjectInfo.ProjectFilename); if (!FPlatformFileManager::Get().GetPlatformFile().FileExists(*InProjectInfo.TemplateFile)) { FFormatNamedArguments Args; Args.Add( TEXT("TemplateFile"), FText::FromString( InProjectInfo.TemplateFile ) ); OutFailReason = FText::Format( LOCTEXT("InvalidTemplate_MissingProject", "Template project \"{TemplateFile}\" does not exist."), Args ); return TOptional(); } SlowTask.EnterProgressFrame(); UTemplateProjectDefs* TemplateDefs = LoadTemplateDefs(SrcFolder); if (TemplateDefs == nullptr) { FFormatNamedArguments Args; Args.Add( TEXT("TemplateFile"), FText::FromString( FPaths::GetBaseFilename(InProjectInfo.TemplateFile) ) ); Args.Add( TEXT("TemplateDefinesFile"), FText::FromString( GetTemplateDefsFilename() ) ); OutFailReason = FText::Format( LOCTEXT("InvalidTemplate_MissingDefs", "Template project \"{TemplateFile}\" does not have definitions file: '{TemplateDefinesFile}'."), Args ); return TOptional(); } SlowTask.EnterProgressFrame(); // create a Content folder in the destination directory const FString DestContentFolder = DestFolder / TEXT("Content"); if (!IFileManager::Get().MakeDirectory(*DestContentFolder, true)) { FFormatNamedArguments FailArgs; FailArgs.Add(TEXT("DestContentFolder"), FText::FromString(DestContentFolder)); OutFailReason = FText::Format(LOCTEXT("FailedToCreateDirectory", "Failed to create directory \"{DestContentFolder}\"."), FailArgs); return TOptional(); } // Add a rule to replace the build settings version with the appropriate number FTemplateReplacement& DefaultBuildSettingsRepl = TemplateDefs->ReplacementsInFiles.AddZeroed_GetRef(); DefaultBuildSettingsRepl.Extensions.Add("cs"); DefaultBuildSettingsRepl.From = TEXT("BuildSettingsVersion.Latest"); DefaultBuildSettingsRepl.To = GetDefaultBuildSettingsVersion(); DefaultBuildSettingsRepl.bCaseSensitive = true; // Fix up the replacement strings using the specified project name TemplateDefs->FixupStrings(TemplateName, ProjectName); // Form a list of all extensions we care about TSet ReplacementsInFilesExtensions; for ( const FTemplateReplacement& Replacement : TemplateDefs->ReplacementsInFiles ) { ReplacementsInFilesExtensions.Append(Replacement.Extensions); } // Keep a list of created files so we can delete them if project creation fails TArray CreatedFiles; SlowTask.EnterProgressFrame(); // Discover and copy all files in the src folder to the destination, excluding a few files and folders TArray FilesToCopy; TArray FilesThatNeedContentsReplaced; TMap ClassRenames; IFileManager::Get().FindFilesRecursive(FilesToCopy, *SrcFolder, TEXT("*"), /*Files=*/true, /*Directories=*/false); SlowTask.EnterProgressFrame(); { // Open a new feedback scope for the loop so we can report how far through the copy we are FScopedSlowTask InnerSlowTask(static_cast(FilesToCopy.Num())); for ( const FString& SrcFilename : FilesToCopy ) { // Update the progress FFormatNamedArguments Args; Args.Add( TEXT("SrcFilename"), FText::FromString( FPaths::GetCleanFilename(SrcFilename) ) ); InnerSlowTask.EnterProgressFrame(1, FText::Format( LOCTEXT( "CreatingProjectStatus_CopyingFile", "Copying File {SrcFilename}..." ), Args )); // Get the file path, relative to the src folder const FString SrcFileSubpath = SrcFilename.RightChop(SrcFolder.Len() + 1); // Skip any files that were configured to be ignored if ( TemplateDefs->FilesToIgnore.Contains(SrcFileSubpath) ) { // This file was marked as "ignored" continue; } // Skip any folders that were configured to be ignored if ( const FString* IgnoredFolder = TemplateDefs->FoldersToIgnore.FindByPredicate([&SrcFileSubpath](const FString& Ignore){ return SrcFileSubpath.StartsWith(Ignore + TEXT("/")); }) ) { // This folder was marked as "ignored" UE_LOG(LogGameProjectGeneration, Verbose, TEXT("'%s': Skipping as it is in an ignored folder '%s'"), *SrcFilename, **IgnoredFolder); continue; } // Retarget any folders that were chosen to be renamed by choosing a new destination subpath now FString DestFileSubpathWithoutFilename = FPaths::GetPath(SrcFileSubpath) + TEXT("/"); for ( const FTemplateFolderRename& FolderRename : TemplateDefs->FolderRenames ) { if ( SrcFileSubpath.StartsWith(FolderRename.From + TEXT("/")) ) { // This was a file in a renamed folder. Retarget to the new location DestFileSubpathWithoutFilename = FolderRename.To / DestFileSubpathWithoutFilename.RightChop( FolderRename.From.Len() ); UE_LOG(LogGameProjectGeneration, Verbose, TEXT("'%s': Moving to '%s' as it matched folder rename ('%s'->'%s')"), *SrcFilename, *DestFileSubpathWithoutFilename, *FolderRename.From, *FolderRename.To); } } // Retarget any files that were chosen to have parts of their names replaced here FString DestBaseFilename = FPaths::GetBaseFilename(SrcFileSubpath); const FString FileExtension = FPaths::GetExtension(SrcFileSubpath); for ( const FTemplateReplacement& Replacement : TemplateDefs->FilenameReplacements ) { if ( Replacement.Extensions.Contains( FileExtension ) ) { // This file matched a filename replacement extension, apply it now FString LastDestBaseFilename = DestBaseFilename; DestBaseFilename = DestBaseFilename.Replace(*Replacement.From, *Replacement.To, Replacement.bCaseSensitive ? ESearchCase::CaseSensitive : ESearchCase::IgnoreCase); if (LastDestBaseFilename != DestBaseFilename) { UE_LOG(LogGameProjectGeneration, Verbose, TEXT("'%s': Renaming to '%s/%s' as it matched file rename ('%s'->'%s')"), *SrcFilename, *DestFileSubpathWithoutFilename, *DestBaseFilename, *Replacement.From, *Replacement.To); } } } // Perform the copy const FString DestFilename = DestFolder / DestFileSubpathWithoutFilename + DestBaseFilename + TEXT(".") + FileExtension; if ( IFileManager::Get().Copy(*DestFilename, *SrcFilename) == COPY_OK ) { CreatedFiles.Add(DestFilename); if ( ReplacementsInFilesExtensions.Contains(FileExtension) ) { FilesThatNeedContentsReplaced.Add(DestFilename); } // Allow project template to extract class renames from this file copy if (FPaths::GetBaseFilename(SrcFilename) != FPaths::GetBaseFilename(DestFilename) && TemplateDefs->IsClassRename(DestFilename, SrcFilename, FileExtension)) { // Looks like a UObject file! ClassRenames.Add(FPaths::GetBaseFilename(SrcFilename), FPaths::GetBaseFilename(DestFilename)); } } else { FFormatNamedArguments FailArgs; FailArgs.Add(TEXT("SrcFilename"), FText::FromString(SrcFilename)); FailArgs.Add(TEXT("DestFilename"), FText::FromString(DestFilename)); OutFailReason = FText::Format(LOCTEXT("FailedToCopyFile", "Failed to copy \"{SrcFilename}\" to \"{DestFilename}\"."), FailArgs); return TOptional(); } } } SlowTask.EnterProgressFrame(); { // Open a new feedback scope for the loop so we can report how far through the process we are FScopedSlowTask InnerSlowTask(static_cast(FilesThatNeedContentsReplaced.Num())); // Open all files with the specified extensions and replace text for ( const FString& FileToFix : FilesThatNeedContentsReplaced ) { InnerSlowTask.EnterProgressFrame(); bool bSuccessfullyProcessed = false; FString FileContents; if ( FFileHelper::LoadFileToString(FileContents, *FileToFix) ) { // Substitute strings in two passes to avoid situations where patterns may match the replaced strings. for (int Idx = 0; Idx < TemplateDefs->ReplacementsInFiles.Num(); Idx++) { const FTemplateReplacement& Replacement = TemplateDefs->ReplacementsInFiles[Idx]; if (Replacement.Extensions.Contains(FPaths::GetExtension(FileToFix))) { FString Placeholder = GetReplacePlaceholder(Idx); FileContents.ReplaceInline(*Replacement.From, *Placeholder, Replacement.bCaseSensitive ? ESearchCase::CaseSensitive : ESearchCase::IgnoreCase); } } for (int Idx = 0; Idx < TemplateDefs->ReplacementsInFiles.Num(); Idx++) { const FTemplateReplacement& Replacement = TemplateDefs->ReplacementsInFiles[Idx]; if (Replacement.Extensions.Contains(FPaths::GetExtension(FileToFix))) { FString Placeholder = GetReplacePlaceholder(Idx); FileContents.ReplaceInline(*Placeholder, *Replacement.To, ESearchCase::CaseSensitive); } } if ( FFileHelper::SaveStringToFile(FileContents, *FileToFix) ) { bSuccessfullyProcessed = true; } } if ( !bSuccessfullyProcessed ) { FFormatNamedArguments Args; Args.Add( TEXT("FileToFix"), FText::FromString( FileToFix ) ); OutFailReason = FText::Format( LOCTEXT("FailedToFixUpFile", "Failed to process file \"{FileToFix}\"."), Args ); return TOptional(); } } } SlowTask.EnterProgressFrame(); // Fixup specific ini values TArray ConfigValuesToSet; AddHardwareConfigValues(InProjectInfo, ConfigValuesToSet); AddLumenConfigValues(InProjectInfo, ConfigValuesToSet, /* do not disable static lighting, as some templates need it */ false); AddRaytracingConfigValues(InProjectInfo, ConfigValuesToSet); AddNewProjectDefaultShadowConfigValues(InProjectInfo, ConfigValuesToSet); AddPostProcessingConfigValues(InProjectInfo, ConfigValuesToSet); AddWorldPartitionConfigValues(InProjectInfo, ConfigValuesToSet); AddUserInterfaceEngineConfigValues(InProjectInfo, ConfigValuesToSet); AddUserInterfaceGameConfigValues(InProjectInfo, ConfigValuesToSet); TemplateDefs->AddConfigValues(ConfigValuesToSet, TemplateName, ProjectName, InProjectInfo.bShouldGenerateCode); FGuid ProjectID = FGuid::NewGuid(); ConfigValuesToSet.Emplace(TEXT("DefaultGame.ini"), TEXT("/Script/EngineSettings.GeneralProjectSettings"), TEXT("ProjectID"), ProjectID.ToString(), /*InShouldReplaceExistingValue=*/true); // Add all classname fixups for (const TPair& Rename : ClassRenames) { const FString ClassRedirectString = FString::Printf(TEXT("(OldClassName=\"%s\",NewClassName=\"%s\")"), *Rename.Key, *Rename.Value); ConfigValuesToSet.Emplace(TEXT("DefaultEngine.ini"), TEXT("/Script/Engine.Engine"), TEXT("+ActiveClassRedirects"), *ClassRedirectString, /*InShouldReplaceExistingValue=*/false); } SlowTask.EnterProgressFrame(); if (!SaveConfigValues(InProjectInfo, ConfigValuesToSet, OutFailReason)) { return TOptional(); } if (!AddSharedContentToProject(InProjectInfo, CreatedFiles, OutFailReason)) { return TOptional(); } if (!TemplateDefs->PreGenerateProject(DestFolder, SrcFolder, InProjectInfo.ProjectFilename, InProjectInfo.TemplateFile, InProjectInfo.bShouldGenerateCode, OutFailReason)) { return TOptional(); } SlowTask.EnterProgressFrame(); // Generate the project file { // Load the source project FProjectDescriptor Project; if (!Project.Load(InProjectInfo.TemplateFile, OutFailReason)) { return TOptional(); } // Update it to current Project.EngineAssociation.Empty(); Project.EpicSampleNameHash = 0; // Fix up module names const FString BaseSourceName = FPaths::GetBaseFilename(InProjectInfo.TemplateFile); const FString BaseNewName = FPaths::GetBaseFilename(InProjectInfo.ProjectFilename); for (FModuleDescriptor& ModuleInfo : Project.Modules) { ModuleInfo.Name = FName(*ModuleInfo.Name.ToString().Replace(*BaseSourceName, *BaseNewName)); } // Save it to disk if (!Project.Save(InProjectInfo.ProjectFilename, OutFailReason)) { return TOptional(); } // Set the engine identifier if it's a foreign project. Do this after saving, so it can be correctly detected as foreign. if (!SetEngineAssociationForForeignProject(InProjectInfo.ProjectFilename, OutFailReason)) { return TOptional(); } // Add it to the list of created files CreatedFiles.Add(InProjectInfo.ProjectFilename); } SlowTask.EnterProgressFrame(); if (InProjectInfo.bShouldGenerateCode) { // Generate project files if (!GenerateCodeProjectFiles(InProjectInfo.ProjectFilename, OutFailReason, OutFailLog)) { return TOptional(); } } SlowTask.EnterProgressFrame(); if (!TemplateDefs->PostGenerateProject(DestFolder, SrcFolder, InProjectInfo.ProjectFilename, InProjectInfo.TemplateFile, InProjectInfo.bShouldGenerateCode, OutFailReason)) { return TOptional(); } if (OutCreatedFiles != nullptr) { OutCreatedFiles->Append(CreatedFiles); } return ProjectID; } bool GameProjectUtils::SetEngineAssociationForForeignProject(const FString& ProjectFileName, FText& OutFailReason) { if(FUProjectDictionary(FPaths::RootDir()).IsForeignProject(ProjectFileName)) { if(!FDesktopPlatformModule::Get()->SetEngineIdentifierForProject(ProjectFileName, FDesktopPlatformModule::Get()->GetCurrentEngineIdentifier())) { OutFailReason = LOCTEXT("FailedToSetEngineIdentifier", "Couldn't set engine identifier for project"); return false; } } return true; } FString GameProjectUtils::GetTemplateDefsFilename() { return TEXT("TemplateDefs.ini"); } FString GameProjectUtils::GetIncludePathForFile(const FString& InFullFilePath, const FString& ModuleRootPath) { FString RelativeHeaderPath = FPaths::ChangeExtension(InFullFilePath, "h"); const FString PublicString = ModuleRootPath / "Public" / ""; if (RelativeHeaderPath.StartsWith(PublicString)) { return RelativeHeaderPath.RightChop(PublicString.Len()); } const FString PrivateString = ModuleRootPath / "Private" / ""; if (RelativeHeaderPath.StartsWith(PrivateString)) { return RelativeHeaderPath.RightChop(PrivateString.Len()); } return RelativeHeaderPath.RightChop(ModuleRootPath.Len()); } bool GameProjectUtils::NameContainsOnlyLegalCharacters(const FString& TestName, FString& OutIllegalCharacters) { bool bContainsIllegalCharacters = false; // Only allow alphanumeric characters in the project name bool bFoundAlphaNumericChar = false; for ( int32 CharIdx = 0 ; CharIdx < TestName.Len() ; ++CharIdx ) { const FString& Char = TestName.Mid( CharIdx, 1 ); if ( !FChar::IsAlnum(Char[0]) && Char != TEXT("_") ) { if ( !OutIllegalCharacters.Contains( Char ) ) { OutIllegalCharacters += Char; } bContainsIllegalCharacters = true; } } return !bContainsIllegalCharacters; } bool GameProjectUtils::NameMatchesPlatformModuleName(const FString& TestName) { for (auto Pair : FDataDrivenPlatformInfoRegistry::GetAllPlatformInfos()) { FString PlatformNameString = Pair.Key.ToString(); if ((PlatformNameString == TestName) || ((PlatformNameString += "TargetPlatform") == TestName)) { return true; } } TArray CustomTargetPlatformModules; GConfig->GetArray(TEXT("CustomTargetPlatforms"), TEXT("ModuleName"), CustomTargetPlatformModules, GEditorIni); for (const FString& ModuleName : CustomTargetPlatformModules) { if (TestName == ModuleName) { return true; } } return false; } bool GameProjectUtils::ProjectFileExists(const FString& ProjectFile) { return FPlatformFileManager::Get().GetPlatformFile().FileExists(*ProjectFile); } bool GameProjectUtils::AnyProjectFilesExistInFolder(const FString& Path) { TArray ExistingFiles; const FString Wildcard = FString::Printf(TEXT("%s/*.%s"), *Path, *FProjectDescriptor::GetExtension()); IFileManager::Get().FindFiles(ExistingFiles, *Wildcard, /*Files=*/true, /*Directories=*/false); return ExistingFiles.Num() > 0; } bool GameProjectUtils::CleanupIsEnabled() { // Clean up files when running Rocket (unless otherwise specified on the command line) return FParse::Param(FCommandLine::Get(), TEXT("norocketcleanup")) == false; } void GameProjectUtils::DeleteCreatedFiles(const FString& RootFolder, const TArray& CreatedFiles) { if (CleanupIsEnabled()) { for ( auto FileToDeleteIt = CreatedFiles.CreateConstIterator(); FileToDeleteIt; ++FileToDeleteIt ) { IFileManager::Get().Delete(**FileToDeleteIt); } // If the project folder is empty after deleting all the files we created, delete the directory as well TArray RemainingFiles; IFileManager::Get().FindFilesRecursive(RemainingFiles, *RootFolder, TEXT("*.*"), /*Files=*/true, /*Directories=*/false); if ( RemainingFiles.Num() == 0 ) { IFileManager::Get().DeleteDirectory(*RootFolder, /*RequireExists=*/false, /*Tree=*/true); } } } void GameProjectUtils::AddHardwareConfigValues(const FProjectInformation& InProjectInfo, TArray& ConfigValues) { if (InProjectInfo.TargetedHardware.IsSet()) { UEnum* HardwareClassEnum = StaticEnum(); if (HardwareClassEnum != nullptr) { FString TargetHardwareString; HardwareClassEnum->GetValueAsString(InProjectInfo.TargetedHardware.GetValue(), /*out*/ TargetHardwareString); if (!TargetHardwareString.IsEmpty()) { ConfigValues.Emplace(TEXT("DefaultEngine.ini"), TEXT("/Script/HardwareTargeting.HardwareTargetingSettings"), TEXT("TargetedHardwareClass"), TargetHardwareString, true /* ShouldReplaceExistingValue */); } } } if (InProjectInfo.DefaultGraphicsPerformance.IsSet()) { UEnum* GraphicsPresetEnum = StaticEnum(); if (GraphicsPresetEnum != nullptr) { FString GraphicsPresetString; GraphicsPresetEnum->GetValueAsString(InProjectInfo.DefaultGraphicsPerformance.GetValue(), /*out*/ GraphicsPresetString); if (!GraphicsPresetString.IsEmpty()) { ConfigValues.Emplace(TEXT("DefaultEngine.ini"), TEXT("/Script/HardwareTargeting.HardwareTargetingSettings"), TEXT("DefaultGraphicsPerformance"), GraphicsPresetString, true /* ShouldReplaceExistingValue */); } } } // Don't override these settings for templates if (InProjectInfo.TemplateFile.IsEmpty()) { // New projects always have DX12 by default on Windows ConfigValues.Emplace(TEXT("DefaultEngine.ini"), TEXT("/Script/WindowsTargetPlatform.WindowsTargetSettings"), TEXT("DefaultGraphicsRHI"), TEXT("DefaultGraphicsRHI_DX12"), false /* ShouldReplaceExistingValue */); // Force clear D3D12TargetedShaderFormats since the BaseEngine list can change at any time. ConfigValues.Emplace(TEXT("DefaultEngine.ini"), TEXT("/Script/WindowsTargetPlatform.WindowsTargetSettings"), TEXT("!D3D12TargetedShaderFormats"), TEXT("ClearArray"), false /* ShouldReplaceExistingValue */); // New projects always have DX12 only supporting SM6 by default on Windows ConfigValues.Emplace(TEXT("DefaultEngine.ini"), TEXT("/Script/WindowsTargetPlatform.WindowsTargetSettings"), TEXT("+D3D12TargetedShaderFormats"), TEXT("PCD3D_SM6"), false /* ShouldReplaceExistingValue */); } } bool GameProjectUtils::GenerateConfigFiles(const FProjectInformation& InProjectInfo, TArray& OutCreatedFiles, FText& OutFailReason, FGuid& OutProjectID) { const FString NewProjectFolder = FPaths::GetPath(InProjectInfo.ProjectFilename); const FString NewProjectName = FPaths::GetBaseFilename(InProjectInfo.ProjectFilename); FString ProjectConfigPath = NewProjectFolder / TEXT("Config"); // DefaultEngine.ini { const FString DefaultEngineIniFilename = ProjectConfigPath / TEXT("DefaultEngine.ini"); FString FileContents; FileContents += LINE_TERMINATOR; FileContents += TEXT("[Audio]") LINE_TERMINATOR; FileContents += TEXT("UseAudioMixer=True") LINE_TERMINATOR; if (WriteOutputFile(DefaultEngineIniFilename, FileContents, OutFailReason)) { OutCreatedFiles.Add(DefaultEngineIniFilename); } else { return false; } TArray ConfigValuesToSet; AddHardwareConfigValues(InProjectInfo, ConfigValuesToSet); AddLumenConfigValues(InProjectInfo, ConfigValuesToSet); AddNewProjectDefaultShadowConfigValues(InProjectInfo, ConfigValuesToSet); AddPostProcessingConfigValues(InProjectInfo, ConfigValuesToSet); AddRaytracingConfigValues(InProjectInfo, ConfigValuesToSet); AddWorldPartitionConfigValues(InProjectInfo, ConfigValuesToSet); AddUserInterfaceEngineConfigValues(InProjectInfo, ConfigValuesToSet); if (!SaveConfigValues(InProjectInfo, ConfigValuesToSet, OutFailReason)) { return false; } } // DefaultEditor.ini { const FString DefaultEditorIniFilename = ProjectConfigPath / TEXT("DefaultEditor.ini"); FString FileContents; if (WriteOutputFile(DefaultEditorIniFilename, FileContents, OutFailReason)) { OutCreatedFiles.Add(DefaultEditorIniFilename); } else { return false; } } // DefaultGame.ini { const FString DefaultGameIniFilename = ProjectConfigPath / TEXT("DefaultGame.ini"); FString FileContents; FileContents += TEXT("[/Script/EngineSettings.GeneralProjectSettings]") LINE_TERMINATOR; OutProjectID = FGuid::NewGuid(); FileContents += TEXT("ProjectID=") + OutProjectID.ToString() + LINE_TERMINATOR; if (WriteOutputFile(DefaultGameIniFilename, FileContents, OutFailReason)) { OutCreatedFiles.Add(DefaultGameIniFilename); } else { return false; } TArray ConfigValuesToSet; AddUserInterfaceGameConfigValues(InProjectInfo, ConfigValuesToSet); if (!SaveConfigValues(InProjectInfo, ConfigValuesToSet, OutFailReason)) { return false; } } // Platforms inis: { if (!GeneratePlatformConfigFiles(InProjectInfo, OutFailReason)) { return false; } } return true; } bool GameProjectUtils::GeneratePlatformConfigFiles(const FProjectInformation& InProjectInfo, FText& OutFailReason) { return true; } bool GameProjectUtils::GenerateBasicSourceCode(TArray& OutCreatedFiles, FText& OutFailReason) { TArray StartupModuleNames; if (GameProjectUtils::GenerateBasicSourceCode(FPaths::GameSourceDir().LeftChop(1), FApp::GetProjectName(), FPaths::ProjectDir(), StartupModuleNames, OutCreatedFiles, OutFailReason)) { GameProjectUtils::UpdateProject( FProjectDescriptorModifier::CreateLambda( [&StartupModuleNames](FProjectDescriptor& Descriptor) { return UpdateStartupModuleNames(Descriptor, &StartupModuleNames); })); return true; } return false; } bool GameProjectUtils::GenerateBasicSourceCode(const FString& NewProjectSourcePath, const FString& NewProjectName, const FString& NewProjectRoot, TArray& OutGeneratedStartupModuleNames, TArray& OutCreatedFiles, FText& OutFailReason) { const FString GameModulePath = NewProjectSourcePath / NewProjectName; const FString EditorName = NewProjectName + TEXT("Editor"); // MyGame.Build.cs { const FString NewBuildFilename = GameModulePath / NewProjectName + TEXT(".Build.cs"); TArray PublicDependencyModuleNames; PublicDependencyModuleNames.Add(TEXT("Core")); PublicDependencyModuleNames.Add(TEXT("CoreUObject")); PublicDependencyModuleNames.Add(TEXT("Engine")); PublicDependencyModuleNames.Add(TEXT("InputCore")); TArray PrivateDependencyModuleNames; if ( GenerateGameModuleBuildFile(NewBuildFilename, NewProjectName, PublicDependencyModuleNames, PrivateDependencyModuleNames, OutFailReason) ) { OutGeneratedStartupModuleNames.Add(NewProjectName); OutCreatedFiles.Add(NewBuildFilename); } else { return false; } } // MyGame.Target.cs { const FString NewTargetFilename = NewProjectSourcePath / NewProjectName + TEXT(".Target.cs"); TArray ExtraModuleNames; ExtraModuleNames.Add( NewProjectName ); if ( GenerateGameModuleTargetFile(NewTargetFilename, NewProjectName, ExtraModuleNames, OutFailReason) ) { OutCreatedFiles.Add(NewTargetFilename); } else { return false; } } // MyGameEditor.Target.cs { const FString NewTargetFilename = NewProjectSourcePath / EditorName + TEXT(".Target.cs"); // Include the MyGame module... TArray ExtraModuleNames; ExtraModuleNames.Add(NewProjectName); if ( GenerateEditorModuleTargetFile(NewTargetFilename, EditorName, ExtraModuleNames, OutFailReason) ) { OutCreatedFiles.Add(NewTargetFilename); } else { return false; } } // MyGame.h { const FString NewHeaderFilename = GameModulePath / NewProjectName + TEXT(".h"); TArray PublicHeaderIncludes; if ( GenerateGameModuleHeaderFile(NewHeaderFilename, PublicHeaderIncludes, OutFailReason) ) { OutCreatedFiles.Add(NewHeaderFilename); } else { return false; } } // MyGame.cpp { const FString NewCPPFilename = GameModulePath / NewProjectName + TEXT(".cpp"); if ( GenerateGameModuleCPPFile(NewCPPFilename, NewProjectName, NewProjectName, OutFailReason) ) { OutCreatedFiles.Add(NewCPPFilename); } else { return false; } } return true; } bool GameProjectUtils::GenerateGameFrameworkSourceCode(const FString& NewProjectSourcePath, const FString& NewProjectName, TArray& OutCreatedFiles, FText& OutFailReason) { const FString GameModulePath = NewProjectSourcePath / NewProjectName; // Used to override the code generation validation since the module we're creating isn't the same as the project we currently have loaded FModuleContextInfo NewModuleInfo; NewModuleInfo.ModuleName = NewProjectName; NewModuleInfo.ModuleType = EHostType::Runtime; NewModuleInfo.ModuleSourcePath = FPaths::ConvertRelativePathToFull(GameModulePath / ""); // Ensure trailing / // MyGameGameMode.h { const UClass* BaseClass = AGameModeBase::StaticClass(); const FString NewClassName = NewProjectName + BaseClass->GetName(); const FString NewHeaderFilename = GameModulePath / NewClassName + TEXT(".h"); FString UnusedSyncLocation; if ( GenerateClassHeaderFile(NewHeaderFilename, NewClassName, FNewClassInfo(BaseClass), TArray(), TEXT(""), TEXT(""), UnusedSyncLocation, NewModuleInfo, false, OutFailReason) ) { OutCreatedFiles.Add(NewHeaderFilename); } else { return false; } } // MyGameGameMode.cpp { const UClass* BaseClass = AGameModeBase::StaticClass(); const FString NewClassName = NewProjectName + BaseClass->GetName(); const FString NewCPPFilename = GameModulePath / NewClassName + TEXT(".cpp"); TArray PropertyOverrides; TArray AdditionalIncludes; FString UnusedSyncLocation; if ( GenerateClassCPPFile(NewCPPFilename, NewClassName, FNewClassInfo(BaseClass), AdditionalIncludes, PropertyOverrides, TEXT(""), UnusedSyncLocation, NewModuleInfo, OutFailReason) ) { OutCreatedFiles.Add(NewCPPFilename); } else { return false; } } return true; } bool GameProjectUtils::BuildCodeProject(const FString& ProjectFilename) { // Build the project while capturing the log output. Passing GWarn to CompileGameProject will allow Slate to display the progress bar. FStringOutputDevice OutputLog; OutputLog.SetAutoEmitLineTerminator(true); GLog->AddOutputDevice(&OutputLog); bool bCompileSucceeded = FDesktopPlatformModule::Get()->CompileGameProject(FPaths::RootDir(), ProjectFilename, GWarn); GLog->RemoveOutputDevice(&OutputLog); // Try to compile the modules if(!bCompileSucceeded) { FText DevEnvName = FSourceCodeNavigation::GetSelectedSourceCodeIDE(); TArray CompileFailedButtons; int32 OpenIDEButton = CompileFailedButtons.Add(FText::Format(LOCTEXT("CompileFailedOpenIDE", "Open with {0}"), DevEnvName)); CompileFailedButtons.Add(LOCTEXT("CompileFailedCancel", "Cancel")); FText LogText = FText::FromString(OutputLog.Replace(LINE_TERMINATOR, TEXT("\n")).TrimEnd()); int32 CompileFailedChoice = SOutputLogDialog::Open(LOCTEXT("CompileFailedTitle", "Compile Failed"), FText::Format(LOCTEXT("CompileFailedHeader", "The project could not be compiled. Would you like to open it in {0}?"), DevEnvName), LogText, FText::GetEmpty(), CompileFailedButtons); FText FailReason; if(CompileFailedChoice == OpenIDEButton && !GameProjectUtils::OpenCodeIDE(ProjectFilename, FailReason)) { FMessageDialog::Open(EAppMsgType::Ok, FailReason); } } return bCompileSucceeded; } bool GameProjectUtils::GenerateCodeProjectFiles(const FString& ProjectFilename, FText& OutFailReason, FText& OutFailLog) { FStringOutputDevice OutputLog; OutputLog.SetAutoEmitLineTerminator(true); GLog->AddOutputDevice(&OutputLog); bool bHaveProjectFiles = FDesktopPlatformModule::Get()->GenerateProjectFiles(FPaths::RootDir(), ProjectFilename, GWarn); GLog->RemoveOutputDevice(&OutputLog); if ( !bHaveProjectFiles ) { OutFailReason = LOCTEXT("ErrorWhileGeneratingProjectFiles", "An error occurred while trying to generate project files."); OutFailLog = FText::FromString(OutputLog); return false; } return true; } int32 FindSpecificBuildFiles(const TCHAR* Path, TMap& BuildFiles) { class FBuildFileVistor : public IPlatformFile::FDirectoryVisitor { public: TMap& BuildFiles; int32& NumBuildFilesFound; FString CheckFilename; bool bSawAnyBuildFile; FBuildFileVistor(TMap& InBuildFiles, int32& InNumBuildFilesFound) : BuildFiles(InBuildFiles), NumBuildFilesFound(InNumBuildFilesFound), bSawAnyBuildFile(false) { } virtual bool Visit(const TCHAR* FilenameOrDirectory, bool bIsDirectory) { if (bIsDirectory) { return true; } static const FString BuildFileSuffix(TEXT(".Build.cs")); CheckFilename = FilenameOrDirectory; if (!CheckFilename.EndsWith(BuildFileSuffix)) { return true; } bSawAnyBuildFile = true; CheckFilename = FPaths::GetCleanFilename(CheckFilename); FString* Found = BuildFiles.Find(CheckFilename); if (Found && Found->Len() == 0) { *Found = FilenameOrDirectory; ++NumBuildFilesFound; } return false; } }; class FBuildDirectoryVistor : public IPlatformFile::FDirectoryVisitor { public: FBuildFileVistor FileVisitor; FString CheckDirectory; FBuildDirectoryVistor(TMap& InBuildFiles, int32& InNumBuildFilesFound) : FileVisitor(InBuildFiles, InNumBuildFilesFound) { } virtual bool Visit(const TCHAR* FilenameOrDirectory, bool bIsDirectory) { if (bIsDirectory) { static const FString Dot(TEXT(".")); CheckDirectory = FPaths::GetCleanFilename(FilenameOrDirectory); if (!CheckDirectory.StartsWith(Dot) && CheckDirectory != TEXT("Public") && CheckDirectory != TEXT("Private")) { return ScanDirectory(FilenameOrDirectory); } } return true; } bool ScanDirectory(const TCHAR* FilenameOrDirectory) { // Iterate files only the first time FileVisitor.bSawAnyBuildFile = false; IFileManager::Get().IterateDirectory(FilenameOrDirectory, FileVisitor); if (!FileVisitor.bSawAnyBuildFile && FileVisitor.NumBuildFilesFound < FileVisitor.BuildFiles.Num()) { // Iterate directories only the second time IFileManager::Get().IterateDirectory(FilenameOrDirectory, *this); } return FileVisitor.NumBuildFilesFound < FileVisitor.BuildFiles.Num(); } }; int32 NumBuildFilesFound = 0; FBuildDirectoryVistor DirectoryVisitor(BuildFiles, NumBuildFilesFound); DirectoryVisitor.ScanDirectory(Path); return NumBuildFilesFound; } void GameProjectUtils::ResetCurrentProjectModulesCache() { IProjectManager::Get().GetCurrentProjectModuleContextInfos().Reset(); } const TArray& GameProjectUtils::GetCurrentProjectModules() { const FProjectDescriptor* const CurrentProject = IProjectManager::Get().GetCurrentProject(); check(CurrentProject); TArray& RetModuleInfos = IProjectManager::Get().GetCurrentProjectModuleContextInfos(); if (RetModuleInfos.Num() > 0) { return RetModuleInfos; } RetModuleInfos.Reset(CurrentProject->Modules.Num() + 1); if (!GameProjectUtils::ProjectHasCodeFiles() || CurrentProject->Modules.Num() == 0) { // If this project doesn't currently have any code in it, we need to add a dummy entry for the game // so that we can still use the class wizard (this module will be created once we add a class) FModuleContextInfo ModuleInfo; ModuleInfo.ModuleName = FApp::GetProjectName(); ModuleInfo.ModuleType = EHostType::Runtime; ModuleInfo.ModuleSourcePath = FPaths::ConvertRelativePathToFull(FPaths::GameSourceDir() / ModuleInfo.ModuleName / ""); // Ensure trailing / RetModuleInfos.Emplace(ModuleInfo); } TMap BuildFilePathsByName; BuildFilePathsByName.Reserve(CurrentProject->Modules.Num()); for (const FModuleDescriptor& ModuleDesc : CurrentProject->Modules) { BuildFilePathsByName.Add(ModuleDesc.Name.ToString() + TEXT(".Build.cs")); } FindSpecificBuildFiles(*FPaths::GameSourceDir(), BuildFilePathsByName); // Resolve out the paths for each module and add the cut-down into to our output array for (const FModuleDescriptor& ModuleDesc : CurrentProject->Modules) { FModuleContextInfo ModuleInfo; ModuleInfo.ModuleName = ModuleDesc.Name.ToString(); ModuleInfo.ModuleType = ModuleDesc.Type; // Try and find the .Build.cs file for this module within our currently loaded project's Source directory FString* FullPath = BuildFilePathsByName.Find(ModuleInfo.ModuleName + TEXT(".Build.cs")); if (!FullPath) { continue; } // Chop the .Build.cs file off the end of the path ModuleInfo.ModuleSourcePath = FPaths::GetPath(*FullPath); ModuleInfo.ModuleSourcePath = FPaths::ConvertRelativePathToFull(ModuleInfo.ModuleSourcePath / ""); // Ensure trailing / RetModuleInfos.Emplace(ModuleInfo); } return RetModuleInfos; } TArray GameProjectUtils::GetCurrentProjectPluginModules() { const FProjectDescriptor* const CurrentProject = IProjectManager::Get().GetCurrentProject(); check(CurrentProject); TArray RetModuleInfos; if (!GameProjectUtils::ProjectHasCodeFiles() || CurrentProject->Modules.Num() == 0) { // Don't get plugins if the game project has no source tree. return RetModuleInfos; } // Resolve out the paths for each module and add the cut-down into to our output array for (const auto& Plugin : IPluginManager::Get().GetDiscoveredPlugins()) { // Only get plugins that are a part of the game project if (Plugin->GetLoadedFrom() == EPluginLoadedFrom::Project) { for (const auto& PluginModule : Plugin->GetDescriptor().Modules) { FModuleContextInfo ModuleInfo; ModuleInfo.ModuleName = PluginModule.Name.ToString(); ModuleInfo.ModuleType = PluginModule.Type; // Try and find the .Build.cs file for this module within the plugin source tree FString TmpPath; if (!FindSourceFileInProject(ModuleInfo.ModuleName + ".Build.cs", Plugin->GetBaseDir(), TmpPath)) { continue; } // Chop the .Build.cs file off the end of the path ModuleInfo.ModuleSourcePath = FPaths::GetPath(TmpPath); ModuleInfo.ModuleSourcePath = FPaths::ConvertRelativePathToFull(ModuleInfo.ModuleSourcePath / ""); // Ensure trailing / RetModuleInfos.Emplace(ModuleInfo); } } } return RetModuleInfos; } bool GameProjectUtils::IsValidSourcePath(const FString& InPath, const FModuleContextInfo& ModuleInfo, FText* const OutFailReason) { const FString AbsoluteInPath = FPaths::ConvertRelativePathToFull(InPath) / ""; // Ensure trailing / // Validate the path contains no invalid characters if(!FPaths::ValidatePath(AbsoluteInPath, OutFailReason)) { return false; } if(!AbsoluteInPath.StartsWith(ModuleInfo.ModuleSourcePath)) { if(OutFailReason) { FFormatNamedArguments Args; Args.Add(TEXT("ModuleName"), FText::FromString(ModuleInfo.ModuleName)); Args.Add(TEXT("RootSourcePath"), FText::FromString(ModuleInfo.ModuleSourcePath)); *OutFailReason = FText::Format( LOCTEXT("SourcePathInvalidForModule", "All source code for '{ModuleName}' must exist within '{RootSourcePath}'"), Args ); } return false; } return true; } bool GameProjectUtils::CalculateSourcePaths(const FString& InPath, const FModuleContextInfo& ModuleInfo, FString& OutHeaderPath, FString& OutSourcePath, FText* const OutFailReason) { const FString AbsoluteInPath = FPaths::ConvertRelativePathToFull(InPath) / ""; // Ensure trailing / OutHeaderPath = AbsoluteInPath; OutSourcePath = AbsoluteInPath; EClassLocation ClassPathLocation = EClassLocation::UserDefined; if(!GetClassLocation(InPath, ModuleInfo, ClassPathLocation, OutFailReason)) { return false; } const FString RootPath = ModuleInfo.ModuleSourcePath; const FString PublicPath = RootPath / "Public" / ""; // Ensure trailing / const FString PrivatePath = RootPath / "Private" / ""; // Ensure trailing / const FString ClassesPath = RootPath / "Classes" / ""; // Ensure trailing / // The root path must exist; we will allow the creation of sub-folders, but not the module root! // We ignore this check if the project doesn't already have source code in it, as the module folder won't yet have been created const bool bHasCodeFiles = GameProjectUtils::ProjectHasCodeFiles(); if(!IFileManager::Get().DirectoryExists(*RootPath) && bHasCodeFiles) { if(OutFailReason) { FFormatNamedArguments Args; Args.Add(TEXT("ModuleSourcePath"), FText::FromString(RootPath)); *OutFailReason = FText::Format(LOCTEXT("SourcePathMissingModuleRoot", "The specified module path does not exist on disk: {ModuleSourcePath}"), Args); } return false; } // The rules for placing header files are as follows: // 1) If InPath is the source root, and GetClassLocation has said the class header should be in the Public folder, put it in the Public folder // 2) Otherwise, just place the header at InPath (the default set above) if(AbsoluteInPath == RootPath) { OutHeaderPath = (ClassPathLocation == EClassLocation::Public) ? PublicPath : AbsoluteInPath; } // The rules for placing source files are as follows: // 1) If InPath is the source root, and GetClassLocation has said the class header should be in the Public folder, put the source file in the Private folder // 2) If InPath is contained within the Public or Classes folder of this module, place it in the equivalent path in the Private folder // 3) Otherwise, just place the source file at InPath (the default set above) if(AbsoluteInPath == RootPath) { OutSourcePath = (ClassPathLocation == EClassLocation::Public) ? PrivatePath : AbsoluteInPath; } else if(ClassPathLocation == EClassLocation::Public) { OutSourcePath = AbsoluteInPath.Replace(*PublicPath, *PrivatePath); } else if(ClassPathLocation == EClassLocation::Classes) { OutSourcePath = AbsoluteInPath.Replace(*ClassesPath, *PrivatePath); } return !OutHeaderPath.IsEmpty() && !OutSourcePath.IsEmpty(); } bool GameProjectUtils::GetClassLocation(const FString& InPath, const FModuleContextInfo& ModuleInfo, EClassLocation& OutClassLocation, FText* const OutFailReason) { const FString AbsoluteInPath = FPaths::ConvertRelativePathToFull(InPath) / ""; // Ensure trailing / OutClassLocation = EClassLocation::UserDefined; if(!IsValidSourcePath(InPath, ModuleInfo, OutFailReason)) { return false; } const FString RootPath = ModuleInfo.ModuleSourcePath; const FString PublicPath = RootPath / "Public" / ""; // Ensure trailing / const FString PrivatePath = RootPath / "Private" / ""; // Ensure trailing / const FString ClassesPath = RootPath / "Classes" / ""; // Ensure trailing / // If either the Public or Private path exists, and we're in the root, force the header/source file to use one of these folders const bool bPublicPathExists = IFileManager::Get().DirectoryExists(*PublicPath); const bool bPrivatePathExists = IFileManager::Get().DirectoryExists(*PrivatePath); const bool bForceInternalPath = AbsoluteInPath == RootPath && (bPublicPathExists || bPrivatePathExists); if(AbsoluteInPath == RootPath) { OutClassLocation = (bPublicPathExists || bForceInternalPath) ? EClassLocation::Public : EClassLocation::UserDefined; } else if(AbsoluteInPath.StartsWith(PublicPath)) { OutClassLocation = EClassLocation::Public; } else if(AbsoluteInPath.StartsWith(PrivatePath)) { OutClassLocation = EClassLocation::Private; } else if(AbsoluteInPath.StartsWith(ClassesPath)) { OutClassLocation = EClassLocation::Classes; } else { OutClassLocation = EClassLocation::UserDefined; } return true; } GameProjectUtils::EProjectDuplicateResult GameProjectUtils::DuplicateProjectForUpgrade( const FString& InProjectFile, FString& OutNewProjectFile ) { IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); // Get the directory part of the project name FString OldDirectoryName = FPaths::GetPath(InProjectFile); FPaths::NormalizeDirectoryName(OldDirectoryName); FString NewDirectoryName = OldDirectoryName; // Strip off any previous version number from the project name for(int32 LastSpace; NewDirectoryName.FindLastChar(' ', LastSpace); ) { const TCHAR *End = *NewDirectoryName + LastSpace + 1; if(End[0] != '4' || End[1] != '.' || !FChar::IsDigit(End[2])) { break; } End += 3; while(FChar::IsDigit(*End)) { End++; } if(*End != 0) { break; } NewDirectoryName.LeftInline(LastSpace, EAllowShrinking::No); NewDirectoryName.TrimEndInline(); } // Append the new version number NewDirectoryName += FString::Printf(TEXT(" %s"), *FEngineVersion::Current().ToString(EVersionComponent::Minor)); // Find a directory name that doesn't exist FString BaseDirectoryName = NewDirectoryName; for(int32 Idx = 2; IFileManager::Get().DirectoryExists(*NewDirectoryName); Idx++) { NewDirectoryName = FString::Printf(TEXT("%s - %d"), *BaseDirectoryName, Idx); } // Recursively find all the files we need to copy, excluding those that are within the directories listed in SourceDirectoriesToSkip struct FGatherFilesToCopyHelper { public: FGatherFilesToCopyHelper(FString InRootSourceDirectory) : RootSourceDirectory(MoveTemp(InRootSourceDirectory)) { static const FString RelativeDirectoriesToSkip[] = { TEXT("Binaries"), TEXT("DerivedDataCache"), TEXT("Intermediate"), TEXT("Saved/Autosaves"), TEXT("Saved/Backup"), TEXT("Saved/Cooked"), TEXT("Saved/Config"), TEXT("Saved/HardwareSurvey"), TEXT("Saved/Logs"), TEXT("Saved/StagedBuilds"), }; SourceDirectoriesToSkip.Reserve(UE_ARRAY_COUNT(RelativeDirectoriesToSkip)); for (const FString& RelativeDirectoryToSkip : RelativeDirectoriesToSkip) { SourceDirectoriesToSkip.Emplace(RootSourceDirectory / RelativeDirectoryToSkip); } } void GatherFilesToCopy(TArray& OutSourceDirectories, TArray& OutSourceFiles) { GatherFilesToCopy(RootSourceDirectory, OutSourceDirectories, OutSourceFiles); } private: void GatherFilesToCopy(const FString& InSourceDirectoryPath, TArray& OutSourceDirectories, TArray& OutSourceFiles) { const FString SourceDirectorySearchWildcard = InSourceDirectoryPath / TEXT("*"); OutSourceDirectories.Emplace(InSourceDirectoryPath); TArray SourceFilenames; IFileManager::Get().FindFiles(SourceFilenames, *SourceDirectorySearchWildcard, true, false); OutSourceFiles.Reserve(OutSourceFiles.Num() + SourceFilenames.Num()); for (const FString& SourceFilename : SourceFilenames) { OutSourceFiles.Emplace(InSourceDirectoryPath / SourceFilename); } TArray SourceSubDirectoryNames; IFileManager::Get().FindFiles(SourceSubDirectoryNames, *SourceDirectorySearchWildcard, false, true); for (const FString& SourceSubDirectoryName : SourceSubDirectoryNames) { const FString SourceSubDirectoryPath = InSourceDirectoryPath / SourceSubDirectoryName; if (!SourceDirectoriesToSkip.Contains(SourceSubDirectoryPath)) { GatherFilesToCopy(SourceSubDirectoryPath, OutSourceDirectories, OutSourceFiles); } } } FString RootSourceDirectory; TArray SourceDirectoriesToSkip; }; TArray SourceDirectories; TArray SourceFiles; FGatherFilesToCopyHelper(OldDirectoryName).GatherFilesToCopy(SourceDirectories, SourceFiles); // Copy everything bool bCopySucceeded = true; bool bUserCanceled = false; GWarn->BeginSlowTask(LOCTEXT("CreatingCopyOfProject", "Creating copy of project..."), true, true); for(int32 Idx = 0; Idx < SourceDirectories.Num() && bCopySucceeded; Idx++) { FString TargetDirectory = NewDirectoryName + SourceDirectories[Idx].Mid(OldDirectoryName.Len()); bUserCanceled = GWarn->ReceivedUserCancel(); bCopySucceeded = !bUserCanceled && PlatformFile.CreateDirectory(*TargetDirectory); GWarn->UpdateProgress(Idx + 1, SourceDirectories.Num() + SourceFiles.Num()); } for(int32 Idx = 0; Idx < SourceFiles.Num() && bCopySucceeded; Idx++) { FString TargetFile = NewDirectoryName + SourceFiles[Idx].Mid(OldDirectoryName.Len()); bUserCanceled = GWarn->ReceivedUserCancel(); bCopySucceeded = !bUserCanceled && PlatformFile.CopyFile(*TargetFile, *SourceFiles[Idx]); GWarn->UpdateProgress(SourceDirectories.Num() + Idx + 1, SourceDirectories.Num() + SourceFiles.Num()); } GWarn->EndSlowTask(); // Wipe the directory if the user canceled or we couldn't update if(!bCopySucceeded) { PlatformFile.DeleteDirectoryRecursively(*NewDirectoryName); if(bUserCanceled) { return EProjectDuplicateResult::UserCanceled; } else { return EProjectDuplicateResult::Failed; } } // Otherwise fixup the output project filename OutNewProjectFile = NewDirectoryName / FPaths::GetCleanFilename(InProjectFile); return EProjectDuplicateResult::Succeeded; } void GameProjectUtils::UpdateSupportedTargetPlatforms(const FName& InPlatformName, const bool bIsSupported) { const FString& ProjectFilename = FPaths::GetProjectFilePath(); if (!ProjectFilename.IsEmpty()) { // First attempt to check out the file if SCC is enabled if (ISourceControlModule::Get().IsEnabled()) { FText UnusedFailReason; CheckoutGameProjectFile(ProjectFilename, UnusedFailReason); } // Second make sure the file is writable if (FPlatformFileManager::Get().GetPlatformFile().IsReadOnly(*ProjectFilename)) { FPlatformFileManager::Get().GetPlatformFile().SetReadOnly(*ProjectFilename, false); } IProjectManager::Get().UpdateSupportedTargetPlatformsForCurrentProject(InPlatformName, bIsSupported); } } void GameProjectUtils::ClearSupportedTargetPlatforms() { const FString& ProjectFilename = FPaths::GetProjectFilePath(); if (!ProjectFilename.IsEmpty()) { // First attempt to check out the file if SCC is enabled if (ISourceControlModule::Get().IsEnabled()) { FText UnusedFailReason; CheckoutGameProjectFile(ProjectFilename, UnusedFailReason); } // Second make sure the file is writable if (FPlatformFileManager::Get().GetPlatformFile().IsReadOnly(*ProjectFilename)) { FPlatformFileManager::Get().GetPlatformFile().SetReadOnly(*ProjectFilename, false); } IProjectManager::Get().ClearSupportedTargetPlatformsForCurrentProject(); } } bool GameProjectUtils::UpdateAdditionalPluginDirectory(const FString& InDir, const bool bAddOrRemove) { const FString& ProjectFilename = FPaths::GetProjectFilePath(); if (!ProjectFilename.IsEmpty()) { // First attempt to check out the file if SCC is enabled if (ISourceControlModule::Get().IsEnabled()) { FText UnusedFailReason; CheckoutGameProjectFile(ProjectFilename, UnusedFailReason); } // Second make sure the file is writable if (FPlatformFileManager::Get().GetPlatformFile().IsReadOnly(*ProjectFilename)) { FPlatformFileManager::Get().GetPlatformFile().SetReadOnly(*ProjectFilename, false); } return IProjectManager::Get().UpdateAdditionalPluginDirectory(InDir, bAddOrRemove); } return false; } const TCHAR* GameProjectUtils::GetDefaultBuildSettingsVersion() { return TEXT("BuildSettingsVersion.V5"); } bool GameProjectUtils::ReadTemplateFile(const FString& TemplateFileName, FString& OutFileContents, FText& OutFailReason) { const FString FullFileName = UClassTemplateEditorSubsystem::GetEngineTemplateDirectory() / TemplateFileName; if ( FFileHelper::LoadFileToString(OutFileContents, *FullFileName) ) { return true; } FFormatNamedArguments Args; Args.Add( TEXT("FullFileName"), FText::FromString( FullFileName ) ); OutFailReason = FText::Format( LOCTEXT("FailedToReadTemplateFile", "Failed to read template file \"{FullFileName}\""), Args ); return false; } bool GameProjectUtils::WriteOutputFile(const FString& OutputFilename, const FString& OutputFileContents, FText& OutFailReason) { if ( FFileHelper::SaveStringToFile(OutputFileContents, *OutputFilename ) ) { return true; } FFormatNamedArguments Args; Args.Add( TEXT("OutputFilename"), FText::FromString( OutputFilename ) ); OutFailReason = FText::Format( LOCTEXT("FailedToWriteOutputFile", "Failed to write output file \"{OutputFilename}\". Perhaps the file is Read-Only?"), Args ); return false; } FString GameProjectUtils::MakeCopyrightLine() { const FString CopyrightNotice = GetDefault()->CopyrightNotice; if (!CopyrightNotice.IsEmpty()) { return FString(TEXT("// ")) + CopyrightNotice; } else { return FString(); } } FString GameProjectUtils::MakeCommaDelimitedList(const TArray& InList, bool bPlaceQuotesAroundEveryElement) { FString ReturnString; for ( auto ListIt = InList.CreateConstIterator(); ListIt; ++ListIt ) { FString ElementStr; if ( bPlaceQuotesAroundEveryElement ) { ElementStr = FString::Printf( TEXT("\"%s\""), **ListIt); } else { ElementStr = *ListIt; } if ( ReturnString.Len() > 0 ) { // If this is not the first item in the list, prepend with a comma ElementStr = FString::Printf(TEXT(", %s"), *ElementStr); } ReturnString += ElementStr; } return ReturnString; } FString GameProjectUtils::MakeIncludeList(const TArray& InList) { FString ReturnString; for ( auto ListIt = InList.CreateConstIterator(); ListIt; ++ListIt ) { ReturnString += FString::Printf(IncludePathFormatString, **ListIt); ReturnString += LINE_TERMINATOR; } return ReturnString; } FString GameProjectUtils::DetermineModuleIncludePath(const FModuleContextInfo& ModuleInfo, const FString& FileRelativeTo) { FString ModuleIncludePath; if(FindSourceFileInProject(ModuleInfo.ModuleName + ".h", ModuleInfo.ModuleSourcePath, ModuleIncludePath)) { // Work out where the module header is; // if it's Public then we can include it without any path since all Public and Classes folders are on the include path // if it's located elsewhere, then we'll need to include it relative to the module source root as we can't guarantee // that other folders are on the include paths EClassLocation ModuleLocation; if(GetClassLocation(ModuleIncludePath, ModuleInfo, ModuleLocation)) { if(ModuleLocation == EClassLocation::Public || ModuleLocation == EClassLocation::Classes) { ModuleIncludePath = ModuleInfo.ModuleName + ".h"; } else { // If the path to our new class is the same as the path to the module, we can include it directly const FString ModulePath = FPaths::ConvertRelativePathToFull(FPaths::GetPath(ModuleIncludePath)); const FString ClassPath = FPaths::ConvertRelativePathToFull(FPaths::GetPath(FileRelativeTo)); if(ModulePath == ClassPath) { ModuleIncludePath = ModuleInfo.ModuleName + ".h"; } else { // Updates ModuleIncludePath internally if(!FPaths::MakePathRelativeTo(ModuleIncludePath, *ModuleInfo.ModuleSourcePath)) { // Failed; just assume we can include it without any relative path ModuleIncludePath = ModuleInfo.ModuleName + ".h"; } } } } else { // Failed; just assume we can include it without any relative path ModuleIncludePath = ModuleInfo.ModuleName + ".h"; } } else { // This could potentially fail when generating new projects if the module file hasn't yet been created; just assume we can include it without any relative path ModuleIncludePath = ModuleInfo.ModuleName + ".h"; } return ModuleIncludePath; } /** * Generates UObject class constructor definition with property overrides. * * @param Out String to assign generated constructor to. * @param PrefixedClassName Prefixed class name for which we generate the constructor. * @param PropertyOverridesStr String with property overrides in the constructor. * @param OutFailReason Template read function failure reason. * * @returns True on success. False otherwise. */ bool GenerateConstructorDefinition(FString& Out, const FString& PrefixedClassName, const FString& PropertyOverridesStr, FText& OutFailReason) { FString Template; if (!GameProjectUtils::ReadTemplateFile(TEXT("UObjectClassConstructorDefinition.template"), Template, OutFailReason)) { return false; } Out = Template.Replace(TEXT("%PREFIXED_CLASS_NAME%"), *PrefixedClassName, ESearchCase::CaseSensitive); Out = Out.Replace(TEXT("%PROPERTY_OVERRIDES%"), *PropertyOverridesStr, ESearchCase::CaseSensitive); return true; } /** * Generates UObject class constructor declaration. * * @param Out String to assign generated constructor to. * @param PrefixedClassName Prefixed class name for which we generate the constructor. * @param OutFailReason Template read function failure reason. * * @returns True on success. False otherwise. */ bool GenerateConstructorDeclaration(FString& Out, const FString& PrefixedClassName, FText& OutFailReason) { FString Template; if (!GameProjectUtils::ReadTemplateFile(TEXT("UObjectClassConstructorDeclaration.template"), Template, OutFailReason)) { return false; } Out = Template.Replace(TEXT("%PREFIXED_CLASS_NAME%"), *PrefixedClassName, ESearchCase::CaseSensitive); return true; } bool GameProjectUtils::GenerateClassHeaderFile(const FString& NewHeaderFileName, const FString UnPrefixedClassName, const FNewClassInfo ParentClassInfo, const TArray& ClassSpecifierList, const FString& ClassProperties, const FString& ClassFunctionDeclarations, FString& OutSyncLocation, const FModuleContextInfo& ModuleInfo, bool bDeclareConstructor, FText& OutFailReason) { FString Template; bool bTemplateFound = false; if (GEditor) { if (UClassTemplateEditorSubsystem* TemplateSubsystem = GEditor->GetEditorSubsystem()) { for (const UClass* BaseClass = ParentClassInfo.BaseClass; BaseClass != nullptr; BaseClass = BaseClass->GetSuperClass()) { if (const UClassTemplate* ClassTemplate = TemplateSubsystem->FindClassTemplate(BaseClass)) { bTemplateFound = ClassTemplate->ReadHeader(Template, OutFailReason); if (!bTemplateFound) { return false; } break; } } } } if (!bTemplateFound && !ReadTemplateFile(ParentClassInfo.GetHeaderTemplateFilename(), Template, OutFailReason)) { return false; } const FString ClassPrefix = ParentClassInfo.GetClassPrefixCPP(); const FString PrefixedClassName = ClassPrefix + UnPrefixedClassName; const FString PrefixedBaseClassName = ClassPrefix + ParentClassInfo.GetClassNameCPP(); FString BaseClassIncludeDirective; FString BaseClassIncludePath; if(ParentClassInfo.GetIncludePath(BaseClassIncludePath)) { BaseClassIncludeDirective = FString::Printf(IncludePathFormatString, *BaseClassIncludePath); } FString ModuleAPIMacro; { EClassLocation ClassPathLocation = EClassLocation::UserDefined; if ( GetClassLocation(NewHeaderFileName, ModuleInfo, ClassPathLocation) ) { // If this class isn't Private, make sure and include the API macro so it can be linked within other modules if ( ClassPathLocation != EClassLocation::Private ) { ModuleAPIMacro = ModuleInfo.ModuleName.ToUpper() + "_API "; // include a trailing space for the template formatting } } } FString EventualConstructorDeclaration; if (bDeclareConstructor) { if (!GenerateConstructorDeclaration(EventualConstructorDeclaration, PrefixedClassName, OutFailReason)) { return false; } } // Not all of these will exist in every class template FString FinalOutput = Template.Replace(TEXT("%COPYRIGHT_LINE%"), *MakeCopyrightLine(), ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%UNPREFIXED_CLASS_NAME%"), *UnPrefixedClassName, ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%CLASS_MODULE_API_MACRO%"), *ModuleAPIMacro, ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%UCLASS_SPECIFIER_LIST%"), *MakeCommaDelimitedList(ClassSpecifierList, false), ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%PREFIXED_CLASS_NAME%"), *PrefixedClassName, ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%PREFIXED_BASE_CLASS_NAME%"), *PrefixedBaseClassName, ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%MODULE_NAME%"), *ModuleInfo.ModuleName, ESearchCase::CaseSensitive); // Special case where where the wildcard starts with a tab and ends with a new line const bool bLeadingTab = true; const bool bTrailingNewLine = true; FinalOutput = ReplaceWildcard(FinalOutput, TEXT("%EVENTUAL_CONSTRUCTOR_DECLARATION%"), *EventualConstructorDeclaration, bLeadingTab, bTrailingNewLine); FinalOutput = ReplaceWildcard(FinalOutput, TEXT("%CLASS_PROPERTIES%"), *ClassProperties, bLeadingTab, bTrailingNewLine); FinalOutput = ReplaceWildcard(FinalOutput, TEXT("%CLASS_FUNCTION_DECLARATIONS%"), *ClassFunctionDeclarations, bLeadingTab, bTrailingNewLine); if (BaseClassIncludeDirective.Len() == 0) { FinalOutput = FinalOutput.Replace(TEXT("%BASE_CLASS_INCLUDE_DIRECTIVE%") LINE_TERMINATOR, TEXT(""), ESearchCase::CaseSensitive); } FinalOutput = FinalOutput.Replace(TEXT("%BASE_CLASS_INCLUDE_DIRECTIVE%"), *BaseClassIncludeDirective, ESearchCase::CaseSensitive); HarvestCursorSyncLocation( FinalOutput, OutSyncLocation ); return WriteOutputFile(NewHeaderFileName, FinalOutput, OutFailReason); } static bool TryParseIncludeDirective(const FString& Text, int StartPos, int EndPos, FString& IncludePath) { // Check if the line starts with a # character int Pos = StartPos; while (Pos < EndPos && FChar::IsWhitespace(Text[Pos])) { Pos++; } if (Pos == EndPos || Text[Pos++] != '#') { return false; } while (Pos < EndPos && FChar::IsWhitespace(Text[Pos])) { Pos++; } // Check it's an include directive const TCHAR* IncludeText = TEXT("include"); for (int Idx = 0; IncludeText[Idx] != 0; Idx++) { if (Pos == EndPos || Text[Pos] != IncludeText[Idx]) { return false; } Pos++; } while (Pos < EndPos && FChar::IsWhitespace(Text[Pos])) { Pos++; } // Parse out the quoted include path if (Pos == EndPos || Text[Pos++] != '"') { return false; } int IncludePathPos = Pos; while (Pos < EndPos && Text[Pos] != '"') { Pos++; } IncludePath = Text.Mid(IncludePathPos, Pos - IncludePathPos); return true; } static bool IsUsingOldStylePch(FString BaseDir) { // Find all the cpp files under the base directory TArray Files; IFileManager::Get().FindFilesRecursive(Files, *BaseDir, TEXT("*.cpp"), true, false, false); // Parse the first include directive for up to 16 include paths TArray FirstIncludedFiles; for (int Idx = 0; Idx < Files.Num() && Idx < 16; Idx++) { FString Text; FFileHelper::LoadFileToString(Text, *Files[Idx]); int LinePos = 0; while(LinePos < Text.Len()) { int EndOfLinePos = LinePos; while (EndOfLinePos < Text.Len() && Text[EndOfLinePos] != '\n') { EndOfLinePos++; } FString IncludePath; if (TryParseIncludeDirective(Text, LinePos, EndOfLinePos, IncludePath)) { FirstIncludedFiles.AddUnique(FPaths::GetCleanFilename(IncludePath)); break; } LinePos = EndOfLinePos + 1; } } return FirstIncludedFiles.Num() == 1 && Files.Num() > 1; } bool GameProjectUtils::GenerateClassCPPFile(const FString& NewCPPFileName, const FString UnPrefixedClassName, const FNewClassInfo ParentClassInfo, const TArray& AdditionalIncludes, const TArray& PropertyOverrides, const FString& AdditionalMemberDefinitions, FString& OutSyncLocation, const FModuleContextInfo& ModuleInfo, FText& OutFailReason) { FString Template; bool bTemplateFound = false; if (GEditor) { if (UClassTemplateEditorSubsystem* TemplateSubsystem = GEditor->GetEditorSubsystem()) { for (const UClass* BaseClass = ParentClassInfo.BaseClass; BaseClass != nullptr; BaseClass = BaseClass->GetSuperClass()) { if (const UClassTemplate* ClassTemplate = TemplateSubsystem->FindClassTemplate(BaseClass)) { bTemplateFound = ClassTemplate->ReadSource(Template, OutFailReason); if (!bTemplateFound) { return false; } break; } } } } if (!bTemplateFound && !ReadTemplateFile(ParentClassInfo.GetSourceTemplateFilename(), Template, OutFailReason)) { return false; } const FString ClassPrefix = ParentClassInfo.GetClassPrefixCPP(); const FString PrefixedClassName = ClassPrefix + UnPrefixedClassName; const FString PrefixedBaseClassName = ClassPrefix + ParentClassInfo.GetClassNameCPP(); EClassLocation ClassPathLocation = EClassLocation::UserDefined; if ( !GetClassLocation(NewCPPFileName, ModuleInfo, ClassPathLocation, &OutFailReason) ) { return false; } FString PropertyOverridesStr; for ( int32 OverrideIdx = 0; OverrideIdx < PropertyOverrides.Num(); ++OverrideIdx ) { if ( OverrideIdx > 0 ) { PropertyOverridesStr += LINE_TERMINATOR; } PropertyOverridesStr += TEXT("\t"); PropertyOverridesStr += *PropertyOverrides[OverrideIdx]; } // Calculate the correct include path for the module header FString PchIncludeDirective; if (IsUsingOldStylePch(ModuleInfo.ModuleSourcePath)) { const FString ModuleIncludePath = DetermineModuleIncludePath(ModuleInfo, NewCPPFileName); if (ModuleIncludePath.Len() > 0) { PchIncludeDirective = FString::Printf(IncludePathFormatString, *ModuleIncludePath); } } FString EventualConstructorDefinition; if (PropertyOverrides.Num() != 0) { if (!GenerateConstructorDefinition(EventualConstructorDefinition, PrefixedClassName, PropertyOverridesStr, OutFailReason)) { return false; } } // Not all of these will exist in every class template FString FinalOutput = Template.Replace(TEXT("%COPYRIGHT_LINE%"), *MakeCopyrightLine(), ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%UNPREFIXED_CLASS_NAME%"), *UnPrefixedClassName, ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%PREFIXED_CLASS_NAME%"), *PrefixedClassName, ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%MODULE_NAME%"), *ModuleInfo.ModuleName, ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%PCH_INCLUDE_DIRECTIVE%"), *PchIncludeDirective, ESearchCase::CaseSensitive); // Fixup the header file include for this cpp file { const FString RelativeHeaderIncludePath = GetIncludePathForFile(NewCPPFileName, ModuleInfo.ModuleSourcePath); // First make sure we remove any potentially incorrect, legacy paths generated from some version of #include "%UNPREFIXED_CLASS_NAME%.h" // This didn't take into account potential subfolders for the created class const FString LegacyHeaderInclude = FString::Printf(IncludePathFormatString, *FPaths::GetCleanFilename(RelativeHeaderIncludePath)); FinalOutput = FinalOutput.Replace(*LegacyHeaderInclude, TEXT(""), ESearchCase::CaseSensitive); // Now add the correct include directive which may include a subfolder. const FString HeaderIncludeDirective = FString::Printf(IncludePathFormatString, *RelativeHeaderIncludePath); FinalOutput = FinalOutput.Replace(TEXT("%MY_HEADER_INCLUDE_DIRECTIVE%"), *HeaderIncludeDirective, ESearchCase::CaseSensitive); } // Special case where where the wildcard ends with a new line const bool bLeadingTab = false; const bool bTrailingNewLine = true; FinalOutput = ReplaceWildcard(FinalOutput, TEXT("%ADDITIONAL_INCLUDE_DIRECTIVES%"), *MakeIncludeList(AdditionalIncludes), bLeadingTab, bTrailingNewLine); FinalOutput = ReplaceWildcard(FinalOutput, TEXT("%EVENTUAL_CONSTRUCTOR_DEFINITION%"), *EventualConstructorDefinition, bLeadingTab, bTrailingNewLine); FinalOutput = ReplaceWildcard(FinalOutput, TEXT("%ADDITIONAL_MEMBER_DEFINITIONS%"), *AdditionalMemberDefinitions, bLeadingTab, bTrailingNewLine); HarvestCursorSyncLocation( FinalOutput, OutSyncLocation ); return WriteOutputFile(NewCPPFileName, FinalOutput, OutFailReason); } bool GameProjectUtils::GenerateGameModuleBuildFile(const FString& NewBuildFileName, const FString& ModuleName, const TArray& PublicDependencyModuleNames, const TArray& PrivateDependencyModuleNames, FText& OutFailReason) { FString Template; if (!ReadTemplateFile(TEXT("GameModule.Build.cs.template"), Template, OutFailReason)) { return false; } FString FinalOutput = Template.Replace(TEXT("%COPYRIGHT_LINE%"), *MakeCopyrightLine(), ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%PUBLIC_DEPENDENCY_MODULE_NAMES%"), *MakeCommaDelimitedList(PublicDependencyModuleNames), ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%PRIVATE_DEPENDENCY_MODULE_NAMES%"), *MakeCommaDelimitedList(PrivateDependencyModuleNames), ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%MODULE_NAME%"), *ModuleName, ESearchCase::CaseSensitive); ResetCurrentProjectModulesCache(); return WriteOutputFile(NewBuildFileName, FinalOutput, OutFailReason); } bool GameProjectUtils::GeneratePluginModuleBuildFile(const FString& NewBuildFileName, const FString& ModuleName, const TArray& PublicDependencyModuleNames, const TArray& PrivateDependencyModuleNames, FText& OutFailReason, bool bUseExplicitOrSharedPCHs/* = true*/) { FString Template; if ( !ReadTemplateFile(TEXT("PluginModule.Build.cs.template"), Template, OutFailReason) ) { return false; } FString FinalOutput = Template.Replace(TEXT("%COPYRIGHT_LINE%"), *MakeCopyrightLine(), ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%PUBLIC_DEPENDENCY_MODULE_NAMES%"), *MakeCommaDelimitedList(PublicDependencyModuleNames), ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%PRIVATE_DEPENDENCY_MODULE_NAMES%"), *MakeCommaDelimitedList(PrivateDependencyModuleNames), ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%MODULE_NAME%"), *ModuleName, ESearchCase::CaseSensitive); const FString PCHUsage = bUseExplicitOrSharedPCHs ? TEXT("UseExplicitOrSharedPCHs") : TEXT("UseSharedPCHs"); FinalOutput = FinalOutput.Replace(TEXT("%PCH_USAGE%"), *PCHUsage, ESearchCase::CaseSensitive); return WriteOutputFile(NewBuildFileName, FinalOutput, OutFailReason); } bool GameProjectUtils::GenerateGameModuleTargetFile(const FString& NewBuildFileName, const FString& ModuleName, const TArray& ExtraModuleNames, FText& OutFailReason) { FString Template; if ( !ReadTemplateFile(TEXT("Stub.Target.cs.template"), Template, OutFailReason) ) { return false; } FString FinalOutput = Template.Replace(TEXT("%COPYRIGHT_LINE%"), *MakeCopyrightLine(), ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%EXTRA_MODULE_NAMES%"), *MakeCommaDelimitedList(ExtraModuleNames), ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%MODULE_NAME%"), *ModuleName, ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%TARGET_TYPE%"), TEXT("Game"), ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%DEFAULT_BUILD_SETTINGS_VERSION%"), GetDefaultBuildSettingsVersion(), ESearchCase::CaseSensitive); return WriteOutputFile(NewBuildFileName, FinalOutput, OutFailReason); } bool GameProjectUtils::GenerateEditorModuleBuildFile(const FString& NewBuildFileName, const FString& ModuleName, const TArray& PublicDependencyModuleNames, const TArray& PrivateDependencyModuleNames, FText& OutFailReason) { FString Template; if ( !ReadTemplateFile(TEXT("EditorModule.Build.cs.template"), Template, OutFailReason) ) { return false; } FString FinalOutput = Template.Replace(TEXT("%COPYRIGHT_LINE%"), *MakeCopyrightLine(), ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%PUBLIC_DEPENDENCY_MODULE_NAMES%"), *MakeCommaDelimitedList(PublicDependencyModuleNames), ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%PRIVATE_DEPENDENCY_MODULE_NAMES%"), *MakeCommaDelimitedList(PrivateDependencyModuleNames), ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%MODULE_NAME%"), *ModuleName, ESearchCase::CaseSensitive); ResetCurrentProjectModulesCache(); return WriteOutputFile(NewBuildFileName, FinalOutput, OutFailReason); } bool GameProjectUtils::GenerateEditorModuleTargetFile(const FString& NewBuildFileName, const FString& ModuleName, const TArray& ExtraModuleNames, FText& OutFailReason) { FString Template; if ( !ReadTemplateFile(TEXT("Stub.Target.cs.template"), Template, OutFailReason) ) { return false; } FString FinalOutput = Template.Replace(TEXT("%COPYRIGHT_LINE%"), *MakeCopyrightLine(), ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%EXTRA_MODULE_NAMES%"), *MakeCommaDelimitedList(ExtraModuleNames), ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%MODULE_NAME%"), *ModuleName, ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%TARGET_TYPE%"), TEXT("Editor"), ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%DEFAULT_BUILD_SETTINGS_VERSION%"), GetDefaultBuildSettingsVersion(), ESearchCase::CaseSensitive); return WriteOutputFile(NewBuildFileName, FinalOutput, OutFailReason); } bool GameProjectUtils::GenerateGameModuleCPPFile(const FString& NewBuildFileName, const FString& ModuleName, const FString& GameName, FText& OutFailReason) { FString Template; if ( !ReadTemplateFile(TEXT("GameModule.cpp.template"), Template, OutFailReason) ) { return false; } FString FinalOutput = Template.Replace(TEXT("%COPYRIGHT_LINE%"), *MakeCopyrightLine(), ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%MODULE_NAME%"), *ModuleName, ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%GAME_NAME%"), *GameName, ESearchCase::CaseSensitive); return WriteOutputFile(NewBuildFileName, FinalOutput, OutFailReason); } bool GameProjectUtils::GenerateGameModuleHeaderFile(const FString& NewBuildFileName, const TArray& PublicHeaderIncludes, FText& OutFailReason) { FString Template; if ( !ReadTemplateFile(TEXT("GameModule.h.template"), Template, OutFailReason) ) { return false; } FString FinalOutput = Template.Replace(TEXT("%COPYRIGHT_LINE%"), *MakeCopyrightLine(), ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%PUBLIC_HEADER_INCLUDES%"), *MakeIncludeList(PublicHeaderIncludes), ESearchCase::CaseSensitive); return WriteOutputFile(NewBuildFileName, FinalOutput, OutFailReason); } bool GameProjectUtils::GeneratePluginModuleCPPFile(const FString& CPPFileName, const FString& ModuleName, const FString& StartupSourceCode, FText& OutFailReason) { FString Template; if (!ReadTemplateFile(TEXT("PluginModule.cpp.template"), Template, OutFailReason)) { return false; } FString FinalOutput = Template.Replace(TEXT("%COPYRIGHT_LINE%"), *MakeCopyrightLine(), ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%MODULE_NAME%"), *ModuleName, ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%MODULE_STARTUP_CODE%"), *StartupSourceCode, ESearchCase::CaseSensitive); return WriteOutputFile(CPPFileName, FinalOutput, OutFailReason); } bool GameProjectUtils::GeneratePluginModuleHeaderFile(const FString& HeaderFileName, const TArray& PublicHeaderIncludes, FText& OutFailReason) { FString Template; if (!ReadTemplateFile(TEXT("PluginModule.h.template"), Template, OutFailReason)) { return false; } FString FinalOutput = Template.Replace(TEXT("%COPYRIGHT_LINE%"), *MakeCopyrightLine(), ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%PUBLIC_HEADER_INCLUDES%"), *MakeIncludeList(PublicHeaderIncludes), ESearchCase::CaseSensitive); return WriteOutputFile(HeaderFileName, FinalOutput, OutFailReason); } FString GameProjectUtils::ReplaceWildcard(const FString& Input, const FString& From, const FString& To, bool bLeadingTab, bool bTrailingNewLine) { FString Result = Input; FString WildCard = bLeadingTab ? TEXT("\t") : TEXT(""); WildCard.Append(From); if (bTrailingNewLine) { WildCard.Append(LINE_TERMINATOR); } int32 NumReplacements = Result.ReplaceInline(*WildCard, *To, ESearchCase::CaseSensitive); // if replacement fails, try again using just the plain wildcard without tab and/or new line if (NumReplacements == 0) { Result = Result.Replace(*From, *To, ESearchCase::CaseSensitive); } return Result; } void GameProjectUtils::OnUpdateProjectConfirm() { UpdateProject(); } void GameProjectUtils::UpdateProject(const FProjectDescriptorModifier& Modifier) { UpdateProject_Impl(&Modifier); } void GameProjectUtils::UpdateProject() { UpdateProject_Impl(nullptr); } void GameProjectUtils::UpdateProject_Impl(const FProjectDescriptorModifier* Modifier) { const FString& ProjectFilename = FPaths::GetProjectFilePath(); const FString& ShortFilename = FPaths::GetCleanFilename(ProjectFilename); FText FailReason; FText UpdateMessage; SNotificationItem::ECompletionState NewCompletionState; if (UpdateGameProjectFile_Impl(ProjectFilename, FDesktopPlatformModule::Get()->GetCurrentEngineIdentifier(), Modifier, FailReason)) { // Refresh the current in-memory project information from the new file on-disk. IProjectManager::Get().LoadProjectFile(ProjectFilename); // The project was updated successfully. FFormatNamedArguments Args; Args.Add( TEXT("ShortFilename"), FText::FromString( ShortFilename ) ); UpdateMessage = FText::Format( LOCTEXT("ProjectFileUpdateComplete", "{ShortFilename} was successfully updated."), Args ); NewCompletionState = SNotificationItem::CS_Success; } else { // The user chose to update, but the update failed. Notify the user. FFormatNamedArguments Args; Args.Add( TEXT("ShortFilename"), FText::FromString( ShortFilename ) ); Args.Add( TEXT("FailReason"), FailReason ); UpdateMessage = FText::Format( LOCTEXT("ProjectFileUpdateFailed", "{ShortFilename} failed to update. {FailReason}"), Args ); NewCompletionState = SNotificationItem::CS_Fail; } if ( UpdateGameProjectNotification.IsValid() ) { UpdateGameProjectNotification.Pin()->SetCompletionState(NewCompletionState); UpdateGameProjectNotification.Pin()->SetText(UpdateMessage); UpdateGameProjectNotification.Pin()->ExpireAndFadeout(); UpdateGameProjectNotification.Reset(); } } void GameProjectUtils::UpdateProject(const TArray* StartupModuleNames) { UpdateProject( FProjectDescriptorModifier::CreateLambda( [StartupModuleNames](FProjectDescriptor& Desc) { if (StartupModuleNames != nullptr) { return UpdateStartupModuleNames(Desc, StartupModuleNames); } return false; })); } void GameProjectUtils::OnUpdateProjectCancel() { if ( UpdateGameProjectNotification.IsValid() ) { UpdateGameProjectNotification.Pin()->SetCompletionState(SNotificationItem::CS_None); UpdateGameProjectNotification.Pin()->ExpireAndFadeout(); UpdateGameProjectNotification.Reset(); } } void GameProjectUtils::TryMakeProjectFileWriteable(const FString& ProjectFile) { if (IProjectManager::Get().IsSuppressingProjectFileWrite()) { return; } // First attempt to check out the file if SCC is enabled if ( ISourceControlModule::Get().IsEnabled() ) { FText FailReason; GameProjectUtils::CheckoutGameProjectFile(ProjectFile, FailReason); } // Check if it's writable if(FPlatformFileManager::Get().GetPlatformFile().IsReadOnly(*ProjectFile)) { FText ShouldMakeProjectWriteable = LOCTEXT("ShouldMakeProjectWriteable_Message", "'{ProjectFilename}' is read-only and cannot be updated. Would you like to make it writeable?"); FFormatNamedArguments Arguments; Arguments.Add( TEXT("ProjectFilename"), FText::FromString(ProjectFile)); if(FMessageDialog::Open(EAppMsgType::YesNo, FText::Format(ShouldMakeProjectWriteable, Arguments)) == EAppReturnType::Yes) { FPlatformFileManager::Get().GetPlatformFile().SetReadOnly(*ProjectFile, false); } } } bool GameProjectUtils::UpdateGameProjectFile(const FString& ProjectFile, const FString& EngineIdentifier, const FProjectDescriptorModifier& Modifier, FText& OutFailReason) { return UpdateGameProjectFile_Impl(ProjectFile, EngineIdentifier, &Modifier, OutFailReason); } bool GameProjectUtils::UpdateGameProjectFile(const FString& ProjectFile, const FString& EngineIdentifier, FText& OutFailReason) { return UpdateGameProjectFile_Impl(ProjectFile, EngineIdentifier, nullptr, OutFailReason); } bool GameProjectUtils::UpdateGameProjectFile_Impl(const FString& ProjectFile, const FString& EngineIdentifier, const FProjectDescriptorModifier* Modifier, FText& OutFailReason) { // Make sure we can write to the project file TryMakeProjectFileWriteable(ProjectFile); // Load the descriptor FProjectDescriptor Descriptor; if(Descriptor.Load(ProjectFile, OutFailReason)) { if (Modifier && Modifier->IsBound() && !Modifier->Execute(Descriptor)) { // If modifier returns false it means that we want to drop changes. return true; } // Update file on disk return Descriptor.Save(ProjectFile, OutFailReason) && FDesktopPlatformModule::Get()->SetEngineIdentifierForProject(ProjectFile, EngineIdentifier); } return false; } bool GameProjectUtils::UpdateGameProjectFile(const FString& ProjectFilename, const FString& EngineIdentifier, const TArray* StartupModuleNames, FText& OutFailReason) { return UpdateGameProjectFile(ProjectFilename, EngineIdentifier, FProjectDescriptorModifier::CreateLambda( [StartupModuleNames](FProjectDescriptor& Desc) { if (StartupModuleNames != nullptr) { return UpdateStartupModuleNames(Desc, StartupModuleNames); } return false; } ), OutFailReason); } bool GameProjectUtils::CheckoutGameProjectFile(const FString& ProjectFilename, FText& OutFailReason) { if ( !ensure(ProjectFilename.Len()) ) { OutFailReason = LOCTEXT("NoProjectFilename", "The project filename was not specified."); return false; } if ( !ISourceControlModule::Get().IsEnabled() ) { OutFailReason = LOCTEXT("SCCDisabled", "Revision control is not enabled. Enable revision control in the preferences menu."); return false; } FString AbsoluteFilename = FPaths::ConvertRelativePathToFull(ProjectFilename); ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(AbsoluteFilename, EStateCacheUsage::ForceUpdate); TArray FilesToBeCheckedOut; FilesToBeCheckedOut.Add(AbsoluteFilename); bool bSuccessfullyCheckedOut = false; OutFailReason = LOCTEXT("SCCStateInvalid", "Could not determine revision control state."); if(SourceControlState.IsValid()) { if(SourceControlState->IsCheckedOut() || SourceControlState->IsAdded() || !SourceControlState->IsSourceControlled()) { // Already checked out or opened for add... or not in the depot at all bSuccessfullyCheckedOut = true; } else if(SourceControlState->CanCheckout() || SourceControlState->IsCheckedOutOther()) { bSuccessfullyCheckedOut = (SourceControlProvider.Execute(ISourceControlOperation::Create(), FilesToBeCheckedOut) == ECommandResult::Succeeded); if (!bSuccessfullyCheckedOut) { OutFailReason = LOCTEXT("SCCCheckoutFailed", "Failed to check out the project file."); } } else if(!SourceControlState->IsCurrent()) { OutFailReason = LOCTEXT("SCCNotCurrent", "The project file is not at head revision."); } } return bSuccessfullyCheckedOut; } FString GameProjectUtils::GetDefaultProjectTemplateFilename() { return TEXT(""); } void FindCodeFiles(const TCHAR* BaseDirectory, TFunctionRef SourceFoundThreadSafeCallback) { // Can't use IterateDirectoryRecursive here as we rely on some directory filtering without aborting whole enumeration struct FDirectoryVisitor : public IPlatformFile::FDirectoryVisitor { TFunctionRef SourceFoundThreadSafeCallback; FRWLock DirectoriesLock; TArray& Directories; FDirectoryVisitor(TFunctionRef InSourceFoundThreadSafeCallback, TArray& InDirectories) : IPlatformFile::FDirectoryVisitor(EDirectoryVisitorFlags::ThreadSafe) , SourceFoundThreadSafeCallback(InSourceFoundThreadSafeCallback) , Directories(InDirectories) { } virtual bool Visit(const TCHAR* FilenameOrDirectory, bool bIsDirectory) override { if (bIsDirectory) { FString CleanDirectoryName(FPaths::GetCleanFilename(FilenameOrDirectory)); // Skip directories like (i.e. .git) when finding code files to improve performance. if (!CleanDirectoryName.StartsWith(TEXT("."))) { FString Directory(FilenameOrDirectory); FRWScopeLock ScopeLock(DirectoriesLock, SLT_Write); Directories.Emplace(MoveTemp(Directory)); } } else { FStringView FileName(FilenameOrDirectory); if (FileName.EndsWith(TEXT(".h")) || FileName.EndsWith(TEXT(".cpp"))) { return SourceFoundThreadSafeCallback(FilenameOrDirectory); } } return true; } }; TArray DirectoriesToVisitNext; DirectoriesToVisitNext.Add(BaseDirectory); TAtomic bResult(true); FDirectoryVisitor Visitor(SourceFoundThreadSafeCallback, DirectoriesToVisitNext); IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); while (bResult && DirectoriesToVisitNext.Num() > 0) { TArray DirectoriesToVisit = MoveTemp(DirectoriesToVisitNext); ParallelFor( DirectoriesToVisit.Num(), [&PlatformFile , &DirectoriesToVisit, &Visitor, &bResult](int32 Index) { if (bResult && !PlatformFile.IterateDirectory(*DirectoriesToVisit[Index], Visitor)) { bResult = false; } }, EParallelForFlags::Unbalanced ); } } void FindCodeFiles(const TCHAR* BaseDirectory, TArray& FileNames, int32 MaxNumFileNames) { FRWLock FileNamesLock; auto SourceFoundThreadSafeCallback = [&FileNamesLock, &FileNames, MaxNumFileNames](const TCHAR* FileName) -> bool { // Prepare the string out of the lock FString SourceFile(FileName); FRWScopeLock ScopeLock(FileNamesLock, SLT_Write); // Make sure we don't go over the requested MaxNumFileNames even // when multiple threads are reaching this at the same time if (FileNames.Num() < MaxNumFileNames) { FileNames.Emplace(MoveTemp(SourceFile)); } return FileNames.Num() < MaxNumFileNames; }; FindCodeFiles(BaseDirectory, SourceFoundThreadSafeCallback); } void GameProjectUtils::GetProjectCodeFilenames(TArray& OutProjectCodeFilenames) { FindCodeFiles(*FPaths::GameSourceDir(), OutProjectCodeFilenames, INT_MAX); } int32 GameProjectUtils::GetProjectCodeFileCount() { TArray Filenames; GetProjectCodeFilenames(Filenames); return Filenames.Num(); } void GameProjectUtils::GetProjectSourceDirectoryInfo(int32& OutNumCodeFiles, int64& OutDirectorySize) { TRACE_CPUPROFILER_EVENT_SCOPE(GameProjectUtils::GetProjectSourceDirectoryInfo) TAtomic NumCodeFiles(0); TAtomic DirectorySize(0); auto SourceFoundThreadSafeCallback = [&DirectorySize, &NumCodeFiles](const TCHAR* FileName) -> bool { NumCodeFiles++; DirectorySize += IFileManager::Get().FileSize(FileName); return true; }; FindCodeFiles(*FPaths::GameSourceDir(), SourceFoundThreadSafeCallback); OutDirectorySize = DirectorySize; OutNumCodeFiles = NumCodeFiles; } bool GameProjectUtils::ProjectHasCodeFiles() { TArray FileNames; FindCodeFiles(*FPaths::GameSourceDir(), FileNames, 1); return FileNames.Num() > 0; } TArray GameProjectUtils::GetRequiredAdditionalDependencies(const FNewClassInfo& ClassInfo) { TArray Out; switch (ClassInfo.ClassType) { case FNewClassInfo::EClassType::SlateWidget: case FNewClassInfo::EClassType::SlateWidgetStyle: Out.Reserve(2); Out.Add(TEXT("Slate")); Out.Add(TEXT("SlateCore")); break; case FNewClassInfo::EClassType::UObject: auto ClassPackageName = ClassInfo.BaseClass->GetOutermost()->GetFName().ToString(); checkf(ClassPackageName.StartsWith(TEXT("/Script/")), TEXT("Class outermost should start with /Script/")); Out.Add(ClassPackageName.Mid(8)); // Skip the /Script/ prefix. break; } return Out; } GameProjectUtils::EAddCodeToProjectResult GameProjectUtils::AddCodeToProject_Internal(const FString& NewClassName, const FString& NewClassPath, const FModuleContextInfo& ModuleInfo, const FNewClassInfo ParentClassInfo, const TSet& DisallowedHeaderNames, FString& OutHeaderFilePath, FString& OutCppFilePath, FText& OutFailReason, EReloadStatus& OutReloadStatus) { if ( !ParentClassInfo.IsSet() ) { OutFailReason = LOCTEXT("MissingParentClass", "You must specify a parent class"); return EAddCodeToProjectResult::InvalidInput; } const FString CleanClassName = ParentClassInfo.GetCleanClassName(NewClassName); const FString FinalClassName = ParentClassInfo.GetFinalClassName(NewClassName); if (!IsValidClassNameForCreation(FinalClassName, ModuleInfo, DisallowedHeaderNames, OutFailReason)) { return EAddCodeToProjectResult::InvalidInput; } if ( !FApp::HasProjectName() ) { OutFailReason = LOCTEXT("AddCodeToProject_NoGameName", "You can not add code because you have not loaded a project."); return EAddCodeToProjectResult::FailedToAddCode; } FString NewHeaderPath; FString NewCppPath; if ( !CalculateSourcePaths(NewClassPath, ModuleInfo, NewHeaderPath, NewCppPath, &OutFailReason) ) { return EAddCodeToProjectResult::FailedToAddCode; } FScopedSlowTask SlowTask( 7, LOCTEXT( "AddingCodeToProject", "Adding code to project..." ) ); SlowTask.MakeDialog(); SlowTask.EnterProgressFrame(); auto RequiredDependencies = GetRequiredAdditionalDependencies(ParentClassInfo); RequiredDependencies.Remove(ModuleInfo.ModuleName); // Update project file if needed. auto bUpdateProjectModules = false; // If the project does not already contain code, add the primary game module TArray CreatedFiles; TArray StartupModuleNames; const bool bProjectHadCodeFiles = ProjectHasCodeFiles(); if (!bProjectHadCodeFiles) { // Delete any generated intermediate code files. This ensures that blueprint projects with custom build settings can be converted to code projects without causing errors. IFileManager::Get().DeleteDirectory(*(FPaths::ProjectIntermediateDir() / TEXT("Source")), false, true); // We always add the basic source code to the root directory, not the potential sub-directory provided by NewClassPath const FString SourceDir = FPaths::GameSourceDir().LeftChop(1); // Trim the trailing / // Assuming the game name is the same as the primary game module name const FString GameModuleName = FApp::GetProjectName(); if ( GenerateBasicSourceCode(SourceDir, GameModuleName, FPaths::ProjectDir(), StartupModuleNames, CreatedFiles, OutFailReason) ) { bUpdateProjectModules = true; } else { DeleteCreatedFiles(SourceDir, CreatedFiles); return EAddCodeToProjectResult::FailedToAddCode; } } if (RequiredDependencies.Num() > 0 || bUpdateProjectModules) { UpdateProject( FProjectDescriptorModifier::CreateLambda( [&StartupModuleNames, &RequiredDependencies, &ModuleInfo, bUpdateProjectModules](FProjectDescriptor& Descriptor) { bool bNeedsUpdate = false; bNeedsUpdate |= UpdateStartupModuleNames(Descriptor, bUpdateProjectModules ? &StartupModuleNames : nullptr); bNeedsUpdate |= UpdateRequiredAdditionalDependencies(Descriptor, RequiredDependencies, ModuleInfo.ModuleName); return bNeedsUpdate; })); } SlowTask.EnterProgressFrame(); // Class Header File const FString NewHeaderFilename = NewHeaderPath / ParentClassInfo.GetHeaderFilename(NewClassName); { FString UnusedSyncLocation; TArray ClassSpecifiers; // Set UCLASS() specifiers based on parent class type. Currently, only UInterface uses this. if (ParentClassInfo.ClassType == FNewClassInfo::EClassType::UInterface) { ClassSpecifiers.Add(TEXT("MinimalAPI")); } if ( GenerateClassHeaderFile(NewHeaderFilename, CleanClassName, ParentClassInfo, ClassSpecifiers, TEXT(""), TEXT(""), UnusedSyncLocation, ModuleInfo, false, OutFailReason) ) { CreatedFiles.Add(NewHeaderFilename); } else { DeleteCreatedFiles(NewHeaderPath, CreatedFiles); return EAddCodeToProjectResult::FailedToAddCode; } } SlowTask.EnterProgressFrame(); // Class CPP file const FString NewCppFilename = NewCppPath / ParentClassInfo.GetSourceFilename(NewClassName); { FString UnusedSyncLocation; if ( GenerateClassCPPFile(NewCppFilename, CleanClassName, ParentClassInfo, TArray(), TArray(), TEXT(""), UnusedSyncLocation, ModuleInfo, OutFailReason) ) { CreatedFiles.Add(NewCppFilename); } else { DeleteCreatedFiles(NewCppPath, CreatedFiles); return EAddCodeToProjectResult::FailedToAddCode; } } SlowTask.EnterProgressFrame(); TArray CreatedFilesForExternalAppRead; CreatedFilesForExternalAppRead.Reserve(CreatedFiles.Num()); for (const FString& CreatedFile : CreatedFiles) { CreatedFilesForExternalAppRead.Add( IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*CreatedFile) ); } bool bGenerateProjectFiles = true; // First see if we can avoid a full generation by adding the new files to an already open project if ( bProjectHadCodeFiles && FSourceCodeNavigation::AddSourceFiles(CreatedFilesForExternalAppRead) ) { // We managed the gather, so we can skip running the full generate bGenerateProjectFiles = false; } if ( bGenerateProjectFiles ) { // Generate project files if we happen to be using a project file. if ( !FDesktopPlatformModule::Get()->GenerateProjectFiles(FPaths::RootDir(), FPaths::GetProjectFilePath(), GWarn) ) { OutFailReason = LOCTEXT("FailedToGenerateProjectFiles", "Failed to generate project files."); return EAddCodeToProjectResult::FailedToHotReload; } } SlowTask.EnterProgressFrame(); // Mark the files for add in SCC ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); if ( ISourceControlModule::Get().IsEnabled() && SourceControlProvider.IsAvailable() ) { SourceControlProvider.Execute(ISourceControlOperation::Create(), CreatedFilesForExternalAppRead); } SlowTask.EnterProgressFrame( 1.0f, LOCTEXT("CompilingCPlusPlusCode", "Compiling new C++ code. Please wait...")); OutHeaderFilePath = NewHeaderFilename; OutCppFilePath = NewCppFilename; OutReloadStatus = EReloadStatus::NotReloaded; #if WITH_LIVE_CODING ILiveCodingModule* LiveCoding = FModuleManager::GetModulePtr(LIVE_CODING_MODULE_NAME); if (LiveCoding != nullptr && LiveCoding->IsEnabledForSession()) { if (!bProjectHadCodeFiles) { FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("LiveCodingNoSources", "Project now includes sources, please close the editor and build from your IDE.")); return EAddCodeToProjectResult::Succeeded; } if (LiveCoding->AutomaticallyCompileNewClasses()) { LiveCoding->Compile(ELiveCodingCompileFlags::None, nullptr); OutReloadStatus = EReloadStatus::Reloaded; } return EAddCodeToProjectResult::Succeeded; } #endif if (!bProjectHadCodeFiles) { // This is the first time we add code to this project so compile its game DLL const FString GameModuleName = FApp::GetProjectName(); check(ModuleInfo.ModuleName == GameModuleName); // Because this project previously didn't have any code, the UBT target name will just be UnrealEditor. Now that we've // added some code, the target name will be changed to match the editor target for the new source. FString NewUBTTargetName = GameModuleName + TEXT("Editor"); FPlatformMisc::SetUBTTargetName(*NewUBTTargetName); IHotReloadInterface& HotReloadSupport = FModuleManager::LoadModuleChecked("HotReload"); if (!HotReloadSupport.RecompileModule(*GameModuleName, *GWarn, ERecompileModuleFlags::ReloadAfterRecompile | ERecompileModuleFlags::ForceCodeProject)) { OutFailReason = LOCTEXT("FailedToCompileNewGameModule", "Failed to compile newly created game module."); return EAddCodeToProjectResult::FailedToHotReload; } // Notify that we've created a brand new module FSourceCodeNavigation::AccessOnNewModuleAdded().Broadcast(*GameModuleName); OutReloadStatus = EReloadStatus::Reloaded; } else if (GetDefault()->bAutomaticallyHotReloadNewClasses) { FModuleStatus ModuleStatus; const FName ModuleFName = *ModuleInfo.ModuleName; if (ensure(FModuleManager::Get().QueryModule(ModuleFName, ModuleStatus))) { // Compile the module that the class was added to so that the newly added class with appear in the Content Browser TArray PackagesToRebind; if (ModuleStatus.bIsLoaded) { const bool bIsHotReloadable = FModuleManager::Get().DoesLoadedModuleHaveUObjects(ModuleFName); if (bIsHotReloadable) { // Is there a UPackage with the same name as this module? const FString PotentialPackageName = FString(TEXT("/Script/")) + ModuleInfo.ModuleName; UPackage* Package = FindPackage(nullptr, *PotentialPackageName); if (Package) { PackagesToRebind.Add(Package); } } } IHotReloadInterface& HotReloadSupport = FModuleManager::LoadModuleChecked("HotReload"); if (PackagesToRebind.Num() > 0) { // Perform a hot reload ECompilationResult::Type CompilationResult = HotReloadSupport.RebindPackages( PackagesToRebind, EHotReloadFlags::WaitForCompletion, *GWarn ); if( CompilationResult != ECompilationResult::Succeeded && CompilationResult != ECompilationResult::UpToDate ) { OutFailReason = FText::Format(LOCTEXT("FailedToHotReloadModuleFmt", "Failed to automatically hot reload the '{0}' module."), FText::FromString(ModuleInfo.ModuleName)); return EAddCodeToProjectResult::FailedToHotReload; } } else { // Perform a regular unload, then reload if (!HotReloadSupport.RecompileModule(ModuleFName, *GWarn, ERecompileModuleFlags::ReloadAfterRecompile)) { OutFailReason = FText::Format(LOCTEXT("FailedToCompileModuleFmt", "Failed to automatically compile the '{0}' module."), FText::FromString(ModuleInfo.ModuleName)); return EAddCodeToProjectResult::FailedToHotReload; } } } OutReloadStatus = EReloadStatus::Reloaded; } return EAddCodeToProjectResult::Succeeded; } bool GameProjectUtils::FindSourceFileInProject(const FString& InFilename, const FString& InSearchPath, FString& OutPath) { TArray Filenames; IFileManager::Get().FindFilesRecursive(Filenames, *InSearchPath, *InFilename, true, false, false); if(Filenames.Num()) { // Assume it's the first match (we should really only find a single file with a given name within a project anyway) OutPath = Filenames[0]; return true; } return false; } void GameProjectUtils::HarvestCursorSyncLocation( FString& FinalOutput, FString& OutSyncLocation ) { OutSyncLocation.Empty(); // Determine the cursor focus location if this file will by synced after creation TArray Lines; FinalOutput.ParseIntoArray( Lines, TEXT( "\n" ), false ); for( int32 LineIdx = 0; LineIdx < Lines.Num(); ++LineIdx ) { const FString& Line = Lines[ LineIdx ]; int32 CharLoc = Line.Find( TEXT( "%CURSORFOCUSLOCATION%" ) ); if( CharLoc != INDEX_NONE ) { // Found the sync marker OutSyncLocation = FString::Printf( TEXT( "%d:%d" ), LineIdx + 1, CharLoc + 1 ); break; } } // If we did not find the sync location, just sync to the top of the file if( OutSyncLocation.IsEmpty() ) { OutSyncLocation = TEXT( "1:1" ); } // Now remove the cursor focus marker FinalOutput = FinalOutput.Replace(TEXT("%CURSORFOCUSLOCATION%"), TEXT(""), ESearchCase::CaseSensitive); } bool GameProjectUtils::InsertFeaturePacksIntoINIFile(const FProjectInformation& InProjectInfo, FText& OutFailReason) { OutFailReason = LOCTEXT("DeprecatedStarterPack", "Deprecated: Ability to add Starter Content has been removed"); return false; } bool GameProjectUtils::AddSharedContentToProject(const FProjectInformation &InProjectInfo, TArray &CreatedFiles, FText& OutFailReason) { //const FString TemplateName = FPaths::GetBaseFilename(InProjectInfo.TemplateFile); const FString SrcFolder = FPaths::GetPath(InProjectInfo.TemplateFile); const FString DestFolder = FPaths::GetPath(InProjectInfo.ProjectFilename); // Now any packs specified in the template def. UTemplateProjectDefs* TemplateDefs = LoadTemplateDefs(SrcFolder); if (TemplateDefs != NULL) { EFeaturePackDetailLevel RequiredDetail = EFeaturePackDetailLevel::High; if (InProjectInfo.TargetedHardware == EHardwareClass::Mobile) { RequiredDetail = EFeaturePackDetailLevel::Standard; } TUniquePtr TempFeaturePack = MakeUnique(); bool bCopied = TempFeaturePack->InsertAdditionalResources(TemplateDefs->SharedContentPacks, RequiredDetail, DestFolder, CreatedFiles); if( bCopied == false ) { FFormatNamedArguments Args; Args.Add(TEXT("TemplateName"), FText::FromString(SrcFolder)); OutFailReason = FText::Format(LOCTEXT("SharedResourceError", "Error adding shared resources for '{TemplateName}'."), Args); return false; } if (!TemplateDefs->Variants.IsEmpty() && InProjectInfo.Variant != NAME_None) { const FTemplateVariant* Variant = TemplateDefs->FindVariant(InProjectInfo.Variant); if (!Variant) { FFormatNamedArguments Args; Args.Add(TEXT("TemplateName"), FText::FromString(SrcFolder)); OutFailReason = FText::Format(LOCTEXT("SharedResourceError", "Unknown variant specified for '{TemplateName}'."), Args); return false; } TUniquePtr TempVariantFeaturePack = MakeUnique(); bool bVariantCopied = TempVariantFeaturePack->InsertAdditionalResources(Variant->SharedContentPacks, RequiredDetail, DestFolder, CreatedFiles); if( bVariantCopied == false ) { FFormatNamedArguments Args; Args.Add(TEXT("VariantName"), Variant->GetDisplayNameText()); Args.Add(TEXT("TemplateName"), FText::FromString(SrcFolder)); OutFailReason = FText::Format(LOCTEXT("SharedResourceError", "Error adding variant '{VariantName}' shared resources for '{TemplateName}'."), Args); return false; } } } return true; } #undef LOCTEXT_NAMESPACE