806 lines
33 KiB
C++
806 lines
33 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "PackageAutoSaver.h"
|
|
#include "UObject/Package.h"
|
|
#include "HAL/FileManager.h"
|
|
#include "Misc/Paths.h"
|
|
#include "Misc/ScopedSlowTask.h"
|
|
#include "Misc/App.h"
|
|
#include "Dom/JsonValue.h"
|
|
#include "Dom/JsonObject.h"
|
|
#include "Serialization/JsonReader.h"
|
|
#include "Policies/PrettyJsonPrintPolicy.h"
|
|
#include "Serialization/JsonSerializer.h"
|
|
#include "Framework/Application/SlateApplication.h"
|
|
#include "EditorStyleSet.h"
|
|
#include "Editor/UnrealEdEngine.h"
|
|
#include "Settings/EditorLoadingSavingSettings.h"
|
|
#include "EditorModeManager.h"
|
|
#include "EditorModes.h"
|
|
#include "UnrealEdMisc.h"
|
|
#include "FileHelpers.h"
|
|
#include "UnrealEdGlobals.h"
|
|
#include "PackageRestore.h"
|
|
#include "Framework/Notifications/NotificationManager.h"
|
|
#include "Widgets/Notifications/SNotificationList.h"
|
|
#include "AutoSaveUtils.h"
|
|
#include "ShaderCompiler.h"
|
|
#include "EditorLevelUtils.h"
|
|
#include "IVREditorModule.h"
|
|
#include "LevelEditorViewport.h"
|
|
#include "Animation/AnimCompressionDerivedDataPublic.h"
|
|
|
|
namespace PackageAutoSaverJson
|
|
{
|
|
typedef TCHAR CharType;
|
|
|
|
typedef TJsonWriter<CharType, TPrettyJsonPrintPolicy<CharType>> FStringWriter;
|
|
typedef TJsonWriterFactory<CharType, TPrettyJsonPrintPolicy<CharType>> FStringWriterFactory;
|
|
|
|
typedef TJsonReader<CharType> FJsonReader;
|
|
typedef TJsonReaderFactory<CharType> FJsonReaderFactory;
|
|
|
|
static const FString TagRestoreEnabled = TEXT("RestoreEnabled");
|
|
static const FString TagPackages = TEXT("Packages");
|
|
static const FString TagPackagePathName = TEXT("PackagePathName");
|
|
static const FString TagAutoSavePath = TEXT("AutoSavePath");
|
|
static const FString RestoreFilename = TEXT("PackageRestoreData.json");
|
|
|
|
/**
|
|
* @param bEnsurePath True to ensure that the directory for the restore file exists
|
|
|
|
* @return Returns the full path to the restore file
|
|
*/
|
|
FString GetRestoreFilename(const bool bEnsurePath);
|
|
|
|
/**
|
|
* Load the restore file from disk (if present)
|
|
*
|
|
* @return The packages that have auto-saves that they can be restored from
|
|
*/
|
|
TMap<FString, FString> LoadRestoreFile();
|
|
|
|
/**
|
|
* Save the file on disk that's used to restore auto-saved packages in the event of a crash
|
|
*
|
|
* @param bRestoreEnabled Is the restore enabled, or is it disabled because we've shut-down cleanly, or are running under the debugger?
|
|
* @param DirtyPackages Packages that may have auto-saves that they could be restored from
|
|
*/
|
|
void SaveRestoreFile(const bool bRestoreEnabled, const TMap<TWeakObjectPtr<UPackage>, FString>& DirtyPackages);
|
|
|
|
/** @return whether the auto-save restore should be enabled (you can force this to true when testing with a debugger attached) */
|
|
bool IsRestoreEnabled()
|
|
{
|
|
// Note: Restore is disabled when running under the debugger, as programmers
|
|
// like to just kill applications and we don't want this to count as a crash
|
|
return !FPlatformMisc::IsDebuggerPresent();
|
|
}
|
|
} // namespace PackageAutoSaverJson
|
|
|
|
/************************************************************************/
|
|
/* FPackageAutoSaver */
|
|
/************************************************************************/
|
|
|
|
DEFINE_LOG_CATEGORY_STATIC(PackageAutoSaver, Log, All);
|
|
|
|
FPackageAutoSaver::FPackageAutoSaver()
|
|
: AutoSaveIndex(0), AutoSaveCount(0.0f), bIsAutoSaving(false), bDelayingDueToFailedSave(false), bAutoDeclineRecovery(FParse::Param(FCommandLine::Get(), TEXT("AutoDeclinePackageRecovery")))
|
|
{
|
|
// Register for the package dirty state updated callback to catch packages that have been cleaned without being saved
|
|
UPackage::PackageDirtyStateChangedEvent.AddRaw(this, &FPackageAutoSaver::OnPackageDirtyStateUpdated);
|
|
|
|
// Register for the "MarkPackageDirty" callback to catch packages that have been modified and need to be saved
|
|
UPackage::PackageMarkedDirtyEvent.AddRaw(this, &FPackageAutoSaver::OnMarkPackageDirty);
|
|
|
|
// Register for the package modified callback to catch packages that have been saved
|
|
UPackage::PackageSavedEvent.AddRaw(this, &FPackageAutoSaver::OnPackageSaved);
|
|
}
|
|
|
|
FPackageAutoSaver::~FPackageAutoSaver()
|
|
{
|
|
UPackage::PackageDirtyStateChangedEvent.RemoveAll(this);
|
|
UPackage::PackageMarkedDirtyEvent.RemoveAll(this);
|
|
UPackage::PackageSavedEvent.RemoveAll(this);
|
|
}
|
|
|
|
void FPackageAutoSaver::UpdateAutoSaveCount(const float DeltaSeconds)
|
|
{
|
|
const UEditorLoadingSavingSettings* LoadingSavingSettings = GetDefault<UEditorLoadingSavingSettings>();
|
|
|
|
const float AutoSaveWarningTime = FMath::Max(0.0f, static_cast<float>(LoadingSavingSettings->AutoSaveTimeMinutes * 60 - LoadingSavingSettings->AutoSaveWarningInSeconds));
|
|
|
|
// Make sure we don't skip the auto-save warning when debugging the editor.
|
|
if (AutoSaveCount < AutoSaveWarningTime && (AutoSaveCount + DeltaSeconds) > AutoSaveWarningTime)
|
|
{
|
|
AutoSaveCount = AutoSaveWarningTime;
|
|
}
|
|
else
|
|
{
|
|
AutoSaveCount += DeltaSeconds;
|
|
}
|
|
|
|
// Update the restore information too, if needed
|
|
if (bNeedRestoreFileUpdate)
|
|
{
|
|
UpdateRestoreFile(PackageAutoSaverJson::IsRestoreEnabled());
|
|
}
|
|
}
|
|
|
|
void FPackageAutoSaver::ResetAutoSaveTimer()
|
|
{
|
|
// Reset the "seconds since last auto-save" counter.
|
|
AutoSaveCount = 0.0f;
|
|
}
|
|
|
|
void FPackageAutoSaver::ForceAutoSaveTimer()
|
|
{
|
|
AutoSaveCount = GetDefault<UEditorLoadingSavingSettings>()->AutoSaveTimeMinutes * 60.0f;
|
|
}
|
|
|
|
void FPackageAutoSaver::ForceMinimumTimeTillAutoSave(const float TimeTillAutoSave)
|
|
{
|
|
const float MinumumTime = (GetDefault<UEditorLoadingSavingSettings>()->AutoSaveTimeMinutes * 60.0f) - TimeTillAutoSave;
|
|
AutoSaveCount = (MinumumTime < AutoSaveCount) ? MinumumTime : AutoSaveCount;
|
|
}
|
|
|
|
void FPackageAutoSaver::AttemptAutoSave()
|
|
{
|
|
const UEditorLoadingSavingSettings* LoadingSavingSettings = GetDefault<UEditorLoadingSavingSettings>();
|
|
FUnrealEdMisc& UnrealEdMisc = FUnrealEdMisc::Get();
|
|
|
|
// Don't auto-save if disabled or if it is not yet time to auto-save.
|
|
const bool bTimeToAutosave = (LoadingSavingSettings->bAutoSaveEnable && AutoSaveCount >= LoadingSavingSettings->AutoSaveTimeMinutes * 60.0f);
|
|
bool bAutosaveHandled = false;
|
|
|
|
if (bTimeToAutosave)
|
|
{
|
|
ClearStalePointers();
|
|
|
|
// If we don't need to perform an auto-save, then just reset the timer and bail
|
|
const bool bNeedsAutoSave = DoPackagesNeedAutoSave();
|
|
if (!bNeedsAutoSave)
|
|
{
|
|
ResetAutoSaveTimer();
|
|
return;
|
|
}
|
|
|
|
// Don't auto-save during interpolation editing, if there's another slow task
|
|
// already in progress, or while a PIE world is playing or when doing automated tests.
|
|
const bool bCanAutosave = CanAutoSave();
|
|
if (bCanAutosave)
|
|
{
|
|
FScopedSlowTask SlowTask(100.f, NSLOCTEXT("AutoSaveNotify", "PerformingAutoSave_Caption", "Auto-saving out of date packages..."));
|
|
SlowTask.MakeDialog();
|
|
|
|
bAutosaveHandled = true;
|
|
|
|
bIsAutoSaving = true;
|
|
UnrealEdMisc.SetAutosaveState(FUnrealEdMisc::EAutosaveState::Saving);
|
|
|
|
GUnrealEd->SaveConfig();
|
|
|
|
// Make sure the auto-save directory exists before attempting to write the file
|
|
const FString AutoSaveDir = AutoSaveUtils::GetAutoSaveDir();
|
|
IFileManager::Get().MakeDirectory(*AutoSaveDir, true);
|
|
|
|
// Auto-save maps and/or content packages based on user settings.
|
|
const int32 NewAutoSaveIndex = (AutoSaveIndex + 1) % 10;
|
|
|
|
bool bLevelSaved = false;
|
|
auto MapsSaveResults = EAutosaveContentPackagesResult::NothingToDo;
|
|
auto AssetsSaveResults = EAutosaveContentPackagesResult::NothingToDo;
|
|
|
|
SlowTask.EnterProgressFrame(50);
|
|
|
|
if (LoadingSavingSettings->bAutoSaveMaps)
|
|
{
|
|
MapsSaveResults = FEditorFileUtils::AutosaveMapEx(AutoSaveDir, NewAutoSaveIndex, false, DirtyMapsForAutoSave);
|
|
if (MapsSaveResults == EAutosaveContentPackagesResult::Success)
|
|
{
|
|
DirtyMapsForAutoSave.Empty();
|
|
}
|
|
}
|
|
|
|
SlowTask.EnterProgressFrame(50);
|
|
|
|
if (LoadingSavingSettings->bAutoSaveContent && UnrealEdMisc.GetAutosaveState() != FUnrealEdMisc::EAutosaveState::Cancelled)
|
|
{
|
|
AssetsSaveResults = FEditorFileUtils::AutosaveContentPackagesEx(AutoSaveDir, NewAutoSaveIndex, false, DirtyContentForAutoSave);
|
|
if (AssetsSaveResults == EAutosaveContentPackagesResult::Success)
|
|
{
|
|
DirtyContentForAutoSave.Empty();
|
|
}
|
|
}
|
|
|
|
const bool bNothingToDo = (MapsSaveResults == EAutosaveContentPackagesResult::NothingToDo && AssetsSaveResults == EAutosaveContentPackagesResult::NothingToDo);
|
|
const bool bSuccess = (MapsSaveResults != EAutosaveContentPackagesResult::Failure && AssetsSaveResults != EAutosaveContentPackagesResult::Failure && !bNothingToDo);
|
|
const bool bFailure = (MapsSaveResults == EAutosaveContentPackagesResult::Failure || AssetsSaveResults == EAutosaveContentPackagesResult::Failure);
|
|
|
|
// Auto-saved, so close any warning notifications.
|
|
CloseAutoSaveNotification(
|
|
bSuccess ? ECloseNotification::Success :
|
|
bFailure ? ECloseNotification::Failed :
|
|
bNothingToDo ? ECloseNotification::NothingToDo :
|
|
ECloseNotification::Postponed);
|
|
|
|
if (bSuccess)
|
|
{
|
|
// If a level was actually saved, update the auto-save index
|
|
AutoSaveIndex = NewAutoSaveIndex;
|
|
|
|
// Update the restore information
|
|
UpdateRestoreFile(PackageAutoSaverJson::IsRestoreEnabled());
|
|
}
|
|
|
|
ResetAutoSaveTimer();
|
|
bDelayingDueToFailedSave = false;
|
|
|
|
if (UnrealEdMisc.GetAutosaveState() == FUnrealEdMisc::EAutosaveState::Cancelled)
|
|
{
|
|
UE_LOG(PackageAutoSaver, Warning, TEXT("Autosave was cancelled."));
|
|
}
|
|
|
|
bIsAutoSaving = false;
|
|
UnrealEdMisc.SetAutosaveState(FUnrealEdMisc::EAutosaveState::Inactive);
|
|
}
|
|
else
|
|
{
|
|
bDelayingDueToFailedSave = true;
|
|
|
|
// Extend the time by 3 seconds if we failed to save because the user was interacting.
|
|
// We do this to avoid cases where they are rapidly clicking and are interrupted by autosaves
|
|
AutoSaveCount = (LoadingSavingSettings->AutoSaveTimeMinutes * 60.0f) - 3.0f;
|
|
|
|
TSharedPtr<SNotificationItem> NotificationItem = AutoSaveNotificationPtr.Pin();
|
|
|
|
// ensure the notification exists
|
|
if (NotificationItem.IsValid())
|
|
{
|
|
// update notification
|
|
NotificationItem->SetText(NSLOCTEXT("AutoSaveNotify", "WaitingToPerformAutoSave", "Waiting to perform Auto-save..."));
|
|
}
|
|
}
|
|
}
|
|
|
|
// The auto save notification must always be ticked,
|
|
// so as to correctly handle pausing and resetting.
|
|
if (!bAutosaveHandled)
|
|
{
|
|
UpdateAutoSaveNotification();
|
|
}
|
|
}
|
|
|
|
void FPackageAutoSaver::LoadRestoreFile()
|
|
{
|
|
PackagesThatCanBeRestored = PackageAutoSaverJson::LoadRestoreFile();
|
|
}
|
|
|
|
void FPackageAutoSaver::UpdateRestoreFile(const bool bRestoreEnabled)
|
|
{
|
|
PackageAutoSaverJson::SaveRestoreFile(bRestoreEnabled, DirtyPackagesForUserSave);
|
|
bNeedRestoreFileUpdate = false;
|
|
}
|
|
|
|
bool FPackageAutoSaver::HasPackagesToRestore() const
|
|
{
|
|
// Don't offer to restore packages during automation testing; the dlg is modal and blocks
|
|
return !GIsAutomationTesting && PackagesThatCanBeRestored.Num() > 0;
|
|
}
|
|
|
|
void FPackageAutoSaver::OfferToRestorePackages()
|
|
{
|
|
bool bRemoveRestoreFile = true;
|
|
|
|
if (HasPackagesToRestore() && !bAutoDeclineRecovery && !FApp::IsUnattended()) // if bAutoDeclineRecovery is true, do like the user selected to decline. (then remove the restore files)
|
|
{
|
|
// If we failed to restore, keep the restore information around
|
|
if (PackageRestore::PromptToRestorePackages(PackagesThatCanBeRestored) == FEditorFileUtils::PR_Failure)
|
|
{
|
|
bRemoveRestoreFile = false;
|
|
}
|
|
}
|
|
|
|
if (bRemoveRestoreFile)
|
|
{
|
|
// We've finished restoring, so remove this file to avoid being prompted about it again
|
|
UpdateRestoreFile(false);
|
|
}
|
|
}
|
|
|
|
void FPackageAutoSaver::OnPackagesDeleted(const TArray<UPackage*>& DeletedPackages)
|
|
{
|
|
ClearStalePointers();
|
|
|
|
for (UPackage* DeletedPackage: DeletedPackages)
|
|
{
|
|
DirtyMapsForAutoSave.Remove(DeletedPackage);
|
|
DirtyContentForAutoSave.Remove(DeletedPackage);
|
|
DirtyPackagesForUserSave.Remove(DeletedPackage);
|
|
}
|
|
bNeedRestoreFileUpdate = true;
|
|
}
|
|
|
|
void FPackageAutoSaver::OnPackageDirtyStateUpdated(UPackage* Pkg)
|
|
{
|
|
UpdateDirtyListsForPackage(Pkg);
|
|
}
|
|
|
|
void FPackageAutoSaver::OnMarkPackageDirty(UPackage* Pkg, bool bWasDirty)
|
|
{
|
|
UpdateDirtyListsForPackage(Pkg);
|
|
}
|
|
|
|
void FPackageAutoSaver::OnPackageSaved(const FString& Filename, UObject* Obj)
|
|
{
|
|
UPackage* const Pkg = Cast<UPackage>(Obj);
|
|
|
|
// If this has come from an auto-save, update the last known filename in the user dirty list so that we can offer is up as a restore file later
|
|
if (IsAutoSaving())
|
|
{
|
|
FString* const AutoSaveFilename = DirtyPackagesForUserSave.Find(Pkg);
|
|
if (AutoSaveFilename)
|
|
{
|
|
// Make the filename relative to the auto-save directory
|
|
// Note: MakePathRelativeTo modifies in-place, hence the copy of Filename
|
|
const FString AutoSaveDir = AutoSaveUtils::GetAutoSaveDir() / "";
|
|
FString RelativeFilename = Filename;
|
|
FPaths::MakePathRelativeTo(RelativeFilename, *AutoSaveDir);
|
|
|
|
(*AutoSaveFilename) = RelativeFilename;
|
|
}
|
|
}
|
|
UpdateDirtyListsForPackage(Pkg);
|
|
}
|
|
|
|
void FPackageAutoSaver::UpdateDirtyListsForPackage(UPackage* Pkg)
|
|
{
|
|
const UPackage* TransientPackage = GetTransientPackage();
|
|
|
|
// Don't auto-save the transient package or packages with the transient flag.
|
|
if (Pkg == TransientPackage || Pkg->HasAnyFlags(RF_Transient) || Pkg->HasAnyPackageFlags(PKG_InMemoryOnly))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Pkg->IsDirty())
|
|
{
|
|
// Always add the package to the user list
|
|
DirtyPackagesForUserSave.FindOrAdd(Pkg);
|
|
|
|
// Only add the package to the auto-save list if we're not auto-saving
|
|
// Note: Packages get dirtied again after they're auto-saved, so this would add them back again, which we don't want
|
|
if (!IsAutoSaving())
|
|
{
|
|
auto FindAssetInPackage = [](UPackage* InPackage)
|
|
{
|
|
UObject* Asset = nullptr;
|
|
ForEachObjectWithPackage(InPackage, [&Asset](UObject* Object)
|
|
{
|
|
if (Object->IsAsset())
|
|
{
|
|
ensure(Asset == nullptr);
|
|
Asset = Object;
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
false);
|
|
return Asset;
|
|
};
|
|
UObject* Asset = FindAssetInPackage(Pkg);
|
|
|
|
// Get the set of all reference worlds.
|
|
FWorldContext& EditorContext = GEditor->GetEditorWorldContext();
|
|
|
|
bool bPackageIsMap = false;
|
|
EditorLevelUtils::ForEachWorlds(EditorContext.World(), [&bPackageIsMap, Pkg](UWorld* World)
|
|
{
|
|
UPackage* Package = CastChecked<UPackage>(World->GetOuter());
|
|
bPackageIsMap = Package == Pkg;
|
|
return !bPackageIsMap;
|
|
},
|
|
true);
|
|
|
|
bool bForMapAutosave = Asset && Asset->GetTypedOuter<UWorld>() /** This handles external packages. */;
|
|
|
|
// Add package into the appropriate list (map or content)
|
|
if (bPackageIsMap || bForMapAutosave)
|
|
{
|
|
DirtyMapsForAutoSave.Add(Pkg);
|
|
}
|
|
else
|
|
{
|
|
DirtyContentForAutoSave.Add(Pkg);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Always remove the package from the auto-save list
|
|
DirtyMapsForAutoSave.Remove(Pkg);
|
|
DirtyContentForAutoSave.Remove(Pkg);
|
|
if (!IsAutoSaving())
|
|
{
|
|
DirtyPackagesForUserSave.Remove(Pkg);
|
|
bNeedRestoreFileUpdate = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool FPackageAutoSaver::CanAutoSave() const
|
|
{
|
|
// Don't allow auto-saving if the auto-save wouldn't save anything
|
|
const bool bPackagesNeedAutoSave = DoPackagesNeedAutoSave();
|
|
|
|
double LastInteractionTime = FSlateApplication::Get().GetLastUserInteractionTime();
|
|
|
|
const UEditorLoadingSavingSettings* LoadingSavingSettings = GetDefault<UEditorLoadingSavingSettings>();
|
|
const float InteractionDelay = float(LoadingSavingSettings->AutoSaveInteractionDelayInSeconds);
|
|
|
|
const bool bDidInteractRecently = (FApp::GetCurrentTime() - LastInteractionTime) < InteractionDelay;
|
|
const bool bAutosaveEnabled = LoadingSavingSettings->bAutoSaveEnable && bPackagesNeedAutoSave;
|
|
const bool bSlowTask = GIsSlowTask;
|
|
const bool bInterpEditMode = GLevelEditorModeTools().IsModeActive(FBuiltinEditorModes::EM_InterpEdit);
|
|
const bool bPlayWorldValid = GUnrealEd->PlayWorld != nullptr;
|
|
const bool bAnyMenusVisible = FSlateApplication::Get().AnyMenusVisible();
|
|
const bool bAutomationTesting = GIsAutomationTesting;
|
|
const bool bIsInteracting = FSlateApplication::Get().HasAnyMouseCaptor() || FSlateApplication::Get().IsDragDropping() || GUnrealEd->IsUserInteracting() || (bDidInteractRecently && !bAutoSaveNotificationLaunched && !bDelayingDueToFailedSave);
|
|
const bool bHasGameOrProjectLoaded = FApp::HasProjectName();
|
|
const bool bAreShadersCompiling = GShaderCompilingManager->IsCompiling();
|
|
const bool bIsVREditorActive = IVREditorModule::Get().IsVREditorEnabled(); // @todo vreditor: Eventually we should support this while in VR (modal VR progress, with sufficient early warning)
|
|
const bool bAreAnimationsCompressing = GAsyncCompressedAnimationsTracker ? GAsyncCompressedAnimationsTracker->GetNumRemainingJobs() > 0 : false;
|
|
|
|
bool bIsSequencerPlaying = false;
|
|
for (FLevelEditorViewportClient* LevelVC: GEditor->GetLevelViewportClients())
|
|
{
|
|
if (LevelVC && LevelVC->AllowsCinematicControl() && LevelVC->ViewState.GetReference()->GetSequencerState() == ESS_Playing)
|
|
{
|
|
bIsSequencerPlaying = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// query any active editor modes and allow them to prevent autosave
|
|
const bool bActiveModesAllowAutoSave = GLevelEditorModeTools().CanAutoSave();
|
|
|
|
return (bAutosaveEnabled && !bSlowTask && !bInterpEditMode && !bPlayWorldValid && !bAnyMenusVisible && !bAutomationTesting && !bIsInteracting && !GIsDemoMode && bHasGameOrProjectLoaded && !bAreShadersCompiling && !bAreAnimationsCompressing && !bIsVREditorActive && !bIsSequencerPlaying && bActiveModesAllowAutoSave);
|
|
}
|
|
|
|
bool FPackageAutoSaver::DoPackagesNeedAutoSave() const
|
|
{
|
|
const UEditorLoadingSavingSettings* LoadingSavingSettings = GetDefault<UEditorLoadingSavingSettings>();
|
|
|
|
const bool bHasDirtyMapsForAutoSave = DirtyMapsForAutoSave.Num() != 0;
|
|
const bool bHasDirtyContentForAutoSave = DirtyContentForAutoSave.Num() != 0;
|
|
const bool bWorldsMightBeDirty = LoadingSavingSettings->bAutoSaveMaps && bHasDirtyMapsForAutoSave;
|
|
const bool bContentPackagesMightBeDirty = LoadingSavingSettings->bAutoSaveContent && bHasDirtyContentForAutoSave;
|
|
const bool bPackagesNeedAutoSave = bWorldsMightBeDirty || bContentPackagesMightBeDirty;
|
|
|
|
return bPackagesNeedAutoSave;
|
|
}
|
|
|
|
FText FPackageAutoSaver::GetAutoSaveNotificationText(const int32 TimeInSecondsUntilAutosave)
|
|
{
|
|
// Don't switch to pending text unless auto-save really is overdue
|
|
if (!bDelayingDueToFailedSave && TimeInSecondsUntilAutosave > -1)
|
|
{
|
|
const UEditorLoadingSavingSettings* LoadingSavingSettings = GetDefault<UEditorLoadingSavingSettings>();
|
|
int32 NumPackagesToAutoSave = 0;
|
|
if (DirtyMapsForAutoSave.Num() != 0 && LoadingSavingSettings->bAutoSaveMaps)
|
|
{
|
|
NumPackagesToAutoSave += DirtyMapsForAutoSave.Num();
|
|
}
|
|
if (DirtyContentForAutoSave.Num() != 0 && LoadingSavingSettings->bAutoSaveContent)
|
|
{
|
|
NumPackagesToAutoSave += DirtyContentForAutoSave.Num();
|
|
}
|
|
|
|
// Count down the time
|
|
FFormatNamedArguments Args;
|
|
Args.Add(TEXT("TimeInSecondsUntilAutosave"), TimeInSecondsUntilAutosave);
|
|
Args.Add(TEXT("DirtyPackagesCount"), NumPackagesToAutoSave);
|
|
return (NumPackagesToAutoSave == 1) ? FText::Format(NSLOCTEXT("AutoSaveNotify", "AutoSaveIn", "Autosave in {TimeInSecondsUntilAutosave} seconds"), Args) : FText::Format(NSLOCTEXT("AutoSaveNotify", "AutoSaveXPackagesIn", "Autosave in {TimeInSecondsUntilAutosave} seconds for {DirtyPackagesCount} items"), Args);
|
|
}
|
|
|
|
// Auto-save is imminent
|
|
return NSLOCTEXT("AutoSaveNotify", "AutoSavePending", "Autosave pending");
|
|
}
|
|
|
|
int32 FPackageAutoSaver::GetTimeTillAutoSave(const bool bIgnoreCanAutoSave) const
|
|
{
|
|
int32 Result = -1;
|
|
if (bIgnoreCanAutoSave || CanAutoSave())
|
|
{
|
|
Result = FMath::CeilToInt(GetDefault<UEditorLoadingSavingSettings>()->AutoSaveTimeMinutes * 60.0f - AutoSaveCount);
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
void FPackageAutoSaver::UpdateAutoSaveNotification()
|
|
{
|
|
const UEditorLoadingSavingSettings* LoadingSavingSettings = GetDefault<UEditorLoadingSavingSettings>();
|
|
|
|
const bool bIgnoreCanAutoSave = true;
|
|
const int32 TimeInSecondsUntilAutosave = GetTimeTillAutoSave(bIgnoreCanAutoSave);
|
|
|
|
const bool UserAllowsAutosave = LoadingSavingSettings->bAutoSaveEnable && !GIsDemoMode;
|
|
const bool InGame = (GUnrealEd->PlayWorld != nullptr);
|
|
|
|
if (UserAllowsAutosave && // The user has set to allow auto-save in preferences
|
|
TimeInSecondsUntilAutosave < LoadingSavingSettings->AutoSaveWarningInSeconds &&
|
|
!InGame && // we want to hide auto-save if we are simulating/playing
|
|
!GLevelEditorModeTools().IsModeActive(FBuiltinEditorModes::EM_InterpEdit) // we want to hide auto-save if we are in matinee
|
|
)
|
|
{
|
|
if (!bAutoSaveNotificationLaunched && !bDelayingDueToFailedSave)
|
|
{
|
|
if (CanAutoSave())
|
|
{
|
|
ClearStalePointers();
|
|
|
|
// Starting a new request! Notify the UI.
|
|
if (AutoSaveNotificationPtr.IsValid())
|
|
{
|
|
AutoSaveNotificationPtr.Pin()->ExpireAndFadeout();
|
|
}
|
|
|
|
// Setup button localized strings
|
|
static FText AutoSaveCancelButtonText = NSLOCTEXT("AutoSaveNotify", "AutoSaveCancel", "Cancel");
|
|
static FText AutoSaveCancelButtonToolTipText = NSLOCTEXT("AutoSaveNotify", "AutoSaveCancelToolTip", "Postpone Autosave");
|
|
static FText AutoSaveSaveButtonText = NSLOCTEXT("AutoSaveNotify", "AutoSaveSave", "Save Now");
|
|
static FText AutoSaveSaveButtonToolTipText = NSLOCTEXT("AutoSaveNotify", "AutoSaveSaveToolTip", "Force Autosave");
|
|
|
|
FNotificationInfo Info(GetAutoSaveNotificationText(TimeInSecondsUntilAutosave));
|
|
Info.Image = FEditorStyle::GetBrush("MainFrame.AutoSaveImage");
|
|
|
|
// Add the buttons with text, tooltip and callback
|
|
Info.ButtonDetails.Add(FNotificationButtonInfo(AutoSaveCancelButtonText, AutoSaveCancelButtonToolTipText, FSimpleDelegate::CreateRaw(this, &FPackageAutoSaver::OnAutoSaveCancel)));
|
|
Info.ButtonDetails.Add(FNotificationButtonInfo(AutoSaveSaveButtonText, AutoSaveSaveButtonToolTipText, FSimpleDelegate::CreateRaw(this, &FPackageAutoSaver::OnAutoSaveSave)));
|
|
|
|
// Force the width so that any text changes don't resize the notification
|
|
Info.WidthOverride = 240.0f;
|
|
|
|
// We will be keeping track of this ourselves
|
|
Info.bFireAndForget = false;
|
|
|
|
// We want the auto-save to be subtle
|
|
Info.bUseLargeFont = false;
|
|
Info.bUseThrobber = false;
|
|
Info.bUseSuccessFailIcons = false;
|
|
|
|
// Launch notification
|
|
AutoSaveNotificationPtr = FSlateNotificationManager::Get().AddNotification(Info);
|
|
|
|
if (AutoSaveNotificationPtr.IsValid())
|
|
{
|
|
AutoSaveNotificationPtr.Pin()->SetCompletionState(SNotificationItem::CS_Pending);
|
|
}
|
|
|
|
// Update launched flag
|
|
bAutoSaveNotificationLaunched = true;
|
|
}
|
|
else // defer until the user finishes using pop-up menus or the notification will dismiss them...
|
|
{
|
|
ForceMinimumTimeTillAutoSave(LoadingSavingSettings->AutoSaveWarningInSeconds);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Update the remaining time on the notification
|
|
TSharedPtr<SNotificationItem> NotificationItem = AutoSaveNotificationPtr.Pin();
|
|
if (NotificationItem.IsValid())
|
|
{
|
|
// update text
|
|
NotificationItem->SetText(GetAutoSaveNotificationText(TimeInSecondsUntilAutosave));
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Ensures notifications are cleaned up
|
|
CloseAutoSaveNotification(false);
|
|
}
|
|
}
|
|
|
|
void FPackageAutoSaver::CloseAutoSaveNotification(const bool Success)
|
|
{
|
|
CloseAutoSaveNotification(Success ? ECloseNotification::Success : ECloseNotification::Postponed);
|
|
}
|
|
|
|
void FPackageAutoSaver::CloseAutoSaveNotification(ECloseNotification::Type Type)
|
|
{
|
|
// If a notification is open close it
|
|
if (bAutoSaveNotificationLaunched)
|
|
{
|
|
TSharedPtr<SNotificationItem> NotificationItem = AutoSaveNotificationPtr.Pin();
|
|
|
|
// ensure the notification exists
|
|
if (NotificationItem.IsValid())
|
|
{
|
|
SNotificationItem::ECompletionState CloseState;
|
|
FText CloseMessage;
|
|
|
|
// Set the test on the notification based on whether it was a successful launch
|
|
if (Type == ECloseNotification::Success)
|
|
{
|
|
CloseMessage = NSLOCTEXT("AutoSaveNotify", "AutoSaving", "Saving");
|
|
CloseState = SNotificationItem::CS_Success;
|
|
}
|
|
else if (Type == ECloseNotification::Postponed)
|
|
{
|
|
CloseMessage = NSLOCTEXT("AutoSaveNotify", "AutoSavePostponed", "Autosave postponed");
|
|
CloseState = SNotificationItem::CS_None; // Set back to none rather than failed, as this is too harsh
|
|
}
|
|
else if (Type == ECloseNotification::Failed)
|
|
{
|
|
CloseMessage = NSLOCTEXT("AutoSaveNotify", "AutoSaveFailed", "Auto-save failed. Please check the log for the details.");
|
|
CloseState = SNotificationItem::CS_Fail;
|
|
}
|
|
else
|
|
{
|
|
CloseMessage = NSLOCTEXT("AutoSaveNotify", "AutoSaveNothingToDo", "Already auto-saved.");
|
|
CloseState = SNotificationItem::CS_None;
|
|
}
|
|
|
|
// update notification
|
|
NotificationItem->SetText(CloseMessage);
|
|
NotificationItem->SetCompletionState(CloseState);
|
|
NotificationItem->ExpireAndFadeout();
|
|
|
|
// clear reference
|
|
AutoSaveNotificationPtr.Reset();
|
|
}
|
|
|
|
// Auto-save has been closed
|
|
bAutoSaveNotificationLaunched = false;
|
|
}
|
|
}
|
|
|
|
void FPackageAutoSaver::OnAutoSaveSave()
|
|
{
|
|
ForceAutoSaveTimer();
|
|
CloseAutoSaveNotification(true);
|
|
}
|
|
|
|
void FPackageAutoSaver::OnAutoSaveCancel()
|
|
{
|
|
ResetAutoSaveTimer();
|
|
CloseAutoSaveNotification(false);
|
|
}
|
|
|
|
void FPackageAutoSaver::ClearStalePointers()
|
|
{
|
|
auto DirtyPackagesForUserSaveTmp = DirtyPackagesForUserSave;
|
|
for (auto It = DirtyPackagesForUserSaveTmp.CreateConstIterator(); It; ++It)
|
|
{
|
|
const TWeakObjectPtr<UPackage>& Package = It->Key;
|
|
if (!Package.IsValid())
|
|
{
|
|
DirtyPackagesForUserSave.Remove(Package);
|
|
}
|
|
}
|
|
|
|
auto DirtyMapsForAutoSaveTmp = DirtyMapsForAutoSave;
|
|
for (auto It = DirtyMapsForAutoSaveTmp.CreateConstIterator(); It; ++It)
|
|
{
|
|
const TWeakObjectPtr<UPackage>& Package = *It;
|
|
if (!Package.IsValid())
|
|
{
|
|
DirtyMapsForAutoSave.Remove(Package);
|
|
}
|
|
}
|
|
|
|
auto DirtyContentForAutoSaveTmp = DirtyContentForAutoSave;
|
|
for (auto It = DirtyContentForAutoSaveTmp.CreateConstIterator(); It; ++It)
|
|
{
|
|
const TWeakObjectPtr<UPackage>& Package = *It;
|
|
if (!Package.IsValid())
|
|
{
|
|
DirtyContentForAutoSave.Remove(Package);
|
|
}
|
|
}
|
|
}
|
|
|
|
/************************************************************************/
|
|
/* PackageAutoSaverJson */
|
|
/************************************************************************/
|
|
|
|
FString PackageAutoSaverJson::GetRestoreFilename(const bool bEnsurePath)
|
|
{
|
|
const FString AutoSaveDir = AutoSaveUtils::GetAutoSaveDir();
|
|
if (bEnsurePath)
|
|
{
|
|
// Make sure the auto-save directory exists before attempting to write the file
|
|
IFileManager::Get().MakeDirectory(*AutoSaveDir, true);
|
|
}
|
|
|
|
const FString Filename = AutoSaveDir / RestoreFilename;
|
|
return Filename;
|
|
}
|
|
|
|
TMap<FString, FString> PackageAutoSaverJson::LoadRestoreFile()
|
|
{
|
|
TMap<FString, FString> PackagesThatCanBeRestored;
|
|
|
|
const FString Filename = GetRestoreFilename(false);
|
|
FArchive* const FileAr = IFileManager::Get().CreateFileReader(*Filename);
|
|
if (!FileAr)
|
|
{
|
|
// File doesn't exist; nothing to restore
|
|
return PackagesThatCanBeRestored;
|
|
}
|
|
|
|
bool bJsonLoaded = false;
|
|
TSharedPtr<FJsonObject> RootObject = MakeShareable(new FJsonObject);
|
|
{
|
|
TSharedRef<FJsonReader> Reader = FJsonReaderFactory::Create(FileAr);
|
|
bJsonLoaded = FJsonSerializer::Deserialize(Reader, RootObject);
|
|
FileAr->Close();
|
|
}
|
|
|
|
if (!bJsonLoaded || !RootObject->GetBoolField(TagRestoreEnabled))
|
|
{
|
|
// File failed to load, or the restore is disabled; nothing to restore
|
|
return PackagesThatCanBeRestored;
|
|
}
|
|
|
|
TArray<TSharedPtr<FJsonValue>> PackagesThatCanBeRestoredArray = RootObject->GetArrayField(TagPackages);
|
|
for (auto It = PackagesThatCanBeRestoredArray.CreateConstIterator(); It; ++It)
|
|
{
|
|
TSharedPtr<FJsonObject> EntryObject = (*It)->AsObject();
|
|
|
|
const FString PackagePathName = EntryObject->GetStringField(TagPackagePathName);
|
|
const FString AutoSavePath = EntryObject->GetStringField(TagAutoSavePath);
|
|
PackagesThatCanBeRestored.Add(PackagePathName, AutoSavePath);
|
|
}
|
|
|
|
return PackagesThatCanBeRestored;
|
|
}
|
|
|
|
void PackageAutoSaverJson::SaveRestoreFile(const bool bRestoreEnabled, const TMap<TWeakObjectPtr<UPackage>, FString>& DirtyPackages)
|
|
{
|
|
TSharedPtr<FJsonObject> RootObject = MakeShareable(new FJsonObject);
|
|
|
|
RootObject->SetBoolField(TagRestoreEnabled, bRestoreEnabled);
|
|
|
|
TArray<TSharedPtr<FJsonValue>> PackagesThatCanBeRestored;
|
|
|
|
// Only bother populating the list of packages if the restore is enabled
|
|
if (bRestoreEnabled)
|
|
{
|
|
PackagesThatCanBeRestored.Reserve(DirtyPackages.Num());
|
|
|
|
// Build up the array of package names with auto-saves that can be restored
|
|
for (auto It = DirtyPackages.CreateConstIterator(); It; ++It)
|
|
{
|
|
const TWeakObjectPtr<UPackage>& Package = It.Key();
|
|
const FString& AutoSavePath = It.Value();
|
|
|
|
UPackage* const PackagePtr = Package.Get();
|
|
if (PackagePtr && !AutoSavePath.IsEmpty())
|
|
{
|
|
const FString& PackagePathName = PackagePtr->GetPathName();
|
|
|
|
TSharedPtr<FJsonObject> EntryObject = MakeShareable(new FJsonObject);
|
|
EntryObject->SetStringField(TagPackagePathName, PackagePathName);
|
|
EntryObject->SetStringField(TagAutoSavePath, AutoSavePath);
|
|
|
|
TSharedPtr<FJsonValue> EntryValue = MakeShareable(new FJsonValueObject(EntryObject));
|
|
PackagesThatCanBeRestored.Add(EntryValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
RootObject->SetArrayField(TagPackages, PackagesThatCanBeRestored);
|
|
|
|
const FString Filename = GetRestoreFilename(true);
|
|
FArchive* const FileAr = IFileManager::Get().CreateFileWriter(*Filename, FILEWRITE_EvenIfReadOnly);
|
|
if (FileAr)
|
|
{
|
|
TSharedRef<FStringWriter> Writer = FStringWriterFactory::Create(FileAr);
|
|
FJsonSerializer::Serialize(RootObject.ToSharedRef(), Writer);
|
|
FileAr->Close();
|
|
}
|
|
}
|