You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
If the module is inside the Public or Classes folder, it is included directly (since all these folders are on the include path), otherwise it's included relative to the module source root (which is now on the include paths due to a recent UBT change). ReviewedBy Ben.Donatelli [CL 2109096 by Jamie Dale in Main branch]
2718 lines
97 KiB
C++
2718 lines
97 KiB
C++
// Copyright 1998-2014 Epic Games, Inc. All Rights Reserved.
|
|
|
|
|
|
#include "GameProjectGenerationPrivatePCH.h"
|
|
#include "UnrealEdMisc.h"
|
|
#include "ISourceControlModule.h"
|
|
#include "MainFrame.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<SNotificationItem> GameProjectUtils::UpdateGameProjectNotification = NULL;
|
|
TWeakPtr<SNotificationItem> 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.
|
|
bool bIsInRootFolder = false;
|
|
if ( !FUProjectDictionary(FPaths::RootDir()).IsForeignProject(ProjectFile) )
|
|
{
|
|
// If we are in the UE4 root, just open the UE4.sln file, otherwise open the generated one.
|
|
FString AbsoluteProjectParentFolder = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*FPaths::GetPath(FPaths::GetPath(ProjectFile)));
|
|
FString AbsoluteRootPath = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*FPaths::RootDir());
|
|
|
|
if ( AbsoluteProjectParentFolder.Right(1) != TEXT("/") )
|
|
{
|
|
AbsoluteProjectParentFolder += TEXT("/");
|
|
}
|
|
|
|
if ( AbsoluteRootPath.Right(1) != TEXT("/") )
|
|
{
|
|
AbsoluteRootPath += TEXT("/");
|
|
}
|
|
|
|
bIsInRootFolder = (AbsoluteProjectParentFolder == AbsoluteRootPath);
|
|
}
|
|
|
|
FString SolutionFolder;
|
|
FString SolutionFilenameWithoutExtension;
|
|
if ( bIsInRootFolder )
|
|
{
|
|
SolutionFolder = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*FPaths::RootDir());
|
|
SolutionFilenameWithoutExtension = TEXT("UE4");
|
|
}
|
|
else
|
|
{
|
|
SolutionFolder = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*FPaths::GetPath(ProjectFile));
|
|
SolutionFilenameWithoutExtension = FPaths::GetBaseFilename(ProjectFile);
|
|
}
|
|
|
|
FString CodeSolutionFile;
|
|
#if PLATFORM_WINDOWS
|
|
CodeSolutionFile = SolutionFilenameWithoutExtension + TEXT(".sln");
|
|
#elif PLATFORM_MAC
|
|
CodeSolutionFile = SolutionFilenameWithoutExtension + TEXT(".xcodeproj");
|
|
#else
|
|
OutFailReason = LOCTEXT( "OpenCodeIDE_UnknownPlatform", "could not open the code editing IDE. The operating system is unknown." );
|
|
return false;
|
|
#endif
|
|
|
|
const FString FullPath = FPaths::Combine(*SolutionFolder, *CodeSolutionFile);
|
|
|
|
#if PLATFORM_MAC || PLATFORM_LINUX
|
|
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<FString>& 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<FString> FilesToCopy;
|
|
GetStarterContentFiles(FilesToCopy);
|
|
|
|
TArray<FString> 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<FAnalyticsEventAttribute> 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& EngineIdentifier)
|
|
{
|
|
const FString& ProjectFilename = FPaths::IsProjectFilePathSet() ? FPaths::GetProjectFilePath() : FString();
|
|
if ( !ProjectFilename.IsEmpty() )
|
|
{
|
|
FText FailReason;
|
|
bool bWasCheckedOut = false;
|
|
if ( !UpdateGameProjectFile(ProjectFilename, EngineIdentifier, NULL, bWasCheckedOut, FailReason) )
|
|
{
|
|
// The user chose to update, but the update failed. Notify the user.
|
|
UE_LOG(LogGameProjectGeneration, Error, TEXT("%s failed to update. %s"), *ProjectFilename, *FailReason.ToString() );
|
|
return false;
|
|
}
|
|
|
|
// The project was updated successfully.
|
|
UE_LOG(LogGameProjectGeneration, Log, TEXT("%s was successfully updated."), *ProjectFilename );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void GameProjectUtils::OpenAddCodeToProjectDialog()
|
|
{
|
|
TSharedRef<SWindow> 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<IMainFrameModule>(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<UClass> 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", UnusedFoundPath, ModuleInfo) )
|
|
{
|
|
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 FNewClassInfo ParentClassInfo, FString& OutHeaderFilePath, FString& OutCppFilePath, FText& OutFailReason)
|
|
{
|
|
const bool bAddCodeSuccessful = AddCodeToProject_Internal(NewClassName, NewClassPath, ParentClassInfo, OutHeaderFilePath, OutCppFilePath, OutFailReason);
|
|
|
|
if( FEngineAnalytics::IsAvailable() )
|
|
{
|
|
const FString ParentClassName = ParentClassInfo.GetClassNameCPP();
|
|
|
|
TArray<FAnalyticsEventAttribute> 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) )
|
|
{
|
|
TemplateDefs = ConstructObject<UTemplateProjectDefs>(UTemplateProjectDefs::StaticClass());
|
|
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<FString> 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<FString> 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;
|
|
}
|
|
|
|
struct FConfigValue
|
|
{
|
|
FString ConfigFile;
|
|
FString ConfigSection;
|
|
FString ConfigKey;
|
|
FString ConfigValue;
|
|
bool bShouldReplaceExistingValue;
|
|
|
|
FConfigValue(const FString& InFile, const FString& InSection, const FString& InKey, const FString& InValue, bool InShouldReplaceExistingValue)
|
|
: ConfigFile(InFile)
|
|
, ConfigSection(InSection)
|
|
, ConfigKey(InKey)
|
|
, ConfigValue(InValue)
|
|
, bShouldReplaceExistingValue(InShouldReplaceExistingValue)
|
|
{}
|
|
};
|
|
|
|
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<FString> 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<FString> CreatedFiles;
|
|
|
|
// Discover and copy all files in the src folder to the destination, excluding a few files and folders
|
|
TArray<FString> FilesToCopy;
|
|
TArray<FString> FilesThatNeedContentsReplaced;
|
|
TMap<FString, FString> 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);
|
|
}
|
|
|
|
if ( FileExtension == TEXT("h") // A header file
|
|
&& FPaths::GetBaseFilename(SrcFilename) != FPaths::GetBaseFilename(DestFilename)) // Whose name changed
|
|
{
|
|
FString FileContents;
|
|
if( ensure( FFileHelper::LoadFileToString( FileContents, *DestFilename ) ) )
|
|
{
|
|
// @todo uht: Checking file contents to see if this is a UObject class. Sort of fragile here.
|
|
if( FileContents.Contains( TEXT( ".generated.h\"" ), ESearchCase::IgnoreCase ) )
|
|
{
|
|
// Looks like a UObject header!
|
|
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<FConfigValue> ConfigValuesToSet;
|
|
const FString ActiveGameNameRedirectsValue_LongName = FString::Printf(TEXT("(OldGameName=\"/Script/%s\",NewGameName=\"/Script/%s\")"), *TemplateName, *ProjectName);
|
|
const FString ActiveGameNameRedirectsValue_ShortName = FString::Printf(TEXT("(OldGameName=\"%s\",NewGameName=\"/Script/%s\")"), *TemplateName, *ProjectName);
|
|
new (ConfigValuesToSet) FConfigValue(TEXT("DefaultEngine.ini"), TEXT("/Script/Engine.Engine"), TEXT("+ActiveGameNameRedirects"), *ActiveGameNameRedirectsValue_LongName, /*InShouldReplaceExistingValue=*/false);
|
|
new (ConfigValuesToSet) FConfigValue(TEXT("DefaultEngine.ini"), TEXT("/Script/Engine.Engine"), TEXT("+ActiveGameNameRedirects"), *ActiveGameNameRedirectsValue_ShortName, /*InShouldReplaceExistingValue=*/false);
|
|
new (ConfigValuesToSet) FConfigValue(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) FConfigValue(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 FConfigValue& ConfigValue = *ConfigIt;
|
|
const FString IniFilename = DestFolder / TEXT("Config") / ConfigValue.ConfigFile;
|
|
bool bSuccessfullyProcessed = false;
|
|
|
|
TArray<FString> 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
|
|
{
|
|
FText LocalFailReason;
|
|
if (IProjectManager::Get().DuplicateProjectFile(TemplateFile, NewProjectFile, FDesktopPlatformModule::Get()->GetCurrentEngineIdentifier(), LocalFailReason))
|
|
{
|
|
CreatedFiles.Add(NewProjectFile);
|
|
}
|
|
else
|
|
{
|
|
OutFailReason = LocalFailReason;
|
|
DeleteCreatedFiles(DestFolder, CreatedFiles);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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<FString> 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<FString>& 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<FString> 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<FString>& 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<FString> 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;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool GameProjectUtils::GenerateBasicSourceCode(const FString& NewProjectSourcePath, const FString& NewProjectName, TArray<FString>& OutGeneratedStartupModuleNames, TArray<FString>& 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<FString> PublicDependencyModuleNames;
|
|
PublicDependencyModuleNames.Add(TEXT("Core"));
|
|
PublicDependencyModuleNames.Add(TEXT("CoreUObject"));
|
|
PublicDependencyModuleNames.Add(TEXT("Engine"));
|
|
PublicDependencyModuleNames.Add(TEXT("InputCore"));
|
|
TArray<FString> 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<FString> 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<FString> 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<FString> 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<FString>& 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.ModuleSourcePath = NewProjectSourcePath;
|
|
NewModuleInfo.ModuleName = NewProjectName;
|
|
|
|
// 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<FString>(), 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<FString>(), 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<FString>(), TArray<FString>(), 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<FString> 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<FString> 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<FString> StarterContentFiles;
|
|
GetStarterContentFiles(StarterContentFiles);
|
|
|
|
return (StarterContentFiles.Num() > 0);
|
|
}
|
|
|
|
GameProjectUtils::FModuleContextInfo GameProjectUtils::GetCurrentModuleContextInfo()
|
|
{
|
|
FModuleContextInfo ModuleInfo;
|
|
|
|
ModuleInfo.ModuleSourcePath = FPaths::GameSourceDir();
|
|
|
|
// Assuming the game name is the same as the primary game module name
|
|
ModuleInfo.ModuleName = FApp::GetGameName();
|
|
|
|
return ModuleInfo;
|
|
}
|
|
|
|
FString GameProjectUtils::GetSourceRootPath(const bool bIncludeModuleName, const FModuleContextInfo& ModuleInfo)
|
|
{
|
|
FString SourceDir = ModuleInfo.ModuleSourcePath;
|
|
|
|
if(bIncludeModuleName)
|
|
{
|
|
SourceDir /= ModuleInfo.ModuleName;
|
|
}
|
|
|
|
SourceDir /= "";
|
|
|
|
return FPaths::ConvertRelativePathToFull(SourceDir);
|
|
}
|
|
|
|
bool GameProjectUtils::IsValidSourcePath(const FString& InPath, const bool bIncludeModuleName, const FModuleContextInfo& ModuleInfo, FText* const OutFailReason)
|
|
{
|
|
FString RootPath = GetSourceRootPath(bIncludeModuleName, ModuleInfo);
|
|
|
|
// Only allow partial module name matches if we already have code; the first class added to a project *must* be for the game module
|
|
const bool bHasCodeFiles = GameProjectUtils::ProjectHasCodeFiles();
|
|
if(bIncludeModuleName && bHasCodeFiles)
|
|
{
|
|
// If we're including the module name, then we want to allow variations of it, eg) MyModule, MyModuleEditor, MyModuleClient
|
|
// Those variations are valid, so we trim the last / from the path so that the StartsWith check below allows these variations
|
|
RootPath = RootPath.LeftChop(1); // Trim trailing /
|
|
}
|
|
|
|
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(RootPath))
|
|
{
|
|
if(OutFailReason)
|
|
{
|
|
FFormatNamedArguments Args;
|
|
Args.Add(TEXT("RootSourcePath"), FText::FromString(GetSourceRootPath(bIncludeModuleName, ModuleInfo)));
|
|
const FText FormatString = (bIncludeModuleName)
|
|
? LOCTEXT("SourcePathInvalidModuleRoot", "All source code must exist within a valid module folder in the projects source path, e.g. {RootSourcePath}")
|
|
: LOCTEXT("SourcePathInvalidRoot", "All source code must exist within the projects source path: {RootSourcePath}");
|
|
*OutFailReason = FText::Format( FormatString, Args );
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool GameProjectUtils::CalculateSourcePaths(const FString& InPath, FString& OutModuleName, FString& OutHeaderPath, FString& OutSourcePath, const FModuleContextInfo& ModuleInfo, FText* const OutFailReason)
|
|
{
|
|
const FString AbsoluteInPath = FPaths::ConvertRelativePathToFull(InPath) / ""; // Ensure trailing /
|
|
OutHeaderPath = AbsoluteInPath;
|
|
OutSourcePath = AbsoluteInPath;
|
|
|
|
EClassLocation ClassPathLocation = EClassLocation::UserDefined;
|
|
if(!GetClassLocation(InPath, OutModuleName, ClassPathLocation, ModuleInfo, OutFailReason))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const FString BaseRootPath = GetSourceRootPath(false/*bIncludeModuleName*/, ModuleInfo);
|
|
const FString RootPath = BaseRootPath / OutModuleName / ""; // Ensure trailing /
|
|
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, FString& OutModuleName, EClassLocation& OutClassLocation, const FModuleContextInfo& ModuleInfo, FText* const OutFailReason)
|
|
{
|
|
const FString AbsoluteInPath = FPaths::ConvertRelativePathToFull(InPath) / ""; // Ensure trailing /
|
|
OutModuleName.Empty();
|
|
OutClassLocation = EClassLocation::UserDefined;
|
|
|
|
if(!IsValidSourcePath(InPath, true/*bIncludeModuleName*/, ModuleInfo, OutFailReason))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// We've validated that this path includes a partial match for our module (eg, MyModule, MyModuleEditor, MyModuleClient)
|
|
// so extract the actual name of the module from the path so that we can generate the internal folder names correctly
|
|
const FString BaseRootPath = GetSourceRootPath(false/*bIncludeModuleName*/, ModuleInfo);
|
|
const int32 ModuleNameStartIndex = BaseRootPath.Len();
|
|
const int32 ModuleNameEndIndex = AbsoluteInPath.Find(TEXT("/"), ESearchCase::IgnoreCase, ESearchDir::FromStart, ModuleNameStartIndex);
|
|
check(ModuleNameEndIndex != INDEX_NONE); // this should never happen since AbsoluteInPath ends in a /, and we verified it started with BaseRootPath in IsValidSourcePath
|
|
OutModuleName = AbsoluteInPath.Mid(ModuleNameStartIndex, ModuleNameEndIndex - ModuleNameStartIndex);
|
|
|
|
const FString RootPath = BaseRootPath / OutModuleName / ""; // Ensure trailing /
|
|
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<FString> RootDirectoryNames;
|
|
IFileManager::Get().FindFiles(RootDirectoryNames, *(OldDirectoryName / TEXT("*")), false, true);
|
|
|
|
// Find all the source directories
|
|
TArray<FString> 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<FString> SourceFiles;
|
|
for(int32 Idx = 0; Idx < SourceDirectories.Num(); Idx++)
|
|
{
|
|
TArray<FString> 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>(UGeneralProjectSettings::StaticClass()->GetDefaultObject())->CopyrightNotice;
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
FString GameProjectUtils::MakeCommaDelimitedList(const TArray<FString>& 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<FString>& 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<FString>& 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;
|
|
{
|
|
FString ModuleName;
|
|
EClassLocation ClassPathLocation = EClassLocation::UserDefined;
|
|
if ( GetClassLocation(NewHeaderFileName, ModuleName, ClassPathLocation, ModuleInfo) )
|
|
{
|
|
// 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 = 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<FString> 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<FString>& AdditionalIncludes, const TArray<FString>& 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();
|
|
|
|
FString ModuleName;
|
|
EClassLocation ClassPathLocation = EClassLocation::UserDefined;
|
|
if ( !GetClassLocation(NewCPPFileName, ModuleName, ClassPathLocation, ModuleInfo, &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(ModuleName + ".h", ModuleIncludePath, ModuleInfo))
|
|
{
|
|
// 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
|
|
FString UnusedModuleName;
|
|
EClassLocation ModuleLocation;
|
|
if(GetClassLocation(ModuleIncludePath, UnusedModuleName, ModuleLocation, ModuleInfo))
|
|
{
|
|
if(ModuleLocation == EClassLocation::Public || ModuleLocation == EClassLocation::Classes)
|
|
{
|
|
ModuleIncludePath = 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 = ModuleName + ".h";
|
|
}
|
|
else
|
|
{
|
|
// Make the include relative to the source root of the module
|
|
const FString ModuleSourceRoot = GetSourceRootPath(true/*bIncludeModuleName*/, ModuleInfo);
|
|
|
|
// Updates ModuleIncludePath internally
|
|
if(!FPaths::MakePathRelativeTo(ModuleIncludePath, *ModuleSourceRoot))
|
|
{
|
|
// Failed; just assume we can include it without any relative path
|
|
ModuleIncludePath = ModuleName + ".h";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Failed; just assume we can include it without any relative path
|
|
ModuleIncludePath = 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 = 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%"), *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<FString>& PublicDependencyModuleNames, const TArray<FString>& 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<FString>& 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<FString>& 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<FString>* 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<FString>& 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<FString>& PublicDependencyModuleNames, const TArray<FString>& 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<FString>& 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<FString>& 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<FString>* StartupModuleNames)
|
|
{
|
|
const FString& ProjectFilename = FPaths::GetProjectFilePath();
|
|
const FString& ShortFilename = FPaths::GetCleanFilename(ProjectFilename);
|
|
FText FailReason;
|
|
FText UpdateMessage;
|
|
SNotificationItem::ECompletionState NewCompletionState;
|
|
bool bWasCheckedOut = false;
|
|
if ( UpdateGameProjectFile(ProjectFilename, FDesktopPlatformModule::Get()->GetCurrentEngineIdentifier(), StartupModuleNames, bWasCheckedOut, 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 );
|
|
if ( bWasCheckedOut )
|
|
{
|
|
UpdateMessage = FText::Format( LOCTEXT("ProjectFileUpdateCheckin", "{ShortFilename} was successfully updated. Please check this file into source control."), 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();
|
|
}
|
|
}
|
|
|
|
bool GameProjectUtils::UpdateGameProjectFile(const FString& ProjectFilename, const FString& EngineIdentifier, const TArray<FString>* StartupModuleNames, bool& OutbWasCheckedOut, FText& OutFailReason)
|
|
{
|
|
// First attempt to check out the file if SCC is enabled
|
|
if ( ISourceControlModule::Get().IsEnabled() )
|
|
{
|
|
OutbWasCheckedOut = GameProjectUtils::CheckoutGameProjectFile(ProjectFilename, OutFailReason);
|
|
if ( !OutbWasCheckedOut )
|
|
{
|
|
// Failed to check out the file
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(FPlatformFileManager::Get().GetPlatformFile().IsReadOnly(*ProjectFilename))
|
|
{
|
|
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(ProjectFilename));
|
|
if ( FMessageDialog::Open(EAppMsgType::YesNo, FText::Format(ShouldMakeProjectWriteable, Arguments)) == EAppReturnType::Yes )
|
|
{
|
|
FPlatformFileManager::Get().GetPlatformFile().SetReadOnly(*ProjectFilename, false);
|
|
}
|
|
}
|
|
OutbWasCheckedOut = false;
|
|
}
|
|
|
|
// Now tell the project manager to update the file
|
|
if (!IProjectManager::Get().UpdateLoadedProjectFileToCurrent(StartupModuleNames, EngineIdentifier, OutFailReason))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
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<FString> 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<FCheckOut>(), 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<FString> 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 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);
|
|
|
|
// Get the context info for the current project
|
|
const FModuleContextInfo ModuleInfo = GetCurrentModuleContextInfo();
|
|
|
|
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 ModuleName;
|
|
FString NewHeaderPath;
|
|
FString NewCppPath;
|
|
if ( !CalculateSourcePaths(NewClassPath, ModuleName, NewHeaderPath, NewCppPath, ModuleInfo, &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<FString> 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<FString> 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<FString>(), 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<FString>(), TArray<FString>(), 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<FString> FilesToCheckOut;
|
|
for ( auto FileIt = CreatedFiles.CreateConstIterator(); FileIt; ++FileIt )
|
|
{
|
|
FilesToCheckOut.Add( IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(**FileIt) );
|
|
}
|
|
|
|
SourceControlProvider.Execute(ISourceControlOperation::Create<FMarkForAdd>(), FilesToCheckOut);
|
|
}
|
|
|
|
OutHeaderFilePath = NewHeaderFilename;
|
|
OutCppFilePath = NewCppFilename;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool GameProjectUtils::FindSourceFileInProject(const FString& InFilename, FString& OutPath, const FModuleContextInfo& ModuleInfo)
|
|
{
|
|
TArray<FString> Filenames;
|
|
const FString FilenameWidcard = TEXT("*") + InFilename;
|
|
IFileManager::Get().FindFilesRecursive(Filenames, *ModuleInfo.ModuleSourcePath, *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
|