// Copyright Epic Games, Inc. All Rights Reserved. #include "Commandlets/ImportAssetsCommandlet.h" #include "AutomatedAssetImportData.h" #include "Modules/ModuleManager.h" #include "Factories/Factory.h" #include "IAssetTools.h" #include "AssetToolsModule.h" #include "Serialization/JsonReader.h" #include "Serialization/JsonSerializer.h" #include "JsonObjectConverter.h" #include "HAL/FileManager.h" #include "Misc/FileHelper.h" #include "Factories/ImportSettings.h" #include "ISourceControlModule.h" #include "SourceControlHelpers.h" #include "Editor.h" #include "FileHelpers.h" #include "Misc/FeedbackContext.h" #include "HAL/PlatformFileManager.h" #include "GameFramework/WorldSettings.h" #include "UObject/SavePackage.h" void UImportAssetsCommandlet::PrintUsage() { UE_LOG(LogAutomatedImport, Display, TEXT("LogAutomatedImport Usage: LogAutomatedImport {arglist}")); UE_LOG(LogAutomatedImport, Display, TEXT("Arglist:")); UE_LOG(LogAutomatedImport, Display, TEXT("-help or -?")); UE_LOG(LogAutomatedImport, Display, TEXT("\tDisplays this help")); UE_LOG(LogAutomatedImport, Display, TEXT("-source=\"path\"")); UE_LOG(LogAutomatedImport, Display, TEXT("\tThe source file to import. This must be specified when importing a single asset\n[IGNORED when using -importparams]")); UE_LOG(LogAutomatedImport, Display, TEXT("-dest=\"path\"")); UE_LOG(LogAutomatedImport, Display, TEXT("\tThe destination path in the project's content directory to import to.\nThis must be specified when importing a single asset\n[IGNORED when using -importparams]")); UE_LOG(LogAutomatedImport, Display, TEXT("-factory={factory class name}")); UE_LOG(LogAutomatedImport, Display, TEXT("\tForces the asset to be opened with a specific UFactory class type. If not specified import type will be auto detected.\n[IGNORED when using -importparams]")); UE_LOG(LogAutomatedImport, Display, TEXT("-importsettings=\"path to import settings json file\"")); UE_LOG(LogAutomatedImport, Display, TEXT("\tPath to a json file that has asset import parameters when importing multiple files. If this argument is used all other import arguments are ignored as they are specified in the json file")); UE_LOG(LogAutomatedImport, Display, TEXT("-replaceexisting")); UE_LOG(LogAutomatedImport, Display, TEXT("\tWhether or not to replace existing assets when importing")); UE_LOG(LogAutomatedImport, Display, TEXT("-nosourcecontrol")); UE_LOG(LogAutomatedImport, Display, TEXT("\tDisables revision control. Prevents checking out, adding files, and submitting files")); UE_LOG(LogAutomatedImport, Display, TEXT("-submitdesc")); UE_LOG(LogAutomatedImport, Display, TEXT("\tSubmit description/comment to use checking in to revision control. If this is empty no files will be submitted")); UE_LOG(LogAutomatedImport, Display, TEXT("-skipreadonly")); UE_LOG(LogAutomatedImport, Display, TEXT("\tIf an asset cannot be saved because it is read only, the commandlet will not clear the read only flag and will not save the file")); } UImportAssetsCommandlet::UImportAssetsCommandlet(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { } bool UImportAssetsCommandlet::ParseParams(const FString& InParams) { TArray Tokens; TArray Params; TMap ParamVals; ParseCommandLine(*InParams, Tokens, Params, ParamVals); if( Params.Contains(TEXT("?")) || Params.Contains(TEXT("help") ) ) { bShowHelp = true; } bAllowSourceControl = !Params.Contains(TEXT("nosourcecontrol")); GlobalImportData = NewObject(this); GlobalImportData->bSkipReadOnly = Params.Contains(TEXT("skipreadonly")); FString SourcePathParam = ParamVals.FindRef(TEXT("source")); if(!SourcePathParam.IsEmpty()) { GlobalImportData->Filenames.Add(SourcePathParam); } GlobalImportData->DestinationPath = ParamVals.FindRef(TEXT("dest")); GlobalImportData->FactoryName = ParamVals.FindRef(TEXT("factoryname")); GlobalImportData->bReplaceExisting = Params.Contains(TEXT("replaceexisting")); GlobalImportData->LevelToLoad = ParamVals.FindRef(TEXT("level")); if (!GlobalImportData->LevelToLoad.IsEmpty()) { FText FailReason; if (!FPackageName::IsValidLongPackageName(GlobalImportData->LevelToLoad, false, &FailReason)) { UE_LOG(LogAutomatedImport, Error, TEXT("Invalid level specified: %s"), *FailReason.ToString()); } } ImportSettingsPath = ParamVals.FindRef(TEXT("importsettings")); GlobalImportData->Initialize(nullptr); if(ImportSettingsPath.IsEmpty() && (GlobalImportData->Filenames.Num() == 0 || GlobalImportData->DestinationPath.IsEmpty())) { UE_LOG(LogAutomatedImport, Error, TEXT("Invalid Arguments. Missing, Source (-source), Destination (-dest), or Import settings file (-importsettings)")); } const bool bEnoughParams = (ParamVals.Num() > 1) || !ImportSettingsPath.IsEmpty(); return bEnoughParams; } bool UImportAssetsCommandlet::ParseImportSettings(const FString& InImportSettingsFile) { bool bInvalidParse = false; bool bSuccess = false; FString JsonString; if(FFileHelper::LoadFileToString(JsonString, *InImportSettingsFile)) { TSharedRef> JsonReader = TJsonReaderFactory<>::Create(JsonString); TSharedPtr RootObject; if(FJsonSerializer::Deserialize(JsonReader, RootObject) && RootObject.IsValid()) { const TArray< TSharedPtr > ImportGroupsJsonArray = RootObject->GetArrayField(TEXT("ImportGroups")); for(const TSharedPtr& ImportGroupsJson : ImportGroupsJsonArray) { const TSharedPtr ImportGroupsJsonObject = ImportGroupsJson->AsObject(); if(ImportGroupsJsonObject.IsValid()) { // All import data is based off of the global data defaults UAutomatedAssetImportData* Data = DuplicateObject(GlobalImportData, this); // Parse data from the json object if(FJsonObjectConverter::JsonObjectToUStruct(ImportGroupsJsonObject.ToSharedRef(), UAutomatedAssetImportData::StaticClass(), Data, 0, 0 )) { Data->Initialize(ImportGroupsJsonObject); if(Data->IsValid()) { ImportDataList.Add(Data); } } else { bInvalidParse = true; } } else { bInvalidParse = true; } } } else { UE_LOG(LogAutomatedImport, Error, TEXT("Json settings file was found but was invalid: %s"), *JsonReader->GetErrorMessage()); } } else { UE_LOG(LogAutomatedImport, Error, TEXT("Import settings file %s could not be found"), *InImportSettingsFile); } return bSuccess; } static bool SavePackage(UPackage* Package, const FString& PackageFilename) { FSavePackageArgs SaveArgs; SaveArgs.TopLevelFlags = RF_Standalone; SaveArgs.Error = GWarn; return GEditor->SavePackage(Package, nullptr, *PackageFilename, SaveArgs); } bool UImportAssetsCommandlet::ImportAndSave(const TArray& AssetImportList) { bool bImportAndSaveSucceeded = true; FAssetToolsModule& AssetToolsModule = FModuleManager::Get().LoadModuleChecked("AssetTools"); ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); for(const UAutomatedAssetImportData* ImportData : AssetImportList) { UE_LOG(LogAutomatedImport, Log, TEXT("Importing group %s"), *ImportData->GetDisplayName() ); UFactory* Factory = ImportData->Factory; const TSharedPtr* ImportSettingsJsonObject = nullptr; if(ImportData->ImportGroupJsonData.IsValid()) { ImportData->ImportGroupJsonData->TryGetObjectField(TEXT("ImportSettings"), ImportSettingsJsonObject); } if(Factory != nullptr && ImportSettingsJsonObject) { IImportSettingsParser* ImportSettings = Factory->GetImportSettingsParser(); if(ImportSettings) { ImportSettings->ParseFromJson(ImportSettingsJsonObject->ToSharedRef()); } } else if(Factory == nullptr && ImportSettingsJsonObject) { UE_LOG(LogAutomatedImport, Warning, TEXT("A vaild factory name must be specfied in order to specify settings")); } // Load a level if specified bImportAndSaveSucceeded = LoadLevel(ImportData->LevelToLoad); // Clear dirty packages that were created as a result of loading the level. We do not want to save these ClearDirtyPackages(); TArray ImportedAssets = AssetToolsModule.Get().ImportAssetsAutomated(ImportData); if(ImportedAssets.Num() > 0 && bImportAndSaveSucceeded) { TArray DirtyPackages; TArray PackageStates; FEditorFileUtils::GetDirtyContentPackages(DirtyPackages); FEditorFileUtils::GetDirtyWorldPackages(DirtyPackages); bool bUseSourceControl = bHasSourceControl && SourceControlProvider.IsAvailable(); if(bUseSourceControl) { SourceControlProvider.GetState(DirtyPackages, PackageStates, EStateCacheUsage::ForceUpdate); } for(int32 PackageIndex = 0; PackageIndex < DirtyPackages.Num(); ++PackageIndex) { UPackage* PackageToSave = DirtyPackages[PackageIndex]; FString PackageFilename = SourceControlHelpers::PackageFilename(PackageToSave); bool bShouldAttemptToSave = false; bool bShouldAttemptToAdd = false; if(bUseSourceControl) { FSourceControlStateRef PackageSCState = PackageStates[PackageIndex]; bool bPackageCanBeCheckedOut = false; if(PackageSCState->IsCheckedOutOther()) { // Cannot checkout, file is already checked out UE_LOG(LogAutomatedImport, Error, TEXT("%s is already checked out by someone else, can not submit!"), *PackageFilename); bImportAndSaveSucceeded = false; } else if(!PackageSCState->IsCurrent()) { // Cannot checkout, file is not at head revision UE_LOG(LogAutomatedImport, Error, TEXT("%s is not at the head revision and cannot be checked out"), *PackageFilename); bImportAndSaveSucceeded = false; } else if(PackageSCState->CanCheckout()) { const bool bWasCheckedOut = SourceControlHelpers::CheckOutOrAddFile(PackageFilename); bShouldAttemptToSave = bWasCheckedOut; if(!bWasCheckedOut) { UE_LOG(LogAutomatedImport, Error, TEXT("%s could not be checked out"), *PackageFilename); bImportAndSaveSucceeded = false; } } else { // package was not checked out by another user and is at the current head revision and could not be checked out // this means it should be added after save because it doesn't exist bShouldAttemptToSave = true; bShouldAttemptToAdd = true; } } else { bool bIsReadOnly = IFileManager::Get().IsReadOnly(*PackageFilename); if(bIsReadOnly && ImportData->bSkipReadOnly) { bShouldAttemptToSave = false; if(bIsReadOnly) { UE_LOG(LogAutomatedImport, Error, TEXT("%s is read only and -skipreadonly was specified. Will not save"), *PackageFilename); bImportAndSaveSucceeded = false; } } else if(bIsReadOnly) { bShouldAttemptToSave = FPlatformFileManager::Get().GetPlatformFile().SetReadOnly(*PackageFilename, false); if(!bShouldAttemptToSave) { UE_LOG(LogAutomatedImport, Error, TEXT("%s is read only and could not be made writable. Will not save"), *PackageFilename); bImportAndSaveSucceeded = false; } } else { bShouldAttemptToSave = true; } } if(bShouldAttemptToSave) { SavePackage(PackageToSave, PackageFilename); if(bShouldAttemptToAdd) { const bool bWasAdded = SourceControlHelpers::MarkFileForAdd(PackageFilename); if(!bWasAdded) { UE_LOG(LogAutomatedImport, Error, TEXT("%s could not be added to revision control"), *PackageFilename); bImportAndSaveSucceeded = false; } } } } } else { bImportAndSaveSucceeded = false; UE_LOG(LogAutomatedImport, Error, TEXT("Failed to import all assets in group %s"), *ImportData->GetDisplayName()); } } return bImportAndSaveSucceeded; } bool UImportAssetsCommandlet::LoadLevel(const FString& LevelToLoad) { bool bResult = false; if (!LevelToLoad.IsEmpty()) { UE_LOG(LogAutomatedImport, Log, TEXT("Loading Map %s"), *LevelToLoad); FString Filename; if (FPackageName::TryConvertLongPackageNameToFilename(LevelToLoad, Filename)) { UPackage* Package = LoadPackage(NULL, *Filename, 0); UWorld* World = UWorld::FindWorldInPackage(Package); if (World) { // Clean up any previous world. The world should have already been saved UWorld* ExistingWorld = GEditor->GetEditorWorldContext().World(); GEngine->DestroyWorldContext(ExistingWorld); ExistingWorld->DestroyWorld(true, World); GWorld = World; World->WorldType = EWorldType::Editor; FWorldContext& WorldContext = GEngine->CreateNewWorldContext(World->WorldType); WorldContext.SetCurrentWorld(World); // add the world to the root set so that the garbage collection to delete replaced actors doesn't garbage collect the whole world World->AddToRoot(); // initialize the levels in the world World->InitWorld(UWorld::InitializationValues().AllowAudioPlayback(false)); World->GetWorldSettings()->PostEditChange(); World->UpdateWorldComponents(true, false); bResult = true; } } } else { // a map was not specified, ignore bResult = true; } if (!bResult) { UE_LOG(LogAutomatedImport, Error, TEXT("Could not find or load level %s"), *LevelToLoad); } return bResult; } void UImportAssetsCommandlet::ClearDirtyPackages() { TArray DirtyPackages; FEditorFileUtils::GetDirtyContentPackages(DirtyPackages); FEditorFileUtils::GetDirtyWorldPackages(DirtyPackages); for(UPackage* Package : DirtyPackages) { Package->SetDirtyFlag(false); } } int32 UImportAssetsCommandlet::Main(const FString& InParams) { bool bEnoughParams = ParseParams(InParams); int32 Result = 0; if(!bEnoughParams || bShowHelp) { PrintUsage(); } else { // Hack: A huge amount of packages are marked dirty on startup. This is normally prevented in editor but commandlets have special powers. // We only want to save assets that were created or modified at import time so clear all existing ones now ClearDirtyPackages(); if(bAllowSourceControl) { ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); SourceControlProvider.Init(); bHasSourceControl = SourceControlProvider.IsEnabled(); if(!bHasSourceControl) { UE_LOG(LogAutomatedImport, Error, TEXT("Could not connect to revision control!")) } } else { bHasSourceControl = false; } if(!ImportSettingsPath.IsEmpty()) { // Use settings file for importing assets ParseImportSettings(ImportSettingsPath); } else if(GlobalImportData->IsValid()) { // Use single import path ImportDataList.Add(GlobalImportData); } if(!ImportAndSave(ImportDataList)) { UE_LOG(LogAutomatedImport, Error, TEXT("Could not import all groups")); } else { Result = 0; } } return Result; }