// Copyright 1998-2014 Epic Games, Inc. All Rights Reserved. #include "GameProjectGenerationPrivatePCH.h" #include "UnrealEdMisc.h" #include "ISourceControlModule.h" #include "MainFrame.h" #include "DefaultTemplateProjectDefs.h" #include "Runtime/Analytics/Analytics/Public/Interfaces/IAnalyticsProvider.h" #include "EngineAnalytics.h" #include "EngineBuildSettings.h" #include "DesktopPlatformModule.h" #include "TargetPlatform.h" #include "ClassIconFinder.h" #include "UProjectInfo.h" #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 PLATFORM_MAX_FILEPATH_LENGTH static_assert(PLATFORM_MAX_FILEPATH_LENGTH - MAX_PROJECT_PATH_BUFFER_SPACE > 0, "File system path shorter than project creation buffer space."); #define MAX_CLASS_NAME_LENGTH 32 // Enforce a reasonable class name length so the path is not too long for PLATFORM_MAX_FILEPATH_LENGTH TWeakPtr GameProjectUtils::UpdateGameProjectNotification = NULL; TWeakPtr GameProjectUtils::WarningProjectNameNotification = NULL; FString GameProjectUtils::FNewClassInfo::GetClassName() const { switch(ClassType) { case EClassType::UObject: return BaseClass ? FName::NameToDisplayString(BaseClass->GetName(), false) : TEXT(""); case EClassType::EmptyCpp: return TEXT("None"); case EClassType::SlateWidget: return TEXT("Slate Widget"); case EClassType::SlateWidgetStyle: return TEXT("Slate Widget Style"); default: break; } return TEXT(""); } FString GameProjectUtils::FNewClassInfo::GetClassDescription() const { switch(ClassType) { case EClassType::UObject: { if(BaseClass) { FString ClassDescription = BaseClass->GetToolTipText().ToString(); 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 = ClassDescription.Left(FullStopIndex + 1); } // Strip out any new-lines in the description ClassDescription = ClassDescription.Replace(TEXT("\n"), TEXT(" ")); return ClassDescription; } } break; case EClassType::EmptyCpp: return TEXT("An empty C++ class with a default constructor and destructor"); case EClassType::SlateWidget: return TEXT("A custom Slate widget, deriving from SCompoundWidget"); case EClassType::SlateWidgetStyle: return TEXT("A custom Slate widget style, deriving from FSlateWidgetStyle, along with its associated UObject wrapper class"); default: break; } return TEXT(""); } const FSlateBrush* GameProjectUtils::FNewClassInfo::GetClassIcon() const { // Safe to do even if BaseClass is null, since FindIconForClass will return the default icon return FClassIconFinder::FindIconForClass(BaseClass); } FString GameProjectUtils::FNewClassInfo::GetClassPrefixCPP() const { switch(ClassType) { case EClassType::UObject: return BaseClass ? BaseClass->GetPrefixCPP() : TEXT(""); case EClassType::EmptyCpp: return TEXT(""); case EClassType::SlateWidget: return TEXT("S"); case EClassType::SlateWidgetStyle: return TEXT("F"); default: break; } return TEXT(""); } FString GameProjectUtils::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"); default: break; } return TEXT(""); } FString GameProjectUtils::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 = CleanClassName.LeftChop(5); // 5 for "Style" } if(CleanClassName.EndsWith(TEXT("Widget"))) { CleanClassName = CleanClassName.LeftChop(6); // 6 for "Widget" } } break; default: break; } return CleanClassName; } FString GameProjectUtils::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 GameProjectUtils::FNewClassInfo::GetIncludePath(FString& OutIncludePath) const { switch(ClassType) { case EClassType::UObject: if(BaseClass && BaseClass->HasMetaData(TEXT("IncludePath"))) { OutIncludePath = BaseClass->GetMetaData(TEXT("IncludePath")); return true; } break; default: break; } return false; } FString GameProjectUtils::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 GameProjectUtils::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 GameProjectUtils::FNewClassInfo::GetHeaderTemplateFilename() const { switch(ClassType) { case EClassType::UObject: 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"); default: break; } return TEXT(""); } FString GameProjectUtils::FNewClassInfo::GetSourceTemplateFilename() const { switch(ClassType) { case EClassType::UObject: 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"); 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 = PLATFORM_MAX_FILEPATH_LENGTH - 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 projects path must not be longer than {MaxProjectPathLength} characters." ), Args ); return false; } if ( FPaths::GetExtension(ProjectFile) != IProjectManager::GetProjectFileExtension() ) { FFormatNamedArguments Args; Args.Add( TEXT("ProjectFileExtension"), FText::FromString( IProjectManager::GetProjectFileExtension() ) ); 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 (NameContainsUnderscoreAndXB1Installed(BaseProjectFile)) { OutFailReason = LOCTEXT( "ProjectNameContainsIllegalCharactersOnXB1", "Project names may not contain an underscore when the Xbox One XDK is installed." ); 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( "ProjectFileAlreadyExists", "{ProjectFile} already exists." ), Args ); return false; } if ( FPaths::ConvertRelativePathToFull(FPaths::GetPath(ProjectFile)).StartsWith( FPaths::ConvertRelativePathToFull(FPaths::EngineDir())) ) { FFormatNamedArguments Args; Args.Add( TEXT("ProjectFile"), FText::FromString( ProjectFile ) ); OutFailReason = FText::Format( LOCTEXT( "ProjectFileCannotBeUnderEngineFolder", "{ProjectFile} cannot be saved under the Engine folder. Create the project in a different directory." ), Args ); return false; } if ( AnyProjectFilesExistInFolder(FPaths::GetPath(ProjectFile)) ) { FFormatNamedArguments Args; Args.Add( TEXT("ProjectFileExtension"), FText::FromString( IProjectManager::GetProjectFileExtension() ) ); Args.Add( TEXT("ProjectFilePath"), FText::FromString( FPaths::GetPath(ProjectFile) ) ); OutFailReason = FText::Format( LOCTEXT( "AProjectFileAlreadyExistsAtLoction", "Another .{ProjectFileExtension} file already exists in {ProjectFilePath}" ), Args ); 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 = PLATFORM_MAX_FILEPATH_LENGTH - 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 projects path must not be longer than {MaxProjectPathLength} characters." ), Args ); return false; } if ( FPaths::GetExtension(ProjectFile) != IProjectManager::GetProjectFileExtension() ) { FFormatNamedArguments Args; Args.Add( TEXT("ProjectFileExtension"), FText::FromString( IProjectManager::GetProjectFileExtension() ) ); 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 (NameContainsUnderscoreAndXB1Installed(BaseProjectFile)) { OutFailReason = LOCTEXT( "ProjectNameContainsIllegalCharactersOnXB1", "Project names may not contain an underscore when the Xbox One XDK is installed." ); 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("UE4"); } // Get the solution filename FString CodeSolutionFile; #if PLATFORM_WINDOWS CodeSolutionFile = SolutionFilenameWithoutExtension + TEXT(".sln"); #elif PLATFORM_MAC CodeSolutionFile = SolutionFilenameWithoutExtension + TEXT(".xcodeproj"); #elif PLATFORM_LINUX STUBBED("Linux solution filename"); #else OutFailReason = LOCTEXT( "OpenCodeIDE_UnknownPlatform", "could not open the code editing IDE. The operating system is unknown." ); return false; #endif // Open the solution with the default application const FString FullPath = FPaths::Combine(*SolutionFolder, *CodeSolutionFile); #if PLATFORM_MAC if ( IFileManager::Get().DirectoryExists(*FullPath) ) #else if ( FPaths::FileExists(FullPath) ) #endif { FPlatformProcess::LaunchFileInDefaultExternalApplication( *FullPath ); return true; } else { FFormatNamedArguments Args; Args.Add( TEXT("Path"), FText::FromString( FullPath ) ); OutFailReason = FText::Format( LOCTEXT( "OpenCodeIDE_MissingFile", "Could not edit the code editing IDE. {Path} could not be found." ), Args ); return false; } } void GameProjectUtils::GetStarterContentFiles(TArray& OutFilenames) { FString const SrcFolder = FPaths::StarterContentDir(); FString const ContentFolder = SrcFolder / TEXT("Content"); // only copying /Content IFileManager::Get().FindFilesRecursive(OutFilenames, *ContentFolder, TEXT("*"), /*Files=*/true, /*Directories=*/false); } bool GameProjectUtils::CopyStarterContent(const FString& DestProjectFolder, FText& OutFailReason) { FString const SrcFolder = FPaths::StarterContentDir(); TArray FilesToCopy; GetStarterContentFiles(FilesToCopy); TArray CreatedFiles; for (FString SrcFilename : FilesToCopy) { // Update the slow task dialog const bool bAllowNewSlowTask = false; FFormatNamedArguments Args; Args.Add(TEXT("SrcFilename"), FText::FromString(FPaths::GetCleanFilename(SrcFilename))); FScopedSlowTask SlowTaskMessage(FText::Format(LOCTEXT("CreatingProjectStatus_CopyingFile", "Copying File {SrcFilename}..."), Args), bAllowNewSlowTask); FString FileRelPath = FPaths::GetPath(SrcFilename); FPaths::MakePathRelativeTo(FileRelPath, *SrcFolder); // Perform the copy. For file collisions, leave existing file. const FString DestFilename = DestProjectFolder + TEXT("/") + FileRelPath + TEXT("/") + FPaths::GetCleanFilename(SrcFilename); if (!FPaths::FileExists(DestFilename)) { if (IFileManager::Get().Copy(*DestFilename, *SrcFilename, false) == COPY_OK) { CreatedFiles.Add(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); DeleteCreatedFiles(DestProjectFolder, CreatedFiles); return false; } } } return true; } bool GameProjectUtils::CreateProject(const FString& NewProjectFile, const FString& TemplateFile, bool bShouldGenerateCode, bool bCopyStarterContent, FText& OutFailReason) { if ( !IsValidProjectFileForCreation(NewProjectFile, OutFailReason) ) { return false; } const bool bAllowNewSlowTask = true; FScopedSlowTask SlowTaskMessage( LOCTEXT( "CreatingProjectStatus", "Creating project..." ), bAllowNewSlowTask ); bool bProjectCreationSuccessful = false; FString TemplateName; if ( TemplateFile.IsEmpty() ) { bProjectCreationSuccessful = GenerateProjectFromScratch(NewProjectFile, bShouldGenerateCode, bCopyStarterContent, OutFailReason); TemplateName = bShouldGenerateCode ? TEXT("Basic Code") : TEXT("Blank"); } else { bProjectCreationSuccessful = CreateProjectFromTemplate(NewProjectFile, TemplateFile, bShouldGenerateCode, bCopyStarterContent, OutFailReason); TemplateName = FPaths::GetBaseFilename(TemplateFile); } if( FEngineAnalytics::IsAvailable() ) { TArray EventAttributes; EventAttributes.Add(FAnalyticsEventAttribute(TEXT("Template"), TemplateName)); EventAttributes.Add(FAnalyticsEventAttribute(TEXT("ProjectType"), bShouldGenerateCode ? TEXT("C++ Code") : TEXT("Content Only"))); EventAttributes.Add(FAnalyticsEventAttribute(TEXT("Outcome"), bProjectCreationSuccessful ? TEXT("Successful") : TEXT("Failed"))); FEngineAnalytics::GetProvider().RecordEvent( TEXT( "Editor.NewProject.ProjectCreated" ), EventAttributes ); } return bProjectCreationSuccessful; } bool GameProjectUtils::BuildGameBinaries(const FString& ProjectFilename, FText& OutFailReason) { const bool bAllowNewSlowTask = true; FScopedSlowTask SlowTaskMessage( LOCTEXT( "BuildingProjectStatus", "Building project..." ), bAllowNewSlowTask ); // Compile the *editor* for the project if ( FModuleManager::Get().CompileGameProjectEditor(ProjectFilename, *GLog) ) { return true; } FFormatNamedArguments Args; Args.Add( TEXT("ProjectFilename"), FText::FromString( ProjectFilename ) ); OutFailReason = FText::Format( LOCTEXT("FailedToCompileNewProject", "Failed to compile {ProjectFileName}."), Args ); return false; } void GameProjectUtils::CheckForOutOfDateGameProjectFile() { if ( FPaths::IsProjectFilePathSet() ) { FProjectStatus ProjectStatus; if (IProjectManager::Get().QueryStatusForCurrentProject(ProjectStatus)) { if ( ProjectStatus.bRequiresUpdate ) { const FText UpdateProjectText = LOCTEXT("UpdateProjectFilePrompt", "Project file is saved in an older format. Would you like to update it?"); const FText UpdateProjectConfirmText = LOCTEXT("UpdateProjectFileConfirm", "Update"); const FText UpdateProjectCancelText = LOCTEXT("UpdateProjectFileCancel", "Not Now"); FNotificationInfo Info(UpdateProjectText); Info.bFireAndForget = false; Info.bUseLargeFont = false; Info.bUseThrobber = false; Info.bUseSuccessFailIcons = false; Info.FadeOutDuration = 3.f; Info.ButtonDetails.Add(FNotificationButtonInfo(UpdateProjectConfirmText, FText(), FSimpleDelegate::CreateStatic(&GameProjectUtils::OnUpdateProjectConfirm))); Info.ButtonDetails.Add(FNotificationButtonInfo(UpdateProjectCancelText, FText(), FSimpleDelegate::CreateStatic(&GameProjectUtils::OnUpdateProjectCancel))); 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::UpdateGameProject(const FString& ProjectFile, const FString& EngineIdentifier, FText& OutFailReason) { return UpdateGameProjectFile(ProjectFile, EngineIdentifier, NULL, OutFailReason); } void GameProjectUtils::OpenAddCodeToProjectDialog() { TSharedRef AddCodeWindow = SNew(SWindow) .Title(LOCTEXT( "AddCodeWindowHeader", "Add Code")) .ClientSize( FVector2D(940, 540) ) .SizingRule( ESizingRule::FixedSize ) .SupportsMinimize(false) .SupportsMaximize(false); AddCodeWindow->SetContent( SNew(SNewClassDialog) ); IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked(TEXT("MainFrame")); if (MainFrameModule.GetParentWindow().IsValid()) { FSlateApplication::Get().AddWindowAsNativeChild(AddCodeWindow, MainFrameModule.GetParentWindow().ToSharedRef()); } else { FSlateApplication::Get().AddWindow(AddCodeWindow); } } bool GameProjectUtils::IsValidClassNameForCreation(const FString& NewClassName, const FModuleContextInfo& ModuleInfo, 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; } // 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; } } return true; } bool GameProjectUtils::AddCodeToProject(const FString& NewClassName, const FString& NewClassPath, const FModuleContextInfo& ModuleInfo, const FNewClassInfo ParentClassInfo, FString& OutHeaderFilePath, FString& OutCppFilePath, FText& OutFailReason) { const bool bAddCodeSuccessful = AddCodeToProject_Internal(NewClassName, NewClassPath, ModuleInfo, ParentClassInfo, OutHeaderFilePath, OutCppFilePath, OutFailReason); 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"), bAddCodeSuccessful ? TEXT("Successful") : TEXT("Failed"))); FEngineAnalytics::GetProvider().RecordEvent( TEXT( "Editor.AddCodeToProject.CodeAdded" ), EventAttributes ); } return bAddCodeSuccessful; } UTemplateProjectDefs* GameProjectUtils::LoadTemplateDefs(const FString& ProjectDirectory) { UTemplateProjectDefs* TemplateDefs = NULL; const FString TemplateDefsIniFilename = 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 = FindObject(ANY_PACKAGE, *ClassName, false); if (nullptr != OverrideClass) { ClassToConstruct = OverrideClass; } else { UE_LOG(LogGameProjectGeneration, Error, TEXT("Failed to find template project defs class '%s', using default."), *ClassName); } } TemplateDefs = ConstructObject(ClassToConstruct); TemplateDefs->LoadConfig(UTemplateProjectDefs::StaticClass(), *TemplateDefsIniFilename); } return TemplateDefs; } bool GameProjectUtils::GenerateProjectFromScratch(const FString& NewProjectFile, bool bShouldGenerateCode, bool bCopyStarterContent, FText& OutFailReason) { const FString NewProjectFolder = FPaths::GetPath(NewProjectFile); const FString NewProjectName = FPaths::GetBaseFilename(NewProjectFile); TArray CreatedFiles; // Generate config files if (!GenerateConfigFiles(NewProjectFolder, NewProjectName, bShouldGenerateCode, bCopyStarterContent, CreatedFiles, OutFailReason)) { DeleteCreatedFiles(NewProjectFolder, CreatedFiles); return false; } // 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 ); DeleteCreatedFiles(NewProjectFolder, CreatedFiles); return false; } TArray StartupModuleNames; if ( bShouldGenerateCode ) { // Generate basic source code files if ( !GenerateBasicSourceCode(NewProjectFolder / TEXT("Source"), NewProjectName, StartupModuleNames, CreatedFiles, OutFailReason) ) { DeleteCreatedFiles(NewProjectFolder, CreatedFiles); return false; } // Generate game framework source code files if ( !GenerateGameFrameworkSourceCode(NewProjectFolder / TEXT("Source"), NewProjectName, CreatedFiles, OutFailReason) ) { DeleteCreatedFiles(NewProjectFolder, CreatedFiles); return false; } } // Generate the project file { FText LocalFailReason; if (IProjectManager::Get().GenerateNewProjectFile(NewProjectFile, StartupModuleNames, FDesktopPlatformModule::Get()->GetCurrentEngineIdentifier(), LocalFailReason)) { CreatedFiles.Add(NewProjectFile); } else { OutFailReason = LocalFailReason; DeleteCreatedFiles(NewProjectFolder, CreatedFiles); return false; } } if ( bShouldGenerateCode ) { // Generate project files if ( !GenerateCodeProjectFiles(NewProjectFile, OutFailReason) ) { DeleteGeneratedProjectFiles(NewProjectFile); DeleteCreatedFiles(NewProjectFolder, CreatedFiles); return false; } } if (bCopyStarterContent) { // Copy the starter content if ( !CopyStarterContent(NewProjectFolder, OutFailReason) ) { DeleteGeneratedProjectFiles(NewProjectFile); DeleteCreatedFiles(NewProjectFolder, CreatedFiles); return false; } } UE_LOG(LogGameProjectGeneration, Log, TEXT("Created new project with %d files (plus project files)"), CreatedFiles.Num()); return true; } bool GameProjectUtils::CreateProjectFromTemplate(const FString& NewProjectFile, const FString& TemplateFile, bool bShouldGenerateCode, bool bCopyStarterContent, FText& OutFailReason) { const FString ProjectName = FPaths::GetBaseFilename(NewProjectFile); const FString TemplateName = FPaths::GetBaseFilename(TemplateFile); const FString SrcFolder = FPaths::GetPath(TemplateFile); const FString DestFolder = FPaths::GetPath(NewProjectFile); if ( !FPlatformFileManager::Get().GetPlatformFile().FileExists(*TemplateFile) ) { FFormatNamedArguments Args; Args.Add( TEXT("TemplateFile"), FText::FromString( TemplateFile ) ); OutFailReason = FText::Format( LOCTEXT("InvalidTemplate_MissingProject", "Template project \"{TemplateFile}\" does not exist."), Args ); return false; } UTemplateProjectDefs* TemplateDefs = LoadTemplateDefs(SrcFolder); if ( TemplateDefs == NULL ) { FFormatNamedArguments Args; Args.Add( TEXT("TemplateFile"), FText::FromString( FPaths::GetBaseFilename(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 false; } // 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 ( auto ReplacementIt = TemplateDefs->ReplacementsInFiles.CreateConstIterator(); ReplacementIt; ++ReplacementIt ) { ReplacementsInFilesExtensions.Append((*ReplacementIt).Extensions); } // Keep a list of created files so we can delete them if project creation fails TArray CreatedFiles; // 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); for ( auto FileIt = FilesToCopy.CreateConstIterator(); FileIt; ++FileIt ) { const FString SrcFilename = (*FileIt); // 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 bool bThisFileIsIgnored = false; for ( auto IgnoreIt = TemplateDefs->FilesToIgnore.CreateConstIterator(); IgnoreIt; ++IgnoreIt ) { if ( SrcFileSubpath == *IgnoreIt ) { // This file was marked as "ignored" bThisFileIsIgnored = true; break; } } if ( bThisFileIsIgnored ) { // This file was marked as "ignored" continue; } // Skip any folders that were configured to be ignored bool bThisFolderIsIgnored = false; for ( auto IgnoreIt = TemplateDefs->FoldersToIgnore.CreateConstIterator(); IgnoreIt; ++IgnoreIt ) { if ( SrcFileSubpath.StartsWith((*IgnoreIt) + TEXT("/") ) ) { // This folder was marked as "ignored" bThisFolderIsIgnored = true; break; } } if ( bThisFolderIsIgnored ) { // This folder was marked as "ignored" continue; } // Update the slow task dialog const bool bAllowNewSlowTask = false; FFormatNamedArguments Args; Args.Add( TEXT("SrcFilename"), FText::FromString( FPaths::GetCleanFilename(SrcFilename) ) ); FScopedSlowTask SlowTaskMessage( FText::Format( LOCTEXT( "CreatingProjectStatus_CopyingFile", "Copying File {SrcFilename}..." ), Args ), bAllowNewSlowTask ); // Retarget any folders that were chosen to be renamed by choosing a new destination subpath now FString DestFileSubpathWithoutFilename = FPaths::GetPath(SrcFileSubpath) + TEXT("/"); for ( auto RenameIt = TemplateDefs->FolderRenames.CreateConstIterator(); RenameIt; ++RenameIt ) { const FTemplateFolderRename& FolderRename = *RenameIt; 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() ); } } // 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 ( auto ReplacementIt = TemplateDefs->FilenameReplacements.CreateConstIterator(); ReplacementIt; ++ReplacementIt ) { const FTemplateReplacement& Replacement = *ReplacementIt; if ( Replacement.Extensions.Contains( FileExtension ) ) { // This file matched a filename replacement extension, apply it now DestBaseFilename = DestBaseFilename.Replace(*Replacement.From, *Replacement.To, Replacement.bCaseSensitive ? ESearchCase::CaseSensitive : ESearchCase::IgnoreCase); } } // 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); DeleteCreatedFiles(DestFolder, CreatedFiles); return false; } } // Open all files with the specified extensions and replace text for ( auto FileIt = FilesThatNeedContentsReplaced.CreateConstIterator(); FileIt; ++FileIt ) { const FString FileToFix = *FileIt; bool bSuccessfullyProcessed = false; FString FileContents; if ( FFileHelper::LoadFileToString(FileContents, *FileToFix) ) { for ( auto ReplacementIt = TemplateDefs->ReplacementsInFiles.CreateConstIterator(); ReplacementIt; ++ReplacementIt ) { const FTemplateReplacement& Replacement = *ReplacementIt; if ( Replacement.Extensions.Contains( FPaths::GetExtension(FileToFix) ) ) { FileContents = FileContents.Replace(*Replacement.From, *Replacement.To, Replacement.bCaseSensitive ? ESearchCase::CaseSensitive : ESearchCase::IgnoreCase); } } 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 ); DeleteCreatedFiles(DestFolder, CreatedFiles); return false; } } // Fixup specific ini values TArray ConfigValuesToSet; TemplateDefs->AddConfigValues(ConfigValuesToSet, TemplateName, ProjectName, bShouldGenerateCode); new (ConfigValuesToSet) FTemplateConfigValue(TEXT("DefaultGame.ini"), TEXT("/Script/EngineSettings.GeneralProjectSettings"), TEXT("ProjectID"), FGuid::NewGuid().ToString(), /*InShouldReplaceExistingValue=*/true); // Add all classname fixups for ( auto RenameIt = ClassRenames.CreateConstIterator(); RenameIt; ++RenameIt ) { const FString ClassRedirectString = FString::Printf(TEXT("(OldClassName=\"%s\",NewClassName=\"%s\")"), *RenameIt.Key(), *RenameIt.Value()); new (ConfigValuesToSet) FTemplateConfigValue(TEXT("DefaultEngine.ini"), TEXT("/Script/Engine.Engine"), TEXT("+ActiveClassRedirects"), *ClassRedirectString, /*InShouldReplaceExistingValue=*/false); } // Fix all specified config values for ( auto ConfigIt = ConfigValuesToSet.CreateConstIterator(); ConfigIt; ++ConfigIt ) { const FTemplateConfigValue& ConfigValue = *ConfigIt; const FString IniFilename = DestFolder / TEXT("Config") / 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 ( auto LineIt = FileLines.CreateConstIterator(); LineIt; ++LineIt ) { FString Line = *LineIt; Line.Trim().TrimTrailing(); bool bShouldExcludeLineFromOutput = false; // If we not yet found the target key parse each line looking for it if ( !bFoundTargetKey ) { // Check for an empty line. No work needs to be done on these lines if ( Line.Len() == 0 ) { } // Comment lines start with ";". Skip these lines entirely. else if ( Line.StartsWith(TEXT(";")) ) { } // If this is a section line, update the section else if ( Line.StartsWith(TEXT("[")) ) { // 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 ( LineIt.GetIndex() < FileLines.Num() - 1 ) { // 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 += 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) ) { bSuccessfullyProcessed = true; } } if ( !bSuccessfullyProcessed ) { OutFailReason = LOCTEXT("FailedToFixUpDefaultEngine", "Failed to process file DefaultEngine.ini"); DeleteCreatedFiles(DestFolder, CreatedFiles); return false; } } // Generate the project file { // Load the source project FProjectDescriptor Project; if(!Project.Load(TemplateFile, OutFailReason)) { DeleteCreatedFiles(DestFolder, CreatedFiles); return false; } // Update it to current Project.EngineAssociation = FDesktopPlatformModule::Get()->GetCurrentEngineIdentifier(); Project.EpicSampleNameHash = 0; // Fix up module names const FString BaseSourceName = FPaths::GetBaseFilename(TemplateFile); const FString BaseNewName = FPaths::GetBaseFilename(NewProjectFile); for ( auto ModuleIt = Project.Modules.CreateIterator(); ModuleIt; ++ModuleIt ) { FModuleDescriptor& ModuleInfo = *ModuleIt; ModuleInfo.Name = FName(*ModuleInfo.Name.ToString().Replace(*BaseSourceName, *BaseNewName)); } // Save it to disk if(!Project.Save(NewProjectFile, OutFailReason)) { DeleteCreatedFiles(DestFolder, CreatedFiles); return false; } // Add it to the list of created files CreatedFiles.Add(NewProjectFile); } if ( bShouldGenerateCode ) { // resource folder const FString GameModuleSourcePath = DestFolder / TEXT("Source") / ProjectName; if (GenerateGameResourceFiles(GameModuleSourcePath, ProjectName, CreatedFiles, OutFailReason) == false) { DeleteCreatedFiles(DestFolder, CreatedFiles); return false; } // Generate project files if ( !GenerateCodeProjectFiles(NewProjectFile, OutFailReason) ) { DeleteGeneratedProjectFiles(NewProjectFile); DeleteCreatedFiles(DestFolder, CreatedFiles); return false; } } if (bCopyStarterContent) { // Copy the starter content if ( !CopyStarterContent(DestFolder, OutFailReason) ) { DeleteGeneratedProjectFiles(NewProjectFile); DeleteCreatedFiles(DestFolder, CreatedFiles); return false; } } if (!TemplateDefs->PostGenerateProject(DestFolder, SrcFolder, NewProjectFile, TemplateFile, bShouldGenerateCode, OutFailReason)) { DeleteGeneratedProjectFiles(NewProjectFile); DeleteCreatedFiles(DestFolder, CreatedFiles); return false; } return true; } FString GameProjectUtils::GetTemplateDefsFilename() { return TEXT("TemplateDefs.ini"); } 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::NameContainsUnderscoreAndXB1Installed(const FString& TestName) { bool bContainsIllegalCharacters = false; // Only allow alphanumeric characters in the project name for ( int32 CharIdx = 0 ; CharIdx < TestName.Len() ; ++CharIdx ) { const FString& Char = TestName.Mid( CharIdx, 1 ); if ( Char == TEXT("_") ) { const ITargetPlatform* Platform = GetTargetPlatformManager()->FindTargetPlatform(TEXT("XboxOne")); if (Platform) { FString NotInstalledDocLink; if (Platform->IsSdkInstalled(true, NotInstalledDocLink)) { bContainsIllegalCharacters = true; } } } } return bContainsIllegalCharacters; } 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, *IProjectManager::GetProjectFileExtension()); 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::DeleteGeneratedProjectFiles(const FString& NewProjectFile) { if (CleanupIsEnabled()) { const FString NewProjectFolder = FPaths::GetPath(NewProjectFile); const FString NewProjectName = FPaths::GetBaseFilename(NewProjectFile); // Since it is hard to tell which files were created from the code project file generation process, just delete the entire ProjectFiles folder. const FString IntermediateProjectFileFolder = NewProjectFolder / TEXT("Intermediate") / TEXT("ProjectFiles"); IFileManager::Get().DeleteDirectory(*IntermediateProjectFileFolder, /*RequireExists=*/false, /*Tree=*/true); // Delete the solution file const FString SolutionFileName = NewProjectFolder / NewProjectName + TEXT(".sln"); IFileManager::Get().Delete( *SolutionFileName ); } } void GameProjectUtils::DeleteGeneratedBuildFiles(const FString& NewProjectFolder) { if (CleanupIsEnabled()) { // Since it is hard to tell which files were created from the build process, just delete the entire Binaries and Build folders. const FString BinariesFolder = NewProjectFolder / TEXT("Binaries"); const FString BuildFolder = NewProjectFolder / TEXT("Intermediate") / TEXT("Build"); IFileManager::Get().DeleteDirectory(*BinariesFolder, /*RequireExists=*/false, /*Tree=*/true); IFileManager::Get().DeleteDirectory(*BuildFolder, /*RequireExists=*/false, /*Tree=*/true); } } bool GameProjectUtils::GenerateConfigFiles(const FString& NewProjectPath, const FString& NewProjectName, bool bShouldGenerateCode, bool bCopyStarterContent, TArray& OutCreatedFiles, FText& OutFailReason) { FString ProjectConfigPath = NewProjectPath / TEXT("Config"); // DefaultEngine.ini { const FString DefaultEngineIniFilename = ProjectConfigPath / TEXT("DefaultEngine.ini"); FString FileContents; FileContents += TEXT("[URL]") LINE_TERMINATOR; FileContents += FString::Printf(TEXT("GameName=%s") LINE_TERMINATOR, *NewProjectName); FileContents += LINE_TERMINATOR; if (bCopyStarterContent) { // for generated/blank projects with starter content, set startup map to be the starter content map // otherwise, we leave it to be what the template wants. TArray StarterContentMapFiles; const FString FileWildcard = FString(TEXT("*")) + FPackageName::GetMapPackageExtension(); // assume the first map in the /Maps folder is the default map IFileManager::Get().FindFilesRecursive(StarterContentMapFiles, *FPaths::StarterContentDir(), *FileWildcard, /*Files=*/true, /*Directories=*/false); if (StarterContentMapFiles.Num() > 0) { FString StarterContentContentDir = FPaths::StarterContentDir() + TEXT("Content/"); const FString BaseMapFilename = FPaths::GetBaseFilename(StarterContentMapFiles[0]); FString MapPathRelToContent = FPaths::GetPath(StarterContentMapFiles[0]); FPaths::MakePathRelativeTo(MapPathRelToContent, *StarterContentContentDir); const FString MapPackagePath = FString(TEXT("/Game/")) + MapPathRelToContent + TEXT("/") + BaseMapFilename; FileContents += TEXT("[/Script/EngineSettings.GameMapsSettings]") LINE_TERMINATOR; FileContents += FString::Printf(TEXT("EditorStartupMap=%s") LINE_TERMINATOR, *MapPackagePath); FileContents += FString::Printf(TEXT("GameDefaultMap=%s") LINE_TERMINATOR, *MapPackagePath); if (bShouldGenerateCode) { FileContents += FString::Printf(TEXT("GlobalDefaultGameMode=\"/Script/%s.%sGameMode\"") LINE_TERMINATOR, *NewProjectName, *NewProjectName); } } } if (WriteOutputFile(DefaultEngineIniFilename, FileContents, OutFailReason)) { OutCreatedFiles.Add(DefaultEngineIniFilename); } else { return false; } } // DefaultGame.ini { const FString DefaultGameIniFilename = ProjectConfigPath / TEXT("DefaultGame.ini"); FString FileContents; FileContents += TEXT("[/Script/EngineSettings.GeneralProjectSettings]") LINE_TERMINATOR; FileContents += FString::Printf( TEXT("ProjectID=%s") LINE_TERMINATOR, *FGuid::NewGuid().ToString() ); FileContents += LINE_TERMINATOR; if ( WriteOutputFile(DefaultGameIniFilename, FileContents, OutFailReason) ) { OutCreatedFiles.Add(DefaultGameIniFilename); } else { return false; } } // DefaultEditor.ini { const FString DefaultEditorIniFilename = ProjectConfigPath / TEXT("DefaultEditor.ini"); FString FileContents; FileContents += TEXT("[EditoronlyBP]") LINE_TERMINATOR; FileContents += TEXT("bAllowClassAndBlueprintPinMatching=true") LINE_TERMINATOR; FileContents += TEXT("bReplaceBlueprintWithClass=true") LINE_TERMINATOR; FileContents += TEXT("bDontLoadBlueprintOutsideEditor=true") LINE_TERMINATOR; FileContents += TEXT("bBlueprintIsNotBlueprintType=true") LINE_TERMINATOR; if (WriteOutputFile(DefaultEditorIniFilename, FileContents, OutFailReason)) { OutCreatedFiles.Add(DefaultEditorIniFilename); } else { return false; } } return true; } bool GameProjectUtils::GenerateBasicSourceCode(const FString& NewProjectSourcePath, const FString& NewProjectName, 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 resource folder if (GenerateGameResourceFiles(GameModulePath, NewProjectName, OutCreatedFiles, OutFailReason) == false) { 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; PublicHeaderIncludes.Add(TEXT("Engine.h")); 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 / // MyGamePlayerController.h { const UClass* BaseClass = APlayerController::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, OutFailReason) ) { OutCreatedFiles.Add(NewHeaderFilename); } else { return false; } } // MyGameGameMode.h { const UClass* BaseClass = AGameMode::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, OutFailReason) ) { OutCreatedFiles.Add(NewHeaderFilename); } else { return false; } } // MyGamePlayerController.cpp FString PrefixedPlayerControllerClassName; { const UClass* BaseClass = APlayerController::StaticClass(); const FString NewClassName = NewProjectName + BaseClass->GetName(); const FString NewCPPFilename = GameModulePath / NewClassName + TEXT(".cpp"); PrefixedPlayerControllerClassName = FString(BaseClass->GetPrefixCPP()) + NewClassName; if ( GenerateClassCPPFile(NewCPPFilename, NewClassName, FNewClassInfo(BaseClass), TArray(), TArray(), TEXT(""), NewModuleInfo, OutFailReason) ) { OutCreatedFiles.Add(NewCPPFilename); } else { return false; } } // MyGameGameMode.cpp { const UClass* BaseClass = AGameMode::StaticClass(); const FString NewClassName = NewProjectName + BaseClass->GetName(); const FString NewCPPFilename = GameModulePath / NewClassName + TEXT(".cpp"); TArray PropertyOverrides; PropertyOverrides.Add( FString::Printf( TEXT("PlayerControllerClass = %s::StaticClass();"), *PrefixedPlayerControllerClassName ) ); // PropertyOverrides references PlayerController class so we need to include its header to properly compile under non-unity const UClass* PlayerControllerBaseClass = APlayerController::StaticClass(); const FString PlayerControllerClassName = NewProjectName + PlayerControllerBaseClass->GetName() + TEXT(".h"); TArray AdditionalIncludes; AdditionalIncludes.Add(PlayerControllerClassName); if ( GenerateClassCPPFile(NewCPPFilename, NewClassName, FNewClassInfo(BaseClass), AdditionalIncludes, PropertyOverrides, TEXT(""), NewModuleInfo, OutFailReason) ) { OutCreatedFiles.Add(NewCPPFilename); } else { return false; } } return true; } bool GameProjectUtils::GenerateCodeProjectFiles(const FString& ProjectFilename, FText& OutFailReason) { if ( FModuleManager::Get().GenerateCodeProjectFiles(ProjectFilename, *GLog) ) { return true; } FFormatNamedArguments Args; Args.Add( TEXT("ProjectFilename"), FText::FromString( ProjectFilename ) ); OutFailReason = FText::Format( LOCTEXT("FailedToGenerateCodeProjectFiles", "Failed to generate code project files for \"{ProjectFilename}\"."), Args ); return false; } bool GameProjectUtils::IsStarterContentAvailableForNewProjects() { TArray StarterContentFiles; GetStarterContentFiles(StarterContentFiles); return (StarterContentFiles.Num() > 0); } TArray GameProjectUtils::GetCurrentProjectModules() { const FProjectDescriptor* const CurrentProject = IProjectManager::Get().GetCurrentProject(); check(CurrentProject); TArray RetModuleInfos; 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::GetGameName(); ModuleInfo.ModuleType = EHostType::Runtime; ModuleInfo.ModuleSourcePath = FPaths::ConvertRelativePathToFull(FPaths::GameSourceDir() / ModuleInfo.ModuleName / ""); // Ensure trailing / RetModuleInfos.Emplace(ModuleInfo); } // 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 TmpPath; if (!FindSourceFileInProject(ModuleInfo.ModuleName + ".Build.cs", FPaths::GameSourceDir(), 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; } bool 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 = NewDirectoryName.Left(LastSpace).TrimTrailing(); } // Append the new version number NewDirectoryName += FString::Printf(TEXT(" %s"), *GEngineVersion.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); } // Find all the root directory names TArray RootDirectoryNames; IFileManager::Get().FindFiles(RootDirectoryNames, *(OldDirectoryName / TEXT("*")), false, true); // Find all the source directories TArray SourceDirectories; SourceDirectories.Add(OldDirectoryName); for(int32 Idx = 0; Idx < RootDirectoryNames.Num(); Idx++) { if(RootDirectoryNames[Idx] != TEXT("Binaries") && RootDirectoryNames[Idx] != TEXT("Intermediate") && RootDirectoryNames[Idx] != TEXT("Saved")) { FString SourceDirectory = OldDirectoryName / RootDirectoryNames[Idx]; SourceDirectories.Add(SourceDirectory); IFileManager::Get().FindFilesRecursive(SourceDirectories, *SourceDirectory, TEXT("*"), false, true, false); } } // Find all the source files TArray SourceFiles; for(int32 Idx = 0; Idx < SourceDirectories.Num(); Idx++) { TArray SourceNames; IFileManager::Get().FindFiles(SourceNames, *(SourceDirectories[Idx] / TEXT("*")), true, false); for(int32 NameIdx = 0; NameIdx < SourceNames.Num(); NameIdx++) { SourceFiles.Add(SourceDirectories[Idx] / SourceNames[NameIdx]); } } // Copy everything bool bCopySucceeded = true; GWarn->BeginSlowTask(LOCTEXT("CreatingCopyOfProject", "Creating copy of project..."), true); for(int32 Idx = 0; Idx < SourceDirectories.Num() && bCopySucceeded; Idx++) { FString TargetDirectory = NewDirectoryName + SourceDirectories[Idx].Mid(OldDirectoryName.Len()); bCopySucceeded = 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()); bCopySucceeded = PlatformFile.CopyFile(*TargetFile, *SourceFiles[Idx]); GWarn->UpdateProgress(SourceDirectories.Num() + Idx + 1, SourceDirectories.Num() + SourceFiles.Num()); } GWarn->EndSlowTask(); // Wipe the directory if we couldn't update if(!bCopySucceeded) { PlatformFile.DeleteDirectoryRecursively(*NewDirectoryName); return false; } // Otherwise fixup the output project filename OutNewProjectFile = NewDirectoryName / FPaths::GetCleanFilename(InProjectFile); return true; } 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::ReadTemplateFile(const FString& TemplateFileName, FString& OutFileContents, FText& OutFailReason) { const FString FullFileName = FPaths::EngineContentDir() / TEXT("Editor") / TEXT("Templates") / 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() { if(FEngineBuildSettings::IsInternalBuild()) { return FString(TEXT("// ")) + Cast(UGeneralProjectSettings::StaticClass()->GetDefaultObject())->CopyrightNotice; } return ""; } 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( TEXT("#include \"%s\"") LINE_TERMINATOR, **ListIt); } return ReturnString; } 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, FText& OutFailReason) { FString Template; if ( !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(LINE_TERMINATOR TEXT("#include \"%s\""), *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 } } } // 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("%CLASS_PROPERTIES%"), *ClassProperties, ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%CLASS_FUNCTION_DECLARATIONS%"), *ClassFunctionDeclarations, ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%BASE_CLASS_INCLUDE_DIRECTIVE%"), *BaseClassIncludeDirective, ESearchCase::CaseSensitive); // 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); return WriteOutputFile(NewHeaderFileName, FinalOutput, OutFailReason); } bool GameProjectUtils::GenerateClassCPPFile(const FString& NewCPPFileName, const FString UnPrefixedClassName, const FNewClassInfo ParentClassInfo, const TArray& AdditionalIncludes, const TArray& PropertyOverrides, const FString& AdditionalMemberDefinitions, const FModuleContextInfo& ModuleInfo, FText& OutFailReason) { FString Template; if ( !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 AdditionalIncludesStr; for (int32 IncludeIdx = 0; IncludeIdx < AdditionalIncludes.Num(); ++IncludeIdx) { if (IncludeIdx > 0) { AdditionalIncludesStr += LINE_TERMINATOR; } AdditionalIncludesStr += FString::Printf(TEXT("#include \"%s\""), *AdditionalIncludes[IncludeIdx]); } 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 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(NewCPPFileName)); 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"; } // 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("%MODULE_NAME%"), *ModuleInfo.ModuleName, ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%MODULE_INCLUDE_PATH%"), *ModuleIncludePath, ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%PREFIXED_CLASS_NAME%"), *PrefixedClassName, ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%PROPERTY_OVERRIDES%"), *PropertyOverridesStr, ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%ADDITIONAL_MEMBER_DEFINITIONS%"), *AdditionalMemberDefinitions, ESearchCase::CaseSensitive); FinalOutput = FinalOutput.Replace(TEXT("%ADDITIONAL_INCLUDE_DIRECTIVES%"), *AdditionalIncludesStr, ESearchCase::CaseSensitive); 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); 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); return WriteOutputFile(NewBuildFileName, FinalOutput, OutFailReason); } bool GameProjectUtils::GenerateGameResourceFile(const FString& NewResourceFolderName, const FString& TemplateFilename, const FString& GameName, TArray& OutCreatedFiles, FText& OutFailReason) { FString Template; if (!ReadTemplateFile(TemplateFilename, Template, OutFailReason)) { return false; } FString FinalOutput = Template.Replace(TEXT("%GAME_NAME%"), *GameName, ESearchCase::CaseSensitive); FString OutputFilename = TemplateFilename.Replace(TEXT("_GAME_NAME_"), *GameName); FString FullOutputFilename = NewResourceFolderName / OutputFilename; struct Local { static bool WriteFile(const FString& InDestFile, const FText& InFileDescription, FText& OutFailureReason, FString* InFileContents, TArray* OutCreatedFileList) { if (WriteOutputFile(InDestFile, *InFileContents, OutFailureReason)) { OutCreatedFileList->Add(InDestFile); return true; } return false; } }; return SourceControlHelpers::CheckoutOrMarkForAdd(FullOutputFilename, LOCTEXT("ResourceFileDescription", "resource"), FOnPostCheckOut::CreateStatic(&Local::WriteFile, &FinalOutput, &OutCreatedFiles), OutFailReason); } bool GameProjectUtils::GenerateGameResourceFiles(const FString& NewResourceFolderName, const FString& GameName, TArray& OutCreatedFiles, FText& OutFailReason) { bool bSucceeded = true; FString TemplateFilename; #if PLATFORM_WINDOWS FString IconPartialName = TEXT("_GAME_NAME_"); // Icon (just copy this) TemplateFilename = FString::Printf(TEXT("Resources/Windows/%s.ico"), *IconPartialName); FString FullTemplateFilename = FPaths::EngineContentDir() / TEXT("Editor") / TEXT("Templates") / TemplateFilename; FString OutputFilename = TemplateFilename.Replace(*IconPartialName, *GameName); FString FullOutputFilename = NewResourceFolderName / OutputFilename; bSucceeded &= SourceControlHelpers::CopyFileUnderSourceControl(FullOutputFilename, FullTemplateFilename, LOCTEXT("IconFileDescription", "icon"), OutFailReason); if(bSucceeded) { OutCreatedFiles.Add(FullOutputFilename); } // RC TemplateFilename = TEXT("Resources/Windows/_GAME_NAME_.rc"); bSucceeded &= GenerateGameResourceFile(NewResourceFolderName, TemplateFilename, GameName, OutCreatedFiles, OutFailReason); #elif PLATFORM_MAC //@todo MAC: Implement MAC version of these files... #endif return bSucceeded; } 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); 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); 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); } void GameProjectUtils::OnUpdateProjectConfirm() { UpdateProject(NULL); } void GameProjectUtils::UpdateProject(const TArray* StartupModuleNames) { const FString& ProjectFilename = FPaths::GetProjectFilePath(); const FString& ShortFilename = FPaths::GetCleanFilename(ProjectFilename); FText FailReason; FText UpdateMessage; SNotificationItem::ECompletionState NewCompletionState; if ( UpdateGameProjectFile(ProjectFilename, FDesktopPlatformModule::Get()->GetCurrentEngineIdentifier(), StartupModuleNames, FailReason) ) { // 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::OnUpdateProjectCancel() { if ( UpdateGameProjectNotification.IsValid() ) { UpdateGameProjectNotification.Pin()->SetCompletionState(SNotificationItem::CS_None); UpdateGameProjectNotification.Pin()->ExpireAndFadeout(); UpdateGameProjectNotification.Reset(); } } void GameProjectUtils::TryMakeProjectFileWriteable(const FString& ProjectFile) { // 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 TArray* StartupModuleNames, FText& OutFailReason) { // Make sure we can write to the project file TryMakeProjectFileWriteable(ProjectFile); // Load the descriptor FProjectDescriptor Descriptor; if(Descriptor.Load(ProjectFile, OutFailReason)) { // Freshen version information Descriptor.EngineAssociation = EngineIdentifier; // Replace the modules names, if specified if(StartupModuleNames != NULL) { Descriptor.Modules.Empty(); for(int32 Idx = 0; Idx < StartupModuleNames->Num(); Idx++) { Descriptor.Modules.Add(FModuleDescriptor(*(*StartupModuleNames)[Idx])); } } // Update file on disk return Descriptor.Save(ProjectFile, OutFailReason); } return false; } 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", "Source control is not enabled. Enable source 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 source 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(""); } int32 GameProjectUtils::GetProjectCodeFileCount() { TArray Filenames; IFileManager::Get().FindFilesRecursive(Filenames, *FPaths::GameSourceDir(), TEXT("*.h"), true, false, false); IFileManager::Get().FindFilesRecursive(Filenames, *FPaths::GameSourceDir(), TEXT("*.cpp"), true, false, false); return Filenames.Num(); } bool GameProjectUtils::ProjectHasCodeFiles() { return GameProjectUtils::GetProjectCodeFileCount() > 0; } bool GameProjectUtils::AddCodeToProject_Internal(const FString& NewClassName, const FString& NewClassPath, const FModuleContextInfo& ModuleInfo, const FNewClassInfo ParentClassInfo, FString& OutHeaderFilePath, FString& OutCppFilePath, FText& OutFailReason) { if ( !ParentClassInfo.IsSet() ) { OutFailReason = LOCTEXT("NoParentClass", "You must specify a parent class"); return false; } const FString CleanClassName = ParentClassInfo.GetCleanClassName(NewClassName); const FString FinalClassName = ParentClassInfo.GetFinalClassName(NewClassName); if ( !IsValidClassNameForCreation(FinalClassName, ModuleInfo, OutFailReason) ) { return false; } if ( !FApp::HasGameName() ) { OutFailReason = LOCTEXT("AddCodeToProject_NoGameName", "You can not add code because you have not loaded a project."); return false; } FString NewHeaderPath; FString NewCppPath; if ( !CalculateSourcePaths(NewClassPath, ModuleInfo, NewHeaderPath, NewCppPath, &OutFailReason) ) { return false; } const bool bAllowNewSlowTask = true; FScopedSlowTask SlowTaskMessage( LOCTEXT( "AddingCodeToProject", "Adding code to project..." ), bAllowNewSlowTask ); // If the project does not already contain code, add the primary game module TArray CreatedFiles; if ( !ProjectHasCodeFiles() ) { // 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::GetGameName(); TArray StartupModuleNames; if ( GenerateBasicSourceCode(SourceDir, GameModuleName, StartupModuleNames, CreatedFiles, OutFailReason) ) { UpdateProject(&StartupModuleNames); } else { DeleteCreatedFiles(SourceDir, CreatedFiles); return false; } } // Class Header File FString SyncLocation; const FString NewHeaderFilename = NewHeaderPath / ParentClassInfo.GetHeaderFilename(NewClassName); { if ( GenerateClassHeaderFile(NewHeaderFilename, CleanClassName, ParentClassInfo, TArray(), TEXT(""), TEXT(""), SyncLocation, ModuleInfo, OutFailReason) ) { CreatedFiles.Add(NewHeaderFilename); } else { DeleteCreatedFiles(NewHeaderPath, CreatedFiles); return false; } } // Class CPP file const FString NewCppFilename = NewCppPath / ParentClassInfo.GetSourceFilename(NewClassName); { if ( GenerateClassCPPFile(NewCppFilename, CleanClassName, ParentClassInfo, TArray(), TArray(), TEXT(""), ModuleInfo, OutFailReason) ) { CreatedFiles.Add(NewCppFilename); } else { DeleteCreatedFiles(NewCppPath, CreatedFiles); return false; } } // Generate project files if we happen to be using a project file. if ( !FModuleManager::Get().GenerateCodeProjectFiles( FPaths::GetProjectFilePath(), *GLog ) ) { OutFailReason = LOCTEXT("FailedToGenerateProjectFiles", "Failed to generate project files."); return false; } // Mark the files for add in SCC ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); if ( ISourceControlModule::Get().IsEnabled() && SourceControlProvider.IsAvailable() ) { TArray FilesToCheckOut; for ( auto FileIt = CreatedFiles.CreateConstIterator(); FileIt; ++FileIt ) { FilesToCheckOut.Add( IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(**FileIt) ); } SourceControlProvider.Execute(ISourceControlOperation::Create(), FilesToCheckOut); } OutHeaderFilePath = NewHeaderFilename; OutCppFilePath = NewCppFilename; return true; } bool GameProjectUtils::FindSourceFileInProject(const FString& InFilename, const FString& InSearchPath, FString& OutPath) { TArray Filenames; const FString FilenameWidcard = TEXT("*") + InFilename; IFileManager::Get().FindFilesRecursive(Filenames, *InSearchPath, *FilenameWidcard, 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; } #undef LOCTEXT_NAMESPACE