782 lines
35 KiB
C++
782 lines
35 KiB
C++
|
|
// Copyright Epic Games, Inc. All Rights Reserved.
|
||
|
|
|
||
|
|
/*=============================================================================
|
||
|
|
PackFactory.cpp: Factory for importing asset and feature packs
|
||
|
|
=============================================================================*/
|
||
|
|
|
||
|
|
#include "Factories/PackFactory.h"
|
||
|
|
#include "HAL/PlatformFilemanager.h"
|
||
|
|
#include "Misc/MessageDialog.h"
|
||
|
|
#include "HAL/FileManager.h"
|
||
|
|
#include "Misc/FileHelper.h"
|
||
|
|
#include "Misc/Paths.h"
|
||
|
|
#include "Serialization/MemoryWriter.h"
|
||
|
|
#include "Serialization/MemoryReader.h"
|
||
|
|
#include "Misc/ConfigCacheIni.h"
|
||
|
|
#include "Misc/FeedbackContext.h"
|
||
|
|
#include "Misc/App.h"
|
||
|
|
#include "Modules/ModuleManager.h"
|
||
|
|
#include "UObject/UnrealType.h"
|
||
|
|
#include "UObject/PropertyPortFlags.h"
|
||
|
|
#include "UObject/LinkerLoad.h"
|
||
|
|
#include "Framework/Application/SlateApplication.h"
|
||
|
|
#include "Engine/Engine.h"
|
||
|
|
#include "SourceControlHelpers.h"
|
||
|
|
#include "ISourceControlModule.h"
|
||
|
|
#include "Settings/EditorLoadingSavingSettings.h"
|
||
|
|
#include "GameFramework/PlayerInput.h"
|
||
|
|
#include "GameFramework/InputSettings.h"
|
||
|
|
#include "IPlatformFilePak.h"
|
||
|
|
#include "SourceCodeNavigation.h"
|
||
|
|
#include "Misc/HotReloadInterface.h"
|
||
|
|
#include "Misc/AES.h"
|
||
|
|
#include "GameProjectGenerationModule.h"
|
||
|
|
#include "Dialogs/SOutputLogDialog.h"
|
||
|
|
#include "Templates/UniquePtr.h"
|
||
|
|
#include "Logging/MessageLog.h"
|
||
|
|
#include "Misc/CoreDelegates.h"
|
||
|
|
|
||
|
|
#if WITH_LIVE_CODING
|
||
|
|
#include "ILiveCodingModule.h"
|
||
|
|
#endif
|
||
|
|
|
||
|
|
DEFINE_LOG_CATEGORY_STATIC(LogPackFactory, Log, All);
|
||
|
|
|
||
|
|
UPackFactory::UPackFactory(const FObjectInitializer& PCIP)
|
||
|
|
: Super(PCIP)
|
||
|
|
{
|
||
|
|
// Since this factory can output multiple and any number of class it doesn't really have a
|
||
|
|
// SupportedClass per say, but one must be defined, so we just reference ourself
|
||
|
|
SupportedClass = UPackFactory::StaticClass();
|
||
|
|
|
||
|
|
Formats.Add(TEXT("upack;Asset Pack"));
|
||
|
|
Formats.Add(TEXT("upack;Feature Pack"));
|
||
|
|
|
||
|
|
bEditorImport = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
namespace PackFactoryHelper
|
||
|
|
{
|
||
|
|
// Utility function to copy a single pak entry out of the Source archive and in to the Destination archive using Buffer as temporary space
|
||
|
|
bool BufferedCopyFile(FArchive& DestAr, FArchive& Source, const FPakEntry& Entry, TArray<uint8>& Buffer, const FPakFile& PakFile)
|
||
|
|
{
|
||
|
|
// Align down
|
||
|
|
const int64 BufferSize = Buffer.Num() & ~(FAES::AESBlockSize - 1);
|
||
|
|
int64 RemainingSizeToCopy = Entry.Size;
|
||
|
|
while (RemainingSizeToCopy > 0)
|
||
|
|
{
|
||
|
|
const int64 SizeToCopy = FMath::Min(BufferSize, RemainingSizeToCopy);
|
||
|
|
// If file is encrypted so we need to account for padding
|
||
|
|
int64 SizeToRead = Entry.IsEncrypted() ? Align(SizeToCopy, FAES::AESBlockSize) : SizeToCopy;
|
||
|
|
|
||
|
|
Source.Serialize(Buffer.GetData(), SizeToRead);
|
||
|
|
if (Entry.IsEncrypted())
|
||
|
|
{
|
||
|
|
FAES::FAESKey Key;
|
||
|
|
FPakPlatformFile::GetPakEncryptionKey(Key, PakFile.GetInfo().EncryptionKeyGuid);
|
||
|
|
checkf(Key.IsValid(), TEXT("Trying to copy an encrypted file between pak files, but no decryption key is available"));
|
||
|
|
FAES::DecryptData(Buffer.GetData(), SizeToRead, Key);
|
||
|
|
}
|
||
|
|
DestAr.Serialize(Buffer.GetData(), SizeToCopy);
|
||
|
|
RemainingSizeToCopy -= SizeToRead;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Utility function to uncompress and copy a single pak entry out of the Source archive and in to the Destination archive using PersistentBuffer as temporary space
|
||
|
|
bool UncompressCopyFile(FArchive& DestAr, FArchive& Source, const FPakEntry& Entry, TArray<uint8>& PersistentBuffer, const FPakFile& PakFile)
|
||
|
|
{
|
||
|
|
if (Entry.UncompressedSize == 0)
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
int64 WorkingSize = Entry.CompressionBlockSize;
|
||
|
|
int32 MaxCompressionBlockSize = FCompression::CompressMemoryBound(PakFile.GetInfo().GetCompressionMethod(Entry.CompressionMethodIndex), WorkingSize);
|
||
|
|
WorkingSize += MaxCompressionBlockSize;
|
||
|
|
if (PersistentBuffer.Num() < WorkingSize)
|
||
|
|
{
|
||
|
|
PersistentBuffer.SetNumUninitialized(WorkingSize);
|
||
|
|
}
|
||
|
|
|
||
|
|
uint8* UncompressedBuffer = PersistentBuffer.GetData() + MaxCompressionBlockSize;
|
||
|
|
|
||
|
|
for (uint32 BlockIndex = 0, BlockIndexNum = Entry.CompressionBlocks.Num(); BlockIndex < BlockIndexNum; ++BlockIndex)
|
||
|
|
{
|
||
|
|
uint32 CompressedBlockSize = Entry.CompressionBlocks[BlockIndex].CompressedEnd - Entry.CompressionBlocks[BlockIndex].CompressedStart;
|
||
|
|
uint32 UncompressedBlockSize = (uint32)FMath::Min<int64>(Entry.UncompressedSize - Entry.CompressionBlockSize * BlockIndex, Entry.CompressionBlockSize);
|
||
|
|
Source.Seek(Entry.CompressionBlocks[BlockIndex].CompressedStart + (PakFile.GetInfo().HasRelativeCompressedChunkOffsets() ? Entry.Offset : 0));
|
||
|
|
uint32 SizeToRead = Entry.IsEncrypted() ? Align(CompressedBlockSize, FAES::AESBlockSize) : CompressedBlockSize;
|
||
|
|
Source.Serialize(PersistentBuffer.GetData(), SizeToRead);
|
||
|
|
|
||
|
|
if (Entry.IsEncrypted())
|
||
|
|
{
|
||
|
|
FAES::FAESKey Key;
|
||
|
|
FPakPlatformFile::GetPakEncryptionKey(Key, PakFile.GetInfo().EncryptionKeyGuid);
|
||
|
|
checkf(Key.IsValid(), TEXT("Trying to copy an encrypted file between pak files, but no decryption key is available"));
|
||
|
|
FAES::DecryptData(PersistentBuffer.GetData(), SizeToRead, Key);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!FCompression::UncompressMemory(PakFile.GetInfo().GetCompressionMethod(Entry.CompressionMethodIndex), UncompressedBuffer, UncompressedBlockSize, PersistentBuffer.GetData(), CompressedBlockSize))
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
DestAr.Serialize(UncompressedBuffer, UncompressedBlockSize);
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Utility function to extract a pak entry out of the memory reader containing the pak file and place in the destination archive.
|
||
|
|
// Uses Buffer or PersistentCompressionBuffer depending on whether the entry is compressed or not.
|
||
|
|
void ExtractFile(const FPakEntry& Entry, FBufferReader& PakReader, TArray<uint8>& Buffer, TArray<uint8>& PersistentCompressionBuffer, FArchive& DestAr, const FPakFile& PakFile)
|
||
|
|
{
|
||
|
|
// 0 is uncompressed
|
||
|
|
if (Entry.CompressionMethodIndex == 0)
|
||
|
|
{
|
||
|
|
PackFactoryHelper::BufferedCopyFile(DestAr, PakReader, Entry, Buffer, PakFile);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
PackFactoryHelper::UncompressCopyFile(DestAr, PakReader, Entry, PersistentCompressionBuffer, PakFile);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Utility function to extract a pak entry out of the memory reader containing the pak file and place in a string.
|
||
|
|
// Uses Buffer or PersistentCompressionBuffer depending on whether the entry is compressed or not.
|
||
|
|
void ExtractFileToString(const FPakEntry& Entry, FBufferReader& PakReader, TArray<uint8>& Buffer, TArray<uint8>& PersistentCompressionBuffer, FString& FileContents, const FPakFile& PakFile)
|
||
|
|
{
|
||
|
|
TArray<uint8> Contents;
|
||
|
|
FMemoryWriter MemWriter(Contents);
|
||
|
|
|
||
|
|
ExtractFile(Entry, PakReader, Buffer, PersistentCompressionBuffer, MemWriter, PakFile);
|
||
|
|
|
||
|
|
// Add a line feed at the end because the FString archive read will consume the last byte
|
||
|
|
Contents.Add('\n');
|
||
|
|
|
||
|
|
// Insert the length of the string to the front of the memory chunk so we can use FString archive read
|
||
|
|
const int32 StringLength = Contents.Num();
|
||
|
|
Contents.InsertUninitialized(0, sizeof(int32));
|
||
|
|
*(reinterpret_cast<int32*>(Contents.GetData())) = StringLength;
|
||
|
|
|
||
|
|
FMemoryReader MemReader(Contents);
|
||
|
|
MemReader << FileContents;
|
||
|
|
}
|
||
|
|
|
||
|
|
struct FPackConfigParameters
|
||
|
|
{
|
||
|
|
FPackConfigParameters()
|
||
|
|
: bContainsSource(false), bCompileSource(true)
|
||
|
|
{
|
||
|
|
}
|
||
|
|
|
||
|
|
uint8 bContainsSource : 1;
|
||
|
|
uint8 bCompileSource : 1;
|
||
|
|
FString GameName;
|
||
|
|
FString InstallMessage;
|
||
|
|
TArray<FString> AdditionalFilesToAdd;
|
||
|
|
};
|
||
|
|
|
||
|
|
// Takes a string that represents the contents of a config file and sets up the supported config properties based on it
|
||
|
|
// Currently we support Action and Axis Mappings and a GameName (for setting up redirects)
|
||
|
|
void ProcessPackConfig(const FString& ConfigString, FPackConfigParameters& ConfigParameters)
|
||
|
|
{
|
||
|
|
FConfigFile PackConfig;
|
||
|
|
PackConfig.ProcessInputFileContents(ConfigString);
|
||
|
|
|
||
|
|
// Input Settings
|
||
|
|
static FArrayProperty* ActionMappingsProp = FindFieldChecked<FArrayProperty>(UInputSettings::StaticClass(), UInputSettings::GetActionMappingsPropertyName());
|
||
|
|
static FArrayProperty* AxisMappingsProp = FindFieldChecked<FArrayProperty>(UInputSettings::StaticClass(), UInputSettings::GetAxisMappingsPropertyName());
|
||
|
|
|
||
|
|
UInputSettings* InputSettingsCDO = GetMutableDefault<UInputSettings>();
|
||
|
|
bool bCheckedOut = false;
|
||
|
|
|
||
|
|
FConfigSection* InputSettingsSection = PackConfig.Find("InputSettings");
|
||
|
|
if (InputSettingsSection)
|
||
|
|
{
|
||
|
|
TArray<FInputActionKeyMapping> ActionMappingsToAdd;
|
||
|
|
TArray<FInputAxisKeyMapping> AxisMappingsToAdd;
|
||
|
|
|
||
|
|
for (auto SettingPair: *InputSettingsSection)
|
||
|
|
{
|
||
|
|
|
||
|
|
if (SettingPair.Key.ToString().Contains("ActionMappings"))
|
||
|
|
{
|
||
|
|
FInputActionKeyMapping ActionKeyMapping;
|
||
|
|
ActionMappingsProp->Inner->ImportText(*SettingPair.Value.GetValue(), &ActionKeyMapping, PPF_None, nullptr);
|
||
|
|
|
||
|
|
if (!InputSettingsCDO->DoesActionExist(ActionKeyMapping.ActionName))
|
||
|
|
{
|
||
|
|
ActionMappingsToAdd.Add(ActionKeyMapping);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else if (SettingPair.Key.ToString().Contains("AxisMappings"))
|
||
|
|
{
|
||
|
|
FInputAxisKeyMapping AxisKeyMapping;
|
||
|
|
AxisMappingsProp->Inner->ImportText(*SettingPair.Value.GetValue(), &AxisKeyMapping, PPF_None, nullptr);
|
||
|
|
|
||
|
|
if (!InputSettingsCDO->DoesAxisExist(AxisKeyMapping.AxisName))
|
||
|
|
{
|
||
|
|
AxisMappingsToAdd.Add(AxisKeyMapping);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (ActionMappingsToAdd.Num() > 0 || AxisMappingsToAdd.Num() > 0)
|
||
|
|
{
|
||
|
|
if (ISourceControlModule::Get().IsEnabled())
|
||
|
|
{
|
||
|
|
FText ErrorMessage;
|
||
|
|
|
||
|
|
const FString InputSettingsFilename = FPaths::ConvertRelativePathToFull(InputSettingsCDO->GetDefaultConfigFilename());
|
||
|
|
if (!SourceControlHelpers::CheckoutOrMarkForAdd(InputSettingsFilename, FText::FromString(InputSettingsFilename), NULL, ErrorMessage))
|
||
|
|
{
|
||
|
|
UE_LOG(LogPackFactory, Error, TEXT("%s"), *ErrorMessage.ToString());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const FInputActionKeyMapping& ActionKeyMapping: ActionMappingsToAdd)
|
||
|
|
{
|
||
|
|
InputSettingsCDO->AddActionMapping(ActionKeyMapping);
|
||
|
|
}
|
||
|
|
for (const FInputAxisKeyMapping& AxisKeyMapping: AxisMappingsToAdd)
|
||
|
|
{
|
||
|
|
InputSettingsCDO->AddAxisMapping(AxisKeyMapping);
|
||
|
|
}
|
||
|
|
|
||
|
|
InputSettingsCDO->SaveKeyMappings();
|
||
|
|
InputSettingsCDO->UpdateDefaultConfigFile();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
FConfigSection* RedirectsSection = PackConfig.Find("Redirects");
|
||
|
|
if (RedirectsSection)
|
||
|
|
{
|
||
|
|
if (FConfigValue* GameName = RedirectsSection->Find("GameName"))
|
||
|
|
{
|
||
|
|
ConfigParameters.GameName = GameName->GetValue();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
FConfigSection* AdditionalFilesSection = PackConfig.Find("AdditionalFilesToAdd");
|
||
|
|
if (AdditionalFilesSection)
|
||
|
|
{
|
||
|
|
for (auto FilePair: *AdditionalFilesSection)
|
||
|
|
{
|
||
|
|
if (FilePair.Key.ToString().Contains("Files"))
|
||
|
|
{
|
||
|
|
FString Filename = FPaths::GetCleanFilename(FilePair.Value.GetValue());
|
||
|
|
FString Directory = FPaths::RootDir() / FPaths::GetPath(FilePair.Value.GetValue());
|
||
|
|
FPaths::MakeStandardFilename(Directory);
|
||
|
|
FPakFile::MakeDirectoryFromPath(Directory);
|
||
|
|
|
||
|
|
if (Filename.Contains(TEXT("*")))
|
||
|
|
{
|
||
|
|
TArray<FString> FoundFiles;
|
||
|
|
IFileManager::Get().FindFilesRecursive(FoundFiles, *Directory, *Filename, true, false);
|
||
|
|
ConfigParameters.AdditionalFilesToAdd.Append(FoundFiles);
|
||
|
|
if (!ConfigParameters.bContainsSource)
|
||
|
|
{
|
||
|
|
for (const FString& FoundFile: FoundFiles)
|
||
|
|
{
|
||
|
|
if (FoundFile.StartsWith(TEXT("Source/")) || FoundFile.Contains(TEXT("/Source/")))
|
||
|
|
{
|
||
|
|
ConfigParameters.bContainsSource = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
ConfigParameters.AdditionalFilesToAdd.Add(Directory / Filename);
|
||
|
|
if (!ConfigParameters.bContainsSource && (ConfigParameters.AdditionalFilesToAdd.Last().StartsWith(TEXT("Source/")) || ConfigParameters.AdditionalFilesToAdd.Last().Contains(TEXT("/Source/"))))
|
||
|
|
{
|
||
|
|
ConfigParameters.bContainsSource = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
FConfigSection* FeaturePackSettingsSection = PackConfig.Find("FeaturePackSettings");
|
||
|
|
if (FeaturePackSettingsSection)
|
||
|
|
{
|
||
|
|
if (FConfigValue* CompileSource = FeaturePackSettingsSection->Find("CompileSource"))
|
||
|
|
{
|
||
|
|
ConfigParameters.bCompileSource = FCString::ToBool(*CompileSource->GetValue());
|
||
|
|
}
|
||
|
|
if (FConfigValue* InstallMessage = FeaturePackSettingsSection->Find("InstallMessage"))
|
||
|
|
{
|
||
|
|
ConfigParameters.InstallMessage = InstallMessage->GetValue();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} // namespace PackFactoryHelper
|
||
|
|
|
||
|
|
UObject* UPackFactory::FactoryCreateBinary(
|
||
|
|
UClass* Class,
|
||
|
|
UObject* InParent,
|
||
|
|
FName Name,
|
||
|
|
EObjectFlags Flags,
|
||
|
|
UObject* Context,
|
||
|
|
const TCHAR* FileType,
|
||
|
|
const uint8*& Buffer,
|
||
|
|
const uint8* BufferEnd,
|
||
|
|
FFeedbackContext* Warn)
|
||
|
|
{
|
||
|
|
FBufferReader PakReader((void*)Buffer, BufferEnd - Buffer, false);
|
||
|
|
TRefCountPtr<FPakFile> PakFilePtr = new FPakFile(&PakReader);
|
||
|
|
FPakFile& PakFile = *PakFilePtr;
|
||
|
|
|
||
|
|
UObject* ReturnAsset = nullptr;
|
||
|
|
|
||
|
|
if (PakFile.IsValid() && PakFile.HasFilenames())
|
||
|
|
{
|
||
|
|
static FString ContentFolder(TEXT("/Content/"));
|
||
|
|
FString ContentDestinationRoot = FPaths::ProjectContentDir();
|
||
|
|
|
||
|
|
const int32 ChopIndex = PakFile.GetMountPoint().Find(ContentFolder);
|
||
|
|
if (ChopIndex != INDEX_NONE)
|
||
|
|
{
|
||
|
|
ContentDestinationRoot /= PakFile.GetMountPoint().RightChop(ChopIndex + ContentFolder.Len());
|
||
|
|
}
|
||
|
|
|
||
|
|
TArray<uint8> CopyBuffer;
|
||
|
|
TArray<uint8> PersistentCompressionBuffer;
|
||
|
|
CopyBuffer.AddUninitialized(8 * 1024 * 1024); // 8MB buffer for extracting
|
||
|
|
int32 ErrorCount = 0;
|
||
|
|
int32 FileCount = 0;
|
||
|
|
|
||
|
|
FModuleContextInfo SourceModuleInfo;
|
||
|
|
PackFactoryHelper::FPackConfigParameters ConfigParameters;
|
||
|
|
|
||
|
|
TArray<FString> WrittenFiles;
|
||
|
|
TArray<FString> WrittenSourceFiles;
|
||
|
|
|
||
|
|
// Process the config files and identify if we have source files
|
||
|
|
for (FPakFile::FPakEntryIterator It(PakFile); It; ++It, ++FileCount)
|
||
|
|
{
|
||
|
|
const FString* EntryFilename = It.TryGetFilename();
|
||
|
|
check(EntryFilename);
|
||
|
|
if (EntryFilename->StartsWith(TEXT("Config/")) || EntryFilename->Contains(TEXT("/Config/")))
|
||
|
|
{
|
||
|
|
const FPakEntry& Entry = It.Info();
|
||
|
|
PakReader.Seek(Entry.Offset);
|
||
|
|
FPakEntry EntryInfo;
|
||
|
|
EntryInfo.Serialize(PakReader, PakFile.GetInfo().Version);
|
||
|
|
|
||
|
|
if (EntryInfo.IndexDataEquals(Entry))
|
||
|
|
{
|
||
|
|
FString ConfigString;
|
||
|
|
PackFactoryHelper::ExtractFileToString(Entry, PakReader, CopyBuffer, PersistentCompressionBuffer, ConfigString, PakFile);
|
||
|
|
PackFactoryHelper::ProcessPackConfig(ConfigString, ConfigParameters);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
UE_LOG(LogPackFactory, Error, TEXT("Index data mismatch for entry: \"%s\"."), **EntryFilename);
|
||
|
|
ErrorCount++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else if (!ConfigParameters.bContainsSource && (EntryFilename->StartsWith(TEXT("Source/")) || EntryFilename->Contains(TEXT("/Source/"))))
|
||
|
|
{
|
||
|
|
ConfigParameters.bContainsSource = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
bool bProjectHadSourceFiles = false;
|
||
|
|
|
||
|
|
// If we have source files, set up the project files if necessary and the game name redirects for blueprints saved with class
|
||
|
|
// references to the module name from the source template
|
||
|
|
if (ConfigParameters.bContainsSource)
|
||
|
|
{
|
||
|
|
FGameProjectGenerationModule& GameProjectModule = FModuleManager::LoadModuleChecked<FGameProjectGenerationModule>(TEXT("GameProjectGeneration"));
|
||
|
|
bProjectHadSourceFiles = GameProjectModule.Get().ProjectHasCodeFiles();
|
||
|
|
|
||
|
|
if (!bProjectHadSourceFiles)
|
||
|
|
{
|
||
|
|
TArray<FString> StartupModuleNames;
|
||
|
|
TArray<FString> CreatedFiles;
|
||
|
|
FText OutFailReason;
|
||
|
|
if (GameProjectModule.Get().GenerateBasicSourceCode(CreatedFiles, OutFailReason))
|
||
|
|
{
|
||
|
|
WrittenFiles.Append(CreatedFiles);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
UE_LOG(LogPackFactory, Error, TEXT("Unable to create basic source code: '%s'"), *OutFailReason.ToString());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const FModuleContextInfo& ModuleInfo: GameProjectModule.Get().GetCurrentProjectModules())
|
||
|
|
{
|
||
|
|
// Pick the module to insert the code in. For now always pick the first Runtime module
|
||
|
|
if (ModuleInfo.ModuleType == EHostType::Runtime)
|
||
|
|
{
|
||
|
|
SourceModuleInfo = ModuleInfo;
|
||
|
|
|
||
|
|
// Setup the game name redirect
|
||
|
|
if (!ConfigParameters.GameName.IsEmpty())
|
||
|
|
{
|
||
|
|
const FString EngineIniFilename = FPaths::ConvertRelativePathToFull(GetDefault<UEngine>()->GetDefaultConfigFilename());
|
||
|
|
|
||
|
|
if (ISourceControlModule::Get().IsEnabled())
|
||
|
|
{
|
||
|
|
FText ErrorMessage;
|
||
|
|
|
||
|
|
if (!SourceControlHelpers::CheckoutOrMarkForAdd(EngineIniFilename, FText::FromString(EngineIniFilename), NULL, ErrorMessage))
|
||
|
|
{
|
||
|
|
UE_LOG(LogPackFactory, Error, TEXT("%s"), *ErrorMessage.ToString());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const FString RedirectsSection(TEXT("/Script/Engine.Engine"));
|
||
|
|
const FString LongOldGameName = FString::Printf(TEXT("/Script/%s"), *ConfigParameters.GameName);
|
||
|
|
const FString LongNewGameName = FString::Printf(TEXT("/Script/%s"), *ModuleInfo.ModuleName);
|
||
|
|
|
||
|
|
FConfigCacheIni Config(EConfigCacheType::Temporary);
|
||
|
|
FConfigFile& NewFile = Config.Add(EngineIniFilename, FConfigFile());
|
||
|
|
FConfigCacheIni::LoadLocalIniFile(NewFile, TEXT("DefaultEngine"), false);
|
||
|
|
FConfigSection* PackageRedirects = Config.GetSectionPrivate(*RedirectsSection, true, false, EngineIniFilename);
|
||
|
|
|
||
|
|
PackageRedirects->Add(TEXT("+ActiveGameNameRedirects"), FString::Printf(TEXT("(OldGameName=\"%s\",NewGameName=\"%s\")"), *LongOldGameName, *LongNewGameName));
|
||
|
|
PackageRedirects->Add(TEXT("+ActiveGameNameRedirects"), FString::Printf(TEXT("(OldGameName=\"%s\",NewGameName=\"%s\")"), *ConfigParameters.GameName, *LongNewGameName));
|
||
|
|
|
||
|
|
NewFile.UpdateSections(*EngineIniFilename, *RedirectsSection);
|
||
|
|
|
||
|
|
FString FinalIniFileName;
|
||
|
|
GConfig->LoadGlobalIniFile(FinalIniFileName, *RedirectsSection, NULL, true);
|
||
|
|
|
||
|
|
FLinkerLoad::AddGameNameRedirect(*LongOldGameName, *LongNewGameName);
|
||
|
|
FLinkerLoad::AddGameNameRedirect(*ConfigParameters.GameName, *LongNewGameName);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Process everything else and copy out to disk
|
||
|
|
for (FPakFile::FPakEntryIterator It(PakFile); It; ++It, ++FileCount)
|
||
|
|
{
|
||
|
|
const FString* EntryFilename = It.TryGetFilename();
|
||
|
|
check(EntryFilename);
|
||
|
|
// config files already handled
|
||
|
|
if (EntryFilename->StartsWith(TEXT("Config/")) || EntryFilename->Contains(TEXT("/Config/")))
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Media and manifest files don't get written out as part of the install
|
||
|
|
if (EntryFilename->Contains(TEXT("manifest.json")) || EntryFilename->StartsWith(TEXT("Media/")) || EntryFilename->Contains(TEXT("/Media/")))
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
const FPakEntry& Entry = It.Info();
|
||
|
|
PakReader.Seek(Entry.Offset);
|
||
|
|
FPakEntry EntryInfo;
|
||
|
|
EntryInfo.Serialize(PakReader, PakFile.GetInfo().Version);
|
||
|
|
|
||
|
|
if (EntryInfo.IndexDataEquals(Entry))
|
||
|
|
{
|
||
|
|
if (EntryFilename->StartsWith(TEXT("Source/")) || EntryFilename->Contains(TEXT("/Source/")))
|
||
|
|
{
|
||
|
|
FString DestFilename = *EntryFilename;
|
||
|
|
if (DestFilename.StartsWith(TEXT("Source/")))
|
||
|
|
{
|
||
|
|
DestFilename.RightChopInline(7, false);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
const int32 SourceIndex = DestFilename.Find(TEXT("/Source/"));
|
||
|
|
if (SourceIndex != INDEX_NONE)
|
||
|
|
{
|
||
|
|
DestFilename.RightChopInline(SourceIndex + 8, false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
DestFilename = SourceModuleInfo.ModuleSourcePath / DestFilename;
|
||
|
|
UE_LOG(LogPackFactory, Log, TEXT("%s (%ld) -> %s"), **EntryFilename, Entry.Size, *DestFilename);
|
||
|
|
|
||
|
|
FString SourceContents;
|
||
|
|
PackFactoryHelper::ExtractFileToString(Entry, PakReader, CopyBuffer, PersistentCompressionBuffer, SourceContents, PakFile);
|
||
|
|
|
||
|
|
FGameProjectGenerationModule& GameProjectModule = FModuleManager::LoadModuleChecked<FGameProjectGenerationModule>(TEXT("GameProjectGeneration"));
|
||
|
|
|
||
|
|
// Add the PCH for the project above the default pack include
|
||
|
|
const FString StringToReplace = FString::Printf(TEXT("%s.h"), *ConfigParameters.GameName);
|
||
|
|
const FString StringToReplaceWith = FString::Printf(TEXT("%s\"%s#include \"%s"),
|
||
|
|
*GameProjectModule.Get().DetermineModuleIncludePath(SourceModuleInfo, DestFilename),
|
||
|
|
LINE_TERMINATOR,
|
||
|
|
*StringToReplace);
|
||
|
|
|
||
|
|
if (FFileHelper::SaveStringToFile(SourceContents, *DestFilename))
|
||
|
|
{
|
||
|
|
WrittenFiles.Add(*DestFilename);
|
||
|
|
WrittenSourceFiles.Add(*DestFilename);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
UE_LOG(LogPackFactory, Error, TEXT("Unable to write file \"%s\"."), *DestFilename);
|
||
|
|
++ErrorCount;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
FString DestFilename = *EntryFilename;
|
||
|
|
if (DestFilename.StartsWith(TEXT("Content/")))
|
||
|
|
{
|
||
|
|
DestFilename.RightChopInline(8, false);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
const int32 ContentIndex = DestFilename.Find(ContentFolder);
|
||
|
|
if (ContentIndex != INDEX_NONE)
|
||
|
|
{
|
||
|
|
DestFilename.RightChopInline(ContentIndex + 9, false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
DestFilename = ContentDestinationRoot / DestFilename;
|
||
|
|
UE_LOG(LogPackFactory, Log, TEXT("%s (%ld) -> %s"), **EntryFilename, Entry.Size, *DestFilename);
|
||
|
|
|
||
|
|
TUniquePtr<FArchive> FileHandle(IFileManager::Get().CreateFileWriter(*DestFilename));
|
||
|
|
|
||
|
|
if (FileHandle)
|
||
|
|
{
|
||
|
|
PackFactoryHelper::ExtractFile(Entry, PakReader, CopyBuffer, PersistentCompressionBuffer, *FileHandle, PakFile);
|
||
|
|
WrittenFiles.Add(*DestFilename);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
UE_LOG(LogPackFactory, Error, TEXT("Unable to create file \"%s\"."), *DestFilename);
|
||
|
|
++ErrorCount;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
UE_LOG(LogPackFactory, Error, TEXT("Index data mismatch for entry: \"%s\"."), **EntryFilename);
|
||
|
|
ErrorCount++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
UE_LOG(LogPackFactory, Log, TEXT("Finished extracting %d files (including %d errors)."), FileCount, ErrorCount);
|
||
|
|
|
||
|
|
if (ConfigParameters.AdditionalFilesToAdd.Num() > 0)
|
||
|
|
{
|
||
|
|
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
|
||
|
|
|
||
|
|
for (const FString& FileToCopy: ConfigParameters.AdditionalFilesToAdd)
|
||
|
|
{
|
||
|
|
if (FileToCopy.StartsWith(TEXT("Source/")) || FileToCopy.Contains(TEXT("/Source/")))
|
||
|
|
{
|
||
|
|
FString DestFilename = FileToCopy;
|
||
|
|
if (DestFilename.StartsWith(TEXT("Source/")))
|
||
|
|
{
|
||
|
|
DestFilename.RightChopInline(7, false);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
const int32 SourceIndex = DestFilename.Find(TEXT("/Source/"));
|
||
|
|
if (SourceIndex != INDEX_NONE)
|
||
|
|
{
|
||
|
|
DestFilename.RightChopInline(SourceIndex + 8, false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
DestFilename = SourceModuleInfo.ModuleSourcePath / DestFilename;
|
||
|
|
|
||
|
|
FString DestDirectory = FPaths::GetPath(DestFilename);
|
||
|
|
|
||
|
|
if (PlatformFile.CreateDirectoryTree(*DestDirectory))
|
||
|
|
{
|
||
|
|
FString SourceContents;
|
||
|
|
if (FFileHelper::LoadFileToString(SourceContents, *FileToCopy))
|
||
|
|
{
|
||
|
|
FGameProjectGenerationModule& GameProjectModule = FModuleManager::LoadModuleChecked<FGameProjectGenerationModule>(TEXT("GameProjectGeneration"));
|
||
|
|
|
||
|
|
// Add the PCH for the project above the default pack include
|
||
|
|
const FString StringToReplace = FString::Printf(TEXT("%s.h"), *ConfigParameters.GameName);
|
||
|
|
const FString StringToReplaceWith = FString::Printf(TEXT("%s\"%s#include \"%s"),
|
||
|
|
*GameProjectModule.Get().DetermineModuleIncludePath(SourceModuleInfo, DestFilename),
|
||
|
|
LINE_TERMINATOR,
|
||
|
|
*StringToReplace);
|
||
|
|
|
||
|
|
SourceContents = SourceContents.Replace(*StringToReplace, *StringToReplaceWith, ESearchCase::CaseSensitive);
|
||
|
|
|
||
|
|
if (FFileHelper::SaveStringToFile(SourceContents, *DestFilename))
|
||
|
|
{
|
||
|
|
WrittenFiles.Add(*DestFilename);
|
||
|
|
WrittenSourceFiles.Add(*DestFilename);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
UE_LOG(LogPackFactory, Error, TEXT("Unable to write file \"%s\"."), *DestFilename);
|
||
|
|
++ErrorCount;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
UE_LOG(LogPackFactory, Error, TEXT("Unable to read file \"%s\"."), *FileToCopy);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
FString DestFilename = FileToCopy;
|
||
|
|
if (DestFilename.StartsWith(TEXT("Content/")))
|
||
|
|
{
|
||
|
|
DestFilename.RightChopInline(8, false);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
const int32 ContentIndex = DestFilename.Find(ContentFolder);
|
||
|
|
if (ContentIndex != INDEX_NONE)
|
||
|
|
{
|
||
|
|
DestFilename.RightChopInline(ContentIndex + 9, false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
DestFilename = ContentDestinationRoot / DestFilename;
|
||
|
|
|
||
|
|
FString DestDirectory = FPaths::GetPath(DestFilename);
|
||
|
|
|
||
|
|
if (PlatformFile.CreateDirectoryTree(*DestDirectory))
|
||
|
|
{
|
||
|
|
if (PlatformFile.CopyFile(*DestFilename, *FileToCopy))
|
||
|
|
{
|
||
|
|
WrittenFiles.Add(DestFilename);
|
||
|
|
UE_LOG(LogPackFactory, Log, TEXT("Copied \"%s\" to \"%s\""), *FileToCopy, *DestFilename);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
UE_LOG(LogPackFactory, Error, TEXT("Unable to copy file \"%s\" to \"%s\"."), *FileToCopy, *DestFilename);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
UE_LOG(LogPackFactory, Error, TEXT("Unable to create directory \"%s\"."), *FileToCopy, *DestFilename);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (WrittenFiles.Num() > 0)
|
||
|
|
{
|
||
|
|
// If we wrote out source files, kick off the hot reload process
|
||
|
|
if (WrittenSourceFiles.Num() > 0)
|
||
|
|
{
|
||
|
|
// Update the game projects before we attempt to build
|
||
|
|
FGameProjectGenerationModule& GameProjectModule = FModuleManager::LoadModuleChecked<FGameProjectGenerationModule>(TEXT("GameProjectGeneration"));
|
||
|
|
FText FailReason, FailLog;
|
||
|
|
if (!GameProjectModule.UpdateCodeProject(FailReason, FailLog))
|
||
|
|
{
|
||
|
|
SOutputLogDialog::Open(NSLOCTEXT("PackFactory", "CreateBinary", "Create binary"), FailReason, FailLog, FText::GetEmpty());
|
||
|
|
}
|
||
|
|
|
||
|
|
bool bCompileSource = ConfigParameters.bCompileSource;
|
||
|
|
#if WITH_LIVE_CODING
|
||
|
|
if (bCompileSource)
|
||
|
|
{
|
||
|
|
ILiveCodingModule* LiveCoding = FModuleManager::GetModulePtr<ILiveCodingModule>(LIVE_CODING_MODULE_NAME);
|
||
|
|
if (LiveCoding != nullptr && LiveCoding->IsEnabledForSession())
|
||
|
|
{
|
||
|
|
FMessageDialog::Open(EAppMsgType::Ok, NSLOCTEXT("PackFactory", "CannotCompileWithLiveCoding", "Unable to compile source code while Live Coding is enabled. Please close the editor and build from your IDE."));
|
||
|
|
bCompileSource = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
#endif
|
||
|
|
if (bCompileSource)
|
||
|
|
{
|
||
|
|
// Compile the new code, either using the in editor hot-reload (if an existing module), or as a brand new module (if no existing code)
|
||
|
|
IHotReloadInterface& HotReloadSupport = FModuleManager::LoadModuleChecked<IHotReloadInterface>("HotReload");
|
||
|
|
if (bProjectHadSourceFiles)
|
||
|
|
{
|
||
|
|
// We can only hot-reload via DoHotReloadFromEditor when we already had code in our project
|
||
|
|
if (!HotReloadSupport.IsCurrentlyCompiling())
|
||
|
|
{
|
||
|
|
HotReloadSupport.DoHotReloadFromEditor(EHotReloadFlags::WaitForCompletion);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
// We didn't previously have source, so the UBT target name will be UE4Editor, and attempts to recompile will end up building the wrong target. Now that we have source,
|
||
|
|
// we need to change the UBT target to be the newly created editor module
|
||
|
|
FPlatformMisc::SetUBTTargetName(*(FString(FApp::GetProjectName()) + TEXT("Editor")));
|
||
|
|
|
||
|
|
if (!HotReloadSupport.RecompileModule(FApp::GetProjectName(), *GWarn, ERecompileModuleFlags::ReloadAfterRecompile | ERecompileModuleFlags::ForceCodeProject))
|
||
|
|
{
|
||
|
|
FMessageDialog::Open(EAppMsgType::Ok, NSLOCTEXT("PackFactory", "FailedToCompileNewGameModule", "Failed to compile newly created game module."));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Ask about editing code where applicable
|
||
|
|
if (FSlateApplication::Get().SupportsSourceAccess())
|
||
|
|
{
|
||
|
|
// Code successfully added, notify the user and ask about opening the IDE now
|
||
|
|
const FText Message = NSLOCTEXT("PackFactory", "CodeAdded", "Added source file(s). Would you like to edit the code now?");
|
||
|
|
if (FMessageDialog::Open(EAppMsgType::YesNo, Message) == EAppReturnType::Yes)
|
||
|
|
{
|
||
|
|
FSourceCodeNavigation::OpenSourceFiles(WrittenSourceFiles);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find an asset to return (It will be marked as dirty)
|
||
|
|
for (const FString& Filename: WrittenFiles)
|
||
|
|
{
|
||
|
|
static const FString AssetExtension(TEXT(".uasset"));
|
||
|
|
if (Filename.EndsWith(AssetExtension))
|
||
|
|
{
|
||
|
|
FString GameFileName = Filename;
|
||
|
|
if (FPaths::MakePathRelativeTo(GameFileName, *FPaths::ProjectContentDir()))
|
||
|
|
{
|
||
|
|
int32 SlashIndex = INDEX_NONE;
|
||
|
|
GameFileName = FString(TEXT("/Game/")) / GameFileName.LeftChop(AssetExtension.Len());
|
||
|
|
if (GameFileName.FindLastChar(TEXT('/'), SlashIndex))
|
||
|
|
{
|
||
|
|
const FString AssetName = GameFileName.RightChop(SlashIndex + 1);
|
||
|
|
ReturnAsset = LoadObject<UObject>(nullptr, *(GameFileName + TEXT(".") + AssetName));
|
||
|
|
if (ReturnAsset)
|
||
|
|
{
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// If source control is enabled mark all the added files for checkout/add
|
||
|
|
if (ISourceControlModule::Get().IsEnabled() && GetDefault<UEditorLoadingSavingSettings>()->bSCCAutoAddNewFiles)
|
||
|
|
{
|
||
|
|
for (const FString& Filename: WrittenFiles)
|
||
|
|
{
|
||
|
|
FText ErrorMessage;
|
||
|
|
if (!SourceControlHelpers::CheckoutOrMarkForAdd(Filename, FText::FromString(Filename), NULL, ErrorMessage))
|
||
|
|
{
|
||
|
|
UE_LOG(LogPackFactory, Error, TEXT("%s"), *ErrorMessage.ToString());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!ConfigParameters.InstallMessage.IsEmpty())
|
||
|
|
{
|
||
|
|
FMessageLog("AssetTools").Warning(FText::FromString(ConfigParameters.InstallMessage));
|
||
|
|
FMessageLog("AssetTools").Open();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
if (!PakFile.IsValid())
|
||
|
|
{
|
||
|
|
UE_LOG(LogPackFactory, Warning, TEXT("Invalid pak file."));
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
UE_LOG(LogPakFile, Error, TEXT("Pakfiles were loaded without Filenames, creation aborted."));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return ReturnAsset;
|
||
|
|
}
|