// Copyright Epic Games, Inc. All Rights Reserved. #include "AutoReimport/AutoReimportManager.h" #include "HAL/PlatformFilemanager.h" #include "HAL/FileManager.h" #include "Misc/Paths.h" #include "Misc/WildcardString.h" #include "Modules/ModuleManager.h" #include "UObject/Class.h" #include "UObject/UObjectHash.h" #include "UObject/UObjectIterator.h" #include "Misc/PackageName.h" #include "UObject/GCObject.h" #include "Styling/SlateTypes.h" #include "EditorReimportHandler.h" #include "Misc/Attribute.h" #include "TickableEditorObject.h" #include "Settings/EditorLoadingSavingSettings.h" #include "Factories/Factory.h" #include "EditorFramework/AssetImportData.h" #include "AssetData.h" #include "Editor.h" #include "FileHelpers.h" #include "AutoReimport/AutoReimportUtilities.h" #include "Logging/MessageLog.h" #include "AutoReimport/ContentDirectoryMonitor.h" #include "PackageTools.h" #include "ObjectTools.h" #include "AssetRegistryModule.h" #include "Framework/Notifications/NotificationManager.h" #include "Widgets/Notifications/SNotificationList.h" #include "AutoReimport/ReimportFeedbackContext.h" #include "MessageLogModule.h" #include "AutoReimport/AssetSourceFilenameCache.h" #define LOCTEXT_NAMESPACE "AutoReimportManager" enum class EStateMachineNode { CallOnce, CallMany }; /** Enum and value to specify the current state of our processing */ enum class ECurrentState : uint8 { Idle, Paused, Aborting, PromptUser, Initializing, ProcessAdditions, ProcessModifications, ProcessDeletions, SavePackages }; /** A simple state machine class that calls functions mapped on enum values. If any function returns a new enum type, it moves onto that function */ struct FStateMachine { typedef TFunction(const DirectoryWatcher::FTimeLimit&)> FunctionType; /** Constructor that specifies the initial state of the machine */ FStateMachine(ECurrentState InitialState): CurrentState(InitialState) {} /** Add an enum->function mapping for this state machine */ void Add(ECurrentState State, EStateMachineNode NodeType, FunctionType&& Function) { Nodes.Add(State, FStateMachineNode(NodeType, MoveTemp(Function))); } /** Set a new state for this machine */ void SetState(ECurrentState NewState) { CurrentState = NewState; } /** Tick this state machine with the given time limit. Will continuously enumerate the machine until TimeLimit is reached */ void Tick(const DirectoryWatcher::FTimeLimit& TimeLimit) { while (!TimeLimit.Exceeded()) { const FStateMachineNode& State = Nodes[CurrentState]; TOptional NewState = State.Endpoint(TimeLimit); if (NewState.IsSet()) { CurrentState = NewState.GetValue(); } else if (State.Type == EStateMachineNode::CallOnce) { break; } } } private: /** The current state of this machine */ ECurrentState CurrentState; private: struct FStateMachineNode { FStateMachineNode(EStateMachineNode InType, FunctionType&& InEndpoint): Endpoint(MoveTemp(InEndpoint)), Type(InType) {} /** The function endpoint for this node */ FunctionType Endpoint; /** Whether this endpoint should be called multiple times in a frame, or just once */ EStateMachineNode Type; }; /** A map of enum value -> callback information */ TMap Nodes; }; /* Deals with auto reimporting of objects when the objects file on disk is modified*/ class FAutoReimportManager: public FTickableEditorObject , public FGCObject , public TSharedFromThis { public: FAutoReimportManager(); ~FAutoReimportManager(); /** Get a list of currently monitored directories */ TArray GetMonitoredDirectories() const; /** Report an external change to the manager, such that a subsequent equal change reported by the os be ignored */ void IgnoreNewFile(const FString& Filename); void IgnoreFileModification(const FString& Filename); void IgnoreMovedFile(const FString& SrcFilename, const FString& DstFilename); void IgnoreDeletedFile(const FString& Filename); /** Destroy this manager */ void Destroy(); private: /** FTickableEditorObject interface*/ virtual void Tick(float DeltaTime) override; virtual ETickableTickType GetTickableTickType() const override { return ETickableTickType::Conditional; } /** Automation testing will tick its own file cache which can result in race conditions with the reimport manager. */ virtual bool IsTickable() const override { return !GIsAutomationTesting; } virtual TStatId GetStatId() const override; /** FGCObject interface*/ virtual void AddReferencedObjects(FReferenceCollector& Collector) override; private: /** Called when a new asset path has been mounted or unmounted */ void OnContentPathChanged(const FString& InAssetPath, const FString& FileSystemPath); /** Called when an asset has been renamed */ void OnAssetRenamed(const FAssetData& AssetData, const FString& OldPath); private: /** Callback for when an editor user setting has changed */ void HandleLoadingSavingSettingChanged(FName PropertyName); /** Set up monitors to all the monitored content directories */ void SetUpDirectoryMonitors(); /** Retrieve a semi-colon separated string of file extensions supported by all available editor import factories */ static FString GetAllFactoryExtensions(); private: /** Get the number of unprocessed changes that are not part of the current processing operation */ int32 GetNumUnprocessedChanges() const; /** Populate the message log with a list of pending changes to files */ void PopupateMessageLogWithPendingChanges(FMessageLog& MessageLog) const; private: /** A state machine holding information about the current state of the manager */ FStateMachine StateMachine; private: /** Idle processing */ TOptional Idle(); /** Prompt the user whether they would like to import the changes */ TOptional PromptUser(); /** Set up the initial work for the import operation */ TOptional InitializeOperation(); /** Process any remaining pending additions we have */ TOptional ProcessAdditions(const DirectoryWatcher::FTimeLimit& TimeLimit); /** Save any packages that were created inside ProcessAdditions */ TOptional SavePackages(); /** Process any remaining pending modifications we have */ TOptional ProcessModifications(const DirectoryWatcher::FTimeLimit& TimeLimit); /** Process any remaining pending deletions we have */ TOptional ProcessDeletions(); /** Wait for a user's input. Just updates the progress text for now */ TOptional Paused(); /** Abort the process */ TOptional Abort(); /** Cleanup an operation that just processed some changes */ void Cleanup(); /** Check whether we should pause the operation or not */ TOptional HandlePauseAbort(ECurrentState InCurrentState); /** Get the text to display on the confirmation notification */ FText GetConfirmNotificationText() const; private: /** Feedback context that can selectively override the global context to consume progress events for saving of assets */ TSharedPtr FeedbackContextOverride; /** Array of objects that detect changes to directories */ TArray> DirectoryMonitors; /** A list of packages to save when we've added a bunch of assets */ TArray PackagesToSave; /** Reentracy guard for when we are making changes to assets */ bool bGuardAssetChanges; /** A timeout used to refresh directory monitors when the user has made an interactive change to the settings */ DirectoryWatcher::FTimeLimit ResetMonitorsTimeout; /** User confirmation popup */ TSharedPtr ConfirmNotification; /** The paused state of the state machine */ ECurrentState PausedState; private: void OnPauseClicked() { switch (State) { case EProcessState::Paused: State = EProcessState::Running; break; case EProcessState::Running: State = EProcessState::Paused; break; default: break; } } void OnAbortClicked() { State = EProcessState::Aborted; } /** Flags for paused/aborted */ enum class EProcessState { Running, Paused, Aborted }; EProcessState State; }; FAutoReimportManager::FAutoReimportManager() : StateMachine(ECurrentState::Idle), bGuardAssetChanges(false), PausedState(ECurrentState::Idle) { UEditorLoadingSavingSettings* Settings = GetMutableDefault(); Settings->OnSettingChanged().AddRaw(this, &FAutoReimportManager::HandleLoadingSavingSettingChanged); FPackageName::OnContentPathMounted().AddRaw(this, &FAutoReimportManager::OnContentPathChanged); FPackageName::OnContentPathDismounted().AddRaw(this, &FAutoReimportManager::OnContentPathChanged); FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked("MessageLog"); if (!MessageLogModule.IsRegisteredLogListing("AssetReimport")) { MessageLogModule.RegisterLogListing("AssetReimport", LOCTEXT("AssetReimportLabel", "Asset Reimport")); } FAssetSourceFilenameCache::Get().OnAssetRenamed().AddRaw(this, &FAutoReimportManager::OnAssetRenamed); // Only set this up for content directories if the user has this enabled if (Settings->bMonitorContentDirectories) { SetUpDirectoryMonitors(); } State = EProcessState::Running; StateMachine.Add(ECurrentState::Idle, EStateMachineNode::CallOnce, [this](const DirectoryWatcher::FTimeLimit& T) { return this->Idle(); }); StateMachine.Add(ECurrentState::Paused, EStateMachineNode::CallOnce, [this](const DirectoryWatcher::FTimeLimit& T) { return this->Paused(); }); StateMachine.Add(ECurrentState::Aborting, EStateMachineNode::CallOnce, [this](const DirectoryWatcher::FTimeLimit& T) { return this->Abort(); }); StateMachine.Add(ECurrentState::PromptUser, EStateMachineNode::CallOnce, [this](const DirectoryWatcher::FTimeLimit& T) { return this->PromptUser(); }); StateMachine.Add(ECurrentState::Initializing, EStateMachineNode::CallOnce, [this](const DirectoryWatcher::FTimeLimit& T) { return this->InitializeOperation(); }); StateMachine.Add(ECurrentState::ProcessAdditions, EStateMachineNode::CallMany, [this](const DirectoryWatcher::FTimeLimit& T) { return this->ProcessAdditions(T); }); StateMachine.Add(ECurrentState::ProcessModifications, EStateMachineNode::CallMany, [this](const DirectoryWatcher::FTimeLimit& T) { return this->ProcessModifications(T); }); StateMachine.Add(ECurrentState::ProcessDeletions, EStateMachineNode::CallMany, [this](const DirectoryWatcher::FTimeLimit& T) { return this->ProcessDeletions(); }); StateMachine.Add(ECurrentState::SavePackages, EStateMachineNode::CallOnce, [this](const DirectoryWatcher::FTimeLimit& T) { return this->SavePackages(); }); } FAutoReimportManager::~FAutoReimportManager() { if (ConfirmNotification.IsValid()) { ConfirmNotification->SetText(FText()); } } TArray FAutoReimportManager::GetMonitoredDirectories() const { TArray Dirs; for (const TUniquePtr& Monitor: DirectoryMonitors) { Dirs.Emplace(Monitor->GetDirectory(), Monitor->GetMountPoint()); } return Dirs; } void FAutoReimportManager::IgnoreNewFile(const FString& Filename) { for (const TUniquePtr& Monitor: DirectoryMonitors) { if (Filename.StartsWith(Monitor->GetDirectory())) { Monitor->IgnoreNewFile(Filename); } } } void FAutoReimportManager::IgnoreFileModification(const FString& Filename) { for (const TUniquePtr& Monitor: DirectoryMonitors) { if (Filename.StartsWith(Monitor->GetDirectory())) { Monitor->IgnoreFileModification(Filename); } } } void FAutoReimportManager::IgnoreMovedFile(const FString& SrcFilename, const FString& DstFilename) { for (const TUniquePtr& Monitor: DirectoryMonitors) { const bool bSrcInFolder = SrcFilename.StartsWith(Monitor->GetDirectory()); const bool bDstInFolder = DstFilename.StartsWith(Monitor->GetDirectory()); if (bSrcInFolder && bDstInFolder) { Monitor->IgnoreMovedFile(SrcFilename, DstFilename); } else if (bSrcInFolder) { Monitor->IgnoreDeletedFile(SrcFilename); } else if (bDstInFolder) { Monitor->IgnoreNewFile(DstFilename); } } } void FAutoReimportManager::IgnoreDeletedFile(const FString& Filename) { for (const TUniquePtr& Monitor: DirectoryMonitors) { if (Filename.StartsWith(Monitor->GetDirectory())) { Monitor->IgnoreDeletedFile(Filename); } } } void FAutoReimportManager::Destroy() { FAssetRegistryModule* AssetRegistryModule = FModuleManager::GetModulePtr("AssetRegistry"); if (AssetRegistryModule) { FAssetSourceFilenameCache::Get().OnAssetRenamed().RemoveAll(this); AssetRegistryModule->Get().OnInMemoryAssetDeleted().RemoveAll(this); } if (UEditorLoadingSavingSettings* Settings = GetMutableDefault()) { Settings->OnSettingChanged().RemoveAll(this); } FPackageName::OnContentPathMounted().RemoveAll(this); FPackageName::OnContentPathDismounted().RemoveAll(this); // Force a save of all the caches DirectoryMonitors.Empty(); } void FAutoReimportManager::OnAssetRenamed(const FAssetData& AssetData, const FString& OldPath) { if (bGuardAssetChanges) { return; } // This code moves a source content file that reside alongside assets when the assets are renamed. We do this under the following conditions: // 1. The sourcefile is solely referenced from the the asset that has been moved // 2. Said asset only references a single file // // Additionally, we rename the source file if it matched the name of the asset before the rename/move. // - If we rename the source file, then we also update the reimport paths for the asset TOptional ImportInfo = FAssetSourceFilenameCache::ExtractAssetImportInfo(AssetData); if (!ImportInfo.IsSet() || ImportInfo->SourceFiles.Num() != 1) { return; } const FString& RelativeFilename = ImportInfo->SourceFiles[0].RelativeFilename; FString OldPackagePath = FPackageName::GetLongPackagePath(OldPath) / TEXT(""); FString NewReimportPath; // We move the file with the asset provided it is the only file referenced, and sits right beside the uasset file if (!RelativeFilename.GetCharArray().ContainsByPredicate([](const TCHAR Char) { return Char == '/' || Char == '\\'; })) { // File resides in the same folder as the asset, so we can potentially rename the source file too const FString AbsoluteSrcPath = FPaths::ConvertRelativePathToFull(FPackageName::LongPackageNameToFilename(OldPackagePath)); const FString AbsoluteDstPath = FPaths::ConvertRelativePathToFull(FPackageName::LongPackageNameToFilename(AssetData.PackagePath.ToString() / TEXT(""))); const FString OldAssetName = FPackageName::GetLongPackageAssetName(FPackageName::ObjectPathToPackageName(OldPath)); FString NewFileName = FPaths::GetBaseFilename(RelativeFilename); bool bRequireReimportPathUpdate = false; if (UPackageTools::SanitizePackageName(NewFileName) == OldAssetName) { NewFileName = AssetData.AssetName.ToString(); bRequireReimportPathUpdate = true; } const FString SrcFile = AbsoluteSrcPath / RelativeFilename; const FString DstFile = AbsoluteDstPath / NewFileName + TEXT(".") + FPaths::GetExtension(RelativeFilename); // We can't do this if multiple assets reference the same file. We should be checking for > 1 referencing asset, but the asset registry // filter lookup won't return the recently renamed package because it will be Empty by now, so we check for *anything* referencing the asset (assuming that we'll never find *this* asset). const IAssetRegistry& Registry = FModuleManager::LoadModuleChecked("AssetRegistry").Get(); if (Utils::FindAssetsPertainingToFile(Registry, SrcFile).Num() == 0) { if (!FPlatformFileManager::Get().GetPlatformFile().FileExists(*DstFile) && IFileManager::Get().Move(*DstFile, *SrcFile, false /*bReplace */, false, true /* attributes */, true /* don't retry */)) { IgnoreMovedFile(SrcFile, DstFile); if (bRequireReimportPathUpdate) { NewReimportPath = DstFile; } } } } if (NewReimportPath.IsEmpty() && FPackageName::GetLongPackagePath(OldPath) != AssetData.PackagePath.ToString()) { // The asset has been moved, try and update its referenced path FString OldSourceFilePath = FPaths::ConvertRelativePathToFull(FPackageName::LongPackageNameToFilename(OldPackagePath), RelativeFilename); if (FPaths::FileExists(OldSourceFilePath)) { NewReimportPath = MoveTemp(OldSourceFilePath); } } if (!NewReimportPath.IsEmpty()) { TArray Paths; Paths.Add(NewReimportPath); // Update the reimport file names FReimportManager::Instance()->UpdateReimportPaths(AssetData.GetAsset(), Paths); } } int32 FAutoReimportManager::GetNumUnprocessedChanges() const { return Utils::Reduce(DirectoryMonitors, [](const TUniquePtr& Monitor, int32 Total) { return Total + Monitor->GetNumUnprocessedChanges(); }, 0); } void FAutoReimportManager::PopupateMessageLogWithPendingChanges(FMessageLog& MessageLog) const { for (const TUniquePtr& Monitor: DirectoryMonitors) { const FString& BasePath = Monitor->GetDirectory(); Monitor->IterateUnprocessedChanges([&MessageLog, &BasePath](const DirectoryWatcher::FUpdateCacheTransaction& Transaction, const FDateTime& TimeOfChange) { FText ThisMessage; FString FullFilename = BasePath / Transaction.Filename.Get(); switch (Transaction.Action) { case DirectoryWatcher::EFileAction::Added: ThisMessage = FText::Format(LOCTEXT("PendingChange_Add", "'{0}' has been created."), FText::FromString(FullFilename)); break; case DirectoryWatcher::EFileAction::Removed: ThisMessage = FText::Format(LOCTEXT("PendingChange_Delete", "'{0}' has been deleted."), FText::FromString(FullFilename)); break; case DirectoryWatcher::EFileAction::Modified: ThisMessage = FText::Format(LOCTEXT("PendingChange_Modified", "'{0}' has been modified."), FText::FromString(FullFilename)); break; case DirectoryWatcher::EFileAction::Moved: ThisMessage = FText::Format(LOCTEXT("PendingChange_Moved", "'{0}' has been moved/renamed to {1}."), FText::FromString(BasePath / Transaction.MovedFromFilename.Get()), FText::FromString(FullFilename)); break; } MessageLog.Message(EMessageSeverity::Info, ThisMessage); return true; }); } } FText FAutoReimportManager::GetConfirmNotificationText() const { const int32 TotalWork = GetNumUnprocessedChanges(); if (TotalWork == 1) { return LOCTEXT("UserConfirmationTextSingle", "A change to a source content file has been detected.\nWould you like to import it?"); } else if (TotalWork > 1) { return FText::Format(LOCTEXT("UserConfirmationTextMultiple", "{0} changes to source content files have been detected.\nWould you like to import them?"), FText::AsNumber(TotalWork)); } return FText(); } TOptional FAutoReimportManager::PromptUser() { UEditorLoadingSavingSettings* Settings = GetMutableDefault(); // Send out a notification asking for confirmation if (Settings->bPromptBeforeAutoImporting && !ConfirmNotification.IsValid()) { FNotificationInfo Info(FText::GetEmpty()); Info.bFireAndForget = false; Info.bUseLargeFont = false; Info.ButtonDetails.Add(FNotificationButtonInfo( LOCTEXT("ImportButtonText", "Import"), FText(), FSimpleDelegate::CreateLambda([this, Settings] { Settings->PostEditChange(); State = EProcessState::Running; }), SNotificationItem::CS_None)); Info.ButtonDetails.Add(FNotificationButtonInfo( LOCTEXT("DontImportButtonText", "Don't Import"), FText(), FSimpleDelegate::CreateLambda([this, Settings] { // going back into idle PausedState = ECurrentState::Aborting; State = EProcessState::Running; if (!Settings->bPromptBeforeAutoImporting) { // User clicked Don't import, with a don't show again. Disable auto reimport Settings->bMonitorContentDirectories = false; Settings->PostEditChange(); } }), SNotificationItem::CS_None)); Info.CheckBoxText = LOCTEXT("DontAskAgain", "Don't ask again"); Info.CheckBoxState = TAttribute::Create(TAttribute::FGetter::CreateStatic([] { return GetDefault()->bPromptBeforeAutoImporting ? ECheckBoxState::Unchecked : ECheckBoxState::Checked; })); Info.CheckBoxStateChanged = FOnCheckStateChanged::CreateStatic([](ECheckBoxState NewState) { GetMutableDefault()->bPromptBeforeAutoImporting = NewState == ECheckBoxState::Unchecked; }); Info.Hyperlink = FSimpleDelegate::CreateLambda([this] { FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked("MessageLog"); FMessageLog MessageLog("AssetReimport"); MessageLog.NewPage(FText::Format(LOCTEXT("WhatChangedMessageLogPageLabel", "Detailed File System Changes from {0}"), FText::AsTime(FDateTime::UtcNow()))); PopupateMessageLogWithPendingChanges(MessageLog); MessageLogModule.OpenMessageLog("AssetReimport"); }); Info.HyperlinkText = LOCTEXT("UserConfirmationHyperlink", "What Changed?"); ConfirmNotification = FSlateNotificationManager::Get().AddNotification(Info); State = EProcessState::Paused; } // Keep ticking the monitors for (const TUniquePtr& Monitor: DirectoryMonitors) { Monitor->Tick(); } if (ConfirmNotification.IsValid()) { ConfirmNotification->SetText(GetConfirmNotificationText()); } // Keep ticking this if we're paused if (State == EProcessState::Paused) { return TOptional(); } if (ConfirmNotification.IsValid()) { ConfirmNotification->SetEnabled(false); ConfirmNotification->SetCompletionState(SNotificationItem::CS_Success); ConfirmNotification->ExpireAndFadeout(); ConfirmNotification = nullptr; } if (PausedState != ECurrentState::PromptUser) { return PausedState; } PausedState = ECurrentState::Idle; // This can get set by the user while we're prompting for action if (!Settings->bMonitorContentDirectories) { return ECurrentState::Aborting; } else { return ECurrentState::Initializing; } } TOptional FAutoReimportManager::InitializeOperation() { int32 TotalWork = 0; for (const TUniquePtr& Monitor: DirectoryMonitors) { TotalWork += Monitor->StartProcessing(); } if (TotalWork > 0) { if (!FeedbackContextOverride.IsValid()) { // Create a new feedback context override FeedbackContextOverride = MakeShareable(new FReimportFeedbackContext( FSimpleDelegate::CreateSP(this, &FAutoReimportManager::OnPauseClicked), FSimpleDelegate::CreateSP(this, &FAutoReimportManager::OnAbortClicked))); } FeedbackContextOverride->Show(TotalWork); return ECurrentState::ProcessAdditions; } return ECurrentState::Idle; } TOptional FAutoReimportManager::ProcessAdditions(const DirectoryWatcher::FTimeLimit& TimeLimit) { TOptional NewState = HandlePauseAbort(ECurrentState::ProcessAdditions); if (NewState.IsSet()) { return NewState; } // Override the global feedback context while we do this to avoid popping up dialogs TGuardValue ScopedContextOverride(GWarn, FeedbackContextOverride.Get()); TGuardValue ScopedAssetChangesGuard(bGuardAssetChanges, true); TMap> Factories; TArray FactoryExtensions; FactoryExtensions.Reserve(16); // Get the list of valid factories for (TObjectIterator It; It; ++It) { UClass* CurrentClass = (*It); if (CurrentClass->IsChildOf(UFactory::StaticClass()) && !(CurrentClass->HasAnyClassFlags(CLASS_Abstract))) { UFactory* Factory = Cast(CurrentClass->GetDefaultObject()); if (Factory->bEditorImport && Factory->ImportPriority >= 0) { FactoryExtensions.Reset(); Factory->GetSupportedFileExtensions(FactoryExtensions); for (const FString& Ext: FactoryExtensions) { Factories.FindOrAdd(Ext).Add(Factory); } } } } for (TTuple>& Pair: Factories) { Pair.Value.Sort([](const UFactory& A, const UFactory& B) { return A.ImportPriority > B.ImportPriority; }); } for (const TUniquePtr& Monitor: DirectoryMonitors) { Monitor->ProcessAdditions(TimeLimit, PackagesToSave, Factories, *FeedbackContextOverride); if (TimeLimit.Exceeded()) { return TOptional(); } } return ECurrentState::ProcessModifications; } TOptional FAutoReimportManager::ProcessModifications(const DirectoryWatcher::FTimeLimit& TimeLimit) { TOptional NewState = HandlePauseAbort(ECurrentState::ProcessModifications); if (NewState.IsSet()) { return NewState; } // Override the global feedback context while we do this to avoid popping up dialogs TGuardValue ScopedContextOverride(GWarn, FeedbackContextOverride.Get()); TGuardValue ScopedAssetChangesGuard(bGuardAssetChanges, true); for (const TUniquePtr& Monitor: DirectoryMonitors) { Monitor->ProcessModifications(TimeLimit, PackagesToSave, *FeedbackContextOverride); if (TimeLimit.Exceeded()) { return TOptional(); } } return ECurrentState::ProcessDeletions; } TOptional FAutoReimportManager::ProcessDeletions() { TOptional NewState = HandlePauseAbort(ECurrentState::ProcessDeletions); if (NewState.IsSet()) { return NewState; } TGuardValue ScopedAssetChangesGuard(bGuardAssetChanges, true); TArray AssetsToDelete; int32 TotalWork = 0; for (const TUniquePtr& Monitor: DirectoryMonitors) { TotalWork += Monitor->GetDeletedFilesNum(); Monitor->ExtractAssetsToDelete(AssetsToDelete); } FeedbackContextOverride->MainTask->EnterProgressFrame(TotalWork); if (AssetsToDelete.Num() > 0) { for (const FAssetData& AssetData: AssetsToDelete) { FeedbackContextOverride->AddMessage(EMessageSeverity::Info, FText::Format(LOCTEXT("Success_DeletedAsset", "Attempting to delete {0} (its source file has been removed)."), FText::FromName(AssetData.AssetName))); } ObjectTools::DeleteAssets(AssetsToDelete); } return ECurrentState::SavePackages; } TOptional FAutoReimportManager::SavePackages() { // We don't override the context specifically when saving packages so the user gets proper feedback // TGuardValue ScopedContextOverride(GWarn, FeedbackContextOverride.Get()); TGuardValue ScopedAssetChangesGuard(bGuardAssetChanges, true); if (PackagesToSave.Num() > 0) { const bool bAlreadyCheckedOut = false; const bool bCheckDirty = false; const bool bPromptToSave = false; FEditorFileUtils::PromptForCheckoutAndSave(PackagesToSave, bCheckDirty, bPromptToSave, nullptr, bAlreadyCheckedOut); PackagesToSave.Empty(); } Cleanup(); return ECurrentState::Idle; } TOptional FAutoReimportManager::HandlePauseAbort(ECurrentState InCurrentState) { if (State == EProcessState::Aborted) { return ECurrentState::Aborting; } else if (State == EProcessState::Paused) { PausedState = InCurrentState; return ECurrentState::Paused; } return TOptional(); } TOptional FAutoReimportManager::Paused() { TOptional NewState = HandlePauseAbort(PausedState); if (NewState.IsSet()) { return NewState.GetValue(); } // No longer paused return PausedState; } TOptional FAutoReimportManager::Abort() { for (const TUniquePtr& Monitor: DirectoryMonitors) { Monitor->Abort(); } PackagesToSave.Empty(); Cleanup(); return ECurrentState::Idle; } TOptional FAutoReimportManager::Idle() { // Check whether we need to reset the monitors or not if (ResetMonitorsTimeout.Exceeded()) { const UEditorLoadingSavingSettings* Settings = GetDefault(); if (Settings->bMonitorContentDirectories) { DirectoryMonitors.Empty(); SetUpDirectoryMonitors(); } else { // Destroy all the existing monitors, including their file caches for (const TUniquePtr& Monitor: DirectoryMonitors) { Monitor->Destroy(); } DirectoryMonitors.Empty(); } ResetMonitorsTimeout = DirectoryWatcher::FTimeLimit(); return TOptional(); } for (const TUniquePtr& Monitor: DirectoryMonitors) { Monitor->Tick(); } const IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked("AssetRegistry").Get(); if (AssetRegistry.IsLoadingAssets()) { return TOptional(); } if (GetNumUnprocessedChanges() > 0) { PausedState = ECurrentState::PromptUser; return ECurrentState::PromptUser; } return TOptional(); } void FAutoReimportManager::Cleanup() { if (FeedbackContextOverride.IsValid()) { FeedbackContextOverride->Hide(); } } void FAutoReimportManager::Tick(float DeltaTime) { // Never spend more than a 60fps frame doing this work (meaning we shouldn't drop below 30fps), we can do more if we're throttling CPU usage (ie editor running in background) const DirectoryWatcher::FTimeLimit TimeLimit(GEditor->ShouldThrottleCPUUsage() ? 1 / 6.f : 1.f / 60.f); StateMachine.Tick(TimeLimit); } TStatId FAutoReimportManager::GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(FAutoReimportManager, STATGROUP_Tickables); } void FAutoReimportManager::AddReferencedObjects(FReferenceCollector& Collector) { Collector.AddReferencedObjects(PackagesToSave); } void FAutoReimportManager::HandleLoadingSavingSettingChanged(FName PropertyName) { if (PropertyName == GET_MEMBER_NAME_CHECKED(UEditorLoadingSavingSettings, bMonitorContentDirectories) || PropertyName == GET_MEMBER_NAME_CHECKED(UEditorLoadingSavingSettings, AutoReimportDirectorySettings)) { ResetMonitorsTimeout = DirectoryWatcher::FTimeLimit(5.f); } } void FAutoReimportManager::OnContentPathChanged(const FString& InAssetPath, const FString& FileSystemPath) { const UEditorLoadingSavingSettings* Settings = GetDefault(); if (Settings->bMonitorContentDirectories) { DirectoryMonitors.Empty(); SetUpDirectoryMonitors(); } } void FAutoReimportManager::SetUpDirectoryMonitors() { struct FParsedSettings { FString SourceDirectory; FString MountPoint; DirectoryWatcher::FMatchRules Rules; }; TArray FinalArray; FString SupportedExtensions = GetAllFactoryExtensions(); for (const FAutoReimportDirectoryConfig& Setting: GetDefault()->AutoReimportDirectorySettings) { FParsedSettings NewMapping; NewMapping.SourceDirectory = Setting.SourceDirectory; NewMapping.MountPoint = Setting.MountPoint; if (!FAutoReimportDirectoryConfig::ParseSourceDirectoryAndMountPoint(NewMapping.SourceDirectory, NewMapping.MountPoint)) { continue; } // Only include extensions that match a factory NewMapping.Rules.SetApplicableExtensions(SupportedExtensions); for (const FAutoReimportWildcard& WildcardConfig: Setting.Wildcards) { NewMapping.Rules.AddWildcardRule(WildcardConfig.Wildcard, WildcardConfig.bInclude); } FinalArray.Add(NewMapping); } for (int32 Index = 0; Index < FinalArray.Num(); ++Index) { const FParsedSettings& Mapping = FinalArray[Index]; // We only create a directory monitor if there are no other's watching parent directories of this one for (int32 OtherIndex = Index + 1; OtherIndex < FinalArray.Num(); ++OtherIndex) { if (FinalArray[Index].SourceDirectory.StartsWith(FinalArray[OtherIndex].SourceDirectory)) { UE_LOG(LogAutoReimportManager, Warning, TEXT("Unable to watch directory %s as it will conflict with another watching %s."), *FinalArray[Index].SourceDirectory, *FinalArray[OtherIndex].SourceDirectory); goto next; } } DirectoryMonitors.Emplace(MakeUnique(Mapping.SourceDirectory, Mapping.Rules, Mapping.MountPoint)); next: continue; } } FString FAutoReimportManager::GetAllFactoryExtensions() { FString AllExtensions; // Use a scratch buffer to avoid unnecessary re-allocation FString Scratch; Scratch.Reserve(32); for (TObjectIterator ClassIt; ClassIt; ++ClassIt) { UClass* Class = *ClassIt; if (Class->IsChildOf(UFactory::StaticClass()) && !Class->HasAnyClassFlags(CLASS_Abstract)) { UFactory* Factory = Cast(Class->GetDefaultObject()); if (Factory->bEditorImport) { for (const FString& Format: Factory->Formats) { int32 Index = INDEX_NONE; if (Format.FindChar(';', Index) && Index > 0) { Scratch.GetCharArray().Reset(); // Include the ; Scratch.AppendChars(*Format, Index + 1); if (AllExtensions.Find(Scratch) == INDEX_NONE) { AllExtensions += Scratch; } } } } } } return AllExtensions; } UAutoReimportManager::UAutoReimportManager(const FObjectInitializer& Init) : Super(Init) { } UAutoReimportManager::~UAutoReimportManager() { } void UAutoReimportManager::Initialize() { Implementation = MakeShareable(new FAutoReimportManager); } void UAutoReimportManager::IgnoreNewFile(const FString& Filename) { Implementation->IgnoreNewFile(Filename); } void UAutoReimportManager::IgnoreFileModification(const FString& Filename) { Implementation->IgnoreFileModification(Filename); } void UAutoReimportManager::IgnoreMovedFile(const FString& SrcFilename, const FString& DstFilename) { Implementation->IgnoreMovedFile(SrcFilename, DstFilename); } void UAutoReimportManager::IgnoreDeletedFile(const FString& Filename) { Implementation->IgnoreDeletedFile(Filename); } TArray UAutoReimportManager::GetMonitoredDirectories() const { return Implementation->GetMonitoredDirectories(); } void UAutoReimportManager::BeginDestroy() { Super::BeginDestroy(); if (Implementation.IsValid()) { Implementation->Destroy(); Implementation = nullptr; } } #undef LOCTEXT_NAMESPACE