Files
UnrealEngine/Engine/Source/Editor/UnrealEd/Private/FileHelpers.cpp
2025-05-18 13:04:45 +08:00

5723 lines
212 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "FileHelpers.h"
#include "FileHelpersInternal.h"
#include "HAL/PlatformFileManager.h"
#include "Misc/MessageDialog.h"
#include "HAL/FileManager.h"
#include "Algo/Copy.h"
#include "Algo/Transform.h"
#include "Containers/VersePath.h"
#include "Misc/CommandLine.h"
#include "Misc/Paths.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/FeedbackContext.h"
#include "Misc/ScopedSlowTask.h"
#include "Misc/RedirectCollector.h"
#include "Misc/App.h"
#include "Misc/FileHelper.h"
#include "Modules/ModuleManager.h"
#include "UObject/Linker.h"
#include "UObject/UObjectHash.h"
#include "UObject/UObjectIterator.h"
#include "ProfilingDebugging/ScopedTimers.h"
#include "Misc/Attribute.h"
#include "Widgets/DeclarativeSyntaxSupport.h"
#include "Styling/SlateTypes.h"
#include "Styling/AppStyle.h"
#include "Engine/Brush.h"
#include "Engine/MapBuildDataRegistry.h"
#include "Editor/EditorEngine.h"
#include "ISourceControlModule.h"
#include "UncontrolledChangelistsModule.h"
#include "SourceControlOperations.h"
#include "Editor/UnrealEdEngine.h"
#include "Serialization/ArchiveReplaceObjectRef.h"
#include "Settings/EditorLoadingSavingSettings.h"
#include "Factories/Factory.h"
#include "Factories/FbxSceneImportFactory.h"
#include "GameMapsSettings.h"
#include "Editor.h"
#include "EditorModeManager.h"
#include "EditorModes.h"
#include "UnrealEdMisc.h"
#include "EditorDirectories.h"
#include "Dialogs/Dialogs.h"
#include "UnrealEdGlobals.h"
#include "LevelEditorSubsystem.h"
#include "EditorLevelUtils.h"
#include "BusyCursor.h"
#include "MRUFavoritesList.h"
#include "Framework/Application/SlateApplication.h"
#include "Exporters/Exporter.h"
#include "PackagesDialog.h"
#include "Interfaces/IMainFrameModule.h"
#include "IAssetTools.h"
#include "AssetToolsModule.h"
#include "DesktopPlatformModule.h"
#include "Logging/TokenizedMessage.h"
#include "Logging/MessageLog.h"
#include "Dialogs/DlgPickPath.h"
#include "IContentBrowserSingleton.h"
#include "ContentBrowserModule.h"
#include "ObjectTools.h"
#include "Framework/Notifications/NotificationManager.h"
#include "Widgets/Notifications/SNotificationList.h"
#include "Engine/Level.h"
#include "Engine/LevelStreaming.h"
#include "GameFramework/WorldSettings.h"
#include "AutoSaveUtils.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "Misc/NamePermissionList.h"
#include "AnalyticsEventAttribute.h"
#include "AssetDefinitionRegistry.h"
#include "HierarchicalLOD.h"
#include "WorldPartition/IWorldPartitionEditorModule.h"
#include "WorldPartition/ActorDescContainer.h"
#include "WorldPartition/WorldPartitionRuntimeHash.h"
#include "WorldPartition/WorldPartition.h"
#include "WorldPartition/WorldPartitionEditorPerProjectUserSettings.h"
#include "WorldPartition/HLOD/HLODLayer.h"
#include "WorldPartition/LoaderAdapter/LoaderAdapterShape.h"
#include "LevelInstance/LevelInstanceSubsystem.h"
#include "PackageSourceControlHelper.h"
#include "InterchangeManager.h"
#include "SourceControlHelpers.h"
#include "InterchangeProjectSettings.h"
DEFINE_LOG_CATEGORY_STATIC(LogFileHelpers, Log, All);
//definition of flag used to do special work when we're attempting to load the "startup map"
bool FEditorFileUtils::bIsLoadingDefaultStartupMap = false;
bool FEditorFileUtils::bIsPromptingForCheckoutAndSave = false;
bool FEditorFileUtils::bSkipExternalObjectSave = false;
TSet<FString> FEditorFileUtils::PackagesNotSavedDuringSaveAll;
TSet<FString> FEditorFileUtils::PackagesNotToPromptAnyMore;
FEditorFileUtils::FOnLoadMapStart FEditorFileUtils::OnLoadMapStart;
FEditorFileUtils::FOnLoadMapEnd FEditorFileUtils::OnLoadMapEnd;
namespace EditorFileUtils
{
static bool bIsExplicitSave = false;
}
static TAutoConsoleVariable<int32> CVarSkipSourceControlCheckForEditablePackages(
TEXT("r.Editor.SkipSourceControlCheckForEditablePackages"),
0,
TEXT("Whether to skip the revision control status check for editable packages, 0: Disable (Default), 1: Enable"));
#define LOCTEXT_NAMESPACE "FileHelpers"
FEditorFileUtils::FOnPrepareWorldsForExplicitSave FEditorFileUtils::OnPrepareWorldsForExplicitSave;
/** A special output device that puts save output in the message log when flushed */
class FSaveErrorOutputDevice : public FOutputDevice
{
public:
virtual void Serialize( const TCHAR* InData, ELogVerbosity::Type Verbosity, const class FName& Category ) override
{
if ( Verbosity == ELogVerbosity::Error || Verbosity == ELogVerbosity::Warning )
{
EMessageSeverity::Type Severity;
if ( Verbosity == ELogVerbosity::Error )
{
Severity = EMessageSeverity::Error;
}
else
{
Severity = EMessageSeverity::Warning;
}
ErrorMessages.Add(FTokenizedMessage::Create(Severity, FText::FromName(InData)));
}
}
virtual void Flush() override
{
if ( ErrorMessages.Num() > 0 )
{
FMessageLog EditorErrors("EditorErrors");
EditorErrors.NewPage(LOCTEXT("SaveOutputPageLabel", "Save Output"));
EditorErrors.AddMessages(ErrorMessages);
EditorErrors.Open();
ErrorMessages.Empty();
}
}
private:
// Holds the errors for the message log.
TArray< TSharedRef< FTokenizedMessage > > ErrorMessages;
};
namespace FileDialogHelpers
{
/**
* @param Title The title of the dialog
* @param FileTypes Filter for which file types are accepted and should be shown
* @param InOutLastPath Keep track of the last location from which the user attempted an import
* @param DefaultFile Default file name to use for saving.
* @param OutOpenFilenames The list of filenames that the user attempted to open
*
* @return true if the dialog opened successfully and the user accepted; false otherwise.
*/
bool SaveFile( const FString& Title, const FString& FileTypes, FString& InOutLastPath, const FString& DefaultFile, FString& OutFilename )
{
OutFilename = FString();
IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get();
bool bFileChosen = false;
TArray<FString> OutFilenames;
if (DesktopPlatform)
{
bFileChosen = DesktopPlatform->SaveFileDialog(
FSlateApplication::Get().FindBestParentWindowHandleForDialogs(nullptr),
Title,
InOutLastPath,
DefaultFile,
FileTypes,
EFileDialogFlags::None,
OutFilenames
);
}
bFileChosen = (OutFilenames.Num() > 0);
if (bFileChosen)
{
// User successfully chose a file; remember the path for the next time the dialog opens.
InOutLastPath = OutFilenames[0];
OutFilename = OutFilenames[0];
}
return bFileChosen;
}
/**
* @param Title The title of the dialog
* @param FileTypes Filter for which file types are accepted and should be shown
* @param InOutLastPath Keep track of the last location from which the user attempted an import
* @param DialogMode Multiple items vs single item.
* @param OutOpenFilenames The list of filenames that the user attempted to open
*
* @return true if the dialog opened successfully and the user accepted; false otherwise.
*/
bool OpenFiles( const FString& Title, const FString& FileTypes, FString& InOutLastPath, EFileDialogFlags::Type DialogMode, TArray<FString>& OutOpenFilenames )
{
IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get();
bool bOpened = false;
if ( DesktopPlatform )
{
bOpened = DesktopPlatform->OpenFileDialog(
FSlateApplication::Get().FindBestParentWindowHandleForDialogs(nullptr),
Title,
InOutLastPath,
TEXT(""),
FileTypes,
DialogMode,
OutOpenFilenames
);
}
bOpened = (OutOpenFilenames.Num() > 0);
if ( bOpened )
{
// User successfully chose a file; remember the path for the next time the dialog opens.
InOutLastPath = OutOpenFilenames[0];
}
return bOpened;
}
}
/**
* Checks if the alternate checkout workflow should be used
* In this workflow:
* - the checkout dialog is not used but checkout is automatic.
* - if any checkout fails, a dialog is shown with the option to revert or save.
* - if SourceControl is unavailable, a warning is shown about making changes while offline, followed by a save.
*/
static bool UseAlternateCheckoutWorkflow()
{
if (ISourceControlModule::Get().GetProvider().GetName() == TEXT("Unreal Revision Control"))
{
if (const UEditorLoadingSavingSettings* Settings = GetDefault<UEditorLoadingSavingSettings>())
{
return Settings->GetAutomaticallyCheckoutOnAssetModification();
}
}
return false;
}
/**
* Prompts user with a confirmation dialog if there are checkouts or modifications in other branches
*
* @return true if checkout should proceed
*/
static bool ConfirmPackageBranchCheckOutStatus(const TArray<UPackage*>& PackagesToCheckOut)
{
//@TODO: Need more info here (in the event multiple packages are trying to be saved at once; the prompt shown is misleading in that case (you might be OK with stomping over one file but not others later on in the list))
bool bModifiedInOtherBranchIgnorePathsInitialized = false;
TArray<FString> ModifiedInOtherBranchIgnorePaths;
for (UPackage* CurPackage : PackagesToCheckOut)
{
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(CurPackage, EStateCacheUsage::Use);
// If checked out or modified in another branch, warn about possible loss of changes and confirm checkout
if (SourceControlState.IsValid() && SourceControlState->IsCheckedOutOrModifiedInOtherBranch())
{
int32 HeadCL;
FString HeadBranch, HeadAction;
FNumberFormattingOptions NoCommas;
NoCommas.UseGrouping = false;
const FString& CurrentBranch = FEngineVersion::Current().GetBranch();
SourceControlState->GetOtherBranchHeadModification(HeadBranch, HeadAction, HeadCL);
FText InfoText;
bool bShowDialog = true;
const FString CurPackageName = CurPackage->GetName();
if (SourceControlState->IsModifiedInOtherBranch())
{
// Lazy-initialize ModifiedInOtherBranchIgnorePaths since most of the time, assets are not modified in other branches.
if (!bModifiedInOtherBranchIgnorePathsInitialized)
{
if (GConfig)
{
GConfig->GetArray(TEXT("SourceControl.SourceControlSettings"), TEXT("ModifiedInOtherBranchIgnorePaths"), ModifiedInOtherBranchIgnorePaths, SourceControlHelpers::GetGlobalSettingsIni());
}
bModifiedInOtherBranchIgnorePathsInitialized = true;
}
if (ModifiedInOtherBranchIgnorePaths.ContainsByPredicate([&CurPackageName](const FString& Path) { return CurPackageName.StartsWith(Path); }))
{
// This file was modified in another branch, but we are configured to ignore it and not show a dialog for files in this path.
bShowDialog = false;
}
else
{
int32 CurrentBranchIdx = SourceControlProvider.GetStateBranchIndex(CurrentBranch);
int32 HeadBranchIdx = SourceControlProvider.GetStateBranchIndex(HeadBranch);
{
if (CurrentBranchIdx != INDEX_NONE && HeadBranchIdx != INDEX_NONE)
{
// modified
if (CurrentBranchIdx < HeadBranchIdx)
{
InfoText = LOCTEXT("WarningModifiedOtherBranchHigher", "Modified in higher branch, consider waiting for package to be merged down.");
}
else
{
InfoText = LOCTEXT("WarningModifiedOtherBranchLower", "Modified in lower branch, keep track of your work. You may need to redo it during the merge.");
}
}
}
}
}
else
{
// checked out
FString Username;
if (!SourceControlState->GetOtherUserBranchCheckedOuts().Split(TEXT("@"), &Username, nullptr))
{
Username = SourceControlState->GetOtherUserBranchCheckedOuts();
}
InfoText = FText::Format(LOCTEXT("WarningCheckedOutOtherBranchHigher", "Please ask if {0}'s change can wait."), FText::FromString(Username));
}
if (bShowDialog)
{
const FText PackageNameText = FText::FromString(CurPackageName);
const FText Message = SourceControlState->IsModifiedInOtherBranch() ? FText::Format(LOCTEXT("WarningModifiedOtherBranch", "WARNING: Package {3} modified in {0} CL {1}\n\n{2}\n\nCheck out packages anyway?"), FText::FromString(HeadBranch), FText::AsNumber(HeadCL, &NoCommas), InfoText, PackageNameText)
: FText::Format(LOCTEXT("WarningCheckedOutOtherBranch", "WARNING: Package {2} checked out in {0}\n\n{1}\n\nCheck out packages anyway?"), FText::FromString(SourceControlState->GetOtherUserBranchCheckedOuts()), InfoText, PackageNameText);
const FText Title = SourceControlState->IsModifiedInOtherBranch() ? FText::FromString("Package Branch Modifications") : FText::FromString("Package Branch Checkouts");
return FMessageDialog::Open(EAppMsgType::YesNo, Message, Title) == EAppReturnType::Yes;
}
}
}
return true;
}
static bool DeleteExistingMapPackages(const FString& ExistingPackageName)
{
// Search for external actor files
TArray<FString> ToDeletePackageFilenames;
const TArray<FString> ExternalPackagesPaths = ULevel::GetExternalObjectsPaths(ExistingPackageName);
for (const FString& ExternalPackagesPath : ExternalPackagesPaths)
{
FString ExternalPackagesFilePath = FPackageName::LongPackageNameToFilename(ExternalPackagesPath);
if (IFileManager::Get().DirectoryExists(*ExternalPackagesFilePath))
{
const bool bSuccess = IFileManager::Get().IterateDirectoryRecursively(*ExternalPackagesFilePath, [&ToDeletePackageFilenames](const TCHAR* FilenameOrDirectory, bool bIsDirectory)
{
if (!bIsDirectory)
{
FString Filename(FilenameOrDirectory);
if (Filename.EndsWith(FPackageName::GetAssetPackageExtension()))
{
ToDeletePackageFilenames.Add(Filename);
}
}
// Continue Directory Iteration
return true;
});
if (!bSuccess)
{
FMessageDialog::Open(EAppMsgType::Ok, FText::Format(NSLOCTEXT("UnrealEd", "Error_IteratingExistingExternalPackageFolder", "Failed iterating existing external package folder {0}."), FText::FromString(ExternalPackagesFilePath)));
return false;
}
}
}
if (ToDeletePackageFilenames.Num() > 0)
{
FPackageSourceControlHelper PackageHelper;
if (!PackageHelper.Delete(ToDeletePackageFilenames))
{
FMessageDialog::Open(EAppMsgType::Ok, NSLOCTEXT("UnrealEd", "Error_DeleteExistingActorPackage", "Unable to delete existing actor packages."));
return false;
}
// Make sure assets are removed
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
AssetRegistry.ScanModifiedAssetFiles(ToDeletePackageFilenames);
}
return true;
}
/**
* Maps loaded level packages to the package filenames.
*/
static TMap<FName, FString> LevelFilenames;
void FEditorFileUtils::RegisterLevelFilename(UObject* Object, const FString& NewLevelFilename)
{
const FName PackageName(*Object->GetOutermost()->GetName());
//UE_LOG(LogFileHelpers, Log, TEXT("RegisterLevelFilename: package %s to name %s"), *PackageName, *NewLevelFilename );
FString* ExistingFilenamePtr = LevelFilenames.Find( PackageName );
if ( ExistingFilenamePtr )
{
// Update the existing entry with the new filename.
*ExistingFilenamePtr = NewLevelFilename;
}
else
{
// Set for the first time.
LevelFilenames.Add( PackageName, NewLevelFilename );
}
// Mirror the world's filename to UnrealEd's title bar.
if ( Object == GWorld )
{
IMainFrameModule& MainFrameModule = FModuleManager::Get().LoadModuleChecked<IMainFrameModule>(TEXT("MainFrame"));
MainFrameModule.SetLevelNameForWindowTitle(NewLevelFilename);
}
}
///////////////////////////////////////////////////////////////////////////////
FString FEditorFileUtils::GetFilename(const FName& PackageName)
{
// First see if it is an in-memory package that already has an associated filename
const FString PackageNameString = PackageName.ToString();
const bool bIncludeReadOnlyRoots = false;
if ( FPackageName::IsValidLongPackageName(PackageNameString, bIncludeReadOnlyRoots) )
{
return FPackageName::LongPackageNameToFilename(PackageNameString, FPackageName::GetMapPackageExtension());
}
FString* Result = LevelFilenames.Find( PackageName );
if ( !Result )
{
//UE_LOG(LogFileHelpers, Log, TEXT("GetFilename with package %s, returning EMPTY"), *PackageName );
return FString(TEXT(""));
}
// Verify that the file still exists, if it does not, reset the level filename
else if ( IFileManager::Get().FileSize( **Result ) == INDEX_NONE )
{
*Result = FString(TEXT(""));
UWorld* World = GWorld;
if ( World && World->GetOutermost()->GetFName() == PackageName )
{
IMainFrameModule& MainFrameModule = FModuleManager::Get().LoadModuleChecked<IMainFrameModule>(TEXT("MainFrame"));
MainFrameModule.SetLevelNameForWindowTitle(*Result);
}
}
//UE_LOG(LogFileHelpers, Log, TEXT("GetFilename with package %s, returning %s"), *PackageName, **Result );
return *Result;
}
FString FEditorFileUtils::GetFilename(UObject* LevelObject)
{
return GetFilename( LevelObject->GetOutermost()->GetFName() );
}
///////////////////////////////////////////////////////////////////////////////
static FString GetDefaultDirectory()
{
return FEditorDirectories::Get().GetLastDirectory(ELastDirectory::UNR);
}
///////////////////////////////////////////////////////////////////////////////
/**
* Returns a file filter string appropriate for a specific file interaction.
*
* @param Interaction A file interaction to get a filter string for.
* @return A filter string.
*/
FString FEditorFileUtils::GetFilterString(EFileInteraction Interaction)
{
FString Result;
TSet<FString> Extensions;
switch( Interaction )
{
case FI_Load:
case FI_Save:
{
Result = FString::Printf( TEXT("Map files (*%s)|*%s|All files (*.*)|*.*"), *FPackageName::GetMapPackageExtension(),
*FPackageName::GetMapPackageExtension());
}
break;
case FI_ImportScene:
{
TArray<UFactory*> Factories;
for (UClass* Class : TObjectRange<UClass>())
{
if (Class->IsChildOf<USceneImportFactory>() && !Class->HasAnyClassFlags(CLASS_Abstract | CLASS_Deprecated | CLASS_NewerVersionExists))
{
UFactory* Factory = Class->GetDefaultObject<UFactory>();
if (Factory->bEditorImport)
{
Factories.Add(Factory);
}
}
}
if (Factories.Num() > 0)
{
FString FileTypes;
FString AllExtensions;
TMultiMap<uint32, UFactory*> FilterIndexToFactory;
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>(TEXT("AssetTools")).Get();
ObjectTools::GenerateFactoryFileExtensions(Factories, FileTypes, AllExtensions, FilterIndexToFactory);
if (UInterchangeManager::IsInterchangeImportEnabled())
{
TArray<FString> InterchangeFileExtensions = UInterchangeManager::GetInterchangeManager().GetSupportedFormats(EInterchangeTranslatorType::Scenes);
ObjectTools::AppendFormatsFileExtensions(InterchangeFileExtensions, FileTypes, AllExtensions);
}
FileTypes = FString::Printf(TEXT("All Files (%s)|%s|%s"), *AllExtensions, *AllExtensions, *FileTypes);
Result = FileTypes;
}
}
break;
case FI_ExportScene:
{
for (UClass* Class : TObjectRange<UClass>())
{
if (!Class->IsChildOf<UExporter>() || Class->HasAnyClassFlags(CLASS_Abstract | CLASS_Deprecated | CLASS_NewerVersionExists))
{
continue;
}
UExporter* Exporter = Class->GetDefaultObject<UExporter>();
if (!Exporter->SupportsObject(UWorld::StaticClass()->GetDefaultObject()))
{
continue;
}
// Ignore generic UObject exporters
if (!Exporter->SupportedClass || !Exporter->SupportedClass->IsChildOf<UWorld>())
{
continue;
}
for (int32 i = 0; i < Exporter->FormatExtension.Num(); ++i)
{
FString FormatExtensionLower = Exporter->FormatExtension[i].ToLower();
if (FormatExtensionLower == TEXT("copy"))
{
continue;
}
// Skip over duplicates
if (Extensions.Contains(FormatExtensionLower))
{
continue;
}
Extensions.Add(FormatExtensionLower);
if (Result.Len() > 0)
{
Result += TEXT("|");
}
const FString& FormatDescription = Exporter->FormatDescription[i];
Result += FString::Printf(TEXT("%s (*.%s)|*.%s"), *FormatDescription, *FormatExtensionLower, *FormatExtensionLower);
}
}
}
break;
default:
checkf( 0, TEXT("Unkown EFileInteraction" ) );
}
return Result;
}
///////////////////////////////////////////////////////////////////////////////
// Gather external packages to save for maps an other package that use external packages
// @param InPackage The package that is being saved
// @param InOutPackagesToSave Any packages to save. Array will be modified with additional packages if they are found, then those packages will all be saved
// @param bInNewlyCreated Whether the package was newly created
// @param bInAutosaving Should be set to true if autosaving
// @returns true if all the save operations completed successfully
static bool SaveExternalPackages(UPackage* InPackage, TArray<UPackage*>& InOutPackagesToSave, bool bInNewlyCreated, bool bInAutosaving)
{
bool bSuccess = true;
if (!bInAutosaving && (!FEditorFileUtils::ShouldSkipExternalObjectSave() || bInNewlyCreated))
{
InOutPackagesToSave.Append(InPackage->GetExternalPackages());
if (InOutPackagesToSave.Num())
{
if (!UEditorLoadingAndSavingUtils::SavePackages(InOutPackagesToSave, /*bCheckDirty=*/ !bInNewlyCreated))
{
FMessageDialog::Open(EAppMsgType::Ok, NSLOCTEXT("UnrealEd", "Error_FailedToSaveExternalPackages", "Failed to save external packages"));
bSuccess = false;
}
}
}
return bSuccess;
}
/**
* @param World The world to save.
* @param ForceFilename If non-NULL, save the level package to this name (full path+filename).
* @param OverridePath If non-NULL, override the level path with this path.
* @param FilenamePrefix If non-NULL, prepend this string the the level filename.
* @param bRenamePackageToFile If true, rename the level package to the filename if save was successful.
* @param bCheckDirty If true, don't save the level if it is not dirty.
* @param FinalFilename [out] The full path+filename the level was saved to.
* @param bAutosaving Should be set to true if autosaving; passed to UWorld::SaveWorld.
* @param bPIESaving Should be set to true if saving for PIE; passed to UWorld::SaveWorld.
* @return true if the level was saved.
*/
static bool SaveWorld(UWorld* World,
const FString* ForceFilename,
const TCHAR* OverridePath,
const TCHAR* FilenamePrefix,
bool bRenamePackageToFile,
bool bCheckDirty,
FString& FinalFilename,
bool bAutosaving,
bool bPIESaving)
{
// SaveWorld not reentrant - check that we are not already in the process of saving here (for example, via autosave)
static bool bIsReentrant = false;
if (bIsReentrant)
{
return false;
}
TGuardValue<bool> ReentrantGuard(bIsReentrant, true);
if ( !World )
{
FinalFilename = LOCTEXT("FilenameUnavailable", "Filename Not available!").ToString();
return false;
}
UPackage* Package = Cast<UPackage>( World->GetOuter() );
if ( !Package )
{
FinalFilename = LOCTEXT("FilenameUnavailableInvalidOuter", "Filename Not available. Outer package invalid!").ToString();
return false;
}
// Don't save if the world doesn't need saving.
if ( bCheckDirty && !Package->IsDirty() )
{
FinalFilename = LOCTEXT("FilenameUnavailableNotDirty", "Filename Not available. Package not dirty.").ToString();
return false;
}
TRACE_CPUPROFILER_EVENT_SCOPE(SaveWorld);
FString PackageName = Package->GetName();
FString ExistingFilename;
FString Path;
FString CleanFilename;
// Does a filename already exist for this package?
const bool bPackageExists = FPackageName::DoesPackageExist( PackageName, &ExistingFilename );
if ( ForceFilename )
{
Path = FPaths::GetPath(*ForceFilename);
CleanFilename = FPaths::GetCleanFilename(*ForceFilename);
}
else if ( bPackageExists )
{
if( bPIESaving && FCString::Stristr( *ExistingFilename, *FPackageName::GetMapPackageExtension() ) == NULL )
{
// If package exists, but doesn't feature the default extension, it will not load when launched,
// Change the extension of the map to the default for the auto-save
Path = AutoSaveUtils::GetAutoSaveDir();
CleanFilename = FPackageName::GetLongPackageAssetName(PackageName) + FPackageName::GetMapPackageExtension();
}
else
{
// We're not forcing a filename, so go with the filename that exists.
Path = FPaths::GetPath(ExistingFilename);
CleanFilename = FPaths::GetCleanFilename(ExistingFilename);
}
}
else if ( !bAutosaving && FPackageName::IsValidLongPackageName(PackageName, false) )
{
// If the package is made with a path in a non-read-only root, save it there
const FString ImplicitFilename = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetMapPackageExtension());
Path = FPaths::GetPath(ImplicitFilename);
CleanFilename = FPaths::GetCleanFilename(ImplicitFilename);
}
else
{
// No package filename exists and none was specified, so save the package in the autosaves folder.
Path = AutoSaveUtils::GetAutoSaveDir();
CleanFilename = FPackageName::GetLongPackageAssetName(PackageName) + FPackageName::GetMapPackageExtension();
}
// Optionally override path.
if ( OverridePath )
{
FinalFilename = FString(OverridePath) + TEXT("/");
}
else
{
FinalFilename = Path + TEXT("/");
}
// Apply optional filename prefix.
if ( FilenamePrefix )
{
FinalFilename += FString(FilenamePrefix);
}
// Munge remaining clean filename minus path + extension with path and optional prefix.
FinalFilename += CleanFilename;
// Prepare the new package name
FString NewPackageName;
if ( !FPackageName::TryConvertFilenameToLongPackageName(FinalFilename, NewPackageName) )
{
FMessageDialog::Open( EAppMsgType::Ok, FText::Format( NSLOCTEXT("Editor", "SaveWorld_BadFilename", "Failed to save the map. The filename '{0}' is not within the game or engine content folders found in '{1}'."), FText::FromString( FinalFilename ), FText::FromString( FPaths::RootDir() ) ) );
return false;
}
// Before doing any work, check to see if 1) the package name is in use by another object, 2) the world object can be renamed if necessary; and 3) the file is writable.
bool bSuccess = false;
const FString OriginalWorldName = World->GetName();
const FString OriginalPackageName = Package->GetName();
const FString NewWorldAssetName = FPackageName::GetLongPackageAssetName(NewPackageName);
const bool bNewPackageExists = FPackageName::DoesPackageExist(NewPackageName);
const bool bIsTempPackage = FPackageName::IsTempPackage(World->GetPackage()->GetName());
bool bValidWorldName = true;
bool bPackageNeedsRename = false;
bool bWorldNeedsRename = false;
if ( bRenamePackageToFile )
{
// Rename the world package if needed
if ( Package->GetName() != NewPackageName )
{
bValidWorldName = Package->Rename( *NewPackageName, NULL, REN_Test );
if ( bValidWorldName )
{
bPackageNeedsRename = true;
}
}
if ( bValidWorldName )
{
// Rename the world if the package changed
if ( World->GetName() != NewWorldAssetName )
{
bValidWorldName = World->Rename( *NewWorldAssetName, NULL, REN_Test );
if ( bValidWorldName )
{
bWorldNeedsRename = true;
}
}
}
}
if ( !bValidWorldName )
{
FMessageDialog::Open( EAppMsgType::Ok, NSLOCTEXT("UnrealEd", "Error_LevelNameExists", "A level with that name already exists. Please choose another name.") );
}
else if( IFileManager::Get().IsReadOnly(*FinalFilename) )
{
FMessageDialog::Open( EAppMsgType::Ok, FText::Format(NSLOCTEXT("UnrealEd", "PackageFileIsReadOnly", "Unable to save package to {0} because the file is read-only!"), FText::FromString(FinalFilename)) );
}
else
{
bSuccess = true;
// Save the world package after doing optional garbage collection.
const FScopedBusyCursor BusyCursor;
FFormatNamedArguments Args;
Args.Add( TEXT("MapFilename"), FText::FromString( FPaths::GetCleanFilename(FinalFilename) ) );
FScopedSlowTask SlowTask(100, FText::Format( NSLOCTEXT("UnrealEd", "SavingMap_F", "Saving map: {MapFilename}..." ), Args ));
SlowTask.MakeDialog(true);
SlowTask.EnterProgressFrame(25);
FSoftObjectPath OldPath( World );
bool bAddedAssetPathRedirection = false;
// Rename the package and the object, as necessary
UWorld* DuplicatedWorld = nullptr;
UWorldPartition* RenamedWorldPartition = nullptr;
TArray<FWorldPartitionReference> ActorReferences;
// Save loaded regions
TArray<FBox> LoadedEditorRegions;
// Other packages to save
TArray<UPackage*> PackagesToSave;
// Initialize Physics Scene for save if needed here so that external packages don't get dirtied during the Saving of the map package
bool bForceInitializedWorld = false;
if ( bRenamePackageToFile )
{
if (bPackageNeedsRename)
{
// Reset Loaders before deleting existing packages
ULevelInstanceSubsystem::ResetLoadersForWorldAsset(NewPackageName);
// Delete files at destination
if (!DeleteExistingMapPackages(NewPackageName))
{
return false;
}
RenamedWorldPartition = World->GetWorldPartition();
// Load all unloaded actors before rename. If this is causing issues (oom or other) map will need to be renamed through a provided builder commandlet
// When creating a Partitioned Level for a Level Instance, the WorldPartition is not initialized, so no need to do this.
if (RenamedWorldPartition && RenamedWorldPartition->IsInitialized())
{
LoadedEditorRegions = RenamedWorldPartition->GetUserLoadedEditorRegions();
RenamedWorldPartition->LoadAllActors(ActorReferences);
if (bIsTempPackage)
{
if (UHLODLayer* CurHLODLayer = RenamedWorldPartition->GetDefaultHLODLayer())
{
UHLODLayer* NewHLODLayer = UHLODLayer::DuplicateHLODLayersSetup(CurHLODLayer, NewPackageName, NewWorldAssetName);
RenamedWorldPartition->SetDefaultHLODLayer(NewHLODLayer);
TMap<UHLODLayer*, UHLODLayer*> ReplacementMap;
while (NewHLODLayer)
{
PackagesToSave.Add(NewHLODLayer->GetPackage());
ReplacementMap.Add(CurHLODLayer, NewHLODLayer);
CurHLODLayer = CurHLODLayer->GetParentLayer();
NewHLODLayer = NewHLODLayer->GetParentLayer();
}
FArchiveReplaceObjectRef<UHLODLayer> ReplaceObjectRefAr(RenamedWorldPartition->RuntimeHash, ReplacementMap, EArchiveReplaceObjectFlags::IgnoreOuterRef | EArchiveReplaceObjectFlags::IgnoreArchetypeRef);
}
}
}
// If we are doing a SaveAs on a world that already exists on disk, we need to duplicate it:
// This fixes a problem where level assets had the same guids for objects saved in them, which causes LazyObjectPtr issues when they are both in memory at the same time since they can not be uniquely identified.
if (bPackageExists)
{
ObjectTools::FPackageGroupName NewPGN;
NewPGN.PackageName = NewPackageName;
NewPGN.ObjectName = NewWorldAssetName;
bool bPromptToOverwrite = false;
TSet<UPackage*> PackagesUserRefusedToFullyLoad;
DuplicatedWorld = Cast<UWorld>(ObjectTools::DuplicateSingleObject(World, NewPGN, PackagesUserRefusedToFullyLoad, bPromptToOverwrite));
if (DuplicatedWorld)
{
Package = DuplicatedWorld->GetOutermost();
}
else
{
// Avoid assert during rename when duplicate fails
if (!Package->Rename(*NewPackageName, NULL, REN_Test))
{
FMessageDialog::Open(EAppMsgType::Ok, FText::Format(NSLOCTEXT("UnrealEd", "Error_OverwriteMapCleanup", "Unable to overwrite existing package {0}."), FText::FromString(NewPackageName)));
return false;
}
}
}
if (!DuplicatedWorld)
{
// Explict Reset Loaders of Package here because we want to avoid resetting of all loaders which is the current behavior of UObject::Rename when passing in a UPackage
ResetLoaders(Package);
// Duplicate failed or not needed. Just do a rename.
Package->Rename(*NewPackageName, NULL, REN_NonTransactional | REN_DontCreateRedirectors);
if (bWorldNeedsRename)
{
// Unload package of existing MapBuildData to allow overwrite
if (World->PersistentLevel->MapBuildData && !World->PersistentLevel->MapBuildData->IsLegacyBuildData())
{
FString NewBuiltPackageName = World->GetOutermost()->GetName() + TEXT("_BuiltData");
UObject* ExistingObject = StaticFindObject(nullptr, 0, *NewBuiltPackageName);
if (ExistingObject && ExistingObject != World->PersistentLevel->MapBuildData->GetOutermost())
{
TArray<UPackage*> AllPackagesToUnload;
AllPackagesToUnload.Add(Cast<UPackage>(ExistingObject));
UPackageTools::UnloadPackages(AllPackagesToUnload);
}
}
World->Rename(*NewWorldAssetName, NULL, REN_NonTransactional | REN_DontCreateRedirectors);
}
// We're changing the world path, add a path redirector so that soft object paths get fixed on save
FSoftObjectPath NewPath( World );
GRedirectCollector.AddAssetPathRedirection( OldPath.GetWithoutSubPath(), NewPath.GetWithoutSubPath() );
bAddedAssetPathRedirection = true;
}
}
}
// Mark package as fully loaded, this is usually set implicitly by calling IsFullyLoaded before saving, but that path can get skipped for levels
Package->MarkAsFullyLoaded();
SlowTask.EnterProgressFrame(25);
UWorld* SaveWorld = DuplicatedWorld ? DuplicatedWorld : World;
const bool bNewlyCreated = SaveWorld->GetPackage()->HasAnyPackageFlags(PKG_NewlyCreated);
// Initialize Physics Scene for save if needed here before saving external packages as this can modify those external package objects
// This makes UEditorEngine::Save's own call to InitializePhysicsSceneForSaveIfNecessary redundant but wasn't removed to avoid breaking other code paths
const bool bInitializedPhysicsSceneForSave = GEditor->InitializePhysicsSceneForSaveIfNecessary(SaveWorld, bForceInitializedWorld);
// Save actual map
if (bSuccess)
{
const FString AutoSavingString = (bAutosaving || bPIESaving) ? TEXT("true") : TEXT("false");
const FString KeepDirtyString = bPIESaving ? TEXT("true") : TEXT("false");
FSaveErrorOutputDevice SaveErrors;
bSuccess = GEditor->Exec(NULL, *FString::Printf(TEXT("OBJ SAVEPACKAGE PACKAGE=\"%s\" FILE=\"%s\" SILENT=true AUTOSAVING=%s KEEPDIRTY=%s"), *Package->GetName(), *FinalFilename, *AutoSavingString, *KeepDirtyString), SaveErrors);
SaveErrors.Flush();
}
SlowTask.EnterProgressFrame(50);
if (bSuccess)
{
bSuccess = SaveExternalPackages(Package, PackagesToSave, bNewlyCreated, bAutosaving);
}
if (bSuccess)
{
// Force update before initializing World Partition
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
// Make sure when we exit SaveWorld AssetRegistry is up to date with saved map. Ignore warnings if the map is
// stored in /Temp.
AssetRegistry.ScanModifiedAssetFiles({ FinalFilename }, UE::AssetRegistry::EScanFlags::IgnoreInvalidPathWarning);
if (bPackageNeedsRename || bNewlyCreated || !bNewPackageExists)
{
// Force rescan to make sure assets are found on map open or world partition initialize`
AssetRegistry.ScanSynchronous(ULevel::GetExternalObjectsPaths(NewPackageName), {} /* FilePaths */,
UE::AssetRegistry::EScanFlags::IgnoreInvalidPathWarning | UE::AssetRegistry::EScanFlags::ForceRescan);
}
if (RenamedWorldPartition && RenamedWorldPartition->IsStreamingEnabled())
{
if (LoadedEditorRegions.Num())
{
// Save Snapshot of loaded Editor regions
GetMutableDefault<UWorldPartitionEditorPerProjectUserSettings>()->SetEditorLoadedRegions(SaveWorld, LoadedEditorRegions);
RenamedWorldPartition->LoadLastLoadedRegions(LoadedEditorRegions);
}
else if (bIsTempPackage)
{
if (const FBox WorldBounds = RenamedWorldPartition->GetRuntimeWorldBounds(); WorldBounds.IsValid)
{
UWorldPartitionEditorLoaderAdapter* EditorLoaderAdapter = RenamedWorldPartition->CreateEditorLoaderAdapter<FLoaderAdapterShape>(World, WorldBounds, TEXT("Loaded Region"));
EditorLoaderAdapter->GetLoaderAdapter()->SetUserCreated(true);
EditorLoaderAdapter->GetLoaderAdapter()->Load();
}
}
}
}
// @todo Autosaving should save build data as well
if (bSuccess && !bAutosaving)
{
if (!FEditorFileUtils::SaveMapDataPackages(SaveWorld, /*bCheckDirty*/true))
{
FMessageDialog::Open(EAppMsgType::Ok, NSLOCTEXT("UnrealEd", "Error_FailedToSaveMapDataPackages", "Failed to save map data packages"));
bSuccess = false;
}
}
// Make sure all deferred adds are processed
if (GEditor)
{
GEditor->RunDeferredMarkForAddFiles();
}
// If Physics scene was initialized for save, cleanup
if (bInitializedPhysicsSceneForSave)
{
GEditor->CleanupPhysicsSceneThatWasInitializedForSave(SaveWorld, bForceInitializedWorld);
}
// If the package save was not successful. Trash the duplicated world or rename back if the duplicate failed.
if( bRenamePackageToFile && !bSuccess )
{
if (bPackageNeedsRename)
{
if (DuplicatedWorld)
{
DuplicatedWorld->Rename(nullptr, GetTransientPackage(), REN_NonTransactional | REN_DontCreateRedirectors);
DuplicatedWorld->MarkAsGarbage();
DuplicatedWorld->SetFlags(RF_Transient);
DuplicatedWorld = nullptr;
}
else
{
Package->Rename(*OriginalPackageName, NULL, REN_NonTransactional);
if (bWorldNeedsRename)
{
World->Rename(*OriginalWorldName, NULL, REN_NonTransactional);
}
}
}
}
}
return bSuccess;
}
// Save an individual asset's package as well as any external packages too
// @param InPackage The package to save
// @param PackageName The name of the package to save
// @param FinalPackageSavePath The save path of the package
// @param SaveOutput Output device for error reporting
// @returns true if all the save operations completed successfully
static bool SaveAsset(UPackage* InPackage, const FString& PackageName, const FString& FinalPackageSavePath, FOutputDevice& SaveOutput)
{
TArray<UPackage*> PackagesToSave;
return SaveExternalPackages(InPackage, PackagesToSave, InPackage->HasAnyPackageFlags(PKG_NewlyCreated), false) &&
GEngine->Exec(nullptr, *FString::Printf( TEXT("OBJ SAVEPACKAGE PACKAGE=\"%s\" FILE=\"%s\" SILENT=true"), *PackageName, *FinalPackageSavePath));
}
FString FEditorFileUtils::GetAutoSaveFilename(UPackage* const Package, const FString& AbsoluteAutosaveDir, const int32 AutoSaveIndex, const FString& PackageExt)
{
// Come up with a meaningful name for the auto-save file
const FString PackagePathName = Package->GetPathName();
FString AutoSavePath;
FString PackageRoot;
FString PackagePath;
FString PackageName;
const bool bStripRootLeadingSlash = true;
if(FPackageName::SplitLongPackageName(PackagePathName, PackageRoot, PackagePath, PackageName, bStripRootLeadingSlash))
{
AutoSavePath = AbsoluteAutosaveDir / PackageRoot / PackagePath;
}
else
{
AutoSavePath = AbsoluteAutosaveDir;
PackageName = FPaths::GetBaseFilename(PackagePathName);
}
// Ensure the directory we're about to save to exists
IFileManager::Get().MakeDirectory(*AutoSavePath, true);
// Create an auto-save filename
const FString Filename = AutoSavePath / *FString::Printf(TEXT("%s_Auto%i%s"), *PackageName, AutoSaveIndex, *PackageExt);
return Filename;
}
/** Renames a single level, preserving the common suffix */
bool RenameStreamingLevel( FString& LevelToRename, const FString& OldBaseLevelName, const FString& NewBaseLevelName )
{
// Make sure the level starts with the original level name
if( LevelToRename.StartsWith( OldBaseLevelName ) ) // Not case sensitive
{
// Grab the tail of the streaming level name, basically everything after the old base level name
FString SuffixToPreserve = LevelToRename.Right( LevelToRename.Len() - OldBaseLevelName.Len() );
// Rename the level!
LevelToRename = NewBaseLevelName + SuffixToPreserve;
return true;
}
return false;
}
static bool OpenSaveAsDialog(UClass* SavedClass, const FString& InDefaultPath, const FString& InNewNameSuggestion, FString& OutPackageName)
{
FString DefaultPath = InDefaultPath;
if (DefaultPath.IsEmpty())
{
DefaultPath = TEXT("/Game/Maps");
}
FString NewNameSuggestion = InNewNameSuggestion;
check(!NewNameSuggestion.IsEmpty());
FSaveAssetDialogConfig SaveAssetDialogConfig;
{
SaveAssetDialogConfig.DefaultPath = DefaultPath;
SaveAssetDialogConfig.DefaultAssetName = NewNameSuggestion;
SaveAssetDialogConfig.AssetClassNames.Add(SavedClass->GetClassPathName());
SaveAssetDialogConfig.ExistingAssetPolicy = ESaveAssetDialogExistingAssetPolicy::AllowButWarn;
SaveAssetDialogConfig.DialogTitleOverride = (SavedClass == UWorld::StaticClass())
? LOCTEXT("SaveLevelDialogTitle", "Save Level As")
: LOCTEXT("SaveAssetDialogTitle", "Save Asset As");
}
FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>("ContentBrowser");
FString SaveObjectPath = ContentBrowserModule.Get().CreateModalSaveAssetDialog(SaveAssetDialogConfig);
if ( !SaveObjectPath.IsEmpty() )
{
OutPackageName = FPackageName::ObjectPathToPackageName(SaveObjectPath);
return true;
}
return false;
}
/**
* Prompts the user with a dialog for selecting a filename.
*/
static bool SaveAsImplementation( UWorld* InWorld, const FString& DefaultFilename, const bool bAllowStreamingLevelRename, FString* OutSavedFilename )
{
UEditorLoadingSavingSettings* LoadingSavingSettings = GetMutableDefault<UEditorLoadingSavingSettings>();
// Get default path and filename. If no default filename was supplied, create one.
FString DefaultDirectory = FEditorDirectories::Get().GetLastDirectory(ELastDirectory::LEVEL);
FString Filename = FPaths::GetCleanFilename(DefaultFilename);
if (Filename.IsEmpty())
{
const FString DefaultName = TEXT("NewMap");
FString PackageName;
if (!FPackageName::TryConvertFilenameToLongPackageName(DefaultDirectory / DefaultName, PackageName))
{
// Initial location is invalid (e.g. lies outside of the project): set location to /Game/Maps instead
DefaultDirectory = FPaths::ProjectContentDir() / TEXT("Maps");
ensure(FPackageName::TryConvertFilenameToLongPackageName(DefaultDirectory / DefaultName, PackageName));
}
FString Name;
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
AssetToolsModule.Get().CreateUniqueAssetName(PackageName, TEXT(""), PackageName, Name);
Filename = FPaths::GetCleanFilename(FPackageName::LongPackageNameToFilename(PackageName));
}
// Disable autosaving while the "Save As..." dialog is up.
const bool bOldAutoSaveState = LoadingSavingSettings->bAutoSaveEnable;
LoadingSavingSettings->bAutoSaveEnable = false;
bool bStatus = false;
// Loop through until a valid filename is given or the user presses cancel
bool bFilenameIsValid = false;
FString SaveFilename;
while( !bFilenameIsValid )
{
SaveFilename = FString();
bool bSaveFileLocationSelected = false;
FString DefaultPackagePath;
FPackageName::TryConvertFilenameToLongPackageName(DefaultDirectory / Filename, DefaultPackagePath);
FString PackageName;
bSaveFileLocationSelected = OpenSaveAsDialog(
UWorld::StaticClass(),
FPackageName::GetLongPackagePath(DefaultPackagePath),
FPaths::GetBaseFilename(Filename),
PackageName);
if( bSaveFileLocationSelected )
{
SaveFilename = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetMapPackageExtension());
FText ErrorMessage;
bFilenameIsValid = FEditorFileUtils::IsValidMapFilename(SaveFilename, ErrorMessage);
if ( bFilenameIsValid )
{
// If there is an existing world in memory that shares this name unload it now to prepare for overwrite.
// Don't do this if we are using save as to overwrite the current level since it will just save naturally.
const FString NewPackageName = FPackageName::FilenameToLongPackageName(SaveFilename);
UPackage* ExistingPackage = FindPackage(nullptr, *NewPackageName);
if ( ExistingPackage && ExistingPackage != InWorld->GetOutermost() )
{
bFilenameIsValid = FEditorFileUtils::AttemptUnloadInactiveWorldPackage(ExistingPackage, ErrorMessage);
}
}
if ( !bFilenameIsValid )
{
// Start the loop over, prompting for save again
const FText DisplayFilename = FText::FromString( IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*SaveFilename) );
FFormatNamedArguments Arguments;
Arguments.Add(TEXT("Filename"), DisplayFilename);
Arguments.Add(TEXT("LineTerminators"), FText::FromString(LINE_TERMINATOR LINE_TERMINATOR));
Arguments.Add(TEXT("ErrorMessage"), ErrorMessage);
const FText DisplayMessage = FText::Format( NSLOCTEXT("SaveAsImplementation", "InvalidMapName", "Failed to save map {Filename}{LineTerminators}{ErrorMessage}"), Arguments );
FMessageDialog::Open( EAppMsgType::Ok, DisplayMessage );
continue;
}
FEditorDirectories::Get().SetLastDirectory(ELastDirectory::LEVEL, FPaths::GetPath(SaveFilename));
// Check to see if there are streaming level associated with the P map, and if so, we'll
// prompt to rename those and fixup all of the named-references to levels in the maps.
bool bCanRenameStreamingLevels = false;
FString OldBaseLevelName, NewBaseLevelName;
if( bAllowStreamingLevelRename )
{
const FString OldLevelName = FPaths::GetBaseFilename(Filename);
const FString NewLevelName = FPaths::GetBaseFilename(SaveFilename);
// The old and new level names must have a common suffix. We'll detect that now.
int32 NumSuffixChars = 0;
{
for( int32 CharsFromEndIndex = 0; ; ++CharsFromEndIndex )
{
const int32 OldLevelNameCharIndex = ( OldLevelName.Len() - 1 ) - CharsFromEndIndex;
const int32 NewLevelNameCharIndex = ( NewLevelName.Len() - 1 ) - CharsFromEndIndex;
if( OldLevelNameCharIndex <= 0 || NewLevelNameCharIndex <= 0 )
{
// We've processed all characters in at least one of the strings!
break;
}
if( FChar::ToUpper( OldLevelName[ OldLevelNameCharIndex ] ) != FChar::ToUpper( NewLevelName[ NewLevelNameCharIndex ] ) )
{
// Characters don't match. We have the common suffix now.
break;
}
// We have another common character in the suffix!
++NumSuffixChars;
}
}
// We can only proceed if we found a common suffix
if( NumSuffixChars > 0 )
{
FString CommonSuffix = NewLevelName.Right( NumSuffixChars );
OldBaseLevelName = OldLevelName.Left( OldLevelName.Len() - CommonSuffix.Len() );
NewBaseLevelName = NewLevelName.Left( NewLevelName.Len() - CommonSuffix.Len() );
// OK, make sure this is really the persistent level
if( InWorld->PersistentLevel->IsPersistentLevel() )
{
// Check to see if we actually have anything to rename
bool bAnythingToRename = false;
{
// Check for contained streaming levels
for (ULevelStreaming* CurStreamingLevel : InWorld->GetStreamingLevels())
{
if (CurStreamingLevel)
{
// Update the package name
FString PackageNameToRename = CurStreamingLevel->GetWorldAssetPackageName();
if( RenameStreamingLevel( PackageNameToRename, OldBaseLevelName, NewBaseLevelName ) )
{
bAnythingToRename = true;
}
}
}
}
if( bAnythingToRename )
{
// OK, we can go ahead and rename levels
bCanRenameStreamingLevels = true;
}
}
}
}
if( bCanRenameStreamingLevels )
{
// Prompt to update streaming levels and such
// Return value: 0 = yes, 1 = no, 2 = cancel
const EAppReturnType::Type DlgResult =
FMessageDialog::Open( EAppMsgType::YesNoCancel, EAppReturnType::No,
FText::Format( NSLOCTEXT("UnrealEd", "SaveLevelAs_PromptToRenameStreamingLevels_F", "Would you like to update references to streaming levels and rename those as well?\n\nIf you select Yes, references to streaming levels in {0} will be renamed to {1} (including Level Blueprint level name references.) You should also do this for each of your streaming level maps.\n\nIf you select No, the level will be saved with the specified name and no other changes will be made." ),
FText::FromString(FPaths::GetBaseFilename(Filename)), FText::FromString(FPaths::GetBaseFilename(SaveFilename)) ) );
if( DlgResult != EAppReturnType::Cancel ) // Cancel?
{
if( DlgResult == EAppReturnType::Yes ) // Yes?
{
// Update streaming level names
for (ULevelStreaming* CurStreamingLevel : InWorld->GetStreamingLevels())
{
if (CurStreamingLevel)
{
// Update the package name
FString PackageNameToRename = CurStreamingLevel->GetWorldAssetPackageName();
if( RenameStreamingLevel( PackageNameToRename, OldBaseLevelName, NewBaseLevelName ) )
{
CurStreamingLevel->SetWorldAssetByPackageName(FName( *PackageNameToRename ));
// Level was renamed!
CurStreamingLevel->MarkPackageDirty();
}
}
}
}
// Save the level!
bStatus = FEditorFileUtils::SaveMap( InWorld, SaveFilename );
}
else
{
// User canceled, nothing to do.
}
}
else
{
// Save the level
bStatus = FEditorFileUtils::SaveMap( InWorld, SaveFilename );
}
}
else
{
// User canceled the save dialog, do not prompt again.
break;
}
}
// Restore autosaving to its previous state.
LoadingSavingSettings->bAutoSaveEnable = bOldAutoSaveState;
// Update SCC state
ISourceControlModule::Get().QueueStatusUpdate(InWorld->GetOutermost());
if (bStatus && OutSavedFilename)
{
*OutSavedFilename = SaveFilename;
}
return bStatus;
}
/**
* @return true if GWorld's package is dirty.
*/
static bool IsWorldDirty()
{
UPackage* Package = CastChecked<UPackage>(GWorld->GetOuter());
return Package->IsDirty();
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// FEditorFileUtils
//
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void FEditorFileUtils::SaveAssetsAs(const TArray<UObject*>& Assets, TArray<UObject*>& OutSavedAssets)
{
for (UObject* Asset : Assets)
{
const FString OldPackageName = Asset->GetOutermost()->GetName();
FString OldPackagePath;
FString OldAssetName;
if (Asset->HasAnyFlags(RF_Transient))
{
// determine default package path
const FString DefaultDirectory = FEditorDirectories::Get().GetLastDirectory(ELastDirectory::NEW_ASSET);
FPackageName::TryConvertFilenameToLongPackageName(DefaultDirectory, OldPackagePath);
if (OldPackagePath.IsEmpty())
{
OldPackagePath = TEXT("/Game");
}
// determine default asset name
FString DefaultName = FString(NSLOCTEXT("UnrealEd", "PrefixNew", "New").ToString() + Asset->GetClass()->GetName());
FString UniquePackageName;
FString UniqueAssetName;
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
AssetToolsModule.Get().CreateUniqueAssetName(OldPackagePath / DefaultName, TEXT(""), UniquePackageName, UniqueAssetName);
OldAssetName = FPaths::GetCleanFilename(UniqueAssetName);
}
else
{
OldAssetName = FPackageName::GetLongPackageAssetName(OldPackageName);
OldPackagePath = FPackageName::GetLongPackagePath(OldPackageName);
}
FString NewPackageName;
// get destination for asset
bool FilenameValid = false;
while (!FilenameValid)
{
if (!OpenSaveAsDialog(Asset->GetClass(), OldPackagePath, OldAssetName, NewPackageName))
{
return;
}
FText OutError;
FilenameValid = FFileHelper::IsFilenameValidForSaving(NewPackageName, OutError);
}
// process asset
if (NewPackageName.IsEmpty())
{
OutSavedAssets.Add(Asset); // user canceled
}
else if (NewPackageName != OldPackageName)
{
// duplicate asset at destination
const FString NewAssetName = FPackageName::GetLongPackageAssetName(NewPackageName);
UPackage* DuplicatedPackage = CreatePackage( *NewPackageName);
UObject* DuplicatedAsset = StaticDuplicateObject(Asset, DuplicatedPackage, *NewAssetName);
if (DuplicatedAsset != nullptr)
{
// update duplicated asset & notify asset registry
if (Asset->HasAnyFlags(RF_Transient))
{
DuplicatedAsset->ClearFlags(RF_Transient);
DuplicatedAsset->SetFlags(RF_Public | RF_Standalone);
}
if (Asset->GetOutermost()->HasAnyPackageFlags(PKG_DisallowExport))
{
DuplicatedPackage->SetPackageFlags(PKG_DisallowExport);
}
DuplicatedAsset->MarkPackageDirty();
FAssetRegistryModule::AssetCreated(DuplicatedAsset);
OutSavedAssets.Add(DuplicatedAsset);
// update last save directory
const FString PackageFilename = FPackageName::LongPackageNameToFilename(NewPackageName);
const FString PackagePath = FPaths::GetPath(PackageFilename);
FEditorDirectories::Get().SetLastDirectory(ELastDirectory::NEW_ASSET, PackagePath);
}
else
{
OutSavedAssets.Add(Asset); // error duplicating
}
}
else
{
OutSavedAssets.Add(Asset); // save existing asset
}
}
// save packages
TArray<UPackage*> PackagesToSave;
for (UObject* Asset : OutSavedAssets)
{
PackagesToSave.Add(Asset->GetOutermost());
}
FEditorFileUtils::FPromptForCheckoutAndSaveParams SaveParams;
SaveParams.bCheckDirty = true;
SaveParams.bPromptToSave = false;
FEditorFileUtils::PromptForCheckoutAndSave(PackagesToSave, SaveParams);
}
/**
* Does a saveAs for the specified level.
*
* @param InLevel The level to be SaveAs'd.
* @return true if the world was saved.
*/
bool FEditorFileUtils::SaveLevelAs(ULevel* InLevel, FString* OutSavedFilename)
{
FString DefaultFilename;
if (InLevel->IsPersistentLevel())
{
DefaultFilename = GetFilename( InLevel );
}
else
{
DefaultFilename = FPackageName::LongPackageNameToFilename( InLevel->GetOutermost()->GetName() );
}
// We'll allow the map to be renamed when saving a level as a new file name this way
const bool bAllowStreamingLevelRename = InLevel->IsPersistentLevel();
return SaveAsImplementation( CastChecked<UWorld>(InLevel->GetOuter()), DefaultFilename, bAllowStreamingLevelRename, OutSavedFilename);
}
/**
* Presents the user with a file dialog for importing.
* If the import is not a merge (bMerging is false), AskSaveChanges() is called first.
*/
void FEditorFileUtils::Import()
{
TRACE_CPUPROFILER_EVENT_SCOPE(FEditorFileUtils::Import);
TArray<FString> OpenedFiles;
FString DefaultLocation(GetDefaultDirectory());
if (FileDialogHelpers::OpenFiles(NSLOCTEXT("UnrealEd", "ImportScene", "Import Scene").ToString(), GetFilterString(FI_ImportScene), DefaultLocation, EFileDialogFlags::None, OpenedFiles))
{
Import(OpenedFiles[0]);
}
}
void FEditorFileUtils::Import(const FString& InFilename)
{
const FScopedBusyCursor BusyCursor;
UE::Interchange::FScopedSourceData ScopedSourceData(InFilename);
const bool bIsSceneImport = true; // Only scene import is requested from FEditorFileUtils::Import
const bool bImportThroughInterchange = UInterchangeManager::GetInterchangeManager().CanTranslateSourceData(ScopedSourceData.GetSourceData(), bIsSceneImport);
USceneImportFactory* SceneFactory = nullptr;
if (!bImportThroughInterchange)
{
for (UClass* Class : TObjectRange<UClass>())
{
if (Class->IsChildOf<USceneImportFactory>() && !Class->HasAnyClassFlags(CLASS_Abstract | CLASS_Deprecated | CLASS_NewerVersionExists))
{
USceneImportFactory* TestFactory = Class->GetDefaultObject<USceneImportFactory>();
if (TestFactory->FactoryCanImport(InFilename))
{
/// Pick the first one for now
SceneFactory = TestFactory;
break;
}
}
}
}
if (SceneFactory || bImportThroughInterchange)
{
FString Path = "/Game";
const bool bImportsAssets = bImportThroughInterchange || SceneFactory->ImportsAssets();
//Ask the user for the root path where they want to any content to be placed
if(bImportsAssets)
{
TSharedRef<SDlgPickPath> PickContentPathDlg =
SNew(SDlgPickPath)
.Title(LOCTEXT("ChooseImportRootContentPath", "Choose Location for importing the scene content"));
if (PickContentPathDlg->ShowModal() == EAppReturnType::Cancel)
{
return;
}
Path = PickContentPathDlg->GetPath().ToString();
}
FAssetToolsModule& AssetToolsModule = FModuleManager::Get().LoadModuleChecked<FAssetToolsModule>("AssetTools");
TArray<FString> Files;
Files.Add(InFilename);
const bool bSyncToBrowser = bImportsAssets;
constexpr bool bAllowAsyncImport = true;
constexpr bool bSceneImport = true;
AssetToolsModule.Get().ImportAssets(Files, Path, SceneFactory, bSyncToBrowser, nullptr, bAllowAsyncImport, bSceneImport);
}
else
{
FFormatNamedArguments Args;
Args.Add(TEXT("MapFilename"), FText::FromString(FPaths::GetCleanFilename(InFilename)));
GWarn->BeginSlowTask(FText::Format(NSLOCTEXT("UnrealEd", "ImportingMap_F", "Importing map: {MapFilename}..."), Args), true);
GEditor->Exec(GWorld, *FString::Printf(TEXT("MAP IMPORTADD FILE=\"%s\""), *InFilename));
GWarn->EndSlowTask();
}
GEditor->RedrawLevelEditingViewports();
FEditorDirectories::Get().SetLastDirectory(ELastDirectory::UNR, FPaths::GetPath(InFilename)); // Save path as default for next time.
FEditorDelegates::RefreshAllBrowsers.Broadcast();
}
void FEditorFileUtils::Export(bool bExportSelectedActorsOnly)
{
// @todo: extend this to multiple levels.
UWorld* World = GWorld;
const FString LevelFilename = GetFilename( World );//->GetOutermost()->GetName() );
FString ExportFilename;
FString LastUsedPath = GetDefaultDirectory();
if( FileDialogHelpers::SaveFile( NSLOCTEXT("UnrealEd", "Export", "Export").ToString(), GetFilterString(FI_ExportScene), LastUsedPath, FPaths::GetBaseFilename(LevelFilename), ExportFilename ) )
{
GEditor->ExportMap( World, *ExportFilename, bExportSelectedActorsOnly );
FEditorDirectories::Get().SetLastDirectory(ELastDirectory::UNR, FPaths::GetPath(ExportFilename)); // Save path as default for next time.
}
}
static bool IsCheckOutSelectedDisabled()
{
return !(ISourceControlModule::Get().IsEnabled() && ISourceControlModule::Get().GetProvider().IsAvailable()) || !ISourceControlModule::Get().GetProvider().UsesCheckout();
}
bool FEditorFileUtils::AddCheckoutPackageItems(bool bCheckDirty, const TArray<UPackage*>& PackagesToCheckOut, const TSet<FName>& ReadOnlyPackages, TArray<UPackage*>* OutPackagesNotNeedingCheckout, bool* bOutHavePackageToCheckOut)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FEditorFileUtils_AddCheckoutPackageItems);
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
if (ISourceControlModule::Get().IsEnabled() && SourceControlProvider.IsAvailable())
{
TArray<UPackage*> SourceControlCheckPackages;
if (CVarSkipSourceControlCheckForEditablePackages.GetValueOnAnyThread())
{
for (auto Package : PackagesToCheckOut)
{
if (!Package)
{
continue;
}
FString Filename;
if (FPackageName::DoesPackageExist(Package->GetName(), &Filename))
{
if (IFileManager::Get().IsReadOnly(*Filename))
{
// check if the package is readonly
SourceControlCheckPackages.Add(Package);
}
else
{
auto SourceControlState = SourceControlProvider.GetState(Package, EStateCacheUsage::Use);
if (!SourceControlState)
{
// check if source control doesn't know about the package
SourceControlCheckPackages.Add(Package);
}
}
}
}
}
else
{
SourceControlCheckPackages = PackagesToCheckOut;
}
if (SourceControlCheckPackages.Num())
{
// Update the source control status of all potentially relevant packages
FScopedSlowTask SlowTask(static_cast<float>(SourceControlCheckPackages.Num()), LOCTEXT("UpdatingSourceControlStatus", "Updating revision control status..."));
SlowTask.MakeDialogDelayed(0.5f);
SourceControlProvider.Execute(ISourceControlOperation::Create<FUpdateStatus>(), SourceControlCheckPackages);
SlowTask.EnterProgressFrame(static_cast<float>(SourceControlCheckPackages.Num()));
}
}
FPackagesDialogModule& CheckoutPackagesDialogModule = FModuleManager::LoadModuleChecked<FPackagesDialogModule>(TEXT("PackagesDialog"));
bool bPackagesAdded = false;
bool bShowWarning = false;
bool bOtherBranchWarning = false;
bool bHavePackageToCheckOut = false;
if (OutPackagesNotNeedingCheckout)
{
OutPackagesNotNeedingCheckout->Reset();
}
CheckoutPackagesDialogModule.RemoveAllPackageItems();
// Iterate through all the packages and add them to the dialog if necessary.
for (TArray<UPackage*>::TConstIterator PackageIter(PackagesToCheckOut); PackageIter; ++PackageIter)
{
UPackage* CurPackage = *PackageIter;
FString Filename;
// Assume the package is read only just in case we cant find a file
bool bPkgReadOnly = true;
bool bCareAboutReadOnly = SourceControlProvider.UsesLocalReadOnlyState();
// Find the filename for this package
bool bFoundFile = FPackageName::DoesPackageExist(CurPackage->GetName(), &Filename);
if (bFoundFile)
{
// determine if the package file is read only
bPkgReadOnly = ReadOnlyPackages.Contains(CurPackage->GetFName()) || IFileManager::Get().IsReadOnly(*Filename);
}
FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(CurPackage, EStateCacheUsage::Use);
// Package does not need to be checked out if its already checked out or we are ignoring it for source control
bool bNeedsRevert = SourceControlState.IsValid() && SourceControlState->IsDeleted() && !UPackage::IsEmptyPackage(CurPackage); // Need to revert the delete and then checkout
bool bSCCCanEdit = !SourceControlState.IsValid() || (SourceControlState->CanCheckIn() && !bNeedsRevert) || SourceControlState->IsIgnored() || SourceControlState->IsUnknown() || (bCareAboutReadOnly && !bPkgReadOnly);
bool bIsSourceControlled = SourceControlState.IsValid() && SourceControlState->IsSourceControlled();
if (!bSCCCanEdit && (bIsSourceControlled && (!bCheckDirty || (bCheckDirty && CurPackage->IsDirty()))) && !SourceControlState->IsCheckedOut())
{
if (SourceControlState.IsValid() && (!SourceControlState->IsCurrent() || SourceControlState->IsCheckedOutOther()))
{
if (!PackagesNotToPromptAnyMore.Contains(CurPackage->GetName()))
{
if (!SourceControlState->IsCurrent())
{
// This package is not at the head revision and it should be ghosted as a result
CheckoutPackagesDialogModule.AddPackageItem(CurPackage, ECheckBoxState::Unchecked, true, TEXT("SavePackages.SCC_DlgNotCurrent"), SourceControlState->GetDisplayTooltip().ToString());
}
else if (SourceControlState->IsCheckedOutOther())
{
// This package is checked out by someone else so it should be ghosted
CheckoutPackagesDialogModule.AddPackageItem(CurPackage, ECheckBoxState::Unchecked, true, TEXT("SavePackages.SCC_DlgCheckedOutOther"), SourceControlState->GetDisplayTooltip().ToString());
}
bShowWarning = true;
bPackagesAdded = true;
}
else
{
if (OutPackagesNotNeedingCheckout)
{
// File has already been made writable, just allow it to be saved without prompting
OutPackagesNotNeedingCheckout->Add(CurPackage);
}
}
}
else
{
// Provided it's not in the list to not prompt any more, add it to the dialog
if (!PackagesNotToPromptAnyMore.Contains(CurPackage->GetName()))
{
FText Tooltip = NSLOCTEXT("PackagesDialogModule", "Dlg_NotCheckedOutTip", "Not checked out");
if (SourceControlState.IsValid())
{
if (SourceControlState->IsCheckedOutOrModifiedInOtherBranch())
{
bShowWarning = true;
bOtherBranchWarning = true;
}
Tooltip = SourceControlState->GetDisplayTooltip();
}
bHavePackageToCheckOut = true;
//Add this package to the dialog if its not checked out, in the source control depot, dirty(if we are checking), and read only
//This package could also be marked for delete, which we will treat as SCC_ReadOnly until it is time to check it out. At that time, we will revert it.
CheckoutPackagesDialogModule.AddPackageItem(CurPackage, ECheckBoxState::Checked, false, TEXT("SavePackages.SCC_DlgReadOnly"), Tooltip.ToString());
bPackagesAdded = true;
}
else if (OutPackagesNotNeedingCheckout)
{
// The current package doesn't need to be checked out in order to save as it's already writable.
OutPackagesNotNeedingCheckout->Add(CurPackage);
}
}
}
else if (bPkgReadOnly && bFoundFile && (IsCheckOutSelectedDisabled() || !bCareAboutReadOnly))
{
const FText Tooltip = SourceControlState.IsValid() ? SourceControlState->GetDisplayTooltip() : NSLOCTEXT("PackagesDialogModule", "Dlg_NotCheckedOutTip", "Not checked out");
// Don't disable the item if the server is available. If the user updates source control within the dialog then the item should not be disabled so it can be checked out
bool bIsDisabled = !ISourceControlModule::Get().IsEnabled();
// This package is read only but source control is not available, show the dialog so users can save the package by making the file writable or by connecting to source control.
// If we don't care about read-only state, we should allow the user to make the file writable whatever the state of source control.
CheckoutPackagesDialogModule.AddPackageItem(CurPackage, ECheckBoxState::Unchecked, bIsDisabled, TEXT("SavePackages.SCC_DlgReadOnly"), Tooltip.ToString());
PackagesNotToPromptAnyMore.Remove(CurPackage->GetName());
bPackagesAdded = true;
}
else if (OutPackagesNotNeedingCheckout)
{
// The current package does not need to be checked out in order to save.
OutPackagesNotNeedingCheckout->Add(CurPackage);
PackagesNotToPromptAnyMore.Remove(CurPackage->GetName());
}
}
if (bPackagesAdded)
{
if (bShowWarning)
{
if (!bOtherBranchWarning)
{
CheckoutPackagesDialogModule.SetWarning(
NSLOCTEXT("PackagesDialogModule", "CheckoutPackagesWarnMessage", "Warning: There are modified assets which you will not be able to check out as they are locked or not at the head revision. You may lose your changes if you continue, as you will be unable to submit them to revision control."));
}
else
{
CheckoutPackagesDialogModule.SetWarning(
NSLOCTEXT("PackagesDialogModule", "CheckoutPackagesOtherBranchWarnMessage", "Warning: There are assets checked out or modified in another branch. If you check out files in the current branch, you may lose your changes."));
}
}
else
{
CheckoutPackagesDialogModule.SetWarning(FText::GetEmpty());
}
}
if (bOutHavePackageToCheckOut)
{
*bOutHavePackageToCheckOut = bHavePackageToCheckOut;
}
return bPackagesAdded;
}
void FEditorFileUtils::UpdateCheckoutPackageItems(bool bCheckDirty, TArray<UPackage*> PackagesToCheckOut, TSet<FName> ReadOnlyPackages, TArray<UPackage*>* OutPackagesNotNeedingCheckout)
{
AddCheckoutPackageItems(bCheckDirty, PackagesToCheckOut, ReadOnlyPackages, OutPackagesNotNeedingCheckout, nullptr);
}
bool FEditorFileUtils::PromptToCheckoutPackages(bool bCheckDirty, const TArray<UPackage*>& PackagesToCheckOut, TArray<UPackage*>* OutPackagesCheckedOutOrMadeWritable, TArray<UPackage*>* OutPackagesNotNeedingCheckout, const bool bPromptingAfterModify, const bool bAllowSkip)
{
if (bIsPromptingForCheckoutAndSave)
{
return false;
}
// Prevent re-entrance into this function by setting up a guard value (also used by FEditorFileUtils::PromptForCheckoutAndSave)
TGuardValue<bool> PromptForCheckoutAndSaveGuard(bIsPromptingForCheckoutAndSave, true);
bool bAutomaticCheckout = UseAlternateCheckoutWorkflow();
if (bAutomaticCheckout)
{
return FEditorFileUtils::AutomaticCheckoutOrPromptToRevertPackages(PackagesToCheckOut, OutPackagesCheckedOutOrMadeWritable, OutPackagesNotNeedingCheckout, nullptr);
}
else
{
return PromptToCheckoutPackagesInternal(bCheckDirty, PackagesToCheckOut, OutPackagesCheckedOutOrMadeWritable, OutPackagesNotNeedingCheckout, bPromptingAfterModify, bAllowSkip);
}
}
bool FEditorFileUtils::PromptToCheckoutPackagesInternal(bool bCheckDirty, const TArray<UPackage*>& PackagesToCheckOut, TArray<UPackage*>* OutPackagesCheckedOutOrMadeWritable, TArray<UPackage*>* OutPackagesNotNeedingCheckout, const bool bPromptingAfterModify, const bool bAllowSkip )
{
bool bUserResponse = true;
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
TSet<FName> ReadOnlyPackages;
UE::FileHelpers::Internal::OnPreInteractiveCheckoutPackages.Broadcast(PackagesToCheckOut, ReadOnlyPackages);
// The checkout dialog to show users if any packages need to be checked out
const FText DialogTitle = NSLOCTEXT("PackagesDialogModule", "CheckoutPackagesDialogTitle", "Check Out Assets");
const FText DialogHeading = NSLOCTEXT("PackagesDialogModule", "CheckoutPackagesDialogMessage", "Select assets to check out.");
FPackagesDialogModule& CheckoutPackagesDialogModule = FModuleManager::LoadModuleChecked<FPackagesDialogModule>( TEXT("PackagesDialog") );
// Add any of the packages which do not report as editable by source control, yet are currently in the source control depot
// If the user has specified to check for dirty packages, only add those which are dirty
bool bPackagesAdded = false;
// If we found at least one package that can be checked out, this will be true
bool bHavePackageToCheckOut = false;
const bool bReadOnly = false;
const bool bAllowSourceControlConnection = true;
CheckoutPackagesDialogModule.CreatePackagesDialog(
DialogTitle,
DialogHeading,
bReadOnly,
bAllowSourceControlConnection,
FSimpleDelegate::CreateStatic(&UpdateCheckoutPackageItems, bCheckDirty, PackagesToCheckOut, ReadOnlyPackages, OutPackagesNotNeedingCheckout)
);
// If we got here and we have one package, it's because someone explicitly saved the asset, therefore remove the package from the ignore list.
if(PackagesToCheckOut.Num()==1)
{
const FString& PackageName = PackagesToCheckOut[0]->GetName();
PackagesNotSavedDuringSaveAll.Remove(PackageName);
}
bPackagesAdded = AddCheckoutPackageItems(bCheckDirty, PackagesToCheckOut, ReadOnlyPackages, OutPackagesNotNeedingCheckout, &bHavePackageToCheckOut);
// If any packages were added to the dialog, show the dialog to the user and allow them to select which files to check out
if ( bPackagesAdded )
{
TAttribute<bool> CheckOutSelectedDisabledAttrib;
if( !bHavePackageToCheckOut && !IsCheckOutSelectedDisabled() )
{
// No packages to checkout and we are connected to the server
CheckOutSelectedDisabledAttrib.Set( true );
}
else
{
// There may be packages to check out or we arent connected to the server. We'll determine if we enable the button via a delegate
CheckOutSelectedDisabledAttrib.BindStatic( &IsCheckOutSelectedDisabled );
}
// Prepare the buttons for the checkout dialog
// The checkout button should be disabled if no packages can be checked out.
CheckoutPackagesDialogModule.AddButton(DRT_CheckOut, NSLOCTEXT("PackagesDialogModule", "Dlg_CheckOutButtonp", "Check Out Selected"), NSLOCTEXT("PackagesDialogModule", "Dlg_CheckOutTooltip", "Attempt to Check Out Checked Assets"), CheckOutSelectedDisabledAttrib );
// Make writable button to make checked files writable (to be able to save them)
// Note: this is needed when unable to checkout (not only for Perforce, but also for Plastic SCM and Subversion, even though they don't use read-only flags)
CheckoutPackagesDialogModule.AddButton(DRT_MakeWritable, NSLOCTEXT("PackagesDialogModule", "Dlg_MakeWritableButton", "Make Writable"), NSLOCTEXT("PackagesDialogModule", "Dlg_MakeWritableTooltip", "Makes selected files writable on disk"));
if (bAllowSkip)
{
// Skip button to skip checkout step
CheckoutPackagesDialogModule.AddButton(DRT_Skip, NSLOCTEXT("PackagesDialogModule", "Dlg_SkipButton", "Skip"), NSLOCTEXT("PackagesDialogModule", "Dlg_SkipTooltip", "Save all files that are writable, but don't check any files out from revision control or make them writable."));
}
// The cancel button should be different if we are prompting during a modify.
const FText CancelButtonText = bPromptingAfterModify ? NSLOCTEXT("PackagesDialogModule", "Dlg_AskMeLater", "Ask Me Later") : NSLOCTEXT("PackagesDialogModule", "Dlg_Cancel", "Cancel");
const FText CancelButtonToolTip = bPromptingAfterModify ? NSLOCTEXT("PackagesDialogModule", "Dlg_AskMeLaterToolTip", "Don't ask again until this asset is saved") : NSLOCTEXT("PackagesDialogModule", "Dlg_CancelTooltip", "Cancel Request");
CheckoutPackagesDialogModule.AddButton(DRT_Cancel, CancelButtonText, CancelButtonToolTip);
// loop until a meaningful operation was performed (checked out successfully, made writable etc.)
bool bPerformedOperation = false;
while(!bPerformedOperation)
{
// Show the dialog and store the user's response
EDialogReturnType UserResponse = CheckoutPackagesDialogModule.ShowPackagesDialog(PackagesNotSavedDuringSaveAll);
// If the user has not cancelled out of the dialog
if ( UserResponse == DRT_CheckOut )
{
// Get the packages that should be checked out from the user's choices in the dialog
TArray<UPackage*> PkgsToCheckOut;
CheckoutPackagesDialogModule.GetResults( PkgsToCheckOut, ECheckBoxState::Checked );
if(CheckoutPackages(PkgsToCheckOut, OutPackagesCheckedOutOrMadeWritable) == ECommandResult::Cancelled)
{
CheckoutPackagesDialogModule.SetMessage(NSLOCTEXT("PackagesDialogModule", "CancelledCheckoutPackagesDialogMessage", "Check out operation was cancelled.\nSelect assets to make writable or try to check out again, right-click assets for more options."));
}
else
{
UE::FileHelpers::Internal::OnPackagesInteractivelyCheckedOut.Broadcast(PkgsToCheckOut);
bPerformedOperation = true;
}
}
else if( UserResponse == DRT_MakeWritable )
{
// Get the packages that should be made writable out from the user's choices in the dialog
TArray<UPackage*> PkgsToMakeWritable;
// Both undetermined and checked should be made writable. Undetermined is only available when packages cant be checked out
CheckoutPackagesDialogModule.GetResults( PkgsToMakeWritable, ECheckBoxState::Undetermined );
CheckoutPackagesDialogModule.GetResults( PkgsToMakeWritable, ECheckBoxState::Checked);
MakePackagesWritable(PkgsToMakeWritable, OutPackagesCheckedOutOrMadeWritable, nullptr);
UE::FileHelpers::Internal::OnPackagesInteractivelyMadeWritable.Broadcast(PkgsToMakeWritable);
bPerformedOperation = true;
}
else if (UserResponse == DRT_Save || UserResponse == DRT_Skip)
{
bPerformedOperation = true;
}
else if (UserResponse == DRT_Cancel || UserResponse == DRT_None)
{
// Handle the case of the user canceling out of the dialog
bUserResponse = false;
bPerformedOperation = true;
}
}
}
UE::FileHelpers::Internal::OnPostInteractiveCheckoutPackages.Broadcast(PackagesToCheckOut, bUserResponse);
// Update again to catch potentially new SCC states
ISourceControlModule::Get().QueueStatusUpdate(PackagesToCheckOut);
// If any files were just checked out, remove any pending flag to show a notification prompting for checkout.
if (GUnrealEd && PackagesToCheckOut.Num() > 0)
{
for (UPackage* Package : PackagesToCheckOut)
{
GUnrealEd->PackageToNotifyState.Add(Package, NS_DialogPrompted);
}
}
if (OutPackagesNotNeedingCheckout)
{
ISourceControlModule::Get().QueueStatusUpdate(*OutPackagesNotNeedingCheckout);
}
return bUserResponse;
}
void FEditorFileUtils::MakePackagesWritable(const TArray<UPackage*>& PackagesToMakeWritable, TArray<UPackage*>* OutPackagesMadeWritable, TArray<UPackage*>* OutPackagesMadeWritableFailed)
{
TArray<UPackage*> PackagesMadeWritableSuccess;
TArray<UPackage*> PackagesMadeWritableFailure;
PackagesMadeWritableSuccess.Reserve(PackagesToMakeWritable.Num());
PackagesMadeWritableFailure.Reserve(PackagesToMakeWritable.Num());
// Packages that are deleted in source control may need to be reverted before we can make them writable
if (ISourceControlModule::Get().IsEnabled())
{
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
TArray<UPackage*> PackagesToRevert;
for (UPackage* PackageToMakeWritable : PackagesToMakeWritable)
{
if (FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(PackageToMakeWritable, EStateCacheUsage::Use);
SourceControlState && SourceControlState->IsDeleted() && !UPackage::IsEmptyPackage(PackageToMakeWritable))
{
PackagesToRevert.Add(PackageToMakeWritable);
}
}
if (PackagesToRevert.Num() > 0)
{
SourceControlProvider.Execute(ISourceControlOperation::Create<FRevert>(), PackagesToRevert);
}
}
// Attempt to make writable each package the user checked
FUncontrolledChangelistsModule& UncontrolledChangelistModule = FUncontrolledChangelistsModule::Get();
for (UPackage* PackageToMakeWritable : PackagesToMakeWritable)
{
FString Filename;
bool bFoundFile = FPackageName::DoesPackageExist(PackageToMakeWritable->GetName(), &Filename);
if (bFoundFile)
{
// If we're ignoring the package due to the user ignoring it for saving, remove it from the ignore list
// as getting here means we've explicitly decided to save the asset.
PackagesNotSavedDuringSaveAll.Remove(PackageToMakeWritable->GetName());
// Get the fully qualified filename.
const FString FullFilename = FPaths::ConvertRelativePathToFull(Filename);
// Knock off the read only flag from the current file attributes
if (FPlatformFileManager::Get().GetPlatformFile().SetReadOnly(*Filename, false))
{
// Add to PackagesNotToPromptAnyMore only if not added to Uncontrolled Changelist.
// If added to Uncontrolled Changelist, we want the checkout prompt to be displayed again if the file is reverted
if (!UncontrolledChangelistModule.OnMakeWritable(Filename))
{
PackagesNotToPromptAnyMore.Add(PackageToMakeWritable->GetName());
}
PackagesMadeWritableSuccess.Add(PackageToMakeWritable);
}
else
{
PackagesMadeWritableFailure.Add(PackageToMakeWritable);
}
}
else
{
PackagesMadeWritableSuccess.Add(PackageToMakeWritable);
}
}
if (PackagesMadeWritableFailure.Num() > 0)
{
FString PkgsWhichFailedWritable;
for (const FString& UserFacingPath : IAssetTools::Get().GetUserFacingLongPackageNames(PackagesMadeWritableFailure))
{
PkgsWhichFailedWritable += FString::Printf(TEXT("\n%s"), *UserFacingPath);
}
FFormatNamedArguments Arguments;
Arguments.Add(TEXT("Packages"), FText::FromString(PkgsWhichFailedWritable));
FText MessageFormatting = NSLOCTEXT("FileHelper", "FailedMakingWritableDlgMessageFormatting", "The following assets could not be made writable:{Packages}");
FText Message = FText::Format(MessageFormatting, Arguments);
FText Title = NSLOCTEXT("FileHelper", "FailedMakingWritableDlg_Title", "Unable to make assets writable");
FMessageDialog::Open(EAppMsgType::Ok, Message, Title);
}
if (OutPackagesMadeWritable)
{
OutPackagesMadeWritable->Append(PackagesMadeWritableSuccess);
}
if (OutPackagesMadeWritableFailed)
{
OutPackagesMadeWritableFailed->Append(PackagesMadeWritableFailure);
}
}
ECommandResult::Type FEditorFileUtils::CheckoutPackages(const TArray<UPackage*>& PkgsToCheckOut, TArray<UPackage*>* OutPackagesCheckedOut, const bool bErrorIfAlreadyCheckedOut, const bool bConfirmPackageBranchCheckOutStatus)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FEditorFileUtils_CheckoutPackages);
ECommandResult::Type CheckOutResult = ECommandResult::Succeeded;
FString PkgsWhichFailedCheckout;
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
TArray<UPackage*> FinalPackageCheckoutList;
TArray<UPackage*> FinalPackageMarkForAddList;
TArray<UPackage*> FinalPackageDeleteList;
// Source control may have been enabled in the package checkout dialog.
// Ensure the status is up to date
if(PkgsToCheckOut.Num() > 0)
{
CheckOutResult = SourceControlProvider.Execute(ISourceControlOperation::Create<FUpdateStatus>(), PkgsToCheckOut);
}
if (CheckOutResult != ECommandResult::Cancelled)
{
// If any packages are checked out or modified in another branch, prompt for confirmation
if (bConfirmPackageBranchCheckOutStatus && !ConfirmPackageBranchCheckOutStatus(PkgsToCheckOut))
{
return ECommandResult::Cancelled;
}
// Print out all the packages and set the check out result
auto FailedIntermediateOperations = [&CheckOutResult, &PkgsWhichFailedCheckout, &PkgsToCheckOut]()
{
for (const FString& PackageToCheckOutName : IAssetTools::Get().GetUserFacingLongPackageNames(PkgsToCheckOut))
{
PkgsWhichFailedCheckout += FString::Printf(TEXT("\n%s"), *PackageToCheckOutName);
}
CheckOutResult = ECommandResult::Failed;
};
// Get States as a single operation
TArray<FSourceControlStateRef> SourceControlStates;
ECommandResult::Type IntermediateResult = SourceControlProvider.GetState(PkgsToCheckOut, SourceControlStates, EStateCacheUsage::Use);
if (IntermediateResult == ECommandResult::Succeeded)
{
TArray<UPackage*> PkgsToRevert;
PkgsToRevert.Reserve(PkgsToCheckOut.Num());
for (int Index = 0; Index < SourceControlStates.Num(); ++Index)
{
const FSourceControlStateRef& SourceControlState = SourceControlStates[Index];
if (SourceControlState->IsDeleted() && !UPackage::IsEmptyPackage(PkgsToCheckOut[Index]))
{
PkgsToRevert.Add(PkgsToCheckOut[Index]);
}
}
if (PkgsToRevert.Num() > 0)
{
IntermediateResult = SourceControlProvider.Execute(ISourceControlOperation::Create<FRevert>(), PkgsToRevert);
if (IntermediateResult == ECommandResult::Succeeded)
{
// Force update all states to checkout
IntermediateResult = SourceControlProvider.GetState(PkgsToCheckOut, SourceControlStates, EStateCacheUsage::ForceUpdate);
}
}
// In case we called GetState after a revert
if (IntermediateResult == ECommandResult::Succeeded)
{
// Assemble a final list of packages to check out
for (int32 Index = 0; Index < PkgsToCheckOut.Num(); ++Index)
{
UPackage* PackageToCheckOut = PkgsToCheckOut[Index];
const FSourceControlStateRef& SourceControlState = SourceControlStates[Index];
FString Filename;
const bool bPackageExists = FPackageName::DoesPackageExist(PackageToCheckOut->GetName(), &Filename);
// Mark the package for check out if possible
bool bShowCheckoutError = true;
if (!bPackageExists && UPackage::IsEmptyPackage(PackageToCheckOut))
{
// This package has already been deleted on disk, so update source control to match
if (SourceControlState->CanDelete())
{
bShowCheckoutError = false;
FinalPackageDeleteList.Add(PackageToCheckOut);
}
else if (SourceControlState->IsDeleted() && !bErrorIfAlreadyCheckedOut)
{
bShowCheckoutError = false;
}
}
else if (SourceControlState->CanCheckout())
{
bShowCheckoutError = false;
FinalPackageCheckoutList.Add(PackageToCheckOut);
}
else if (SourceControlState->CanAdd())
{
// Cannot add unsaved packages to source control
if (bPackageExists)
{
bShowCheckoutError = false;
FinalPackageMarkForAddList.Add(PackageToCheckOut);
}
else
{
// Silently skip package that has not been saved yet
// Expected when called by InternalCheckoutAndSavePackages before packages saved
bShowCheckoutError = false;
}
}
else if (SourceControlState->IsAdded())
{
if (!bErrorIfAlreadyCheckedOut)
{
bShowCheckoutError = false;
}
}
else if (!bErrorIfAlreadyCheckedOut && SourceControlState->IsCheckedOut() && !SourceControlState->IsCheckedOutOther())
{
bShowCheckoutError = false;
}
// If the package couldn't be checked out, log it so the list of failures can be displayed afterwards
if (bShowCheckoutError)
{
const FString PackageToCheckOutName = IAssetTools::Get().GetUserFacingLongPackageName(*PackageToCheckOut);
PkgsWhichFailedCheckout += FString::Printf(TEXT("\n%s"), *PackageToCheckOutName);
CheckOutResult = ECommandResult::Failed;
}
}
}
}
if (IntermediateResult != ECommandResult::Succeeded)
{
FailedIntermediateOperations();
}
}
const int32 CombinedPackageCount = FinalPackageCheckoutList.Num() + FinalPackageMarkForAddList.Num() + FinalPackageDeleteList.Num();
if (CombinedPackageCount > 0)
{
FScopedSlowTask SlowTask(static_cast<float>(CombinedPackageCount), LOCTEXT("CheckingOutPackages", "Checking out packages..."));
SlowTask.MakeDialog();
// Attempt to check out each package the user specified to be checked out that is not read only
if (FinalPackageCheckoutList.Num() > 0)
{
CheckOutResult = SourceControlProvider.Execute(ISourceControlOperation::Create<FCheckOut>(), FinalPackageCheckoutList);
SlowTask.EnterProgressFrame(static_cast<float>(FinalPackageCheckoutList.Num()));
}
// Attempt to mark for add each package the user specified that is not already tracked by source control
ECommandResult::Type MarkForAddResult = ECommandResult::Cancelled;
if (FinalPackageMarkForAddList.Num() > 0)
{
MarkForAddResult = SourceControlProvider.Execute(ISourceControlOperation::Create<FMarkForAdd>(), FinalPackageMarkForAddList);
SlowTask.EnterProgressFrame(static_cast<float>(FinalPackageMarkForAddList.Num()));
}
// Attempt to delete each package the user specified that is empty and missing from disk
ECommandResult::Type DeleteResult = ECommandResult::Cancelled;
if (FinalPackageDeleteList.Num() > 0)
{
DeleteResult = SourceControlProvider.Execute(ISourceControlOperation::Create<FDelete>(), FinalPackageDeleteList);
SlowTask.EnterProgressFrame(static_cast<float>(FinalPackageDeleteList.Num()));
}
// Checked out some or all files successfully, so check their state
auto VerifySourceControlState = [&SourceControlProvider, &OutPackagesCheckedOut, &PkgsWhichFailedCheckout, &CheckOutResult](const TArray<UPackage*>& PackagesToVerify, const ECommandResult::Type CommandResult, TFunctionRef<bool(const FSourceControlStateRef&)> IsValidState)
{
if (CommandResult != ECommandResult::Cancelled)
{
for (UPackage* CurPackage : PackagesToVerify)
{
// If we're ignoring the package due to the user ignoring it for saving, remove it from the ignore list
// as getting here means we've explicitly decided to save the asset.
const FString CurPackageName = CurPackage->GetName();
PackagesNotSavedDuringSaveAll.Remove(CurPackageName);
FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(CurPackage, EStateCacheUsage::Use);
if (SourceControlState && IsValidState(SourceControlState.ToSharedRef()))
{
if (OutPackagesCheckedOut)
{
OutPackagesCheckedOut->Add(CurPackage);
}
}
else
{
PkgsWhichFailedCheckout += FString::Printf(TEXT("\n%s"), *IAssetTools::Get().GetUserFacingLongPackageName(*CurPackage));
CheckOutResult = ECommandResult::Failed;
}
}
}
};
VerifySourceControlState(FinalPackageCheckoutList, CheckOutResult, [](const FSourceControlStateRef& SourceControlState)
{
return SourceControlState->IsCheckedOut();
});
VerifySourceControlState(FinalPackageMarkForAddList, MarkForAddResult, [](const FSourceControlStateRef& SourceControlState)
{
return SourceControlState->IsAdded();
});
VerifySourceControlState(FinalPackageDeleteList, DeleteResult, [](const FSourceControlStateRef& SourceControlState)
{
return SourceControlState->IsDeleted();
});
}
// If any packages failed the check out process, report them to the user so they know
if ( !PkgsWhichFailedCheckout.IsEmpty() )
{
FFormatNamedArguments Arguments;
Arguments.Add(TEXT("Packages"), FText::FromString( PkgsWhichFailedCheckout ));
FText MessageFormat = NSLOCTEXT("FileHelper", "FailedCheckoutDlgMessageFormatting", "The following assets could not be successfully checked out from revision control:{Packages}");
FText Message = FText::Format( MessageFormat, Arguments );
FText Title = NSLOCTEXT("FileHelper", "FailedCheckoutDlg_Title", "Unable to Check Out From Revision Control!");
FMessageDialog::Open(EAppMsgType::Ok, Message, Title);
}
return CheckOutResult;
}
ECommandResult::Type FEditorFileUtils::CheckoutPackages(const TArray<FString>& PkgsToCheckOut, TArray<FString>* OutPackagesCheckedOut, const bool bErrorIfAlreadyCheckedOut)
{
ECommandResult::Type CheckOutResult = ECommandResult::Succeeded;
FString PkgsWhichFailedCheckout;
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
// Source control may have been enabled in the package checkout dialog.
// Ensure the status is up to date
if(PkgsToCheckOut.Num() > 0)
{
// We have an array of package names, but the SCC needs an array of their corresponding filenames
TArray<FString> PkgsToCheckOutFilenames;
PkgsToCheckOutFilenames.Reserve(PkgsToCheckOut.Num());
for( auto PkgsToCheckOutIter = PkgsToCheckOut.CreateConstIterator(); PkgsToCheckOutIter; ++PkgsToCheckOutIter )
{
const FString& PackageToCheckOutName = *PkgsToCheckOutIter;
FString PackageFilename;
if(FPackageName::DoesPackageExist(PackageToCheckOutName, &PackageFilename))
{
PkgsToCheckOutFilenames.Add(PackageFilename);
}
}
CheckOutResult = SourceControlProvider.Execute(ISourceControlOperation::Create<FUpdateStatus>(), PkgsToCheckOutFilenames);
}
TArray<FString> FinalPackageCheckoutList;
TArray<FString> FinalPackageMarkForAddList;
if(CheckOutResult != ECommandResult::Cancelled)
{
// Assemble a final list of packages to check out
for( auto PkgsToCheckOutIter = PkgsToCheckOut.CreateConstIterator(); PkgsToCheckOutIter; ++PkgsToCheckOutIter )
{
const FString& PackageToCheckOutName = *PkgsToCheckOutIter;
// The SCC needs the filename
FString PackageFilename;
FPackageName::DoesPackageExist(PackageToCheckOutName, &PackageFilename);
FSourceControlStatePtr SourceControlState;
if(!PackageFilename.IsEmpty())
{
SourceControlState = SourceControlProvider.GetState(PackageFilename, EStateCacheUsage::Use);
}
// If the file was marked for delete, revert it now so it can be checked out below
if ( SourceControlState.IsValid() && SourceControlState->IsDeleted() )
{
SourceControlProvider.Execute(ISourceControlOperation::Create<FRevert>(), PackageFilename);
SourceControlState = SourceControlProvider.GetState(PackageFilename, EStateCacheUsage::ForceUpdate);
}
// Mark the package for check out if possible
bool bShowCheckoutError = true;
if( SourceControlState.IsValid() )
{
if( SourceControlState->CanCheckout() )
{
bShowCheckoutError = false;
FinalPackageCheckoutList.Add(PackageToCheckOutName);
}
else if (SourceControlState->CanAdd())
{
bShowCheckoutError = false;
FinalPackageMarkForAddList.Add(PackageToCheckOutName);
}
else if( !bErrorIfAlreadyCheckedOut && SourceControlState->IsCheckedOut() && !SourceControlState->IsCheckedOutOther() )
{
bShowCheckoutError = false;
}
else if (!bErrorIfAlreadyCheckedOut && SourceControlState->IsAdded())
{
bShowCheckoutError = false;
}
}
// If the package couldn't be checked out, log it so the list of failures can be displayed afterwards
if(bShowCheckoutError)
{
PkgsWhichFailedCheckout += FString::Printf( TEXT("\n%s"), *IAssetTools::Get().GetUserFacingLongPackageName(FName(PackageToCheckOutName)) );
}
}
}
// We have an array of package names, but the SCC needs an array of their corresponding filenames
auto GetFilenamesFromPackageNames = [](const TArray<FString>& PackageNames)
{
TArray<FString> Filenames;
Filenames.Reserve(PackageNames.Num());
for (const FString& PackageName : PackageNames)
{
FString PackageFilename;
if (FPackageName::DoesPackageExist(PackageName, &PackageFilename))
{
Filenames.Add(PackageFilename);
}
}
return Filenames;
};
// Attempt to check out each package the user specified to be checked out that is not read only
if (FinalPackageCheckoutList.Num() > 0)
{
// We have an array of package names, but the SCC needs an array of their corresponding filenames
TArray<FString> Filenames = GetFilenamesFromPackageNames(FinalPackageCheckoutList);
CheckOutResult = SourceControlProvider.Execute(ISourceControlOperation::Create<FCheckOut>(), Filenames);
}
// Attempt to mark for add each package the user specified not already tracked by source control
ECommandResult::Type MarkForAddResult = ECommandResult::Succeeded;
if (FinalPackageMarkForAddList.Num() > 0)
{
TArray<FString> Filenames = GetFilenamesFromPackageNames(FinalPackageMarkForAddList);
MarkForAddResult = SourceControlProvider.Execute(ISourceControlOperation::Create<FMarkForAdd>(), Filenames);
}
TArray<FString> CombinedPackageList = FinalPackageCheckoutList;
CombinedPackageList.Append(FinalPackageMarkForAddList);
if (CombinedPackageList.Num() > 0)
{
{
// Checked out some or all files successfully, so check their state
for (int32 i = 0; i < CombinedPackageList.Num(); ++i)
{
const bool bCheckedOut = (i < FinalPackageCheckoutList.Num()) && (CheckOutResult != ECommandResult::Cancelled);
const bool bMarkedForAdd = (i >= FinalPackageCheckoutList.Num()) && (MarkForAddResult != ECommandResult::Cancelled);
if (!(bCheckedOut || bMarkedForAdd))
{
continue;
}
const FString& CurPackageName = CombinedPackageList[i];
// If we're ignoring the package due to the user ignoring it for saving, remove it from the ignore list
// as getting here means we've explicitly decided to save the asset.
PackagesNotSavedDuringSaveAll.Remove(CurPackageName);
// The SCC needs the filename
FString PackageFilename;
FPackageName::DoesPackageExist(CurPackageName, &PackageFilename);
FSourceControlStatePtr SourceControlState;
if(!PackageFilename.IsEmpty())
{
SourceControlState = SourceControlProvider.GetState(PackageFilename, EStateCacheUsage::Use);
}
if (SourceControlState.IsValid() && (SourceControlState->IsCheckedOut() || SourceControlState->IsAdded()))
{
if ( OutPackagesCheckedOut )
{
OutPackagesCheckedOut->Add(CurPackageName);
}
}
else
{
PkgsWhichFailedCheckout += FString::Printf( TEXT("\n%s"), *IAssetTools::Get().GetUserFacingLongPackageName(FName(CurPackageName)) );
}
}
}
}
// If any packages failed the check out process, report them to the user so they know
if (!PkgsWhichFailedCheckout.IsEmpty())
{
CheckOutResult = ECommandResult::Type::Failed;
FFormatNamedArguments Arguments;
Arguments.Add(TEXT("Packages"), FText::FromString( PkgsWhichFailedCheckout ));
FText MessageFormat = NSLOCTEXT("FileHelper", "FailedCheckoutDlgMessageFormatting", "The following assets could not be successfully checked out from revision control:{Packages}");
FText Message = FText::Format( MessageFormat, Arguments );
FText Title = NSLOCTEXT("FileHelper", "FailedCheckoutDlg_Title", "Unable to Check Out From Revision Control!");
FMessageDialog::Open(EAppMsgType::Ok, Message, Title);
}
return CheckOutResult;
}
/**
* Prompt the user with a check-box dialog allowing them to check out relevant level packages
* from source control
*
* @param bCheckDirty If true, non-dirty packages won't be added to the dialog
* @param SpecificLevelsToCheckOut If specified, only the provided levels' packages will display in the
* dialog if they are under source control; If nothing is specified, all levels
* referenced by GWorld whose packages are under source control will be displayed
* @param OutPackagesNotNeedingCheckout If not null, this array will be populated with packages that the user was not prompted about and do not need to be checked out to save. Useful for saving packages even if the user canceled the checkout dialog.
*
* @return true if the user did not cancel out of the dialog and has potentially checked out some files (or if there is
* no source control integration); false if the user cancelled the dialog
*/
bool FEditorFileUtils::PromptToCheckoutLevels(bool bCheckDirty, const TArray<ULevel*>& SpecificLevelsToCheckOut, TArray<UPackage*>* OutPackagesNotNeedingCheckout )
{
bool bResult = true;
// Only attempt to display the dialog and check out packages if source control integration is present
TArray<UPackage*> PromptPackages;
bool bPackagesAdded = false;
// If levels were specified by the user, they should be the only ones considered potentially relevant
for ( TArray<ULevel*>::TConstIterator SpecificLevelsIter( SpecificLevelsToCheckOut ); SpecificLevelsIter; ++SpecificLevelsIter )
{
UPackage* LevelsWorldPackage = ( *SpecificLevelsIter )->GetOutermost();
check(LevelsWorldPackage);
// If the user has specified to check if the package is dirty, do so before deeming
// the package potentially relevant
if (!bCheckDirty || LevelsWorldPackage->IsDirty())
{
PromptPackages.AddUnique( LevelsWorldPackage );
}
// When prompting for level check out, also add any dependent packages (i.e. external actors)
for (UPackage* OwnedPackage : LevelsWorldPackage->GetExternalPackages())
{
if (OwnedPackage && (!bCheckDirty || OwnedPackage->IsDirty()))
{
PromptPackages.Add(OwnedPackage);
}
}
}
// Prompt the user with the provided packages if they prove to be relevant (i.e. in source control and not checked out)
// Note: The user's dirty flag option is not passed in here because it's already been taken care of within the function (with a special case)
bResult = FEditorFileUtils::PromptToCheckoutPackages( false, PromptPackages, nullptr, OutPackagesNotNeedingCheckout );
return bResult;
}
/**
* Overloaded version of PromptToCheckOutLevels which prompts the user with a check-box dialog allowing
* them to check out the relevant level package if necessary
*
* @param bCheckDirty If true, non-dirty packages won't be added to the dialog
* @param SpecificLevelToCheckOut The level whose package will display in the dialog if it is
* under source control
*
* @return true if the user did not cancel out of the dialog and has potentially checked out some files (or if there is
* no source control integration); false if the user cancelled the dialog
*/
bool FEditorFileUtils::PromptToCheckoutLevels(bool bCheckDirty, ULevel* SpecificLevelToCheckOut)
{
check( SpecificLevelToCheckOut != NULL );
// Add the specified level to an array and use the other version of this function
TArray<ULevel*> LevelsToCheckOut;
LevelsToCheckOut.AddUnique( SpecificLevelToCheckOut );
return FEditorFileUtils::PromptToCheckoutLevels( bCheckDirty, LevelsToCheckOut );
}
void FEditorFileUtils::OpenLevelPickingDialog(const FOnLevelsChosen& OnLevelsChosen, const FOnLevelPickingCancelled& OnLevelPickingCancelled, bool bAllowMultipleSelection)
{
struct FLocal
{
static void OnLevelsSelected(const TArray<FAssetData>& SelectedLevels, FOnLevelsChosen OnLevelsChosenDelegate)
{
if ( SelectedLevels.Num() > 0 )
{
// We selected a level. Save the path to this level to use as the default path next time we open.
const FAssetData& FirstAssetData = SelectedLevels[0];
// Convert from package name to filename. Add a trailing slash to prevent an invalid conversion when an asset is in a root folder (e.g. /Game)
FString FilesystemPath = FPackageName::LongPackageNameToFilename(FirstAssetData.PackagePath.ToString() + TEXT("/"));;
// Remove the slash if needed
if ( FilesystemPath.EndsWith(TEXT("/"), ESearchCase::CaseSensitive) )
{
FilesystemPath.LeftChopInline(1, EAllowShrinking::No);
}
FEditorDirectories::Get().SetLastDirectory(ELastDirectory::LEVEL, FilesystemPath);
OnLevelsChosenDelegate.ExecuteIfBound(SelectedLevels);
}
}
static void OnDialogCancelled(FOnLevelPickingCancelled OnLevelPickingCancelledDelegate)
{
OnLevelPickingCancelledDelegate.ExecuteIfBound();
}
};
// Determine the starting path. Try to use the most recently used directory
FString DefaultPath;
{
FString DefaultFilesystemDirectory = FEditorDirectories::Get().GetLastDirectory(ELastDirectory::LEVEL);
//ensure trailing "/" for directory name since TryConvertFilenameToLongPackageName expects one
if(!DefaultFilesystemDirectory.IsEmpty() && !DefaultFilesystemDirectory.EndsWith("/"))
{
DefaultFilesystemDirectory.AppendChar(TEXT('/'));
}
if (DefaultFilesystemDirectory.IsEmpty() || !FPackageName::TryConvertFilenameToLongPackageName(DefaultFilesystemDirectory, DefaultPath))
{
// No saved path, just use a reasonable default
DefaultPath = TEXT("/Game/Maps");
}
//OpenAssetDialog expects no trailing "/" so remove if necessary
DefaultPath.RemoveFromEnd(TEXT("/"));
}
FOpenAssetDialogConfig OpenAssetDialogConfig;
OpenAssetDialogConfig.DialogTitleOverride = LOCTEXT("OpenLevelDialogTitle", "Open Level");
OpenAssetDialogConfig.DefaultPath = DefaultPath;
OpenAssetDialogConfig.AssetClassNames.Add(UWorld::StaticClass()->GetClassPathName());
OpenAssetDialogConfig.bAllowMultipleSelection = bAllowMultipleSelection;
FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>("ContentBrowser");
ContentBrowserModule.Get().CreateOpenAssetDialog(OpenAssetDialogConfig,
FOnAssetsChosenForOpen::CreateStatic(&FLocal::OnLevelsSelected, OnLevelsChosen),
FOnAssetDialogCancelled::CreateStatic(&FLocal::OnDialogCancelled, OnLevelPickingCancelled));
}
bool FEditorFileUtils::IsValidMapFilename(const FString& MapFilename, FText& OutErrorMessage)
{
if( FPaths::GetExtension(MapFilename, true) != FPackageName::GetMapPackageExtension() )
{
OutErrorMessage = FText::Format( NSLOCTEXT("IsValidMapFilename", "FileIsNotAMap", "Filename does not have a {0} extension."), FText::FromString(FPackageName::GetMapPackageExtension()) );
return false;
}
if( !FFileHelper::IsFilenameValidForSaving( MapFilename, OutErrorMessage ) )
{
return false;
}
// Make sure we can make a package name out of this file
FString PackageName;
if ( !FPackageName::TryConvertFilenameToLongPackageName(MapFilename, PackageName) )
{
TArray<FString> RootContentPaths;
FPackageName::QueryRootContentPaths( RootContentPaths );
const FString AbsoluteMapFilePath = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*MapFilename);
TArray<FString> AbsoluteContentPaths;
bool bValidPathButContainsInvalidCharacters = false;
for( TArray<FString>::TConstIterator RootPathIt( RootContentPaths ); RootPathIt; ++RootPathIt )
{
const FString& RootPath = *RootPathIt;
const FString& ContentFolder = FPackageName::LongPackageNameToFilename( RootPath );
const FString AbsoluteContentFolder = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead( *ContentFolder );
if ( AbsoluteMapFilePath.StartsWith(AbsoluteContentFolder) )
{
bValidPathButContainsInvalidCharacters = true;
}
AbsoluteContentPaths.Add(AbsoluteContentFolder);
}
if ( bValidPathButContainsInvalidCharacters )
{
FString InvalidCharacters = TEXT(".\\:");
OutErrorMessage = FText::Format( NSLOCTEXT("IsValidMapFilename", "NotAValidPackage_InvalidCharacters", "The path contains at least one of these invalid characters below the content folder [{0}]"), FText::FromString(InvalidCharacters) );
}
else
{
FString ValidPathsString;
for( TArray<FString>::TConstIterator RootPathIt( AbsoluteContentPaths ); RootPathIt; ++RootPathIt )
{
ValidPathsString += LINE_TERMINATOR;
ValidPathsString += *RootPathIt;
}
FFormatNamedArguments Arguments;
Arguments.Add(TEXT("LineTerminators"), FText::FromString(LINE_TERMINATOR));
Arguments.Add(TEXT("ValidPaths"), FText::FromString(ValidPathsString));
OutErrorMessage = FText::Format( NSLOCTEXT("IsValidMapFilename", "NotAValidPackage", "File is not in any of the following content folders:{LineTerminators}{ValidPaths}"), Arguments );
}
return false;
}
// Make sure the final package name contains no illegal characters
{
FName PackageFName(*PackageName);
if ( !PackageFName.IsValidGroupName(OutErrorMessage) )
{
return false;
}
}
// If there is a uasset file at the save location with the same name, this is an invalid filename
const FString UAssetFilename = FPaths::GetBaseFilename(MapFilename, false) + FPackageName::GetAssetPackageExtension();
if ( FPaths::FileExists(UAssetFilename) )
{
OutErrorMessage = NSLOCTEXT("IsValidMapFilename", "MapNameInUseByAsset", "Filename is in use by an asset file in the folder.");
return false;
}
return true;
}
bool FEditorFileUtils::AttemptUnloadInactiveWorldPackage(UPackage* PackageToUnload, FText& OutErrorMessage)
{
if ( ensure(PackageToUnload) )
{
UWorld* ExistingWorld = UWorld::FindWorldInPackage(PackageToUnload);
if ( ExistingWorld )
{
bool bContinueUnloadingExistingWorld = false;
switch (ExistingWorld->WorldType)
{
case EWorldType::None:
case EWorldType::Inactive:
// Untyped and inactive worlds are safe to unload
bContinueUnloadingExistingWorld = true;
break;
case EWorldType::Editor:
OutErrorMessage = NSLOCTEXT("SaveAsImplementation", "ExistingWorldNotInactive", "You can not unload a level you are currently editing.");
bContinueUnloadingExistingWorld = false;
break;
case EWorldType::Game:
case EWorldType::PIE:
case EWorldType::EditorPreview:
default:
OutErrorMessage = NSLOCTEXT("SaveAsImplementation", "ExistingWorldInvalid", "The level you are attempting to unload is invalid.");
bContinueUnloadingExistingWorld = false;
break;
}
if ( !bContinueUnloadingExistingWorld )
{
return false;
}
}
TArray<UPackage*> PackagesToUnload;
PackagesToUnload.Add(PackageToUnload);
TWeakObjectPtr<UPackage> WeakPackage = PackageToUnload;
if (!UPackageTools::UnloadPackages(PackagesToUnload, OutErrorMessage))
{
return false;
}
if ( WeakPackage.IsValid() )
{
OutErrorMessage = NSLOCTEXT("SaveAsImplementation", "ExistingPackageFailedToUnload", "Failed to unload existing level.");
return false;
}
}
return true;
}
/**
* Prompts the user to save the current map if necessary, the presents a load dialog and
* loads a new map if selected by the user.
*/
bool FEditorFileUtils::LoadMap()
{
if (GEditor->WarnIfLightingBuildIsCurrentlyRunning())
{
return false;
}
static bool bIsDialogOpen = false;
struct FLocal
{
static void HandleLevelsChosen(const TArray<FAssetData>& SelectedAssets)
{
bIsDialogOpen = false;
if ( SelectedAssets.Num() > 0 )
{
const FAssetData& AssetData = SelectedAssets[0];
if (!GIsDemoMode)
{
// If there are any unsaved changes to the current level, see if the user wants to save those first.
bool bPromptUserToSave = true;
bool bSaveMapPackages = true;
bool bSaveContentPackages = true;
if (FEditorFileUtils::SaveDirtyPackages(bPromptUserToSave, bSaveMapPackages, bSaveContentPackages) == false)
{
return;
}
}
const FString FileToOpen = FPackageName::LongPackageNameToFilename(AssetData.PackageName.ToString(), FPackageName::GetMapPackageExtension());
const bool bLoadAsTemplate = false;
const bool bShowProgress = true;
FEditorFileUtils::LoadMap(FileToOpen, bLoadAsTemplate, bShowProgress);
}
}
static void HandleDialogCancelled()
{
bIsDialogOpen = false;
}
};
if (!bIsDialogOpen)
{
bIsDialogOpen = true;
const bool bAllowMultipleSelection = false;
OpenLevelPickingDialog(FOnLevelsChosen::CreateStatic(&FLocal::HandleLevelsChosen),
FOnLevelPickingCancelled::CreateStatic(&FLocal::HandleDialogCancelled),
bAllowMultipleSelection);
}
return false; // TODO: Because OpenLevelPickingDialog is not modal, this always returned false. UE-55083 tracks making this return a proper value again.
}
static void NotifyBSPNeedsRebuild(const FString& PackageName)
{
static TWeakPtr<SNotificationItem> NotificationPtr;
auto RemoveNotification = []
{
TSharedPtr<SNotificationItem> Notification = NotificationPtr.Pin();
if (Notification.IsValid())
{
Notification->SetEnabled(false);
Notification->SetExpireDuration(0.0f);
Notification->SetFadeOutDuration(0.5f);
Notification->ExpireAndFadeout();
NotificationPtr.Reset();
}
};
// If there's still a notification present from the last time a map was loaded, get rid of it now.
RemoveNotification();
FNotificationInfo Info(LOCTEXT("BSPIssues", "Some issues were detected with BSP/Volume geometry in the loaded level or one of its sub-levels.\nThis is due to a fault in previous versions of the editor which has now been fixed, not user error.\nYou can choose to correct these issues by rebuilding the geometry now if you wish."));
Info.bFireAndForget = true;
Info.bUseLargeFont = false;
Info.ExpireDuration = 25.0f;
Info.FadeOutDuration = 0.5f;
Info.ButtonDetails.Add(FNotificationButtonInfo(
LOCTEXT("RebuildGeometry", "Rebuild Geometry"),
FText(),
FSimpleDelegate::CreateLambda([&RemoveNotification]{
TArray<TWeakObjectPtr<ULevel>> LevelsToRebuild;
ABrush::NeedsRebuild(&LevelsToRebuild);
for (const TWeakObjectPtr<ULevel>& Level : LevelsToRebuild)
{
if (Level.IsValid())
{
GEditor->RebuildLevel(*Level.Get());
}
}
ABrush::OnRebuildDone();
RemoveNotification();
}),
SNotificationItem::CS_None)
);
Info.ButtonDetails.Add(FNotificationButtonInfo(
LOCTEXT("DontRebuild", "Don't Rebuild"),
FText(),
FSimpleDelegate::CreateLambda([&RemoveNotification]{
RemoveNotification();
}),
SNotificationItem::CS_None)
);
Info.Hyperlink = FSimpleDelegate::CreateLambda([PackageName]{
FMessageLog MessageLog("LoadErrors");
MessageLog.NewPage(FText::Format(LOCTEXT("GeometryErrors", "Geometry errors from loading map '{0}'"), FText::FromString(PackageName)));
TArray<TWeakObjectPtr<ULevel>> LevelsToRebuild;
ABrush::NeedsRebuild(&LevelsToRebuild);
for (const auto& Level : LevelsToRebuild)
{
if (Level.IsValid())
{
MessageLog.Message(EMessageSeverity::Info, FText::Format(LOCTEXT("GeometryErrorMap", "Level '{0}' has geometry with invalid normals."), FText::FromString(Level->GetOuter()->GetName())));
}
}
MessageLog.Open();
});
Info.HyperlinkText = LOCTEXT("WhichLevels", "Which levels need a geometry rebuild?");
NotificationPtr = FSlateNotificationManager::Get().AddNotification(Info);
}
/**
* Loads the specified map. Does not prompt the user to save the current map.
*
* @param InFilename Map package filename, including path.
*
* @param LoadAsTemplate Forces the map to load into an untitled outermost package
* preventing the map saving over the original file.
*/
bool FEditorFileUtils::LoadMap(const FString& InFilename, bool LoadAsTemplate, bool bShowProgress)
{
UE_SCOPED_ENGINE_ACTIVITY(TEXT("Loading Map %s"), *InFilename);
OnLoadMapStart.Broadcast();
// Fire delegate when a map is about to be loaded in, with an out-value to report failures from external dependencies which can prevent the map from loading
FCanLoadMap OutCanLoadMap;
FEditorDelegates::OnMapLoad.Broadcast(InFilename, OutCanLoadMap);
if (!OutCanLoadMap.Get())
{
return false;
}
if (GEditor->WarnIfLightingBuildIsCurrentlyRunning())
{
return false;
}
const FScopedBusyCursor BusyCursor;
FString Filename;
FString LongMapPackageName;
FString Extension;
bool bFoundPath = FPackageName::TryConvertToMountedPath(InFilename, &Filename, &LongMapPackageName, nullptr /* ObjectName */, nullptr /* SubObjectName */, &Extension, nullptr /* OutFlexNameType */);
#if PLATFORM_WINDOWS
if (!bFoundPath)
{
// Check if the Filename is actually from network drive and if so attempt to
// resolve to local path (if it's pointing to local machine's shared folder)
FString LocalFilename;
if (FWindowsPlatformProcess::ResolveNetworkPath(InFilename, LocalFilename))
{
bFoundPath = FPackageName::TryConvertToMountedPath(LocalFilename, &Filename, &LongMapPackageName, nullptr /* ObjectName */, nullptr /* SubObjectName */, &Extension, nullptr /* OutFlexNameType */);
}
}
#endif
if (!bFoundPath)
{
FMessageDialog::Open(EAppMsgType::Ok, FText::Format(NSLOCTEXT("Editor", "MapLoad_FriendlyBadFilename", "Map load failed. The filename '{0}' is not within the game or engine content folders found in '{1}'."), FText::FromString(Filename), FText::FromString(FPaths::RootDir())));
return false;
}
if (Extension.IsEmpty())
{
Extension = FPackageName::GetMapPackageExtension();
}
Filename += Extension;
// If a PIE world exists, warn the user that the PIE session will be terminated.
// Abort if the user refuses to terminate the PIE session.
if ( GEditor->ShouldAbortBecauseOfPIEWorld() )
{
return false;
}
// If a level is in memory but never saved to disk, warn the user that the level will be lost.
if (GEditor->ShouldAbortBecauseOfUnsavedWorld())
{
return false;
}
// Save last opened level name.
GConfig->SetString(TEXT("EditorStartup"), TEXT("LastLevel"), *LongMapPackageName, GEditorPerProjectIni);
// Deactivate any editor modes when loading a new map
if (ULevelEditorSubsystem* LevelEditorSubsystem = GEditor->GetEditorSubsystem<ULevelEditorSubsystem>())
{
if (FEditorModeTools* ModeManager = LevelEditorSubsystem->GetLevelEditorModeManager())
{
ModeManager->DeactivateAllModes();
}
}
FString LoadCommand = FString::Printf(TEXT("MAP LOAD FILE=\"%s\" TEMPLATE=%d SHOWPROGRESS=%d FEATURELEVEL=%d"), *Filename, LoadAsTemplate, bShowProgress, (int32)GEditor->DefaultWorldFeatureLevel);
const bool bResult = GEditor->Exec( NULL, *LoadCommand );
UWorld* World = GWorld;
// In case the load failed after GWorld was torn down, default to a new blank map
if( ( !World ) || ( bResult == false ) )
{
World = GEditor->NewMap();
ResetLevelFilenames();
return false;
}
World->IssueEditorLoadWarnings();
ResetLevelFilenames();
//only register the file if the name wasn't changed as a result of loading
if (World->GetOutermost()->GetName() == LongMapPackageName)
{
RegisterLevelFilename( World, Filename );
}
if( !LoadAsTemplate )
{
// Don't set the last directory when loading the simple map or template as it is confusing to users
FEditorDirectories::Get().SetLastDirectory(ELastDirectory::UNR, FPaths::GetPath(Filename)); // Save path as default for next time.
}
if (FPackageName::IsValidLongPackageName(LongMapPackageName))
{
//ensure the name wasn't mangled during load before adding to the Recent File list
if (World->GetOutermost()->GetName() == LongMapPackageName)
{
IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked<IMainFrameModule>( "MainFrame" );
FMainMRUFavoritesList* MRUFavoritesList = MainFrameModule.GetMRUFavoritesList();
if(MRUFavoritesList)
{
MRUFavoritesList->AddMRUItem(LongMapPackageName);
}
}
}
FEditorDelegates::RefreshAllBrowsers.Broadcast();
if( !GIsDemoMode )
{
// Check for deprecated actor classes.
GEditor->Exec(World, TEXT("MAP CHECKDEP NOCLEARLOG"));
FMessageLog("MapCheck").Open( EMessageSeverity::Warning );
}
TRACE_BOOKMARK(TEXT("LoadMap"));
OnLoadMapEnd.Broadcast(FPaths::GetBaseFilename(Filename));
if (GUnrealEd)
{
// Update volume actor visibility for each viewport since we loaded a level which could
// potentially contain volumes.
GUnrealEd->UpdateVolumeActorVisibility(NULL);
// If there are any old mirrored brushes in the map with inverted polys, fix them here
GUnrealEd->FixAnyInvertedBrushes(World);
}
// Request to rebuild BSP if the loading process flagged it as not up-to-date
if (ABrush::NeedsRebuild())
{
NotifyBSPNeedsRebuild(LongMapPackageName);
}
// Fire delegate when a new map is opened, with name of map
FEditorDelegates::OnMapOpened.Broadcast(InFilename, LoadAsTemplate);
return bResult;
}
/**
* Saves the specified map package, returning true on success.
*
* @param World The world to save.
* @param Filename Map package filename, including path.
*
* @return true if the map was saved successfully.
*/
bool FEditorFileUtils::SaveMap(UWorld* InWorld, const FString& Filename )
{
bool bLevelWasSaved = false;
const double SaveStartTime = FPlatformTime::Seconds();
FString FinalFilename;
bLevelWasSaved = SaveWorld( InWorld, &Filename,
nullptr, nullptr,
true, false,
FinalFilename,
false, false );
// Track time spent saving map.
UE_LOG(LogFileHelpers, Log, TEXT("Saving map '%s' took %.3f"), *FPaths::GetBaseFilename(Filename), FPlatformTime::Seconds() - SaveStartTime );
return bLevelWasSaved;
}
/**
* Clears current level filename so that the user must SaveAs on next Save.
* Called by NewMap() after the contents of the map are cleared.
* Also called after loading a map template so that the template isn't overwritten.
*/
void FEditorFileUtils::ResetLevelFilenames()
{
// Empty out any existing filenames.
LevelFilenames.Empty();
// Register a blank filename
const FName PackageName(*GWorld->GetOutermost()->GetName());
const FString EmptyFilename(TEXT(""));
LevelFilenames.Add( PackageName, EmptyFilename );
IMainFrameModule& MainFrameModule = FModuleManager::Get().LoadModuleChecked<IMainFrameModule>(TEXT("MainFrame"));
MainFrameModule.SetLevelNameForWindowTitle(EmptyFilename);
}
bool FEditorFileUtils::AutosaveMap(const FString& AbsoluteAutosaveDir, const int32 AutosaveIndex, const bool bForceIfNotInList, const TSet< TWeakObjectPtr<UPackage>, TWeakObjectPtrSetKeyFuncs<TWeakObjectPtr<UPackage>> >& DirtyPackagesForAutoSave)
{
auto Result = AutosaveMapEx(AbsoluteAutosaveDir, AutosaveIndex, bForceIfNotInList, DirtyPackagesForAutoSave);
check(Result != EAutosaveContentPackagesResult::Failure);
return Result == EAutosaveContentPackagesResult::Success;
}
EAutosaveContentPackagesResult::Type FEditorFileUtils::AutosaveMapEx(const FString& AbsoluteAutosaveDir, const int32 AutosaveIndex, const bool bForceIfNotInList, const TSet< TWeakObjectPtr<UPackage>, TWeakObjectPtrSetKeyFuncs<TWeakObjectPtr<UPackage>> >& DirtyPackagesForAutoSave)
{
const FScopedBusyCursor BusyCursor;
bool bResult = false;
double TotalSaveTime = 0.0f;
double SaveStartTime = FPlatformTime::Seconds();
// Clean up any old worlds.
CollectGarbage( GARBAGE_COLLECTION_KEEPFLAGS );
FWorldContext& EditorContext = GEditor->GetEditorWorldContext();
// Get the set of all reference worlds.
TArray<UWorld*> WorldsArray;
EditorLevelUtils::GetWorlds( EditorContext.World(), WorldsArray, true );
if ( WorldsArray.Num() > 0 )
{
FString FinalFilename;
for ( int32 WorldIndex = 0 ; WorldIndex < WorldsArray.Num(); ++WorldIndex )
{
UWorld* World = WorldsArray[ WorldIndex ];
UPackage* Package = Cast<UPackage>( World->GetOuter() );
check( Package );
// If this world needs saving . . .
if ( Package->IsDirty() && (bForceIfNotInList || DirtyPackagesForAutoSave.Contains(Package)) )
{
const FString AutosaveFilename = GetAutoSaveFilename(Package, AbsoluteAutosaveDir, AutosaveIndex, FPackageName::GetMapPackageExtension());
//UE_LOG(LogFileHelpers, Log, TEXT("Autosaving '%s'"), *AutosaveFilename );
const bool bLevelWasSaved = SaveWorld( World, &AutosaveFilename,
NULL, NULL,
false, true,
FinalFilename,
true, false );
// Remark the package as being dirty, as saving will have undiritied the package.
Package->MarkPackageDirty();
if( bLevelWasSaved == false )
{
UE_LOG(LogFileHelpers, Log, TEXT("Editor autosave (incl. sublevels) failed for file '%s' which belongs to world '%s'. Aborting autosave."), *FinalFilename, *EditorContext.World()->GetOutermost()->GetName() );
return EAutosaveContentPackagesResult::Failure;
}
bResult |= bLevelWasSaved;
}
// Now gather the world external packages and save them if needed
if (World->PersistentLevel)
{
TArray<UPackage*> ExternalPackagesToSave;
for (UPackage* ExternalPackage : World->PersistentLevel->GetLoadedExternalObjectPackages())
{
if (ExternalPackage->IsDirty() && (bForceIfNotInList || DirtyPackagesForAutoSave.Contains(ExternalPackage))
&& FPackageName::IsValidLongPackageName(ExternalPackage->GetName(), /*bIncludeReadOnlyRoots=*/false))
{
// Don't try to save external packages that will get deleted
if (IsValid(ExternalPackage->FindAssetInPackage()))
{
ExternalPackagesToSave.Add(ExternalPackage);
}
}
}
if (ExternalPackagesToSave.Num())
{
FEditorDelegates::PreSaveExternalActors.Broadcast(World);
for (UPackage* ExternalPackage : ExternalPackagesToSave)
{
const FString AutosaveFilename = GetAutoSaveFilename(ExternalPackage, AbsoluteAutosaveDir, AutosaveIndex, FPackageName::GetAssetPackageExtension());
if (!GEditor->Exec(nullptr, *FString::Printf(TEXT("OBJ SAVEPACKAGE PACKAGE=\"%s\" FILE=\"%s\" SILENT=false AUTOSAVING=true KEEPDIRTY=true"), *ExternalPackage->GetName(), *AutosaveFilename)))
{
return EAutosaveContentPackagesResult::Failure;
}
// We saved an actor
bResult = true;
// Verify that the package is still dirty after the save
check(ExternalPackage->IsDirty());
}
FEditorDelegates::PostSaveExternalActors.Broadcast(World);
}
}
}
// Track time spent saving map.
double ThisTime = FPlatformTime::Seconds() - SaveStartTime;
TotalSaveTime += ThisTime;
UE_LOG(LogFileHelpers, Log, TEXT("Editor autosave (incl. external actors) for '%s' took %.3f"), *EditorContext.World()->GetOutermost()->GetName(), ThisTime );
}
if( bResult == true )
{
UE_LOG(LogFileHelpers, Log, TEXT("Editor autosave (incl. sublevels & external actors) for all levels took %.3f"), TotalSaveTime );
}
return bResult ? EAutosaveContentPackagesResult::Success : EAutosaveContentPackagesResult::NothingToDo;
}
bool FEditorFileUtils::AutosaveContentPackages(const FString& AbsoluteAutosaveDir, const int32 AutosaveIndex, const bool bForceIfNotInList, const TSet< TWeakObjectPtr<UPackage>, TWeakObjectPtrSetKeyFuncs<TWeakObjectPtr<UPackage>> >& DirtyPackagesForAutoSave)
{
auto Result = AutosaveContentPackagesEx(AbsoluteAutosaveDir, AutosaveIndex, bForceIfNotInList, DirtyPackagesForAutoSave);
check(Result != EAutosaveContentPackagesResult::Failure);
return Result == EAutosaveContentPackagesResult::Success;
}
EAutosaveContentPackagesResult::Type FEditorFileUtils::AutosaveContentPackagesEx(const FString& AbsoluteAutosaveDir, const int32 AutosaveIndex, const bool bForceIfNotInList, const TSet< TWeakObjectPtr<UPackage>, TWeakObjectPtrSetKeyFuncs<TWeakObjectPtr<UPackage>> >& DirtyPackagesForAutoSave)
{
const FScopedBusyCursor BusyCursor;
double SaveStartTime = FPlatformTime::Seconds();
bool bSavedPkgs = false;
const UPackage* TransientPackage = GetTransientPackage();
TArray<UPackage*> PackagesToSave;
// Check all packages for dirty, non-map, non-transient packages
for ( TObjectIterator<UPackage> PackageIter; PackageIter; ++PackageIter )
{
UPackage* CurPackage = *PackageIter;
// If the package is dirty and is not the transient package, we'd like to autosave it
if ( CurPackage && ( CurPackage != TransientPackage ) && CurPackage->IsDirty() && (bForceIfNotInList || DirtyPackagesForAutoSave.Contains(CurPackage)) )
{
bool bSkipPackage = false;
TArray<UObject*> ObjectsInPackage;
GetObjectsWithPackage(CurPackage, ObjectsInPackage, false);
for (auto ObjIt = ObjectsInPackage.CreateConstIterator(); ObjIt; ++ObjIt)
{
// Also, make sure this is not a map package
if (Cast<UWorld>(*ObjIt))
{
bSkipPackage = true;
break;
}
else if (Cast<UMapBuildDataRegistry>(*ObjIt))
{
// Do not auto save generated map build data packages
bSkipPackage = true;
break;
}
// handles external actor packages
else if ((*ObjIt)->GetTypedOuter<UWorld>())
{
bSkipPackage = true;
break;
}
}
if (bSkipPackage)
{
continue;
}
// Ignore packages with long, invalid names. This culls out packages with paths in read-only roots such as /Temp.
const bool bInvalidLongPackageName = !FPackageName::IsShortPackageName(CurPackage->GetFName()) && !FPackageName::IsValidLongPackageName(CurPackage->GetName(), /*bIncludeReadOnlyRoots=*/false);
if ( !bInvalidLongPackageName )
{
PackagesToSave.Add(CurPackage);
}
}
}
FScopedSlowTask SlowTask(static_cast<float>(PackagesToSave.Num() * 2), LOCTEXT("PerformingAutoSave_Caption", "Auto-saving out of date packages..."));
for (UPackage* CurPackage : PackagesToSave)
{
SlowTask.DefaultMessage = FText::Format(LOCTEXT("AutoSavingPackage", "Saving package {0}"), FText::FromString(IAssetTools::Get().GetUserFacingLongPackageName(*CurPackage)));
SlowTask.EnterProgressFrame();
// In order to save, the package must be fully-loaded first
if( !CurPackage->IsFullyLoaded() )
{
CurPackage->FullyLoad();
}
SlowTask.EnterProgressFrame();
const FString AutosaveFilename = GetAutoSaveFilename(CurPackage, AbsoluteAutosaveDir, AutosaveIndex, FPackageName::GetAssetPackageExtension());
if (!GEditor->Exec(nullptr, *FString::Printf(TEXT("OBJ SAVEPACKAGE PACKAGE=\"%s\" FILE=\"%s\" SILENT=false AUTOSAVING=true"), *CurPackage->GetName(), *AutosaveFilename)))
{
return EAutosaveContentPackagesResult::Failure;
}
// Re-mark the package as dirty, because autosaving it will have cleared the dirty flag
CurPackage->MarkPackageDirty();
bSavedPkgs = true;
}
if ( bSavedPkgs )
{
UE_LOG(LogFileHelpers, Log, TEXT("Auto-saving content packages took %.3f"), FPlatformTime::Seconds() - SaveStartTime );
}
return bSavedPkgs ? EAutosaveContentPackagesResult::Success : EAutosaveContentPackagesResult::NothingToDo;
}
enum class InternalSavePackageResult : int8
{
Success,
Cancel,
Continue,
Error,
};
static void PrepareWorldsForExplicitSave(const TArray<UPackage*>& PackagesToPrepare)
{
if (EditorFileUtils::bIsExplicitSave)
{
// In a given set of packages it can contain at least one World Package (map) and/or at least one Actor package.
// If an external actor is being saved but not its world we still want to collect its owning world to pass to PrepareWorldsForExplicitSave
// In case there is any validation/extra steps needed for that world based on the add/edit of that Actor
// We use a set here to dedupe in case both the actor and its world are included in the dirty packages
bool bFoundActorWorld = false;
TSet<UWorld*> WorldsToSave;
for (UPackage* Package : PackagesToPrepare)
{
if (UWorld* WorldToSave = UWorld::FindWorldInPackage(Package))
{
WorldsToSave.Add(WorldToSave);
}
else if (!bFoundActorWorld)
{
// Currently there is only one world associated with saving actors as actors from multiple worlds can't be opened
// We can skip checking any further Actor packages once we grab the world off the first discovered
if (AActor* ActorToSave = AActor::FindActorInPackage(Package))
{
WorldsToSave.Add(ActorToSave->GetWorld());
bFoundActorWorld = true;
}
}
}
if (!WorldsToSave.IsEmpty())
{
FEditorFileUtils::PrepareWorldsForExplicitSave(WorldsToSave.Array());
}
}
}
static void PrepareSavePackages(const TArray<UPackage*>& PackagesToSave)
{
// Load existing thumbnails to be able to resave them properly
for (UPackage* PackageToSave : PackagesToSave)
{
EnsureLoadingComplete(PackageToSave);
}
// Don't call ResetLoaders on newly created world packages as this will prevent future loading of external actor packages to work propertly
// Linker will fail to resolve SourceLinker of external actor's world package import (see GetPackageLinker test for PKG_InMemoryOnly on TargetPackage's Package Flag)
TArray<UPackage*> PackagesToResetLoaders;
PackagesToResetLoaders.Reserve(PackagesToSave.Num());
Algo::CopyIf(PackagesToSave, PackagesToResetLoaders, [&](UPackage* Package)
{
const bool bIsNewlyCreatedWorldPackage = Package->HasAnyPackageFlags(PKG_NewlyCreated) && UWorld::FindWorldInPackage(Package);
return !bIsNewlyCreatedWorldPackage;
});
ResetLoaders(MakeArrayView<UObject*>((UObject**)PackagesToResetLoaders.GetData(), PackagesToResetLoaders.Num()));
}
/**
* Actually save a package. Prompting for Save as if necessary
*
* @param PackageToSave The package to save.
* @param bUseDialog If true, use the normal behavior.
* If false, do not prompt message dialog. If it can't save the package, skip it. If the package is a map and the name is not valid, skip it.
* @param OutPackageLocallyWritable Set to true if the provided package was locally writable but not under source control (of if source control is disabled).
* @param SaveOutput The output from the save process.
* @return InternalSavePackageResult::Success if package saving was a success
InternalSavePackageResult::Continue if the package saving failed and the user doesn't want to retry
InternalSavePackageResult::Cancel if the user wants to cancel everything
InternalSavePackageResult::Error if an error occured. Check OutFailureReason
*/
static InternalSavePackageResult InternalSavePackage(UPackage* PackageToSave, bool bUseDialog, bool& bOutPackageLocallyWritable, FOutputDevice &SaveOutput)
{
TRACE_CPUPROFILER_EVENT_SCOPE(InternalSavePackage);
// What we will be returning. Assume for now that everything will go fine
InternalSavePackageResult ReturnCode = InternalSavePackageResult::Error;
// Assume the package is locally writable in case SCC is disabled; if SCC is enabled, it will
// correctly set this value later
bOutPackageLocallyWritable = true;
bool bShouldRetrySave = true;
UWorld* AssociatedWorld = UWorld::FindWorldInPackage(PackageToSave);
// Redirector to world saves with file extension for maps
const bool bSavingRedirectorToWorld = !AssociatedWorld && UWorld::FollowWorldRedirectorInPackage(PackageToSave);
const bool bIsMapPackage = AssociatedWorld != NULL || bSavingRedirectorToWorld;
// The name of the package
const FString PackageName = PackageToSave->GetName();
// Place were we should save the file, including the filename
FString FinalPackageSavePath;
// Just the filename
FString FinalPackageFilename;
// True if we should attempt saving
bool bAttemptSave = false;
// If the package already has a valid path to a non read-only location, use it to determine where the file should be saved
const bool bIncludeReadOnlyRoots = GIsAutomationTesting;
const bool bIsValidPath = FPackageName::IsValidLongPackageName(PackageName, bIncludeReadOnlyRoots);
if( bIsValidPath )
{
bAttemptSave = true;
FString ExistingFilename;
const bool bPackageAlreadyExists = FPackageName::DoesPackageExist(PackageName, &ExistingFilename);
if (!bPackageAlreadyExists)
{
// Construct a filename from long package name.
const FString& FileExtension = bIsMapPackage ? FPackageName::GetMapPackageExtension() : FPackageName::GetAssetPackageExtension();
ExistingFilename = FPackageName::LongPackageNameToFilename(PackageName, FileExtension);
// Check if we can use this filename.
FText ErrorText;
if (!FFileHelper::IsFilenameValidForSaving(ExistingFilename, ErrorText))
{
// Display the error (already localized) and exit gracefully.
FMessageDialog::Open(EAppMsgType::Ok, ErrorText);
bAttemptSave = false;
}
}
if (bAttemptSave)
{
// The file already exists, no need to prompt for save as
FString BaseFilename, Extension, Directory;
// Split the path to get the filename without the directory structure
FPaths::NormalizeFilename(ExistingFilename);
FPaths::Split(ExistingFilename, Directory, BaseFilename, Extension);
// The final save path is whatever the existing filename is
FinalPackageSavePath = ExistingFilename;
// Format the filename we found from splitting the path
FinalPackageFilename = FString::Printf( TEXT("%s.%s"), *BaseFilename, *Extension );
}
}
else if ( bUseDialog && bIsMapPackage ) // don't do a SaveAs dialog if dialogs was not requested
{
// @todo Only maps should be allowed to change names at save time, for now.
// If this changes, there must be generic code to rename assets to the new name BEFORE saving to disk.
// Right now, all of this code is specific to maps
// There wont be a "not checked out from SCC but writable on disk" conflict if the package is new.
bOutPackageLocallyWritable = false;
// Make a list of file types
// We have to ask for save as.
FString FileTypes;
FText SavePackageText;
if( bIsMapPackage )
{
FileTypes = FEditorFileUtils::GetFilterString(FI_Save);
FinalPackageFilename = FString::Printf( TEXT("Untitled%s"), *FPackageName::GetMapPackageExtension() );
SavePackageText = NSLOCTEXT("UnrealEd", "SaveMap", "Save Map");
}
else
{
FileTypes = FString::Printf( TEXT("(*%s)|*%s"), *FPackageName::GetAssetPackageExtension(), *FPackageName::GetAssetPackageExtension() );
FinalPackageFilename = FString::Printf( TEXT("%s%s"), *PackageToSave->GetName(), *FPackageName::GetAssetPackageExtension() );
SavePackageText = NSLOCTEXT("UnrealEd", "SaveAsset", "Save Asset");
}
// The number of times the user pressed cancel
int32 NumSkips = 0;
// If the user presses cancel more than this time, they really don't want to save the file
const int32 NumSkipsBeforeAbort = 1;
// if the user hit cancel on the Save dialog, ask again what the user wants to do,
// we shouldn't assume they want to skip the file
// This loop continues indefinitely if the user does not supply a valid filename. They must supply a valid filename or press cancel
const FString Directory = *GetDefaultDirectory();
while( NumSkips < NumSkipsBeforeAbort )
{
FString DefaultLocation = Directory;
FString DefaultPackagePath;
if (!FPackageName::TryConvertFilenameToLongPackageName(DefaultLocation / FinalPackageFilename, DefaultPackagePath))
{
// Original location is invalid; set default location to /Game/Maps
DefaultLocation = FPaths::ProjectContentDir() / TEXT("Maps");
ensure(FPackageName::TryConvertFilenameToLongPackageName(DefaultLocation / FinalPackageFilename, DefaultPackagePath));
}
FString SaveAsPackageName;
bool bSaveFile = OpenSaveAsDialog(
UWorld::StaticClass(),
FPackageName::GetLongPackagePath(DefaultPackagePath),
FPaths::GetBaseFilename(FinalPackageFilename),
SaveAsPackageName);
if (bSaveFile)
{
// Leave out the extension. It will be added below.
FinalPackageFilename = FPackageName::LongPackageNameToFilename(SaveAsPackageName);
}
if( bSaveFile )
{
// If the supplied file name is missing an extension then give it the default package
// file extension.
if( FinalPackageFilename.Len() > 0 && FPaths::GetExtension(FinalPackageFilename).Len() == 0 )
{
FinalPackageFilename += bIsMapPackage ? FPackageName::GetMapPackageExtension() : FPackageName::GetAssetPackageExtension();
}
FText ErrorMessage;
bool bValidFilename = FFileHelper::IsFilenameValidForSaving( FinalPackageFilename, ErrorMessage );
if ( bValidFilename )
{
bValidFilename = bIsMapPackage ? FEditorFileUtils::IsValidMapFilename( FinalPackageFilename, ErrorMessage ) : FPackageName::IsValidLongPackageName( FinalPackageFilename, false, &ErrorMessage );
}
if ( bValidFilename )
{
// If there is an existing world in memory that shares this name unload it now to prepare for overwrite.
// Don't do this if we are using save as to overwrite the current level since it will just save naturally.
const FString NewPackageName = FPackageName::FilenameToLongPackageName(FinalPackageFilename);
UPackage* ExistingPackage = FindPackage(nullptr, *NewPackageName);
if (ExistingPackage && ExistingPackage != PackageToSave)
{
bValidFilename = FEditorFileUtils::AttemptUnloadInactiveWorldPackage(ExistingPackage, ErrorMessage);
}
}
if ( !bValidFilename )
{
// Start the loop over, prompting for save again
const FText DisplayFilename = FText::FromString( IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead( *FinalPackageFilename ) );
FFormatNamedArguments Arguments;
Arguments.Add( TEXT("Filename"), DisplayFilename );
Arguments.Add( TEXT("LineTerminators"), FText::FromString( LINE_TERMINATOR LINE_TERMINATOR ) );
Arguments.Add( TEXT("ErrorMessage"), ErrorMessage );
const FText DisplayMessage = FText::Format( LOCTEXT( "InvalidSaveFilename", "Failed to save to {Filename}{LineTerminators}{ErrorMessage}" ), Arguments );
FMessageDialog::Open( EAppMsgType::Ok, DisplayMessage );
// Start the loop over, prompting for save again
continue;
}
else
{
FinalPackageSavePath = FinalPackageFilename;
// Stop looping, we successfully got a valid path and filename to save
bAttemptSave = true;
break;
}
}
else
{
// if the user hit cancel on the Save dialog, ask again what the user wants to do,
// we shouldn't assume they want to skip the file unless they press cancel several times
++NumSkips;
if( NumSkips == NumSkipsBeforeAbort )
{
// They really want to stop
ReturnCode = InternalSavePackageResult::Cancel;
}
}
}
}
// attempt the save
while( bAttemptSave )
{
bool bWasSuccessful = false;
// Note: Redirector to world uses SAVEPACKAGE instead of SaveMap
if (bIsMapPackage && !bSavingRedirectorToWorld)
{
// have a Helper attempt to save the map
SaveOutput.Log("LogFileHelpers", ELogVerbosity::Log, FString::Printf(TEXT("Saving Map: %s"), *PackageName));
bWasSuccessful = FEditorFileUtils::SaveMap( AssociatedWorld, FinalPackageSavePath );
}
else
{
// normally, we just save the package (and its external packages)
SaveOutput.Log("LogFileHelpers", ELogVerbosity::Log, FString::Printf(TEXT("Saving Package: %s"), *PackageName));
bWasSuccessful = SaveAsset(PackageToSave, PackageName, FinalPackageSavePath, SaveOutput);
}
if (!FPackageName::IsTempPackage(PackageName))
{
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
FUncontrolledChangelistsModule& UncontrolledChangelistsModule = FUncontrolledChangelistsModule::Get();
if (ISourceControlModule::Get().IsEnabled())
{
// Assume the package was correctly checked out from SCC
bOutPackageLocallyWritable = false;
// Trusting the SCC status in the package file cache to minimize network activity during save.
const FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(PackageToSave, EStateCacheUsage::Use);
// If the package is in the depot, and not recognized as editable by source control, and not read-only, then we know the user has made the package locally writable!
const bool bSCCCanEdit = !SourceControlState.IsValid() || SourceControlState->CanCheckIn() || SourceControlState->IsIgnored() || SourceControlState->IsUnknown();
const bool bSCCIsCheckedOut = SourceControlState.IsValid() && SourceControlState->IsCheckedOut();
const bool bInDepot = SourceControlState.IsValid() && SourceControlState->IsSourceControlled();
if ( !bSCCCanEdit && bInDepot && !IFileManager::Get().IsReadOnly( *FinalPackageSavePath ) && SourceControlProvider.UsesLocalReadOnlyState() && !bSCCIsCheckedOut )
{
bOutPackageLocallyWritable = true;
}
}
else
{
// If we are in offline mode, automatically add the modified package to an Uncontrolled Changelist
bOutPackageLocallyWritable = UncontrolledChangelistsModule.IsEnabled() && (!IFileManager::Get().IsReadOnly(*FinalPackageSavePath));
}
if (bWasSuccessful && bOutPackageLocallyWritable)
{
UncontrolledChangelistsModule.OnSaveWritable({ FinalPackageSavePath });
}
}
// Handle all failures the same way.
if ( bUseDialog && !bWasSuccessful )
{
// ask the user what to do if we failed
const FText ErrorPrompt = GEditor->IsPlayingOnLocalPCSession() ?
NSLOCTEXT("UnrealEd", "Prompt_41", "The asset '{0}' ({1}) cannot be saved as the package is locked because you are in play on PC mode.\n\nCancel: Stop saving all assets and return to the editor.\nRetry: Attempt to save the asset again.\nContinue: Skip saving this asset only." ) :
NSLOCTEXT("UnrealEd", "Prompt_26", "The asset '{0}' ({1}) failed to save.\n\nCancel: Stop saving all assets and return to the editor.\nRetry: Attempt to save the asset again.\nContinue: Skip saving this asset only." );
EAppReturnType::Type DialogCode = FMessageDialog::Open( EAppMsgType::CancelRetryContinue, EAppReturnType::Continue, FText::Format(ErrorPrompt, FText::FromString(IAssetTools::Get().GetUserFacingLongPackageName(*PackageToSave)), FText::FromString(FinalPackageFilename)) );
switch (DialogCode)
{
case EAppReturnType::Cancel:
// if this happens, the user wants to stop everything
bAttemptSave = false;
ReturnCode = InternalSavePackageResult::Cancel;
break;
case EAppReturnType::Retry:
bAttemptSave = true;
break;
case EAppReturnType::Continue:
ReturnCode = InternalSavePackageResult::Continue;// this is if it failed to save, but the user wants to skip saving it
bAttemptSave = false;
break;
default:
// Should not get here
check(0);
break;
}
}
else if ( !bWasSuccessful )
{
// We failed at saving because we are in bIsUnattended mode, there is no need to attempt to save again
FText FailureReason = FText::Format(NSLOCTEXT("UnrealEd", "SaveAssetFailed", "The asset '{0}' ({1}) failed to save."), FText::FromString(IAssetTools::Get().GetUserFacingLongPackageName(*PackageToSave)), FText::FromString(FinalPackageFilename));
FMessageDialog::Open( EAppMsgType::Ok, FailureReason );
bAttemptSave = false;
ReturnCode = InternalSavePackageResult::Error;
}
else
{
// If we were successful at saving, there is no need to attempt to save again
bAttemptSave = false;
ReturnCode = InternalSavePackageResult::Success;
}
}
return ReturnCode;
}
/**
* Shows a dialog warning a user about packages which failed to save
*
* @param Packages that should be displayed in the dialog
*/
static void InternalWarnUserAboutFailedSave( const TArray<UPackage*>& InFailedPackages, bool bUseDialog )
{
// Warn the user if any packages failed to save
if ( InFailedPackages.Num() > 0 )
{
FString FailedPackages;
for (const FString& UserFacingPath : IAssetTools::Get().GetUserFacingLongPackageNames(InFailedPackages))
{
FailedPackages += FString::Printf( TEXT("\n%s"), *UserFacingPath );
}
FFormatNamedArguments Arguments;
Arguments.Add(TEXT("Packages"), FText::FromString( FailedPackages ));
FText MessageFormatting = NSLOCTEXT("FileHelper", "FailedSavePromptMessageFormatting", "The following assets failed to save correctly:{Packages}");
FText Message = FText::Format( MessageFormatting, Arguments );
// Display warning
FText Title = NSLOCTEXT("FileHelper", "FailedSavePrompt_Title", "Packages Failed To Save");
FMessageDialog::Open(EAppMsgType::Ok, Message, Title);
}
}
static TArray<UPackage*> InternalGetDirtyPackages(const bool bSaveMapPackages, const bool bSaveContentPackages, const FEditorFileUtils::FShouldIgnorePackageFunctionRef& ShouldIgnorePackageFunction = FEditorFileUtils::FShouldIgnorePackage::Default)
{
if (bSaveContentPackages)
{
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
}
// A list of all packages that need to be saved
TArray<UPackage*> PackagesToSave;
if (bSaveMapPackages)
{
FEditorFileUtils::GetDirtyWorldPackages(PackagesToSave, ShouldIgnorePackageFunction);
}
// Don't iterate through content packages if we don't plan on saving them
if (bSaveContentPackages)
{
FEditorFileUtils::GetDirtyContentPackages(PackagesToSave, ShouldIgnorePackageFunction);
}
return PackagesToSave;
}
static void InternalNotifyNoPackagesSaved(const bool bUseDialog)
{
if (bUseDialog)
{
FNotificationInfo NotificationInfo(LOCTEXT("NoAssetsToSave", "All files are already saved."));
NotificationInfo.bFireAndForget = true;
NotificationInfo.ExpireDuration = 4.0f; // Need this message to last a little longer than normal since the user may have expected there to be modified files.
NotificationInfo.bUseThrobber = true;
FSlateNotificationManager::Get().AddNotification(NotificationInfo);
}
else
{
UE_LOG(LogFileHelpers, Log, TEXT("%s"), *LOCTEXT("NoAssetsToSave", "All files are already saved.").ToString());
}
}
/*
* @param bUseDialog If true, use the normal behavior.
* If false, do not prompt message dialog. If it can't save the package, skip it. If the package is a map and the name is not valid, skip it.
* @param bShowDialogIfError If InternalSavePackage failed, tell the user with a Dialog
* @param OutFailedPackages Packages that failed to save
*/
static bool InternalSavePackagesFast(const TArray<UPackage*>& PackagesToSave, bool bUseDialog, TArray<UPackage*>& OutFailedPackages)
{
TRACE_CPUPROFILER_EVENT_SCOPE(InternalSavePackagesFast);
UE_SCOPED_TIMER(TEXT("InternalSavePackagesFast"), LogFileHelpers, Log);
bool bReturnCode = true;
FSaveErrorOutputDevice SaveErrors;
GWarn->BeginSlowTask(NSLOCTEXT("UnrealEd", "SavingPackagesE", "Saving packages..."), true);
TArray<UPackage*> PackagesToClean;
TArray<UPackage*> FinalPackagesToSave;
FinalPackagesToSave.Reserve(PackagesToSave.Num());
for (TArray<UPackage*>::TConstIterator PkgIter(PackagesToSave); PkgIter; ++PkgIter)
{
UPackage* CurPackage = *PkgIter;
// Check if a file exists for this package
FString Filename;
bool bFoundFile = FPackageName::DoesPackageExist(CurPackage->GetName(), &Filename);
if (bFoundFile)
{
// determine if the package file is read only
const bool bPkgReadOnly = IFileManager::Get().IsReadOnly(*Filename);
// Only save writable files in fast mode
if (!bPkgReadOnly)
{
if (!CurPackage->IsFullyLoaded())
{
// Packages must be fully loaded to save
CurPackage->FullyLoad();
}
const UWorld* const AssociatedWorld = UWorld::FindWorldInPackage(CurPackage);
FText SavingPackageText;
if (AssociatedWorld != nullptr)
{
UE::Core::FVersePath VersePath;
if (IAssetTools::Get().ShowingContentVersePath())
{
VersePath = AssociatedWorld->GetVersePath();
}
SavingPackageText = FText::Format(NSLOCTEXT("UnrealEd", "SavingMapf", "Saving map {0}"), FText::FromString(VersePath.IsValid() ? MoveTemp(VersePath).ToString() : CurPackage->GetName()));
}
else
{
SavingPackageText = FText::Format(NSLOCTEXT("UnrealEd", "SavingAssetf", "Saving asset {0}"), FText::FromString(IAssetTools::Get().GetUserFacingLongPackageName(*CurPackage)));
}
GWarn->StatusForceUpdate(PkgIter.GetIndex(), PackagesToSave.Num(), SavingPackageText);
// Save the package
// if the package we are saving is considered empty, mark it for deletion on disk instead
if (UPackage::IsEmptyPackage(CurPackage))
{
PackagesToClean.Add(CurPackage);
}
// Otherwise, save as usual
else
{
FinalPackagesToSave.Add(CurPackage);
}
}
}
}
// Cleanup packages before saving packages in case we are saving worlds with external packages we could end up with packages being cleaned up by a world package save (that are in our PackagesToClean list)
if (PackagesToClean.Num() > 0)
{
ObjectTools::CleanupAfterSuccessfulDelete(PackagesToClean, true);
}
PrepareSavePackages(FinalPackagesToSave);
for (UPackage* Package : FinalPackagesToSave)
{
bool bPackageLocallyWritable;
const InternalSavePackageResult SaveStatus = InternalSavePackage(Package, bUseDialog, bPackageLocallyWritable, SaveErrors);
if (SaveStatus == InternalSavePackageResult::Cancel)
{
// we don't want to pop up a message box about failing to save packages if they cancel
// instead warn here so there is some trace in the log and also unattended builds can find it
UE_LOG(LogFileHelpers, Warning, TEXT("Cancelled saving package %s"), *Package->GetName());
}
else if (SaveStatus == InternalSavePackageResult::Continue || SaveStatus == InternalSavePackageResult::Error)
{
// The package could not be saved so add it to the failed array
OutFailedPackages.Add(Package);
if (SaveStatus == InternalSavePackageResult::Error)
{
// exit gracefully.
bReturnCode = false;
}
}
}
// Add all files that needs to be marked for add in one command, if any
if (GEditor)
{
GEditor->RunDeferredMarkForAddFiles();
}
GWarn->EndSlowTask();
SaveErrors.Flush();
return bReturnCode;
}
/*
* @param bPromptUserToSave true if we should prompt the user to save dirty packages we found. false to assume all dirty packages should be saved. Regardless of this setting the user will be prompted for checkout(if needed) unless bFastSave is set
* @param bFastSave true if we should do a fast save. (I.E don't prompt the user to save, don't prompt for checkout, and only save packages that are currently writable). Note: Still prompts for SaveAs if a package needs a filename
* @param bCanBeDeclined true if the user prompt should contain a "Don't Save" button in addition to "Cancel", which won't result in a failure return code.
* @param bCheckDirty true if only dirty packages should be saved
*/
static bool InternalSavePackages(const TArray<UPackage*>& PackagesToSave, bool bPromptUserToSave, bool bFastSave, bool bCanBeDeclined, bool bCheckDirty)
{
bool bReturnCode = true;
if (!bFastSave)
{
const bool bAlreadyCheckedOut = false;
FEditorFileUtils::FPromptForCheckoutAndSaveParams SaveParams;
SaveParams.bCheckDirty = bCheckDirty;
SaveParams.bPromptToSave = bPromptUserToSave;
SaveParams.bAlreadyCheckedOut = bAlreadyCheckedOut;
SaveParams.bCanBeDeclined = bCanBeDeclined;
SaveParams.bIsExplicitSave = EditorFileUtils::bIsExplicitSave;
const FEditorFileUtils::EPromptReturnCode Return = FEditorFileUtils::PromptForCheckoutAndSave(PackagesToSave, SaveParams);
if (Return == FEditorFileUtils::EPromptReturnCode::PR_Cancelled)
{
// Only cancel should return false and stop whatever we were doing before.(like closing the editor)
// If failure is returned, the user was given ample times to retry saving the package and didn't want to
// So we should continue with whatever we were doing.
bReturnCode = false;
}
}
else
{
const bool bUseDialog = true;
TArray<UPackage*> FailedPackages;
bReturnCode = InternalSavePackagesFast(PackagesToSave, bUseDialog, FailedPackages);
// Warn the user about any packages which failed to save.
InternalWarnUserAboutFailedSave(FailedPackages, bUseDialog);
}
return bReturnCode;
}
bool FEditorFileUtils::SaveMapDataPackages(UWorld* WorldToSave, bool bCheckDirty)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FEditorFileUtils_SaveMapDataPackages);
TArray<UPackage*> PackagesToSave;
UPackage* WorldPackage = WorldToSave->GetOutermost();
if (!WorldPackage->HasAnyPackageFlags(PKG_PlayInEditor)
&& !WorldPackage->HasAnyFlags(RF_Transient))
{
ULevel* Level = WorldToSave->PersistentLevel;
if (Level->MapBuildData)
{
UPackage* BuiltDataPackage = Level->MapBuildData->GetOutermost();
if (BuiltDataPackage != WorldPackage)
{
return UEditorLoadingAndSavingUtils::SavePackages({ BuiltDataPackage }, bCheckDirty);
}
}
}
return true;
}
/**
* Saves the specified level. SaveAs is performed as necessary.
*
* @param Level The level to be saved.
* @param DefaultFilename File name to use for this level if it doesn't have one yet (or empty string to prompt)
*
* @return true if the level was saved.
*/
bool FEditorFileUtils::SaveLevel(ULevel* Level, const FString& DefaultFilename, FString* OutSavedFilename )
{
bool bLevelWasSaved = false;
if (Level)
{
// Check and see if this is a new map.
const bool bIsPersistentLevelCurrent = Level->IsPersistentLevel();
// If the user trying to save the persistent level?
if ( bIsPersistentLevelCurrent )
{
// Check to see if the persistent level is a new map (ie if it has been saved before).
FString Filename = GetFilename( Level->OwningWorld );
if( !Filename.Len() )
{
// No file name, provided, so use the default file name we were given if we have one
Filename = FString( DefaultFilename );
}
if( !Filename.Len() )
{
if (GIsRunningUnattendedScript) // prevent modal if running in Unattended Script mode
{
return false;
}
else
{
// Present the user with a SaveAs dialog.
const bool bAllowStreamingLevelRename = false;
bLevelWasSaved = SaveAsImplementation(Level->OwningWorld, Filename, bAllowStreamingLevelRename, OutSavedFilename);
return bLevelWasSaved;
}
}
}
////////////////////////////////
// At this point, we know the level we're saving has been saved before,
// so don't bother checking the filename.
UWorld* WorldToSave = Cast<UWorld>( Level->GetOuter() );
if ( WorldToSave )
{
FString FinalFilename;
bLevelWasSaved = SaveWorld( WorldToSave,
DefaultFilename.Len() > 0 ? &DefaultFilename : NULL,
NULL, NULL,
true, false,
FinalFilename,
false, false );
if (bLevelWasSaved && OutSavedFilename)
{
*OutSavedFilename = FinalFilename;
}
}
}
return bLevelWasSaved;
}
bool FEditorFileUtils::SaveDirtyPackages(const bool bPromptUserToSave, const bool bSaveMapPackages, const bool bSaveContentPackages, const bool bFastSave, const bool bNotifyNoPackagesSaved, const bool bCanBeDeclined, bool* bOutPackagesNeededSaving, const FShouldIgnorePackageFunctionRef& ShouldIgnorePackageFunction, bool bInSkipExternalObjectSave)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FEditorFileUtils::SaveDirtyPackages);
bool bReturnCode = true;
if (bOutPackagesNeededSaving != NULL)
{
*bOutPackagesNeededSaving = false;
}
TArray<UPackage*> PackagesToSave = InternalGetDirtyPackages(bSaveMapPackages, bSaveContentPackages, ShouldIgnorePackageFunction);
TGuardValue<bool> IsExplicitSaveGuard(EditorFileUtils::bIsExplicitSave, bPromptUserToSave);
// Need to track the number of packages we're not ignoring for save.
int32 NumPackagesNotIgnored = 0;
for (auto* Package : PackagesToSave)
{
// Count the number of packages to not ignore.
NumPackagesNotIgnored += (PackagesNotSavedDuringSaveAll.Find(Package->GetName()) == NULL) ? 1 : 0;
}
if (PackagesToSave.Num() > 0 && (NumPackagesNotIgnored > 0 || bPromptUserToSave))
{
if (bOutPackagesNeededSaving != NULL)
{
*bOutPackagesNeededSaving = true;
}
TGuardValue<bool> SkipExternalObjectSaveGuard(bSkipExternalObjectSave, bInSkipExternalObjectSave);
const bool bCheckDirty = true;
bReturnCode = InternalSavePackages(PackagesToSave, bPromptUserToSave, bFastSave, bCanBeDeclined, bCheckDirty);
}
else if (bNotifyNoPackagesSaved)
{
InternalNotifyNoPackagesSaved(true);
}
return bReturnCode;
}
bool FEditorFileUtils::SaveDirtyContentPackages(TArray<UClass*>& SaveContentClasses, const bool bPromptUserToSave, const bool bFastSave, const bool bNotifyNoPackagesSaved, const bool bCanBeDeclined)
{
bool bReturnCode = true;
// A list of all packages that need to be saved
TArray<UPackage*> PackagesToSave;
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
// Make a list of all content packages that we should save
for (TObjectIterator<UPackage> It; It; ++It)
{
UPackage* Package = *It;
bool bShouldIgnorePackage = false;
// Only look at root packages.
bShouldIgnorePackage |= Package->GetOuter() != NULL;
// Don't try to save "Transient" package.
bShouldIgnorePackage |= Package == GetTransientPackage();
// Ignore PIE packages.
bShouldIgnorePackage |= Package->HasAnyPackageFlags(PKG_PlayInEditor);
// Ignore packages that haven't been modified.
bShouldIgnorePackage |= !Package->IsDirty();
// Ignore packages with long, invalid names. This culls out packages with paths in read-only roots such as /Temp.
bShouldIgnorePackage |= (!FPackageName::IsShortPackageName(Package->GetFName()) && !FPackageName::IsValidLongPackageName(Package->GetName(), /*bIncludeReadOnlyRoots=*/false));
if (!bShouldIgnorePackage)
{
TArray<UObject*> Objects;
GetObjectsWithPackage(Package, Objects);
for (auto Iter = Objects.CreateIterator(); Iter; ++Iter)
{
bool bNeedToSave = false;
for (const UClass* ClassType : SaveContentClasses)
{
if ((*Iter)->GetClass()->IsChildOf(ClassType))
{
bNeedToSave = true;
break;
}
}
if (bNeedToSave)
{
// add to asset
PackagesToSave.Add(Package);
break;
}
}
}
}
bool bResult = false;
if (PackagesToSave.Num() > 0)
{
const bool bCheckDirty = true;
bResult = InternalSavePackages(PackagesToSave, bPromptUserToSave, bFastSave, bCanBeDeclined, bCheckDirty);
}
else if (bNotifyNoPackagesSaved)
{
InternalNotifyNoPackagesSaved(true);
bResult = true;
}
return bResult;
}
void FEditorFileUtils::PrepareWorldsForExplicitSave(TArray<UWorld*> Worlds)
{
OnPrepareWorldsForExplicitSave.Broadcast(Worlds);
}
/**
* Saves the active level, prompting the use for checkout if necessary.
*
* @return true on success, False on fail
*/
bool FEditorFileUtils::SaveCurrentLevel()
{
TRACE_CPUPROFILER_EVENT_SCOPE(FEditorFileUtils_SaveCurrentLevel);
bool bReturnCode = true;
ULevel* Level = GWorld->GetCurrentLevel();
if (Level)
{
TArray<UPackage*> PackagesToSave;
UPackage* LevelPackage = Level->GetPackage();
// Get Packages to save
if (LevelPackage->IsDirty() || LevelPackage->HasAnyPackageFlags(PKG_NewlyCreated))
{
PackagesToSave.Add(LevelPackage);
}
// Get External Packages to save
const TArray<UPackage*> ExternalPackages = Level->GetLoadedExternalObjectPackages();
for (UPackage* ExternalPackage : ExternalPackages)
{
if (FPackageName::IsValidLongPackageName(ExternalPackage->GetName()))
{
if (ExternalPackage->IsDirty() || ExternalPackage->HasAnyPackageFlags(PKG_NewlyCreated) || UPackage::IsEmptyPackage(ExternalPackage))
{
PackagesToSave.Add(ExternalPackage);
}
}
}
if (PackagesToSave.Num())
{
TGuardValue<bool> IsExplicitSaveGuard(EditorFileUtils::bIsExplicitSave, true);
// If Level gets saved we don't want it to save its external packages because we've already filtered out the ones that need saving and they are part of the PackagesToSave array (Worlds in package with PKG_NewlyCreated will ignore this flag)
TGuardValue<bool> GuardValue(bSkipExternalObjectSave, true);
const bool bPromptUserToSave = false;
const bool bFastSave = false;
const bool bCanBeDeclined = false;
const bool bCheckDirty = false; // force the flag back to false because we already checked conditions to add to PackagesToSave. Some Packages like newly created packages might not be dirty and we still want to save them.
bReturnCode &= InternalSavePackages(PackagesToSave, bPromptUserToSave, bFastSave, bCanBeDeclined, bCheckDirty);
}
}
return bReturnCode;
}
/*
* Helper code for PromptForCheckoutAndSave
* @param FinalSaveList Package to save
* @param bUseDialog Use dialog with InternalSavePackage & if we show errors
* @param OutFailedPackages Packages that couldn't be save
*/
FEditorFileUtils::EPromptReturnCode InternalPromptForCheckoutAndSave(const TArray<UPackage*>& FinalSaveList, bool bUseDialog, TArray<UPackage*>& OutFailedPackages)
{
UE_SCOPED_TIMER(TEXT("InternalPromptForCheckoutAndSave"), LogFileHelpers, Log);
FEditorFileUtils::EPromptReturnCode ReturnResponse = FEditorFileUtils::PR_Success;
const FScopedBusyCursor BusyCursor;
FSaveErrorOutputDevice SaveErrors;
TArray<UPackage*, TInlineAllocator<2>> WritablePackageFiles;
TArray<UPackage*> PackagesToClean;
TArray<UPackage*> PackagesToSave;
PackagesToSave.Reserve(FinalSaveList.Num());
{
auto SaveOperation = ISourceControlOperation::Create<FSave>();
bool bCanSaveToRevisionControl =
ISourceControlModule::Get().IsEnabled() &&
ISourceControlModule::Get().GetProvider().IsAvailable() &&
ISourceControlModule::Get().GetProvider().CanExecuteOperation(SaveOperation);
FScopedSlowTask SlowTask(static_cast<float>(FinalSaveList.Num() * (bCanSaveToRevisionControl ? 3 : 2)), NSLOCTEXT("UnrealEd", "SavingPackages", "Saving packages..."));
SlowTask.MakeDialog();
UWorld* ActorsWorld = nullptr;
for (UPackage* Package : FinalSaveList)
{
if (AActor* Actor = AActor::FindActorInPackage(Package))
{
ActorsWorld = Actor->GetWorld();
break;
}
}
if (ActorsWorld)
{
FEditorDelegates::PreSaveExternalActors.Broadcast(ActorsWorld);
}
for (UPackage* Package : FinalSaveList)
{
SlowTask.EnterProgressFrame(1);
if (!Package->IsFullyLoaded())
{
// Packages must be fully loaded to save.
Package->FullyLoad();
}
// if the package we are saving is considered empty, mark it for deletion on disk instead
if (UPackage::IsEmptyPackage(Package))
{
PackagesToClean.Add(Package);
}
else
{
PackagesToSave.Add(Package);
}
}
PrepareWorldsForExplicitSave(FinalSaveList);
// Cleanup packages before saving packages in case we are saving worlds with external packages we could end up with packages being cleaned up by a world package save
if (PackagesToClean.Num() > 0)
{
ObjectTools::CleanupAfterSuccessfulDelete(PackagesToClean, true);
SlowTask.EnterProgressFrame(static_cast<float>(PackagesToClean.Num()));
}
PrepareSavePackages(PackagesToSave);
for (UPackage* Package : PackagesToSave)
{
const UWorld* const AssociatedWorld = UWorld::FindWorldInPackage(Package);
FText SavingPackageText;
if (AssociatedWorld != nullptr)
{
UE::Core::FVersePath VersePath;
if (IAssetTools::Get().ShowingContentVersePath())
{
VersePath = AssociatedWorld->GetVersePath();
}
SavingPackageText = FText::Format(NSLOCTEXT("UnrealEd", "SavingMapf", "Saving map {0}"), FText::FromString(VersePath.IsValid() ? MoveTemp(VersePath).ToString() : Package->GetName()));
}
else
{
SavingPackageText = FText::Format(NSLOCTEXT("UnrealEd", "SavingAssetf", "Saving asset {0}"), FText::FromString(IAssetTools::Get().GetUserFacingLongPackageName(*Package)));
}
SlowTask.EnterProgressFrame(1, SavingPackageText);
// Save the package
bool bPackageLocallyWritable;
const InternalSavePackageResult SaveStatus = InternalSavePackage(Package, bUseDialog, bPackageLocallyWritable, SaveErrors);
// If InternalSavePackage reported that the provided package was locally writable, add it to the list of writable files
// to warn the user about
if (bPackageLocallyWritable)
{
WritablePackageFiles.Add(Package);
}
if (SaveStatus == InternalSavePackageResult::Cancel)
{
// No need to save anything else, the user wants to cancel everything
ReturnResponse = FEditorFileUtils::PR_Cancelled;
break;
}
else if (SaveStatus == InternalSavePackageResult::Continue || SaveStatus == InternalSavePackageResult::Error)
{
// The package could not be saved so add it to the failed array and change the return response to indicate failure
OutFailedPackages.Add(Package);
ReturnResponse = FEditorFileUtils::PR_Failure;
}
}
if (ActorsWorld)
{
FEditorDelegates::PostSaveExternalActors.Broadcast(ActorsWorld);
}
if (bCanSaveToRevisionControl)
{
SlowTask.EnterProgressFrame(PackagesToSave.Num() + PackagesToClean.Num());
ISourceControlModule::Get().GetProvider().Execute(SaveOperation, USourceControlHelpers::PackageFilenames(PackagesToSave), EConcurrency::Synchronous);
}
}
SaveErrors.Flush();
// Add all files that needs to be marked for add in one command, if any
if (GEditor)
{
GEditor->RunDeferredMarkForAddFiles();
}
// If any packages were saved that weren't actually in source control but instead forcibly made writable,
// then warn the user about those packages. We do not warn if the Uncontrolled Changelists are enabled since
// the file will be picked up.
if (!FUncontrolledChangelistsModule::Get().IsEnabled() && (WritablePackageFiles.Num() > 0))
{
FString WritableFiles;
for (const FString& UserFacingPath : IAssetTools::Get().GetUserFacingLongPackageNames(WritablePackageFiles))
{
// A warning message was created. Try and show it.
WritableFiles += FString::Printf(TEXT("\n%s"), *UserFacingPath);
}
const FText WritableFileWarning = FText::Format(NSLOCTEXT("UnrealEd", "Warning_WritablePackagesNotCheckedOut", "The following assets are writable on disk but not checked out from revision control:{0}"),
FText::FromString(WritableFiles));
UE_LOG(LogFileHelpers, Warning, TEXT("%s"), *WritableFileWarning.ToString());
if (bUseDialog)
{
FSuppressableWarningDialog::FSetupInfo Info(WritableFileWarning, NSLOCTEXT("UnrealEd", "Warning_WritablePackagesNotCheckedOutTitle", "Writable Assets Not Checked Out"), "WritablePackagesNotCheckedOut");
Info.ConfirmText = NSLOCTEXT("ModalDialogs", "WritablePackagesNotCheckedOutConfirm", "Close");
FSuppressableWarningDialog PromptForWritableFiles(Info);
PromptForWritableFiles.ShowModal();
}
}
// Warn the user if any packages failed to save
if (OutFailedPackages.Num() > 0)
{
// Show a dialog for the failed packages
InternalWarnUserAboutFailedSave(OutFailedPackages, bUseDialog);
}
return ReturnResponse;
}
/**
* Optionally prompts the user for which of the provided packages should be saved, and then additionally prompts the user to check-out any of
* the provided packages which are under source control. If the user cancels their way out of either dialog, no packages are saved. It is possible the user
* will be prompted again, if the saving process fails for any reason. In that case, the user will be prompted on a package-by-package basis, allowing them
* to retry saving, skip trying to save the current package, or to again cancel out of the entire dialog. If the user skips saving a package that failed to save,
* the package will be added to the optional OutFailedPackages array, and execution will continue. After all packages are saved (or not), the user is provided with
* a warning about any packages that were writable on disk but not in source control, as well as a warning about which packages failed to save.
*
* @param PackagesToSave The list of packages to save. Both map and content packages are supported
* @param bCheckDirty If true, only packages that are dirty in PackagesToSave will be saved
* @param bPromptToSave If true the user will be prompted with a list of packages to save, otherwise all passed in packages are saved
* @param Title If bPromptToSave true provides a dialog title
* @param Message If bPromptToSave true provides a dialog message
* @param OutFailedPackages [out] If specified, will be filled in with all of the packages that failed to save successfully
* @param bAlreadyCheckedOut If true, the user will not be prompted with the source control dialog
* @param bCanBeDeclined If true, offer a "Don't Save" option in addition to "Cancel", which will not result in a cancellation return code.
*
* @return An enum value signifying success, failure, user declined, or cancellation. If any packages at all failed to save during execution, the return code will be
* failure, even if other packages successfully saved. If the user cancels at any point during any prompt, the return code will be cancellation, even though it
* is possible some packages have been successfully saved (if the cancel comes on a later package that can't be saved for some reason). If the user opts the "Don't
* Save" option on the dialog, the return code will indicate the user has declined out of the prompt. This way calling code can distinguish between a decline and a cancel
* and then proceed as planned, or abort its operation accordingly.
*/
FEditorFileUtils::EPromptReturnCode FEditorFileUtils::PromptForCheckoutAndSave(const TArray<UPackage*>& InPackages, FPromptForCheckoutAndSaveParams& InOutParams)
{
// Check for re-entrance into this function
if ( bIsPromptingForCheckoutAndSave )
{
return PR_Cancelled;
}
// Gather packages owned by the packages we are saving so we can prompt for them as well.
TArray<UPackage*> PackagesToSave(InPackages);
// When saving a package which owns other packages, add those to the prompt as well,
for (UPackage* Package : InPackages)
{
const bool bShouldAddExternalPackages = [Package, InOutParams]() -> bool
{
if (const UObject* MainAsset = Package->FindAssetInPackage())
{
if (const UAssetDefinition* AssetDefinition = UAssetDefinitionRegistry::Get()->GetAssetDefinitionForClass(MainAsset->GetClass()))
{
if(AssetDefinition->ShouldSaveExternalPackages())
{
// if forced according to the top-level asset definition
return true;
}
}
}
// or if we do not check dirty, we aren't already checked out and we prompt
return !InOutParams.bAlreadyCheckedOut && !InOutParams.bCheckDirty && InOutParams.bPromptToSave;
}();
if (bShouldAddExternalPackages)
{
for (UPackage* ExternalPackage : Package->GetExternalPackages())
{
PackagesToSave.AddUnique(ExternalPackage);
}
}
}
if (GIsRunningUnattendedScript)
{
return UEditorLoadingAndSavingUtils::SavePackages(PackagesToSave, InOutParams.bCheckDirty) ? PR_Success : PR_Failure;
}
if ( FApp::IsUnattended() && !InOutParams.bAlreadyCheckedOut )
{
return PR_Cancelled;
}
// Prevent re-entrance into this function by setting up a guard value (also used by FEditorFileUtils::PromptToCheckoutPackages)
TGuardValue<bool> PromptForCheckoutAndSaveGuard(bIsPromptingForCheckoutAndSave, true);
TGuardValue<bool> IsExplicitSaveGuard(EditorFileUtils::bIsExplicitSave, InOutParams.bIsExplicitSave);
// Initialize the value we will return to indicate success
FEditorFileUtils::EPromptReturnCode ReturnResponse = PR_Success;
// Keep a static list of packages that have been unchecked by the user and uncheck them next time
static TArray<TWeakObjectPtr<UPackage>> UncheckedPackages;
// Keep a list of packages that have been filtered to be saved specifically; this could occur as the result of prompting the user
// for which packages to save or from filtering by whether the package is dirty or not. This method allows us to save loop iterations and array copies.
TArray<UPackage*> FilteredPackages;
// Prompt the user for which packages they would like to save
if(InOutParams.bPromptToSave )
{
// Set up the save package dialog
FPackagesDialogModule& PackagesDialogModule = FModuleManager::LoadModuleChecked<FPackagesDialogModule>( TEXT("PackagesDialog") );
PackagesDialogModule.CreatePackagesDialog(InOutParams.Title, InOutParams.Message);
PackagesDialogModule.AddButton(DRT_Save, NSLOCTEXT("PackagesDialogModule", "SaveSelectedButton", "Save Selected"), NSLOCTEXT("PackagesDialogModule", "SaveSelectedButtonTip", "Attempt to save the selected content"));
if (InOutParams.bCanBeDeclined)
{
PackagesDialogModule.AddButton(DRT_DontSave, NSLOCTEXT("PackagesDialogModule", "DontSaveSelectedButton", "Don't Save"), NSLOCTEXT("PackagesDialogModule", "DontSaveSelectedButtonTip", "Do not save any content"));
}
PackagesDialogModule.AddButton(DRT_Cancel, NSLOCTEXT("PackagesDialogModule", "CancelButton", "Cancel"), NSLOCTEXT("PackagesDialogModule", "CancelButtonTip", "Do not save any content and cancel the current operation"));
TArray<UPackage*> AddPackageItemsChecked;
TArray<UPackage*> AddPackageItemsUnchecked;
for ( TArray<UPackage*>::TIterator PkgIter(PackagesToSave); PkgIter; ++PkgIter )
{
UPackage* CurPackage = *PkgIter;
check( CurPackage );
// If the caller set bCheckDirty to true, only consider dirty packages
if ( !InOutParams.bCheckDirty || (InOutParams.bCheckDirty && CurPackage->IsDirty() ) )
{
// Never save the transient package
if ( CurPackage != GetTransientPackage() )
{
// Never save compiled in packages
if (CurPackage->HasAnyPackageFlags(PKG_CompiledIn) == false)
{
if (UncheckedPackages.Contains(MakeWeakObjectPtr(CurPackage)))
{
AddPackageItemsUnchecked.Add(CurPackage);
}
else
{
AddPackageItemsChecked.Add(CurPackage);
}
}
else
{
UE_LOG(LogFileHelpers, Warning, TEXT("PromptForCheckoutAndSave attempted to open the save dialog with a compiled in package: %s"), *CurPackage->GetName());
}
}
else
{
UE_LOG(LogFileHelpers, Warning, TEXT("PromptForCheckoutAndSave attempted to open the save dialog with the transient package"));
}
}
else
{
PkgIter.RemoveCurrent();
}
}
if ( AddPackageItemsUnchecked.Num() > 0 || AddPackageItemsChecked.Num() > 0 )
{
int32 WarningCount = 0;
auto AddPackageItem = [&PackagesDialogModule, &WarningCount](UPackage* Package, ECheckBoxState CheckedState)
{
FString IconName;
FString IconTooltip;
if (!GUnrealEd->HasMountWritePermissionForPackage(Package->GetName()))
{
IconName = TEXT("Icons.WarningWithColor");
IconTooltip = TEXT("Insufficient writing permission to save");
++WarningCount;
}
else if (ISourceControlModule::Get().IsEnabled())
{
if (TSharedPtr<ISourceControlState> State = ISourceControlModule::Get().GetProvider().GetState(Package, EStateCacheUsage::Use))
{
if (TOptional<FText> Warning = State->GetWarningText())
{
IconName = TEXT("Icons.WarningWithColor");
IconTooltip = Warning->ToString();
++WarningCount;
}
}
}
PackagesDialogModule.AddPackageItem(Package, CheckedState, /*Disabled*/false, IconName, IconTooltip);
};
for (auto Iter = AddPackageItemsChecked.CreateIterator(); Iter; ++Iter)
{
AddPackageItem(*Iter, ECheckBoxState::Checked);
}
for (auto Iter = AddPackageItemsUnchecked.CreateIterator(); Iter; ++Iter)
{
AddPackageItem(*Iter, ECheckBoxState::Unchecked);
}
if (WarningCount > 0)
{
PackagesDialogModule.SetWarning(LOCTEXT("Warning_Notification", "Warning: Assets have conflict in Revision Control or cannot be written to disk"));
}
// If valid packages were added to the dialog, display it to the user
const EDialogReturnType UserResponse = PackagesDialogModule.ShowPackagesDialog(PackagesNotSavedDuringSaveAll);
// If the user has responded yes, they want to save the packages they have checked
if ( UserResponse == DRT_Save )
{
PackagesToSave.Reset();
PackagesDialogModule.GetResults(PackagesToSave, ECheckBoxState::Checked );
TArray<UPackage*> UncheckedPackagesRaw;
PackagesDialogModule.GetResults( UncheckedPackagesRaw, ECheckBoxState::Unchecked );
UncheckedPackages.Empty();
for (UPackage* Package : UncheckedPackagesRaw)
{
UncheckedPackages.Add(MakeWeakObjectPtr(Package));
}
}
// If the user has responded they don't wish to save, set the response type accordingly
else if ( UserResponse == DRT_DontSave )
{
UE::FileHelpers::Internal::OnPackagesInteractivelyDiscarded.Broadcast(PackagesToSave);
ReturnResponse = PR_Declined;
}
// If the user has cancelled from the dialog, set the response type accordingly
else
{
ReturnResponse = PR_Cancelled;
}
}
}
else
{
// The user will not be prompted about which files to save, so consider all provided packages directly
for ( TArray<UPackage*>::TIterator PkgIter(PackagesToSave); PkgIter; ++PkgIter )
{
UPackage* CurPackage = *PkgIter;
check( CurPackage );
// (Don't consider non-dirty packages if the caller has specified bCheckDirty as true)
if ( !InOutParams.bCheckDirty || CurPackage->IsDirty() )
{
// Never save the transient package
if ( CurPackage != GetTransientPackage() )
{
// Never save compiled in packages
if (CurPackage->HasAnyPackageFlags(PKG_CompiledIn))
{
PkgIter.RemoveCurrent();
UE_LOG(LogFileHelpers, Warning, TEXT("PromptForCheckoutAndSave attempted to save a compiled in package: %s"), *CurPackage->GetName());
}
}
else
{
PkgIter.RemoveCurrent();
UE_LOG(LogFileHelpers, Warning, TEXT("PromptForCheckoutAndSave attempted to save the transient package"));
}
}
else
{
PkgIter.RemoveCurrent();
}
}
}
// Assemble list of packages to save
// If there are any packages to save and the user didn't decline/cancel, then first prompt to check out any that are under source control,
// and then go ahead and save the specified packages
if ( PackagesToSave.Num() > 0 && ReturnResponse == PR_Success )
{
// Sort packages to save
// Dialog sorts the packagelist, user in dialog can sort it by columns
// This brings the package list closer to the original before the dialog opened when saving dirty packages (Maps first, non-maps after)
// Also there is a a few situations where a dirty package can reference an unsaved map, the user will be prompted to give map a name
// If the dirty non-map package references the unsaved map we must have the dirty non-map package save first
TArray<UPackage*> SortedPackagesToSave;
SortedPackagesToSave.Reserve(PackagesToSave.Num());
for (UPackage* Package : PackagesToSave)
{
if (Package && Package->ContainsMap())
{
SortedPackagesToSave.Add(Package);
}
}
for (UPackage* Package : PackagesToSave)
{
if (Package && !Package->ContainsMap())
{
SortedPackagesToSave.Add(Package);
}
}
PackagesToSave = MoveTemp(SortedPackagesToSave);
TArray<UPackage*> FailedPackages;
TArray<UPackage*> PackagesCheckedOutOrMadeWritable;
TArray<UPackage*> PackagesNotNeedingCheckout;
TArray<UPackage*> PackagesToRevert;
// Prompt to check-out any packages under source control
bool bUserResponse = true;
bool bAutomaticCheckout = UseAlternateCheckoutWorkflow();
if (!InOutParams.bAlreadyCheckedOut)
{
if (bAutomaticCheckout)
{
bUserResponse = FEditorFileUtils::AutomaticCheckoutOrPromptToRevertPackages(PackagesToSave, &PackagesCheckedOutOrMadeWritable, &PackagesNotNeedingCheckout, &PackagesToRevert);
}
else
{
const bool bPromptingAfterModify = false;
const bool bAllowSkip = true;
bUserResponse = FEditorFileUtils::PromptToCheckoutPackagesInternal(false, PackagesToSave, &PackagesCheckedOutOrMadeWritable, &PackagesNotNeedingCheckout, bPromptingAfterModify, bAllowSkip);
}
}
if(InOutParams.bAlreadyCheckedOut || (bUserResponse && (PackagesCheckedOutOrMadeWritable.Num() > 0 || PackagesNotNeedingCheckout.Num() > 0)) )
{
TArray<UPackage*> FinalSaveList;
if (InOutParams.bAlreadyCheckedOut)
{
FinalSaveList = PackagesToSave;
}
else
{
FinalSaveList = PackagesNotNeedingCheckout;
FinalSaveList.Append(PackagesCheckedOutOrMadeWritable);
}
{
const bool bUseDialog = true;
ReturnResponse = InternalPromptForCheckoutAndSave(FinalSaveList, bUseDialog, FailedPackages);
}
// Set the failure array to have the same contents as the local one.
// The local one is required so we can always display the error, even if an array is not provided.
if (InOutParams.OutFailedPackages)
{
*InOutParams.OutFailedPackages = FailedPackages;
}
}
else
{
// The user cancelled the checkout dialog, so set the return response accordingly
ReturnResponse = PR_Cancelled;
}
if (PackagesToRevert.Num() > 0)
{
// Check if the world should be reloaded after the revert.
bool bReloadWorld = false;
if (UWorld* EditorWorld = GEditor->GetEditorWorldContext().World())
{
UPackage* EditorWorldPackage = EditorWorld->GetPackage();
if (PackagesToRevert.Contains(EditorWorldPackage))
{
// If the world file is reverted, the world should be reloaded.
bReloadWorld = true;
}
else
{
// If one of the external files is reverted, the world should be reloaded.
for (UPackage* Package : PackagesToRevert)
{
FString PackageName = Package->GetName();
if (PackageName.Contains(FPackagePath::GetExternalActorsFolderName()) ||
PackageName.Contains(FPackagePath::GetExternalObjectsFolderName()))
{
bReloadWorld = true;
break;
}
}
}
}
// Save the packages that need to be reverted, so the SourceControl can act on them.
TArray<UPackage*> PackagesSaveFailed;
InternalPromptForCheckoutAndSave(PackagesToRevert, /*bUseDialog=*/false, PackagesSaveFailed);
// Revert packages that could not be checked out.
USourceControlHelpers::RevertAndReloadPackages(USourceControlHelpers::PackageFilenames(PackagesToRevert), /*bRevertAll=*/false, /*bReloadWorld=*/bReloadWorld);
}
}
return ReturnResponse;
}
FEditorFileUtils::EPromptReturnCode FEditorFileUtils::PromptForCheckoutAndSave(const TArray<UPackage*>& PackagesToSave, bool bCheckDirty, bool bPromptToSave, const FText& Title, const FText& Message, TArray<UPackage*>* OutFailedPackages, bool bAlreadyCheckedOut, bool bCanBeDeclined)
{
FPromptForCheckoutAndSaveParams SaveParams;
SaveParams.bCheckDirty = bCheckDirty;
SaveParams.bPromptToSave = bPromptToSave;
SaveParams.Title = Title;
SaveParams.Message = Message;
SaveParams.OutFailedPackages = OutFailedPackages;
SaveParams.bAlreadyCheckedOut = bAlreadyCheckedOut;
SaveParams.bCanBeDeclined = bCanBeDeclined;
return PromptForCheckoutAndSave(PackagesToSave, SaveParams);
}
FEditorFileUtils::EPromptReturnCode FEditorFileUtils::PromptForCheckoutAndSave(const TArray<UPackage*>& InPackages, bool bCheckDirty, bool bPromptToSave, TArray<UPackage*>* OutFailedPackages, bool bAlreadyCheckedOut, bool bCanBeDeclined)
{
FPromptForCheckoutAndSaveParams SaveParams;
SaveParams.bCheckDirty = bCheckDirty;
SaveParams.bPromptToSave = bPromptToSave;
SaveParams.OutFailedPackages = OutFailedPackages;
SaveParams.bAlreadyCheckedOut = bAlreadyCheckedOut;
SaveParams.bCanBeDeclined = bCanBeDeclined;
return PromptForCheckoutAndSave(InPackages, SaveParams);
}
/* Return 'true' to indicate that the packages (in OutPackagesCheckedOutOrMadeWritable) should be saved, or 'false' to cancel saving. */
bool FEditorFileUtils::AutomaticCheckoutOrPromptToRevertPackages(const TArray<UPackage*>& PackagesToCheckOut, TArray<UPackage*>* OutPackagesCheckedOutOrMadeWritable, TArray<UPackage*>* OutPackagesNotNeedingCheckout, TArray<UPackage*>* OutPackagesToRevert)
{
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
// Is there anything to work with?
int32 NumPackages = PackagesToCheckOut.Num();
if (NumPackages == 0)
{
return true;
}
// Build map from PackageFileName -> Package.
TMap<FString, UPackage*> PackageMap;
for (UPackage* Package : PackagesToCheckOut)
{
PackageMap.Add(USourceControlHelpers::PackageFilename(Package), Package);
}
// Determine initial states.
SourceControlProvider.Execute(ISourceControlOperation::Create<FUpdateStatus>(), PackagesToCheckOut);
TArray<FSourceControlStateRef> InitialStates;
SourceControlProvider.GetState(PackagesToCheckOut, InitialStates, EStateCacheUsage::Use);
TArray<UPackage*> PackagesCheckOutNeeded;
TArray<UPackage*> PackagesCheckOutImpossible;
TArray<UPackage*> PackagesCheckedOutAlready;
PackagesCheckOutNeeded.Reserve(PackagesToCheckOut.Num());
PackagesCheckOutImpossible.Reserve(PackagesToCheckOut.Num());
PackagesCheckedOutAlready.Reserve(PackagesToCheckOut.Num());
for (const FSourceControlStateRef& State : InitialStates)
{
const FString& PackageFilename = State->GetFilename();
if (State->IsCheckedOut())
{
// No need to check it out.
PackagesCheckedOutAlready.Add(PackageMap[PackageFilename]);
}
else if (!State->CanCheckout())
{
// Can't check it out.
PackagesCheckOutImpossible.Add(PackageMap[PackageFilename]);
}
else
{
// Try to check it out.
PackagesCheckOutNeeded.Add(PackageMap[PackageFilename]);
}
}
// Result value indicates whether to continue with saving.
bool bResult = false;
// Is SourceControl online?
bool bSourceControlEnabled = SourceControlProvider.IsEnabled();
bool bSourceControlAvailable = SourceControlProvider.IsAvailable();
if (bSourceControlAvailable)
{
// Yes, SourceControl is online.
// Try to check out the packages.
TArray<UPackage*> PackagesCheckOutSuccess;
TArray<UPackage*> PackagesCheckOutFailure;
PackagesCheckOutSuccess.Reserve(PackagesCheckOutNeeded.Num());
PackagesCheckOutFailure.Reserve(PackagesCheckOutNeeded.Num());
TArray<UPackage*> PackagesWritableSuccess;
TArray<UPackage*> PackagesWritableFailure;
TArray<UPackage*> PackagesToRevert;
if (PackagesCheckOutNeeded.Num() > 0)
{
FScopedSlowTask SlowTask(static_cast<float>(PackagesCheckOutNeeded.Num()), LOCTEXT("CheckingOutPackages", "Checking out packages..."));
SlowTask.MakeDialog();
int32 NumCheckOutImpossible = 0;
// Loop while attempting to check-out the packages.
// If succeeds, then break out of the loop.
// If failed, inspect the state of the files again and see if somebody just checked out or submitted the file. Then retry with the remaining files.
do
{
NumCheckOutImpossible = PackagesCheckOutImpossible.Num();
ECommandResult::Type Result = SourceControlProvider.Execute(ISourceControlOperation::Create<FCheckOut>(), PackagesCheckOutNeeded);
if (Result == ECommandResult::Succeeded)
{
PackagesCheckOutSuccess.Append(PackagesCheckOutNeeded);
PackagesCheckOutNeeded.Empty();
}
else
{
TArray<FSourceControlStateRef> States;
if (SourceControlProvider.GetState(PackagesCheckOutNeeded, States, EStateCacheUsage::ForceUpdate) == ECommandResult::Succeeded)
{
for (const FSourceControlStateRef& State : States)
{
const FString& PackageFilename = State->GetFilename();
if (State->IsCheckedOut())
{
// If provider supports partial checkout this could happen.
UPackage* Package = PackageMap[PackageFilename];
PackagesCheckOutNeeded.Remove(Package);
PackagesCheckOutSuccess.Add(Package);
}
else if (State->IsCheckedOutOther() || !State->IsCurrent())
{
// Somebody just beat us to it.
UPackage* Package = PackageMap[PackageFilename];
PackagesCheckOutNeeded.Remove(Package);
PackagesCheckOutImpossible.Add(Package);
}
}
}
}
} while (NumCheckOutImpossible != PackagesCheckOutImpossible.Num() && PackagesCheckOutNeeded.Num() > 0);
SlowTask.EnterProgressFrame(static_cast<float>(PackagesCheckOutNeeded.Num()));
}
// Any remaining packages have failed for unknown reasons.
PackagesCheckOutFailure = PackagesCheckOutNeeded;
// Were we able to check out all packages?
if (PackagesCheckOutImpossible.Num() > 0 || PackagesCheckOutFailure.Num() > 0)
{
// No.
// Make the packages writable and proceed to save.
TArray<UPackage*> PackagesNotCheckedOut;
PackagesNotCheckedOut.Append(PackagesCheckOutFailure);
PackagesNotCheckedOut.Append(PackagesCheckOutImpossible);
MakePackagesWritable(PackagesNotCheckedOut, &PackagesWritableSuccess, &PackagesWritableFailure);
}
else
{
// Yes.
// All packages were checked out.
}
// Populate output values.
if (OutPackagesCheckedOutOrMadeWritable)
{
OutPackagesCheckedOutOrMadeWritable->Append(PackagesCheckOutSuccess);
OutPackagesCheckedOutOrMadeWritable->Append(PackagesWritableSuccess);
}
if (OutPackagesNotNeedingCheckout)
{
OutPackagesNotNeedingCheckout->Append(PackagesCheckedOutAlready);
}
if (OutPackagesToRevert)
{
OutPackagesToRevert->Append(PackagesToRevert);
}
// Save if anything was checked out or made writable.
bResult = (PackagesCheckOutSuccess.Num() > 0) || (PackagesWritableSuccess.Num() > 0) || (PackagesCheckedOutAlready.Num() > 0);
}
else if (bSourceControlEnabled)
{
// No, SourceControl is offline.
// Warn the user that they're working in offline mode.
FText OfflineTitle = NSLOCTEXT("FileHelper", "OfflineDialogTitle", "Warning - Offline, Conflicts may occur");
FText OfflineMessage = NSLOCTEXT("FileHelper", "OfflineDialogMessage",
"You've made changes while offline. These and any further changes made offline could conflict with your teammates' work when you reconnect.\r\n\r\n"
"Reconnect as soon as possible to minimize conflicts and continue making changes.");
FSuppressableWarningDialog::FSetupInfo Info(OfflineMessage, OfflineTitle, TEXT("ShowOfflineModeWarning"), GEditorPerProjectIni);
Info.ConfirmText = NSLOCTEXT("FileHelper", "OfflineDialog_ConfirmText", "Ok");
Info.CheckBoxText = NSLOCTEXT("FileHelper", "OfflineDialog_CheckBoxText", "Don't show this again");
Info.bDefaultToSuppressInTheFuture = true;
FSuppressableWarningDialog OfflineModeWarningDialog(Info);
TArray<UPackage*> PackagesWritableSuccess;
TArray<UPackage*> PackagesWritableFailure;
// Show the warning and if the user doesn't cancel, make the files writable and proceed with the save.
bool bMakeWritable = (OfflineModeWarningDialog.ShowModal() != FSuppressableWarningDialog::EResult::Cancel);
if (bMakeWritable)
{
TArray<UPackage*> PackagesToMakeWritable;
PackagesToMakeWritable.Append(PackagesCheckOutNeeded);
PackagesToMakeWritable.Append(PackagesCheckOutImpossible);
MakePackagesWritable(PackagesToMakeWritable, &PackagesWritableSuccess, &PackagesWritableFailure);
}
// Populate output values.
if (OutPackagesCheckedOutOrMadeWritable)
{
OutPackagesCheckedOutOrMadeWritable->Append(PackagesWritableSuccess);
}
if (OutPackagesNotNeedingCheckout)
{
OutPackagesNotNeedingCheckout->Append(PackagesCheckedOutAlready);
}
// Save if anything was made writable.
bResult = (PackagesWritableSuccess.Num() > 0) || (PackagesCheckedOutAlready.Num() > 0);
}
return bResult;
}
bool FEditorFileUtils::SaveWorlds(UWorld* InWorld, const FString& RootPath, const TCHAR* Prefix, TArray<FString>& OutFilenames)
{
const FScopedBusyCursor BusyCursor;
TArray<UWorld*> WorldsArray;
EditorLevelUtils::GetWorlds( InWorld, WorldsArray, true );
TArray<FName> PackageNames;
// Save all packages containing levels that are currently "referenced" by the global world pointer.
bool bSavedAll = true;
FString FinalFilename;
for ( int32 WorldIndex = 0 ; WorldIndex < WorldsArray.Num() ; ++WorldIndex )
{
UWorld* World = WorldsArray[WorldIndex];
const FString WorldPath = FString::Printf(TEXT("%s%s"), *RootPath, *FPackageName::GetLongPackagePath(World->GetOuter()->GetName()));
const bool bLevelWasSaved = SaveWorld( World, NULL,
*WorldPath, Prefix,
false, false,
FinalFilename,
false, true);
if (bLevelWasSaved)
{
OutFilenames.Add(FinalFilename);
}
else
{
bSavedAll = false;
}
}
return bSavedAll;
}
void FEditorFileUtils::LoadDefaultMapAtStartup()
{
FString EditorStartupMap;
// Last opened map.
if (GetDefault<UEditorLoadingSavingSettings>()->LoadLevelAtStartup == ELoadLevelAtStartup::LastOpened)
{
GConfig->GetString(TEXT("EditorStartup"), TEXT("LastLevel"), EditorStartupMap, GEditorPerProjectIni);
}
// Default project map.
if (EditorStartupMap.IsEmpty())
{
EditorStartupMap = GetDefault<UGameMapsSettings>()->EditorStartupMap.GetLongPackageName();
}
const bool bIncludeReadOnlyRoots = true;
if ( FPackageName::IsValidLongPackageName(EditorStartupMap, bIncludeReadOnlyRoots) )
{
FString MapFilenameToLoad = FPackageName::LongPackageNameToFilename( EditorStartupMap );
FScopedSlowTask SlowTask(1.f /* Number of steps */,
FText::Format(
LOCTEXT("LoadDefaultMapAtStartup", "Loading Startup Map: {0}"),
FText::FromString(EditorStartupMap)));
SlowTask.Visibility = ESlowTaskVisibility::Important; // this function can be very slow, users will benefit from our messages
bIsLoadingDefaultStartupMap = true;
FEditorFileUtils::LoadMap( MapFilenameToLoad + FPackageName::GetMapPackageExtension(), GUnrealEd && GUnrealEd->IsTemplateMap(EditorStartupMap), true );
bIsLoadingDefaultStartupMap = false;
}
}
void FEditorFileUtils::FindAllPackageFiles(TArray<FString>& OutPackages)
{
// Check for custom projects
{
TArray<FSourceControlProjectInfo> CustomProjects = ISourceControlModule::Get().GetCustomProjects();
if (!CustomProjects.IsEmpty())
{
for (const FSourceControlProjectInfo& ProjectInfo : CustomProjects)
{
FPackageName::FindPackagesInDirectory(OutPackages, ProjectInfo.ProjectDirectory);
}
return;
}
}
#if UE_BUILD_SHIPPING
FString Key = TEXT("Paths");
#else
// decide which paths to use by commandline parameter
// Used only for testing wrangled content -- not for ship!
FString PathSet(TEXT("Normal"));
FParse::Value(FCommandLine::Get(), TEXT("PATHS="), PathSet);
FString Key = (PathSet == TEXT("Cutdown")) ? TEXT("CutdownPaths") : TEXT("Paths");
#endif
TArray<FString> Paths;
GConfig->GetArray( TEXT("Core.System"), *Key, Paths, GEngineIni );
for (int32 PathIndex = 0; PathIndex < Paths.Num(); PathIndex++)
{
FPackageName::FindPackagesInDirectory(OutPackages, Paths[PathIndex]);
}
}
void FEditorFileUtils::FindAllSubmittablePackageFiles(TMap<FString, FSourceControlStatePtr>& OutPackages, const bool bIncludeMaps)
{
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
const bool bCustomProjects = !ISourceControlModule::Get().GetCustomProjects().IsEmpty();
TArray<FString> Packages;
FEditorFileUtils::FindAllPackageFiles(Packages);
OutPackages.Empty();
OutPackages.Reserve(Packages.Num());
for (TArray<FString>::TConstIterator PackageIter(Packages); PackageIter; ++PackageIter)
{
const FString& Filename = *PackageIter;
FString PackageName;
FString FailureReason;
if (!FPackageName::TryConvertFilenameToLongPackageName(Filename, PackageName, &FailureReason))
{
UE_LOG(LogFileHelpers, Warning, TEXT("%s"), *FailureReason);
continue;
}
FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(FPaths::ConvertRelativePathToFull(Filename), EStateCacheUsage::Use);
// Only include non-map packages that are currently checked out or packages not under source control
if (bCustomProjects)
{
if (SourceControlState.IsValid() &&
(SourceControlState->CanCheckIn() || (!SourceControlState->IsSourceControlled() && SourceControlState->CanAdd())) &&
(bIncludeMaps || !IsMapPackageAsset(*Filename)))
{
OutPackages.Add(MoveTemp(PackageName), MoveTemp(SourceControlState));
}
}
else
{
if (SourceControlState.IsValid() && SourceControlState->IsCurrent() &&
(SourceControlState->CanCheckIn() || (!SourceControlState->IsSourceControlled() && SourceControlState->CanAdd())) &&
(bIncludeMaps || !IsMapPackageAsset(*Filename)))
{
OutPackages.Add(MoveTemp(PackageName), MoveTemp(SourceControlState));
}
}
}
}
void FEditorFileUtils::FindAllSubmittableProjectFiles(TMap<FString, FSourceControlStatePtr>& OutProjectFiles)
{
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
TArray<FSourceControlProjectInfo> CustomProjects = ISourceControlModule::Get().GetCustomProjects();
if (CustomProjects.IsEmpty())
{
// Handle just the project file
FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath()), EStateCacheUsage::Use);
if (SourceControlState.IsValid() && SourceControlState->IsCurrent() &&
(SourceControlState->CanCheckIn() || (!SourceControlState->IsSourceControlled() && SourceControlState->CanAdd())))
{
OutProjectFiles.Add(FPaths::GetProjectFilePath(), MoveTemp(SourceControlState));
}
}
else
{
TArray<FSourceControlStateRef> SourceControlStates;
for (const FSourceControlProjectInfo& ProjectInfo : CustomProjects)
{
// Handle non-package files in the project directory
SourceControlStates.Append(SourceControlProvider.GetCachedStateByPredicate(
[&ProjectInfo](const FSourceControlStateRef& SourceControlState)
{
return FPaths::IsUnderDirectory(SourceControlState->GetFilename(), ProjectInfo.ProjectDirectory);
}));
}
OutProjectFiles.Reserve(SourceControlStates.Num());
for (FSourceControlStateRef& SourceControlState : SourceControlStates)
{
const FString& Filename = SourceControlState->GetFilename();
if (SourceControlState->CanCheckIn() || (!SourceControlState->IsSourceControlled() && SourceControlState->CanAdd()))
{
FString Ext = FPaths::GetExtension(Filename);
if (!FPackageName::IsPackageExtension(*Ext) && !FPackageName::IsTextPackageExtension(*Ext))
{
OutProjectFiles.Add(Filename, MoveTemp(SourceControlState));
}
}
}
}
}
static void FindAllConfigFilesRecursive(TArray<FString>& OutConfigFiles, const FString& ParentDirectory)
{
TArray<FString> IniFilenames;
IFileManager::Get().FindFiles(IniFilenames, *(FPaths::ProjectConfigDir() / ParentDirectory / TEXT("*.ini")), true, false);
for (const FString& IniFilename : IniFilenames)
{
OutConfigFiles.Add(FPaths::ConvertRelativePathToFull(FPaths::ProjectConfigDir() / ParentDirectory / IniFilename));
}
TArray<FString> Subdirectories;
IFileManager::Get().FindFiles(Subdirectories, *(FPaths::ProjectConfigDir() / ParentDirectory / TEXT("*")), false, true);
for (const FString& Subdirectory : Subdirectories)
{
FindAllConfigFilesRecursive(OutConfigFiles, ParentDirectory / Subdirectory);
}
}
void FEditorFileUtils::FindAllConfigFiles(TArray<FString>& OutConfigFiles)
{
FindAllConfigFilesRecursive(OutConfigFiles, FString());
}
void FEditorFileUtils::FindAllSubmittableConfigFiles(TMap<FString, TSharedPtr<class ISourceControlState, ESPMode::ThreadSafe> >& OutConfigFiles)
{
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
TArray<FString> ConfigFilenames;
FEditorFileUtils::FindAllConfigFiles(ConfigFilenames);
for (const FString& ConfigFilename : ConfigFilenames)
{
// Only check files which are intended to be under source control. Ignore all user config files.
if (FPaths::GetCleanFilename(ConfigFilename) != TEXT("DefaultEditorPerProjectUserSettings.ini") && !FPaths::GetCleanFilename(ConfigFilename).StartsWith(TEXT("User")))
{
FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(ConfigFilename, EStateCacheUsage::Use);
// Only include config files that are currently checked out or packages not under source control
if (SourceControlState.IsValid() && SourceControlState->IsCurrent() &&
(SourceControlState->CanCheckIn() || (!SourceControlState->IsSourceControlled() && SourceControlState->CanAdd())))
{
OutConfigFiles.Add(ConfigFilename, SourceControlState);
}
}
}
}
bool FEditorFileUtils::IsMapPackageAsset(const FString& ObjectPath)
{
FString MapFilePath;
return FEditorFileUtils::IsMapPackageAsset(ObjectPath, MapFilePath);
}
bool FEditorFileUtils::IsMapPackageAsset(const FString& ObjectPath, FString& MapFilePath)
{
const FString PackageName = ExtractPackageName(ObjectPath);
if ( PackageName.Len() > 0 )
{
FString PackagePath;
if ( FPackageName::DoesPackageExist(PackageName, &PackagePath) )
{
const FString FileExtension = FPaths::GetExtension(PackagePath, true);
if ( FileExtension == FPackageName::GetMapPackageExtension() )
{
MapFilePath = PackagePath;
return true;
}
TArray<FString> ObjectPathParts;
if (ObjectPath.ParseIntoArray(ObjectPathParts, TEXT("/")) > 1)
{
if (ObjectPathParts[1] == FPackagePath::GetExternalActorsFolderName())
{
MapFilePath = PackagePath;
return true;
}
}
}
}
return false;
}
FString FEditorFileUtils::ExtractPackageName(const FString& ObjectPath)
{
// To find the package name in an object path we need to find the path left of the FIRST delimiter.
// Assets like BSPs, lightmaps etc. can have multiple '.' delimiters.
const int32 PackageDelimiterPos = ObjectPath.Find(TEXT("."), ESearchCase::CaseSensitive, ESearchDir::FromStart);
if ( PackageDelimiterPos != INDEX_NONE )
{
return ObjectPath.Left(PackageDelimiterPos);
}
return ObjectPath;
}
void FEditorFileUtils::GetDirtyWorldPackages(TArray<UPackage*>& OutDirtyPackages, const FShouldIgnorePackageFunctionRef& ShouldIgnorePackageFunction)
{
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
const TSharedRef<FPathPermissionList>& WritableFolderFilter = AssetToolsModule.Get().GetWritableFolderPermissionList();
const bool bHasWritableFolderFilter = WritableFolderFilter->HasFiltering();
TOptional<TSet<UPackage*>> AdditionalSaveCandidates; // Lazily queried if needed by the loop below
for (TObjectIterator<UWorld> WorldIt; WorldIt; ++WorldIt)
{
// Filter out pending-delete worlds that may have leaked, e.g. from PIE sessions which were not cleaned up which cleared the PKG_PlayInEditor flag
if (!IsValid(*WorldIt))
{
continue;
}
UPackage* WorldPackage = WorldIt->GetOutermost();
if (!WorldPackage->HasAnyPackageFlags(PKG_PlayInEditor)
&& !WorldPackage->HasAnyFlags(RF_Transient)
&& (!bHasWritableFolderFilter || WritableFolderFilter->PassesStartsWithFilter(WorldPackage->GetName()))
)
{
bool bDirtyNewWorldPackage = false;
if (WorldPackage->IsDirty() && !ShouldIgnorePackageFunction(WorldPackage))
{
// IF the package is dirty and its not a pie package, add the world package to the list of packages to save
OutDirtyPackages.Add(WorldPackage);
}
// Add the Map built data as well if world is
if (WorldIt->PersistentLevel && WorldIt->PersistentLevel->MapBuildData)
{
UPackage* BuiltDataPackage = WorldIt->PersistentLevel->MapBuildData->GetOutermost();
if (BuiltDataPackage != WorldPackage)
{
if (WorldPackage->IsDirty() && !BuiltDataPackage->IsDirty())
{
// Mark built data package dirty if has not been given name yet
// Otherwise SaveDirtyPackages will fail to create built data file on disk due to re-entrance guard in PromptForCheckoutAndSave preventing a second pop-up window
if (!FPackageName::IsValidLongPackageName(BuiltDataPackage->GetName(), /*bIncludeReadOnlyRoots= */ false))
{
BuiltDataPackage->MarkPackageDirty();
}
}
if (BuiltDataPackage->IsDirty())
{
bDirtyNewWorldPackage = true;
if (!ShouldIgnorePackageFunction(BuiltDataPackage))
{
OutDirtyPackages.Add(BuiltDataPackage);
}
}
}
}
// Make sure we also save the dirty HLOD packages associated with this map.
// @todo_ow
/*if (WorldIt->HierarchicalLODBuilder)
{
const AWorldSettings* WorldSettings = WorldIt->GetWorldSettings();
if (WorldSettings && WorldSettings->bEnableHierarchicalLODSystem)
{
TSet<UPackage*> HLODPackages;
WorldIt->HierarchicalLODBuilder->GetMeshesPackagesToSave(WorldIt->PersistentLevel, HLODPackages);
for (UPackage* HLODPackage : HLODPackages)
{
if (HLODPackage->IsDirty())
{
OutDirtyPackages.Add(HLODPackage);
}
}
}
}*/
// Now gather the world external packages and save them if needed
if (WorldIt->PersistentLevel)
{
for (UPackage* ExternalPackage : WorldIt->PersistentLevel->GetLoadedExternalObjectPackages())
{
if (ExternalPackage->IsDirty())
{
bDirtyNewWorldPackage = true;
if (!ShouldIgnorePackageFunction(ExternalPackage))
{
bool bActorPackageNeedsToSave = true;
// Skip unsaved empty actor packages, unless we've been asked to include them
if (ExternalPackage->HasAnyPackageFlags(PKG_NewlyCreated) && UPackage::IsEmptyPackage(ExternalPackage))
{
if (!AdditionalSaveCandidates)
{
AdditionalSaveCandidates.Emplace();
UE::FileHelpers::Internal::GetAdditionalInteractiveSavePackageCandidates.Broadcast(AdditionalSaveCandidates.GetValue());
}
bActorPackageNeedsToSave &= AdditionalSaveCandidates->Contains(ExternalPackage);
}
// Filter out Actors that might be unsaved (/Temp folder)
bActorPackageNeedsToSave &= FPackageName::IsValidLongPackageName(ExternalPackage->GetName());
if (bActorPackageNeedsToSave)
{
OutDirtyPackages.Add(ExternalPackage);
}
}
}
}
}
if (bDirtyNewWorldPackage && !WorldPackage->IsDirty() && WorldPackage->HasAnyPackageFlags(PKG_NewlyCreated))
{
// If world package does not have a name yet add the world package so a user is prompted to have a name chosen
WorldPackage->MarkPackageDirty();
if (!ShouldIgnorePackageFunction(WorldPackage))
{
OutDirtyPackages.Add(WorldPackage);
}
}
}
}
}
void FEditorFileUtils::GetDirtyContentPackages(TArray<UPackage*>& OutDirtyPackages, const FShouldIgnorePackageFunctionRef& ShouldIgnorePackageFunction)
{
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
const TSharedRef<FPathPermissionList>& WritableFolderFilter = AssetToolsModule.Get().GetWritableFolderPermissionList();
const bool bHasWritableFolderFilter = WritableFolderFilter->HasFiltering();
// Make a list of all content packages that we should save
for (TObjectIterator<UPackage> It; It; ++It)
{
UPackage* Package = *It;
bool bShouldIgnorePackage = false;
// Only look at root packages.
bShouldIgnorePackage |= Package->GetOuter() != NULL;
// Don't try to save "Transient" package.
bShouldIgnorePackage |= Package == GetTransientPackage();
// Don't try to save packages with the RF_Transient flag
bShouldIgnorePackage |= Package->HasAnyFlags(RF_Transient);
// Ignore UHT/Verse generated packages, PIE packages, or packages containing map data
bShouldIgnorePackage |= Package->HasAnyPackageFlags(PKG_CompiledIn | PKG_PlayInEditor | PKG_ContainsMapData);
// Ignore packages that haven't been modified.
bShouldIgnorePackage |= !Package->IsDirty();
if (!bShouldIgnorePackage)
{
UObject* Asset = Package->FindAssetInPackage();
const bool bIsMapPackage = Cast<UWorld>(Asset) != nullptr;
const bool bIsExternalMapObject = Asset && Asset->GetTypedOuter<UWorld>() != nullptr;
// Ignore map packages, they are caught above.
bShouldIgnorePackage |= bIsMapPackage;
// Ignore external actors, they are caught alongside maps
bShouldIgnorePackage |= bIsExternalMapObject;
if (!bShouldIgnorePackage)
{
FString PackageName = Package->GetName();
// Ignore packages with long, invalid names. This culls out packages with paths in read-only roots such as /Temp.
bShouldIgnorePackage |= (!FPackageName::IsShortPackageName(Package->GetFName()) && !FPackageName::IsValidLongPackageName(PackageName, /*bIncludeReadOnlyRoots=*/false));
// Ignore packages that cannot be saved due to a custom filter
if (!bShouldIgnorePackage && bHasWritableFolderFilter)
{
bShouldIgnorePackage |= (!WritableFolderFilter->PassesStartsWithFilter(PackageName));
}
}
}
if (!bShouldIgnorePackage)
{
bShouldIgnorePackage |= ShouldIgnorePackageFunction(Package);
}
if (!bShouldIgnorePackage)
{
OutDirtyPackages.Add(Package);
}
}
}
void FEditorFileUtils::GetDirtyPackages(TArray<UPackage*>& OutDirtyPackages, const FEditorFileUtils::FShouldIgnorePackageFunctionRef& ShouldIgnorePackageFunction)
{
GetDirtyWorldPackages(OutDirtyPackages, ShouldIgnorePackageFunction);
GetDirtyContentPackages(OutDirtyPackages, ShouldIgnorePackageFunction);
}
FEditorFileUtils::FOnLoadMapStart& FEditorFileUtils::GetOnLoadMapStartDelegate()
{
return OnLoadMapStart;
}
FEditorFileUtils::FOnLoadMapEnd& FEditorFileUtils::GetOnLoadMapEndDelegate()
{
return OnLoadMapEnd;
}
UWorld* UEditorLoadingAndSavingUtils::LoadMap(const FString& Filename)
{
const bool bLoadAsTemplate = false;
const bool bShowProgress = true;
if (FEditorFileUtils::LoadMap(Filename, bLoadAsTemplate, bShowProgress))
{
return GEditor->GetEditorWorldContext().World();
}
return nullptr;
}
bool UEditorLoadingAndSavingUtils::SaveMap(UWorld* World, const FString& AssetPath)
{
bool bSucceeded = false;
FString SaveFilename;
if( FPackageName::TryConvertLongPackageNameToFilename(AssetPath, SaveFilename, FPackageName::GetMapPackageExtension()))
{
bSucceeded = FEditorFileUtils::SaveMap(World, SaveFilename);
if (bSucceeded)
{
FAssetRegistryModule::AssetCreated(World);
}
}
return bSucceeded;
}
UWorld* UEditorLoadingAndSavingUtils::NewBlankMap(bool bSaveExistingMap)
{
// Deactivate any editor modes when creating a new map
if (ULevelEditorSubsystem* LevelEditorSubsystem = GEditor->GetEditorSubsystem<ULevelEditorSubsystem>())
{
if (FEditorModeTools* ModeManager = LevelEditorSubsystem->GetLevelEditorModeManager())
{
ModeManager->DeactivateAllModes();
}
}
const bool bPromptUserToSave = false;
const bool bFastSave = !bPromptUserToSave;
const bool bSaveMapPackages = true;
const bool bSaveContentPackages = false;
if (bSaveExistingMap && FEditorFileUtils::SaveDirtyPackages(bPromptUserToSave, bSaveMapPackages, bSaveContentPackages, bFastSave) == false)
{
// something went wrong or the user pressed cancel. Return to the editor so the user doesn't lose their changes
return nullptr;
}
UWorld* World = GEditor->NewMap();
FEditorFileUtils::ResetLevelFilenames();
return World;
}
UWorld* UEditorLoadingAndSavingUtils::NewMapFromTemplate(const FString& PathToTemplateLevel, bool bSaveExistingMap)
{
bool bSaveMapPackages = true;
bool bSaveContentPackages = false;
if (bSaveExistingMap && SaveDirtyPackages(bSaveMapPackages, bSaveContentPackages) == false)
{
return nullptr;
}
const bool bLoadAsTemplate = true;
// Load the template map file - passes LoadAsTemplate==true making the
// level load into an untitled package that won't save over the template
FEditorFileUtils::LoadMap(*PathToTemplateLevel, bLoadAsTemplate);
return GEditor->GetEditorWorldContext().World();
}
UWorld* UEditorLoadingAndSavingUtils::LoadMapWithDialog()
{
if (!FEditorFileUtils::LoadMap())
{
return nullptr;
}
return GEditor->GetEditorWorldContext().World();
}
static bool InternalCheckoutAndSavePackages(const TArray<UPackage*>& PackagesToSave, bool bUseDialog)
{
bool bResult = true;
if (PackagesToSave.Num() > 0)
{
if (bUseDialog)
{
const bool bPromptUserToSave = true;
const bool bFastSave = false;
const bool bCanBeDeclined = true;
const bool bCheckDirty = true;
bResult = InternalSavePackages(PackagesToSave, bPromptUserToSave, bFastSave, bCanBeDeclined, bCheckDirty);
}
else
{
const FScopedBusyCursor BusyCursor;
// Prevent modal window if not requested.
TGuardValue<bool> UnattendedScriptGuard(GIsRunningUnattendedScript, true);
TArray<UPackage*> PackagesCheckedOut;
const bool bErrorIfAlreadyCheckedOut = false;
const bool bConfirmPackageBranchCheckOutStatus = false;
if (ISourceControlModule::Get().IsEnabled())
{
TArray<UPackage*> PackagesToCheckout;
Algo::CopyIf(PackagesToSave, PackagesToCheckout, [](UPackage* Package) { return !FPackageName::IsTempPackage(Package->GetName()); });
FEditorFileUtils::CheckoutPackages(PackagesToCheckout, &PackagesCheckedOut, bErrorIfAlreadyCheckedOut, bConfirmPackageBranchCheckOutStatus);
}
// Cannot mark files for add until after packages saved
TArray<UPackage*> PackagesToMarkForAdd;
for (UPackage* Package : PackagesToSave)
{
// List unsaved packages that were not checked out and are not going to be deleted
if (!PackagesCheckedOut.Contains(Package) && !UPackage::IsEmptyPackage(Package) && !FPackageName::IsTempPackage(Package->GetName()))
{
PackagesToMarkForAdd.Add(Package);
}
}
TArray<UPackage*> FailedPackages;
FEditorFileUtils::EPromptReturnCode ReturnResponse = InternalPromptForCheckoutAndSave(PackagesToSave, bUseDialog, FailedPackages);
// Mark files for add now that packages have saved
PackagesToMarkForAdd.RemoveAll([&FailedPackages](UPackage* Package) { return FailedPackages.Contains(Package); });
if (PackagesToMarkForAdd.Num() > 0 && ISourceControlModule::Get().IsEnabled())
{
FEditorFileUtils::CheckoutPackages(PackagesToMarkForAdd, nullptr, bErrorIfAlreadyCheckedOut, bConfirmPackageBranchCheckOutStatus);
}
bResult = (ReturnResponse == FEditorFileUtils::PR_Success);
}
}
else
{
InternalNotifyNoPackagesSaved(bUseDialog);
}
return bResult;
}
static TArray<UPackage*> InternalGetValidPackages(const TArray<UPackage*>& PackagesToSave, bool bCheckDirty)
{
//Prevent all prompt code
TArray<UPackage*> Packages;
Packages.Reserve(PackagesToSave.Num());
for (UPackage* Package : PackagesToSave)
{
if (Package && !Package->HasAnyFlags(RF_ClassDefaultObject))
{
Package = Package->GetOutermost();
if (Package != GetTransientPackage() && Package->HasAnyPackageFlags(PKG_CompiledIn) == false)
{
if (!bCheckDirty || Package->IsDirty())
{
Package->FullyLoad();
Packages.AddUnique(Package);
}
}
}
}
return Packages;
}
bool UEditorLoadingAndSavingUtils::SavePackages(const TArray<UPackage*>& PackagesToSave, bool bOnlyDirty)
{
TArray<UPackage*> Packages = InternalGetValidPackages(PackagesToSave, bOnlyDirty);
return InternalCheckoutAndSavePackages(Packages, false);
}
bool UEditorLoadingAndSavingUtils::SavePackagesWithDialog(const TArray<UPackage*>& PackagesToSave, bool bOnlyDirty)
{
TArray<UPackage*> Packages = InternalGetValidPackages(PackagesToSave, bOnlyDirty);
return InternalCheckoutAndSavePackages(Packages, true);
}
bool UEditorLoadingAndSavingUtils::SaveDirtyPackages(const bool bSaveMapPackages, const bool bSaveContentPackages)
{
TArray<UPackage*> Packages = InternalGetDirtyPackages(bSaveMapPackages, bSaveContentPackages);
return InternalCheckoutAndSavePackages(Packages, false);
}
bool UEditorLoadingAndSavingUtils::SaveDirtyPackagesWithDialog(const bool bSaveMapPackages, const bool bSaveContentPackages)
{
TArray<UPackage*> Packages = InternalGetDirtyPackages(bSaveMapPackages, bSaveContentPackages);
return InternalCheckoutAndSavePackages(Packages, true);
}
bool UEditorLoadingAndSavingUtils::SaveCurrentLevel()
{
return FEditorFileUtils::SaveCurrentLevel();
}
void UEditorLoadingAndSavingUtils::GetDirtyMapPackages(TArray<UPackage*>& OutDirtyPackages)
{
FEditorFileUtils::GetDirtyWorldPackages(OutDirtyPackages);
}
void UEditorLoadingAndSavingUtils::GetDirtyContentPackages(TArray<UPackage*>& OutDirtyPackages)
{
FEditorFileUtils::GetDirtyContentPackages(OutDirtyPackages);
}
void UEditorLoadingAndSavingUtils::ImportScene(const FString& Filename)
{
FEditorFileUtils::Import(Filename);
}
void UEditorLoadingAndSavingUtils::ExportScene(bool bExportSelectedActorsOnly)
{
FEditorFileUtils::Export(bExportSelectedActorsOnly);
}
void UEditorLoadingAndSavingUtils::UnloadPackages(const TArray<UPackage*>& PackagesToUnload, bool& bOutAnyPackagesUnloaded, FText& OutErrorMessage)
{
bOutAnyPackagesUnloaded = UPackageTools::UnloadPackages(PackagesToUnload, OutErrorMessage);
}
void UEditorLoadingAndSavingUtils::ReloadPackages(const TArray<UPackage*>& PackagesToReload, bool& bOutAnyPackagesReloaded, FText& OutErrorMessage, const EReloadPackagesInteractionMode InteractionMode)
{
bOutAnyPackagesReloaded = UPackageTools::ReloadPackages(PackagesToReload, OutErrorMessage, InteractionMode);
}
#undef LOCTEXT_NAMESPACE