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

1621 lines
55 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "PackageTools.h"
#include "Algo/Transform.h"
#include "BlueprintCompilationManager.h"
#include "UObject/PackageReload.h"
#include "Misc/MessageDialog.h"
#include "Misc/Paths.h"
#include "Misc/Guid.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/ScopedSlowTask.h"
#include "Misc/FeedbackContext.h"
#include "UObject/ObjectMacros.h"
#include "UObject/Object.h"
#include "UObject/GarbageCollection.h"
#include "UObject/Package.h"
#include "UObject/MetaData.h"
#include "UObject/UObjectHash.h"
#include "UObject/GCObjectScopeGuard.h"
#include "Serialization/ArchiveFindCulprit.h"
#include "Misc/PackageName.h"
#include "Editor/Transactor.h"
#include "Editor/EditorPerProjectUserSettings.h"
#include "IAssetTools.h"
#include "ISourceControlOperation.h"
#include "SourceControlOperations.h"
#include "ISourceControlProvider.h"
#include "ISourceControlModule.h"
#include "SourceControlHelpers.h"
#include "Editor.h"
#include "Dialogs/Dialogs.h"
#include "ObjectTools.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Kismet2/KismetReinstanceUtilities.h"
#include "BusyCursor.h"
#include "FileHelpers.h"
#include "Framework/Notifications/NotificationManager.h"
#include "UObject/WeakObjectPtrTemplates.h"
#include "Widgets/Notifications/SNotificationList.h"
#include "AssetRegistry/AssetData.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "ComponentReregisterContext.h"
#include "Engine/AssetManager.h"
#include "Engine/BlueprintGeneratedClass.h"
#include "Engine/GameEngine.h"
#include "Engine/LevelStreaming.h"
#include "Templates/GuardValueAccessors.h"
#include "Engine/Selection.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "Logging/MessageLog.h"
#include "UObject/UObjectGlobals.h"
#include "UObject/UObjectIterator.h"
#include "AssetCompilingManager.h"
#include "ShaderCompiler.h"
#include "DistanceFieldAtlas.h"
#include "MeshCardBuild.h"
#include "MeshCardRepresentation.h"
#include "AssetToolsModule.h"
#include "Subsystems/AssetEditorSubsystem.h"
#include "Misc/AutomationTest.h"
#define LOCTEXT_NAMESPACE "PackageTools"
DEFINE_LOG_CATEGORY_STATIC(LogPackageTools, Log, All);
namespace PackageTools_Private
{
static bool bUnloadPackagesUnloadsPrimaryAssets = true;
FAutoConsoleVariableRef CVarUnloadPackagesUnloadsPrimaryAssets(TEXT("PackageTools.UnloadPackagesUnloadsPrimaryAssets"), bUnloadPackagesUnloadsPrimaryAssets, TEXT("During unload packages, also unload primary assets"), ECVF_Default);
}
/** State passed to RestoreStandaloneOnReachableObjects. */
TSet<UPackage*>* UPackageTools::PackagesBeingUnloaded = nullptr;
TSet<UObject*> UPackageTools::ObjectsThatHadFlagsCleared;
FDelegateHandle UPackageTools::ReachabilityCallbackHandle;
void UPackageTools::FlushAsyncCompilation(TArrayView<UPackage* const> InPackages)
{
TArray<UObject*> ObjectsToFinish;
for (const UPackage* Package : InPackages)
{
ForEachObjectWithPackage(Package, [&ObjectsToFinish](UObject* Object)
{
if (const IInterface_AsyncCompilation* AsyncCompilationIF = Cast<IInterface_AsyncCompilation>(Object))
{
ObjectsToFinish.Add(Object);
}
return true;
});
}
if (ObjectsToFinish.Num())
{
FAssetCompilingManager::Get().FinishCompilationForObjects(ObjectsToFinish);
}
}
struct FPackageToolsCommands
{
FPackageToolsCommands()
: ReloadConsoleCommand(TEXT("PackageTools.ReloadPackage"),
TEXT("Force a reload of the named package, e.g. PackageTools.ReloadPackage /Game/MyAsset"),
FConsoleCommandWithArgsDelegate::CreateRaw(this, &FPackageToolsCommands::ReloadPackageCommand))
{
}
void ReloadPackageCommand(const TArray<FString>& Params)
{
TArray<UPackage*> Packages;
for (const FString& Param : Params)
{
UPackage* ExistingPackage = FindPackage(nullptr, *Param);
if (ExistingPackage)
{
Packages.Add(ExistingPackage);
}
}
if (Packages.Num() > 0)
{
UPackageTools::ReloadPackages(Packages);
}
}
FAutoConsoleCommand ReloadConsoleCommand;
};
static FPackageToolsCommands PackageCommands;
UPackageTools::UPackageTools(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
if (HasAnyFlags(RF_ClassDefaultObject))
{
FCoreUObjectDelegates::OnPackageReloaded.AddStatic(&UPackageTools::HandlePackageReloaded);
}
}
/**
* Called during GC, after reachability analysis is performed but before garbage is purged.
* Restores RF_Standalone to objects in the package-to-be-unloaded that are still reachable.
*/
void UPackageTools::RestoreStandaloneOnReachableObjects()
{
check(GIsEditor);
TRACE_CPUPROFILER_EVENT_SCOPE(UPackageTools::RestoreStandaloneOnReachableObjects);
if (PackagesBeingUnloaded && ObjectsThatHadFlagsCleared.Num() > 0)
{
for (UPackage* PackageBeingUnloaded : *PackagesBeingUnloaded)
{
ForEachObjectWithPackage(PackageBeingUnloaded, [](UObject* Object)
{
if (ObjectsThatHadFlagsCleared.Contains(Object))
{
Object->SetFlags(RF_Standalone);
}
return true;
}, true, RF_NoFlags, EInternalObjectFlags::Unreachable);
}
}
}
/**
* Filters the global set of packages.
*
* @param OutGroupPackages The map that receives the filtered list of group packages.
* @param OutPackageList The array that will contain the list of filtered packages.
*/
void UPackageTools::GetFilteredPackageList(TSet<UPackage*>& OutFilteredPackageMap)
{
// The UObject list is iterated rather than the UPackage list because we need to be sure we are only adding
// group packages that contain things the generic browser cares about. The packages are derived by walking
// the outer chain of each object.
// Assemble a list of packages. Only show packages that match the current resource type filter.
for (UObject* Obj : TObjectRange<UObject>())
{
// This is here to hopefully catch a bit more info about a spurious in-the-wild problem which ultimately
// crashes inside UObjectBaseUtility::GetOutermost(), which is called inside IsObjectBrowsable().
checkf(Obj->IsValidLowLevel(), TEXT("GetFilteredPackageList: bad object found, address: %p, name: %s"), Obj, *Obj->GetName());
// Make sure that we support displaying this object type
bool bIsSupported = ObjectTools::IsObjectBrowsable( Obj );
if( bIsSupported )
{
UPackage* ObjectPackage = Obj->GetOutermost();
if( ObjectPackage != NULL )
{
OutFilteredPackageMap.Add( ObjectPackage );
}
}
}
}
/**
* Fills the OutObjects list with all valid objects that are supported by the current
* browser settings and that reside withing the set of specified packages.
*
* @param InPackages Filters objects based on package.
* @param OutObjects [out] Receives the list of objects
* @param bMustBeBrowsable If specified, does a check to see if object is browsable. Defaults to true.
*/
void UPackageTools::GetObjectsInPackages( const TArray<UPackage*>* InPackages, TArray<UObject*>& OutObjects )
{
if (InPackages)
{
for (UPackage* Package : *InPackages)
{
ForEachObjectWithPackage(Package,[&OutObjects](UObject* Obj)
{
if (ObjectTools::IsObjectBrowsable(Obj))
{
OutObjects.Add(Obj);
}
return true;
});
}
}
else
{
for (TObjectIterator<UObject> It; It; ++It)
{
UObject* Obj = *It;
if (ObjectTools::IsObjectBrowsable(Obj))
{
OutObjects.Add(Obj);
}
}
}
}
bool UPackageTools::HandleFullyLoadingPackages( const TArray<UPackage*>& TopLevelPackages, const FText& OperationText )
{
bool bSuccessfullyCompleted = true;
// whether or not to suppress the ask to fully load message
bool bSuppress = GetDefault<UEditorPerProjectUserSettings>()->bSuppressFullyLoadPrompt;
// Make sure they are all fully loaded.
bool bNeedsUpdate = false;
for( int32 PackageIndex=0; PackageIndex<TopLevelPackages.Num(); PackageIndex++ )
{
UPackage* TopLevelPackage = TopLevelPackages[PackageIndex];
check( TopLevelPackage );
check( TopLevelPackage->GetOuter() == NULL );
// Calling IsFullyLoaded() will mark a package as fully loaded if it does not exist on disk
if( !TopLevelPackage->IsFullyLoaded() )
{
// Ask user to fully load or suppress the message and just fully load
if(bSuppress || EAppReturnType::Yes == FMessageDialog::Open( EAppMsgType::YesNo, EAppReturnType::Yes, FText::Format(
NSLOCTEXT("UnrealEd", "NeedsToFullyLoadPackageF", "Asset {0} is not fully loaded. Do you want to fully load it? Not doing so will abort the '{1}' operation."),
FText::FromString(IAssetTools::Get().GetUserFacingLongPackageName(*TopLevelPackage)), OperationText)))
{
// Fully load package.
const FScopedBusyCursor BusyCursor;
GWarn->BeginSlowTask( NSLOCTEXT("UnrealEd", "FullyLoadingPackages", "Fully loading assets"), true );
TopLevelPackage->FullyLoad();
GWarn->EndSlowTask();
bNeedsUpdate = true;
}
// User declined abort operation.
else
{
bSuccessfullyCompleted = false;
UE_LOG(LogPackageTools, Log, TEXT("Aborting operation as %s was not fully loaded."),*TopLevelPackage->GetName());
break;
}
}
}
// no need to refresh content browser here as UPackage::FullyLoad() already does this
return bSuccessfullyCompleted;
}
/**
* Loads the specified package file (or returns an existing package if it's already loaded.)
*
* @param InFilename File name of package to load
*
* @return The loaded package (or NULL if something went wrong.)
*/
UPackage* UPackageTools::LoadPackage( FString InFilename )
{
// Detach all components while loading a package.
// This is necessary for the cases where the load replaces existing objects which may be referenced by the attached components.
FGlobalComponentReregisterContext ReregisterContext;
// record the name of this file to make sure we load objects in this package on top of in-memory objects in this package
GEditor->UserOpenedFile = InFilename;
// clear any previous load errors
FFormatNamedArguments Arguments;
Arguments.Add(TEXT("PackageName"), FText::FromString(InFilename));
FMessageLog("LoadErrors").NewPage(FText::Format(LOCTEXT("LoadPackageLogPage", "Loading package: {PackageName}"), Arguments));
UPackage* Package = ::LoadPackage( NULL, *InFilename, 0 );
// display any load errors that happened while loading the package
FEditorDelegates::DisplayLoadErrors.Broadcast();
// reset the opened package to nothing
GEditor->UserOpenedFile = FString();
// If a script package was loaded, update the
// actor browser in case a script package was loaded
if ( Package != NULL )
{
if (Package->HasAnyPackageFlags(PKG_ContainsScript))
{
GEditor->BroadcastClassPackageLoadedOrUnloaded();
}
}
return Package;
}
bool UPackageTools::UnloadPackages( const TArray<UPackage*>& TopLevelPackages )
{
FText ErrorMessage;
const bool bResult = UnloadPackages(TopLevelPackages, ErrorMessage);
if(!ErrorMessage.IsEmpty())
{
FMessageDialog::Open( EAppMsgType::Ok, ErrorMessage );
}
return bResult;
}
bool UPackageTools::UnloadPackages(const TArray<UPackage*>& TopLevelPackages, FText& OutErrorMessage, bool bUnloadDirtyPackages)
{
FUnloadPackageParams Params(TopLevelPackages);
Params.bUnloadDirtyPackages = bUnloadDirtyPackages;
const bool bResult = UnloadPackages(Params);
OutErrorMessage = Params.OutErrorMessage;
return bResult;
}
bool UPackageTools::UnloadPackages(FUnloadPackageParams& Params)
{
// Early out if no package is provided
if (Params.Packages.IsEmpty())
{
return true;
}
bool bResult = false;
// Get outermost packages, in case groups were selected.
TSet<UPackage*> PackagesToUnload;
// Split the set of selected top level packages into packages which are dirty (and thus cannot be unloaded)
// and packages that are not dirty (and thus can be unloaded).
TArray<UPackage*> DirtyPackages;
for (UPackage* TopLevelPackage : Params.Packages)
{
if (TopLevelPackage)
{
if (!Params.bUnloadDirtyPackages && TopLevelPackage->IsDirty())
{
DirtyPackages.Add(TopLevelPackage);
}
else
{
UPackage* PackageToUnload = TopLevelPackage->GetOutermost();
if (!PackageToUnload)
{
PackageToUnload = TopLevelPackage;
}
PackagesToUnload.Add(PackageToUnload);
}
}
}
// Inform the user that dirty packages won't be unloaded.
if ( DirtyPackages.Num() > 0 )
{
FString DirtyPackagesList;
for (const FString& UserFacingPath : IAssetTools::Get().GetUserFacingLongPackageNames(DirtyPackages))
{
DirtyPackagesList += FString::Printf(TEXT("\n %s"), *UserFacingPath);
}
FFormatNamedArguments Args;
Args.Add(TEXT("DirtyPackages"), FText::FromString(DirtyPackagesList));
Params.OutErrorMessage = FText::Format( NSLOCTEXT("UnrealEd", "UnloadDirtyPackagesList", "The following assets have been modified and cannot be unloaded:{DirtyPackages}\nSaving these assets will allow them to be unloaded."), Args );
}
if (GEditor)
{
if (UWorld* EditorWorld = GEditor->GetEditorWorldContext().World())
{
// Is the current world being unloaded?
if (PackagesToUnload.Contains(EditorWorld->GetPackage()))
{
TArray<TWeakObjectPtr<UPackage>> WeakPackages;
WeakPackages.Reserve(PackagesToUnload.Num());
for (UPackage* Package : PackagesToUnload)
{
WeakPackages.Add(Package);
}
// Unload the current world
GEditor->CreateNewMapForEditing();
// Remove stale entries in PackagesToUnload (unloaded world, level build data, streaming levels, external actors, etc)
PackagesToUnload.Reset();
for (const TWeakObjectPtr<UPackage>& WeakPackage : WeakPackages)
{
if (UPackage* Package = WeakPackage.Get())
{
PackagesToUnload.Add(Package);
}
}
}
}
}
if (PackagesToUnload.Num() > 0 && GEditor)
{
const FScopedBusyCursor BusyCursor;
// Complete any load/streaming requests, then lock IO.
FlushAsyncLoading();
(*GFlushStreamingFunc)();
GWarn->BeginSlowTask(NSLOCTEXT("UnrealEd", "Unloading", "Unloading"), true);
// Remove potential references to to-be deleted objects from the GB selection set.
GEditor->GetSelectedObjects()->GetElementSelectionSet()->ClearSelection(FTypedElementSelectionOptions());
// Clear undo history because transaction records can hold onto assets we want to unload
if (GEditor->Trans && Params.bResetTransBuffer)
{
GEditor->Trans->Reset(NSLOCTEXT("UnrealEd", "UnloadPackagesResetUndo", "Unload Assets"));
}
// First add all packages to unload to the root set so they don't get garbage collected while we are operating on them
TArray<UPackage*> PackagesAddedToRoot;
for (UPackage* PackageToUnload : PackagesToUnload)
{
if (!PackageToUnload->IsRooted())
{
PackageToUnload->AddToRoot();
PackagesAddedToRoot.Add(PackageToUnload);
}
}
// We need to make sure that there is no async compilation work running for the packages that we are about to unload
// so that it is safe to call ::ResetLoaders
FlushAsyncCompilation(PackagesToUnload.Array());
// Now try to clean up assets in all packages to unload.
bool bScriptPackageWasUnloaded = false;
int32 PackageIndex = 0;
for (UPackage* PackageBeingUnloaded : PackagesToUnload)
{
GWarn->StatusUpdate(PackageIndex++, PackagesToUnload.Num(), FText::Format(NSLOCTEXT("UnrealEd", "Unloadingf", "Unloading {0}..."), FText::FromString(IAssetTools::Get().GetUserFacingLongPackageName(*PackageBeingUnloaded)) ) );
// Flush all pending render commands, as unloading the package may invalidate render resources.
FlushRenderingCommands();
TArray<UObject*> ObjectsInPackage;
// Can't use ForEachObjectWithPackage here as closing the editor may modify UObject hash tables (known case: renaming objects)
GetObjectsWithPackage(PackageBeingUnloaded, ObjectsInPackage, false);
// Close any open asset editors.
for (UObject* Obj : ObjectsInPackage)
{
if (Obj->IsAsset())
{
GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->CloseAllEditorsForAsset(Obj);
}
}
ObjectsInPackage.Reset();
PackageBeingUnloaded->MarkAsUnloaded();
if ( PackageBeingUnloaded->HasAnyPackageFlags(PKG_ContainsScript) )
{
bScriptPackageWasUnloaded = true;
}
GetObjectsWithPackage(PackageBeingUnloaded, ObjectsInPackage, true, RF_Transient, EInternalObjectFlags::Garbage);
// Notify any Blueprints and other systems that are about to be unloaded, also destroy any leftover worlds.
for (UObject* Obj : ObjectsInPackage)
{
// Asset manager can hold hard references to this object and prevent GC
if (PackageTools_Private::bUnloadPackagesUnloadsPrimaryAssets)
{
const FPrimaryAssetId PrimaryAssetId = UAssetManager::Get().GetPrimaryAssetIdForObject(Obj);
if (PrimaryAssetId.IsValid())
{
UAssetManager::Get().UnloadPrimaryAsset(PrimaryAssetId);
}
}
if (UBlueprint* BP = Cast<UBlueprint>(Obj))
{
BP->ClearEditorReferences();
// Remove from cached dependent lists.
for (const TWeakObjectPtr<UBlueprint> Dependency : BP->CachedDependencies)
{
if (UBlueprint* ResolvedDependency = Dependency.Get())
{
ResolvedDependency->CachedDependents.Remove(BP);
}
}
BP->CachedDependencies.Reset();
// Remove from cached dependency lists.
for (const TWeakObjectPtr<UBlueprint> Dependent : BP->CachedDependents)
{
if (UBlueprint* ResolvedDependent = Dependent.Get())
{
ResolvedDependent->CachedDependencies.Remove(BP);
}
}
BP->CachedDependents.Reset();
}
else if (UBlueprintGeneratedClass* BPGC = Cast<UBlueprintGeneratedClass>(Obj))
{
FKismetEditorUtilities::OnBlueprintGeneratedClassUnloaded.Broadcast(BPGC);
}
else if (UWorld* World = Cast<UWorld>(Obj))
{
if (World->bIsWorldInitialized)
{
World->CleanupWorld();
}
}
}
ObjectsInPackage.Reset();
// Clear RF_Standalone flag from objects in the package to be unloaded so they get GC'd.
{
GetObjectsWithPackage(PackageBeingUnloaded, ObjectsInPackage);
for ( UObject* Object : ObjectsInPackage )
{
if (Object->HasAnyFlags(RF_Standalone))
{
Object->ClearFlags(RF_Standalone);
ObjectsThatHadFlagsCleared.Add(Object);
}
}
ObjectsInPackage.Reset();
}
// Cleanup.
bResult = true;
}
// Calling ::ResetLoaders now will force any bulkdata objects still attached to the FLinkerLoad to load
// their payloads into memory. If we don't call this now, then the version that will be called during
// garbage collection will cause the bulkdata objects to be invalidated rather than loading the payloads
// into memory.
// This might seem odd, but if the package we are unloading is being renamed, then the inner UObjects will
// be moved to the newly named package rather than being garbage collected and so we need to make sure that
// their bulkdata objects remain valid, otherwise renamed packages will not save correctly and cease to function.
ResetLoaders(TArray<UObject*>(PackagesToUnload.Array()));
for (UPackage* PackageBeingUnloaded : PackagesToUnload)
{
if (PackageBeingUnloaded->IsDirty())
{
// The package was marked dirty as a result of something that happened above (e.g callbacks in CollectGarbage).
// Dirty packages we actually care about unloading were filtered above so if the package becomes dirty here it should still be unloaded
PackageBeingUnloaded->SetDirtyFlag(false);
}
}
// Set the callback for restoring RF_Standalone post reachability analysis.
// GC will call this function before purging objects, allowing us to restore RF_Standalone
// to any objects that have not been marked RF_Unreachable.
ReachabilityCallbackHandle = FCoreUObjectDelegates::PostReachabilityAnalysis.AddStatic(RestoreStandaloneOnReachableObjects);
PackagesBeingUnloaded = &PackagesToUnload;
// Collect garbage.
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
ObjectsThatHadFlagsCleared.Empty();
PackagesBeingUnloaded = nullptr;
// Now remove from root all the packages we added earlier so they may be GCed if possible
for (UPackage* PackageAddedToRoot : PackagesAddedToRoot)
{
PackageAddedToRoot->RemoveFromRoot();
}
PackagesAddedToRoot.Empty();
GWarn->EndSlowTask();
// Remove the post reachability callback.
FCoreUObjectDelegates::PostReachabilityAnalysis.Remove(ReachabilityCallbackHandle);
CollectGarbage( GARBAGE_COLLECTION_KEEPFLAGS );
// Update the actor browser if a script package was unloaded
if ( bScriptPackageWasUnloaded )
{
GEditor->BroadcastClassPackageLoadedOrUnloaded();
}
}
return bResult;
}
bool UPackageTools::ReloadPackages( const TArray<UPackage*>& TopLevelPackages )
{
FText ErrorMessage;
const bool bResult = ReloadPackages(TopLevelPackages, ErrorMessage, EReloadPackagesInteractionMode::Interactive);
if (!ErrorMessage.IsEmpty())
{
FMessageDialog::Open(EAppMsgType::Ok, ErrorMessage);
}
return bResult;
}
bool UPackageTools::ReloadPackages( const TArray<UPackage*>& TopLevelPackages, FText& OutErrorMessage, const bool bInteractive )
{
return ReloadPackages(TopLevelPackages, OutErrorMessage, bInteractive ? EReloadPackagesInteractionMode::Interactive : EReloadPackagesInteractionMode::AssumeNegative);
}
namespace UE::PackageTools::Private
{
struct FFilteredPackages
{
void Add(UPackage* RealPackage)
{
if (RealPackage->HasAnyPackageFlags(PKG_InMemoryOnly))
{
InMemoryPackages.AddUnique(RealPackage);
}
else if (RealPackage->IsDirty())
{
DirtyPackages.AddUnique(RealPackage);
}
else
{
PackagesToReload.AddUnique(RealPackage);
}
}
void Remove(UPackage* InPackage)
{
PackagesToReload.Remove(InPackage);
}
bool Contains(UPackage* InPackage) const
{
return PackagesToReload.Contains(InPackage);
}
TArray<UPackage*> PackagesToReload;
TArray<UPackage*> DirtyPackages;
TArray<UPackage*> InMemoryPackages;
};
FFilteredPackages GetPackagesToReload(const TArray<UPackage*>& TopLevelPackages)
{
FFilteredPackages Filtered;
Algo::TransformIf(TopLevelPackages, Filtered,
[](UPackage* TopLevelPackage) {return TopLevelPackage;},
[](UPackage* TopLevelPackage)
{
return TopLevelPackage;
});
return Filtered;
}
void PromptUserForDirtyPackages(FFilteredPackages& Filtered, const EReloadPackagesInteractionMode InteractionMode)
{
// How should we handle locally dirty packages?
if (Filtered.DirtyPackages.Num() == 0)
{
return;
}
EAppReturnType::Type ReloadDirtyPackagesResult = EAppReturnType::No;
// Ask the user whether dirty packages should be reloaded.
if (InteractionMode == EReloadPackagesInteractionMode::Interactive)
{
FTextBuilder ReloadDirtyPackagesMsgBuilder;
ReloadDirtyPackagesMsgBuilder.AppendLine(NSLOCTEXT("UnrealEd", "ShouldReloadDirtyPackagesHeader", "The following assets have been modified:"));
{
ReloadDirtyPackagesMsgBuilder.Indent();
for (const FString& UserFacingPath : IAssetTools::Get().GetUserFacingLongPackageNames(Filtered.DirtyPackages))
{
ReloadDirtyPackagesMsgBuilder.AppendLine(UserFacingPath);
}
ReloadDirtyPackagesMsgBuilder.Unindent();
}
ReloadDirtyPackagesMsgBuilder.AppendLine(NSLOCTEXT("UnrealEd", "ShouldReloadDirtyPackagesFooter", "Would you like to reload these assets? This will revert any changes you have made."));
ReloadDirtyPackagesResult = FMessageDialog::Open(EAppMsgType::YesNo, ReloadDirtyPackagesMsgBuilder.ToText());
}
else if (InteractionMode == EReloadPackagesInteractionMode::AssumePositive)
{
ReloadDirtyPackagesResult = EAppReturnType::Yes;
}
if (ReloadDirtyPackagesResult == EAppReturnType::Yes)
{
for (UPackage* DirtyPackage : Filtered.DirtyPackages)
{
DirtyPackage->SetDirtyFlag(false);
Filtered.PackagesToReload.AddUnique(DirtyPackage);
}
Filtered.DirtyPackages.Reset();
}
}
void InformUserAboutDirtyPackages(const FFilteredPackages& Filtered, FTextBuilder& ErrorMessageBuilder)
{
if (Filtered.DirtyPackages.Num() == 0)
{
return;
}
// Inform the user that dirty packages won't be reloaded.
if (!ErrorMessageBuilder.IsEmpty())
{
ErrorMessageBuilder.AppendLine();
}
ErrorMessageBuilder.AppendLine(NSLOCTEXT("UnrealEd", "Error_ReloadDirtyPackagesHeader", "The following assets have been modified and cannot be reloaded:"));
{
ErrorMessageBuilder.Indent();
for (const FString& UserFacingPath : IAssetTools::Get().GetUserFacingLongPackageNames(Filtered.DirtyPackages))
{
ErrorMessageBuilder.AppendLine(UserFacingPath);
}
ErrorMessageBuilder.Unindent();
}
ErrorMessageBuilder.AppendLine(NSLOCTEXT("UnrealEd", "Error_ReloadDirtyPackagesFooter", "Saving these assets will allow them to be reloaded."));
}
void InformUsersAboutInMemoryPackages(const FFilteredPackages& Filtered, FTextBuilder& ErrorMessageBuilder)
{
if (Filtered.InMemoryPackages.Num() == 0)
{
return;
}
if (!ErrorMessageBuilder.IsEmpty())
{
ErrorMessageBuilder.AppendLine();
}
ErrorMessageBuilder.AppendLine(NSLOCTEXT("UnrealEd", "Error_ReloadInMemoryPackagesHeader", "The following assets are in-memory only and cannot be reloaded:"));
{
ErrorMessageBuilder.Indent();
for (const FString& UserFacingPath : IAssetTools::Get().GetUserFacingLongPackageNames(Filtered.InMemoryPackages))
{
ErrorMessageBuilder.AppendLine(UserFacingPath);
}
ErrorMessageBuilder.Unindent();
}
}
struct FScopedTrackFilteredPackages
{
FScopedTrackFilteredPackages(FFilteredPackages& InFiltered) :
Filtered(InFiltered)
{
Algo::Transform(Filtered.PackagesToReload, WeakPackagesToReload, [](UPackage* InPackage) -> TWeakObjectPtr<UPackage> { return InPackage; });
}
~FScopedTrackFilteredPackages()
{
Filtered.PackagesToReload.Reset();
Algo::TransformIf(WeakPackagesToReload, Filtered.PackagesToReload,
[](TWeakObjectPtr<UPackage> InPackage) {return InPackage.IsValid();},
[](TWeakObjectPtr<UPackage> InPackage) -> UPackage* {return InPackage.Get();});
}
TArray<TWeakObjectPtr<UPackage>> WeakPackagesToReload;
FFilteredPackages& Filtered;
};
TWeakObjectPtr<UWorld> GetCurrentWorld()
{
if (GIsEditor)
{
if (UWorld* EditorWorld = GEditor->GetEditorWorldContext().World())
{
return EditorWorld;
}
}
else if (UGameEngine* GameEngine = Cast<UGameEngine>(GEngine))
{
if (UWorld* GameWorld = GameEngine->GetGameWorld())
{
return GameWorld;
}
}
return nullptr;
}
}
bool UPackageTools::ReloadPackages( const TArray<UPackage*>& TopLevelPackages, FText& OutErrorMessage, const EReloadPackagesInteractionMode InteractionMode )
{
bool bResult = false;
TGuardValueAccessors<bool> IsEditorLoadingPackageGuard(UE::GetIsEditorLoadingPackage, UE::SetIsEditorLoadingPackage, true);
FTextBuilder ErrorMessageBuilder;
using namespace UE::PackageTools::Private;
FFilteredPackages Filtered = GetPackagesToReload(TopLevelPackages);
PromptUserForDirtyPackages(Filtered, InteractionMode);
InformUserAboutDirtyPackages(Filtered, ErrorMessageBuilder);
InformUsersAboutInMemoryPackages(Filtered, ErrorMessageBuilder);
TWeakObjectPtr<UWorld> CurrentWorld = GetCurrentWorld();
// Check to see if we need to reload the current world.
FName WorldNameToReload;
FName CurrentWorldPackageName;
TArray<ULevelStreaming*> RemovedStreamingLevels;
if (UWorld* CurrentWorldPtr = CurrentWorld.Get())
{
// Is the current world being reloaded? If so, we just reset the current world and load it again at the end rather than let it go through ReloadPackage
// (which doesn't work for the current world due to some assumptions about worlds, and their lifetimes).
// We also need to skip the build data package as that will also be destroyed by the transition.
if (Filtered.Contains(CurrentWorldPtr->GetOutermost()))
{
// Cache this so we can reload the world later
WorldNameToReload = *CurrentWorldPtr->GetPathName();
CurrentWorldPackageName = CurrentWorldPtr->GetPackage()->GetFName();
// Remove the world package from the reload list
Filtered.Remove(CurrentWorldPtr->GetOutermost());
// Unload the current world
if (GIsEditor)
{
FScopedTrackFilteredPackages TrackPackages(Filtered);
const bool bPromptForSave = InteractionMode == UPackageTools::EReloadPackagesInteractionMode::Interactive;
GEditor->CreateNewMapForEditing(bPromptForSave);
}
else if (UGameEngine* GameEngine = Cast<UGameEngine>(GEngine))
{
// Outside of the editor we need to keep the packages alive to stop the world transition from GC'ing them
TGCObjectsScopeGuard<UPackage> KeepPackagesAlive(Filtered.PackagesToReload);
FString LoadMapError;
GameEngine->LoadMap(GameEngine->GetWorldContextFromWorldChecked(CurrentWorldPtr), FURL(TEXT("/Engine/Maps/Templates/Template_Default")), nullptr, LoadMapError);
}
}
// Cache the current map build data for the levels of the current world so we can see if they change due to a reload (we can skip this if reloading the current world).
else
{
const TArray<ULevel*>& Levels = CurrentWorldPtr->GetLevels();
for (int32 i = Levels.Num() - 1; i >= 0; --i)
{
ULevel* Level = Levels[i];
Level->ReleaseRenderingResources();
if (Filtered.Contains(Level->GetOutermost()))
{
for (ULevelStreaming* StreamingLevel : CurrentWorldPtr->GetStreamingLevels())
{
if (StreamingLevel->GetLoadedLevel() == Level)
{
CurrentWorldPtr->RemoveFromWorld(Level);
StreamingLevel->RemoveLevelFromCollectionForReload();
ULevelStreaming::RemoveLevelAnnotation(Level);
RemovedStreamingLevels.Add(StreamingLevel);
break;
}
}
}
}
}
}
TArray<UPackage*>& PackagesToReload = Filtered.PackagesToReload;
if (PackagesToReload.Num() > 0)
{
const FScopedBusyCursor BusyCursor;
// We need to sort the packages to reload so that dependencies are reloaded before the assets that depend on them
::SortPackagesForReload(PackagesToReload);
// Remove potential references to to-be deleted objects from the global selection sets.
if (GIsEditor)
{
GEditor->ResetAllSelectionSets();
}
// Detach all components while loading a package.
// This is necessary for the cases where the load replaces existing objects which may be referenced by the attached components.
FGlobalComponentReregisterContext ReregisterContext;
bool bScriptPackageWasReloaded = false;
TArray<FReloadPackageData> PackagesToReloadData;
PackagesToReloadData.Reserve(PackagesToReload.Num());
for (UPackage* PackageToReload : PackagesToReload)
{
bScriptPackageWasReloaded |= PackageToReload->HasAnyPackageFlags(PKG_ContainsScript);
PackagesToReloadData.Emplace(PackageToReload, LOAD_None);
}
TArray<UPackage*> ReloadedPackages;
::ReloadPackages(PackagesToReloadData, ReloadedPackages, 500);
TArray<UPackage*> FailedPackages;
for (int32 PackageIndex = 0; PackageIndex < PackagesToReload.Num(); ++PackageIndex)
{
UPackage* ExistingPackage = PackagesToReload[PackageIndex];
UPackage* ReloadedPackage = ReloadedPackages[PackageIndex];
if (ReloadedPackage)
{
bScriptPackageWasReloaded |= ReloadedPackage->HasAnyPackageFlags(PKG_ContainsScript);
bResult = true;
}
else
{
FailedPackages.Add(ExistingPackage);
}
}
// Inform the user of any packages that failed to reload.
if (FailedPackages.Num() > 0)
{
if (!ErrorMessageBuilder.IsEmpty())
{
ErrorMessageBuilder.AppendLine();
}
ErrorMessageBuilder.AppendLine(NSLOCTEXT("UnrealEd", "Error_ReloadFailedPackagesHeader", "The following assets failed to reload:"));
{
ErrorMessageBuilder.Indent();
for (const FString& UserFacingPath : IAssetTools::Get().GetUserFacingLongPackageNames(FailedPackages))
{
ErrorMessageBuilder.AppendLine(UserFacingPath);
}
ErrorMessageBuilder.Unindent();
}
}
// Update the actor browser if a script package was reloaded.
if (GIsEditor && bScriptPackageWasReloaded)
{
GEditor->BroadcastClassPackageLoadedOrUnloaded();
}
}
// Load the previous world (if needed).
if (!WorldNameToReload.IsNone())
{
if (GIsEditor)
{
UWorld::WorldTypePreLoadMap.FindOrAdd(CurrentWorldPackageName) = EWorldType::Editor;
TArray<FName> WorldNamesToReload;
WorldNamesToReload.Add(WorldNameToReload);
GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->OpenEditorsForAssets(WorldNamesToReload);
UWorld::WorldTypePreLoadMap.Remove(CurrentWorldPackageName);
}
else if (UGameEngine* GameEngine = Cast<UGameEngine>(GEngine))
{
FString LoadMapError;
// FURL requires a package name and not an asset path
FString WorldPackage = FPackageName::ObjectPathToPackageName(WorldNameToReload.ToString());
GameEngine->LoadMap(GameEngine->GetWorldContextFromWorldChecked(GameEngine->GetGameWorld()), FURL(*WorldPackage), nullptr, LoadMapError);
}
}
// Update the rendering resources for the levels of the current world if their map build data has changed (we skip this if reloading the current world).
else
{
UWorld* CurrentWorldPtr = CurrentWorld.Get();
check(CurrentWorldPtr);
if (RemovedStreamingLevels.Num() > 0)
{
for (ULevelStreaming* StreamingLevel : RemovedStreamingLevels)
{
ULevel* NewLevel = StreamingLevel->GetLoadedLevel();
ULevelStreaming::LevelAnnotations.AddAnnotation(
NewLevel,
ULevelStreaming::FLevelAnnotation(StreamingLevel));
CurrentWorldPtr->AddToWorld(NewLevel, StreamingLevel->LevelTransform, false);
StreamingLevel->AddLevelToCollectionAfterReload();
}
}
CurrentWorldPtr->PropagateLightingScenarioChange();
}
OutErrorMessage = ErrorMessageBuilder.ToText();
return bResult;
}
void UPackageTools::HandlePackageReloaded(const EPackageReloadPhase InPackageReloadPhase, FPackageReloadedEvent* InPackageReloadedEvent)
{
static TSet<UBlueprint*> BlueprintsToRecompileThisBatch;
if (InPackageReloadPhase == EPackageReloadPhase::PrePackageFixup)
{
GEngine->NotifyToolsOfObjectReplacement(InPackageReloadedEvent->GetRepointedObjects());
// Notify any Blueprints that are about to be unloaded, and destroy any leftover worlds.
TArray<UObject*> Objects;
GetObjectsWithPackage(InPackageReloadedEvent->GetOldPackage(), Objects, true, RF_Transient, EInternalObjectFlags::Garbage);
for (UObject* InObject : Objects)
{
if (UBlueprint* BP = Cast<UBlueprint>(InObject))
{
BP->ClearEditorReferences();
// Remove from cached dependent lists; this will be repopulated on reload, but we
// don't wish to consider every one of our dependencies as a potential referencer,
// and this set will be serialized by the archiver that's used to find those. Any
// references will instead be collected from other fields that reference the asset.
for (const TWeakObjectPtr<UBlueprint> Dependency : BP->CachedDependencies)
{
if (UBlueprint* ResolvedDependency = Dependency.Get())
{
ResolvedDependency->CachedDependents.Remove(BP);
}
}
}
if (UWorld* World = Cast<UWorld>(InObject))
{
if (World->bIsWorldInitialized)
{
World->CleanupWorld();
}
}
}
}
if (InPackageReloadPhase == EPackageReloadPhase::OnPackageFixup)
{
TMap<UClass*, UClass*> OldClassToNewClass;
for (const auto& RepointedObjectPair : InPackageReloadedEvent->GetRepointedObjects())
{
UObject* OldObject = RepointedObjectPair.Key;
UObject* NewObject = RepointedObjectPair.Value;
if(OldObject && NewObject)
{
// Only the blueprint generated class are supported by the FBlueprintCompilationManager so we only reparent those
UClass* OldObjectAsClass = Cast<UBlueprintGeneratedClass>(OldObject);
if(OldObjectAsClass)
{
UClass* NewObjectAsClass = Cast<UClass>(NewObject);
if(ensureMsgf(NewObjectAsClass, TEXT("Class object replaced with non-class object: %s %s"), *(OldObject->GetName()), *(NewObject->GetName())))
{
OldClassToNewClass.Add(OldObjectAsClass, NewObjectAsClass);
}
}
}
}
FBlueprintCompilationManager::ReparentHierarchies(OldClassToNewClass);
for (const auto& RepointedObjectPair : InPackageReloadedEvent->GetRepointedObjects())
{
UObject* OldObject = RepointedObjectPair.Key;
UObject* NewObject = RepointedObjectPair.Value;
if (OldObject && OldObject->IsAsset())
{
if (const UBlueprint* OldBlueprint = Cast<UBlueprint>(OldObject))
{
if (NewObject && CastChecked<UBlueprint>(NewObject)->GeneratedClass && OldBlueprint->GeneratedClass)
{
// Don't change the class on instances that are being thrown away by the reload code. If we update
// the class and recompile the old class ::ReplaceInstancesOfClass will experience some crosstalk
// with the compiler (both trying to create objects of the same class in the same location):
TArray<UObject*> OldInstances;
GetObjectsOfClass(OldBlueprint->GeneratedClass, OldInstances, false);
OldInstances.RemoveAllSwap(
[](UObject* Obj){ return !Obj->HasAnyFlags(RF_NewerVersionExists); }
);
TSet<UObject*> InstancesToLeaveAlone(OldInstances);
FReplaceInstancesOfClassParameters ReplaceInstancesParameters;
ReplaceInstancesParameters.InstancesThatShouldUseOldClass = &InstancesToLeaveAlone;
FBlueprintCompileReinstancer::ReplaceInstancesOfClass(OldBlueprint->GeneratedClass, CastChecked<UBlueprint>(NewObject)->GeneratedClass, ReplaceInstancesParameters);
}
else
{
// we failed to load the UBlueprint and/or it's GeneratedClass. Show a notification indicating that maps may need to be reloaded:
FNotificationInfo Warning(
FText::Format(
NSLOCTEXT("UnrealEd", "Warning_FailedToLoadParentClass", "Failed to load ParentClass for {0}"),
FText::FromName(OldObject->GetFName())
)
);
Warning.ExpireDuration = 3.0f;
FSlateNotificationManager::Get().AddNotification(Warning);
}
}
}
}
}
if (InPackageReloadPhase == EPackageReloadPhase::PostPackageFixup)
{
for (TWeakObjectPtr<UObject> ObjectReferencer : InPackageReloadedEvent->GetObjectReferencers())
{
UObject* ObjectReferencerPtr = ObjectReferencer.Get();
if (!ObjectReferencerPtr)
{
continue;
}
if (!ObjectReferencerPtr->GetClass()->HasAnyClassFlags(CLASS_NewerVersionExists))
{
// Calling PostEditChangeProperty on an actor with an outdated class will trigger a check() during construction scripts.
FPropertyChangedEvent PropertyEvent(nullptr, EPropertyChangeType::Redirected);
ObjectReferencerPtr->PostEditChangeProperty(PropertyEvent);
}
// We need to recompile any Blueprints that had properties changed to make sure their generated class is up-to-date and has no lingering references to the old objects
UBlueprint* BlueprintToRecompile = nullptr;
if (UBlueprint* BlueprintReferencer = Cast<UBlueprint>(ObjectReferencerPtr))
{
BlueprintToRecompile = BlueprintReferencer;
}
else if (UClass* ClassReferencer = Cast<UClass>(ObjectReferencerPtr))
{
BlueprintToRecompile = Cast<UBlueprint>(ClassReferencer->ClassGeneratedBy);
}
else
{
BlueprintToRecompile = ObjectReferencerPtr->GetTypedOuter<UBlueprint>();
}
if (BlueprintToRecompile)
{
BlueprintsToRecompileThisBatch.Add(BlueprintToRecompile);
}
}
// @todo FH: we should eventually have a specific api for hot reloading single objects or external packages' objects
// Call post edit change property on the reloaded objects in the package if they are external
FPropertyChangedEvent PropertyEvent(nullptr, EPropertyChangeType::Redirected);
for (const auto& ObjectPair : InPackageReloadedEvent->GetRepointedObjects())
{
// An object is external, if it has a directly assigned package
if (ObjectPair.Value && ObjectPair.Value->GetExternalPackage())
{
ObjectPair.Value->PostEditChangeProperty(PropertyEvent);
}
}
}
if (InPackageReloadPhase == EPackageReloadPhase::PreBatch)
{
// If this fires then ReloadPackages has probably bee called recursively :(
check(BlueprintsToRecompileThisBatch.Num() == 0);
// Flush all pending render commands, as reloading the package may invalidate render resources.
FlushRenderingCommands();
}
if (InPackageReloadPhase == EPackageReloadPhase::PostBatchPreGC)
{
if (GEditor)
{
// Make sure we don't have any lingering transaction buffer references.
GEditor->ResetTransaction(NSLOCTEXT("UnrealEd", "ReloadedPackage", "Reloaded Package"));
}
// Recompile any BPs that had their references updated
if (BlueprintsToRecompileThisBatch.Num() > 0)
{
FScopedSlowTask CompilingBlueprintsSlowTask(static_cast<float>(BlueprintsToRecompileThisBatch.Num()), NSLOCTEXT("UnrealEd", "CompilingBlueprints", "Compiling Blueprints"));
// Gather up all loaded BP assets.
TArray<UObject*> BPs;
GetObjectsOfClass(UBlueprint::StaticClass(), BPs);
// Keeps track of BPs with a dependent cache set that's ready to be repopulated.
TSet<UBlueprint*> BPsWithResetDependentCache;
BPsWithResetDependentCache.Reserve(BPs.Num());
// Rebuild the dependency/dependent cache sets for each loaded BP.
for (UObject* BP : BPs)
{
UBlueprint* AsBP = CastChecked<UBlueprint>(BP);
// If this BP has been replaced, clear its dependency cache, but don't rebuild it.
if (AsBP->HasAnyFlags(RF_NewerVersionExists))
{
AsBP->CachedDependencies.Empty();
AsBP->CachedUDSDependencies.Empty();
AsBP->bCachedDependenciesUpToDate = true;
// Also clear out its dependent cache, as there will no longer be any dependencies on it.
AsBP->CachedDependents.Empty();
BPsWithResetDependentCache.Add(AsBP);
}
else
{
// Rebuild the dependency cache for the this BP.
AsBP->bCachedDependenciesUpToDate = false;
FBlueprintEditorUtils::EnsureCachedDependenciesUpToDate(AsBP);
}
// For each cached dependency, add this BP into its dependent cache set. Note that replaced
// BPs won't have any dependencies, and so will not be included in any dependent cache sets.
TSet<TWeakObjectPtr<UBlueprint>> LocalCopy_CachedDependencies = AsBP->CachedDependencies;
for (const TWeakObjectPtr<UBlueprint>& DependencyPtr : LocalCopy_CachedDependencies)
{
if (UBlueprint* Dependency = DependencyPtr.Get())
{
// Ensure that the dependency's cached dependent set has been cleared before we start adding to it.
if (!BPsWithResetDependentCache.Contains(Dependency))
{
Dependency->CachedDependents.Empty();
BPsWithResetDependentCache.Add(Dependency);
}
Dependency->CachedDependents.Add(AsBP);
}
else
{
// Remove any entries that cannot be resolved. Not likely, but just in case.
AsBP->CachedDependencies.Remove(DependencyPtr);
}
}
}
// Clear out remaining dependent cache sets that weren't already covered above (those not considered as a dependency and/or any replaced BPs).
if (BPs.Num() > BPsWithResetDependentCache.Num())
{
for (UObject* BP : BPs)
{
UBlueprint* AsBP = CastChecked<UBlueprint>(BP);
if (!BPsWithResetDependentCache.Contains(AsBP))
{
AsBP->CachedDependents.Empty();
BPsWithResetDependentCache.Add(AsBP);
}
}
}
// Sanity check that all BPs have reset their dependent cache sets, to assert that there are no stale references left behind.
check(BPs.Num() == BPsWithResetDependentCache.Num());
for (UBlueprint* BlueprintToRecompile : BlueprintsToRecompileThisBatch)
{
CompilingBlueprintsSlowTask.EnterProgressFrame(1.0f);
FKismetEditorUtilities::CompileBlueprint(BlueprintToRecompile, EBlueprintCompileOptions::SkipGarbageCollection);
}
}
BlueprintsToRecompileThisBatch.Reset();
}
if (InPackageReloadPhase == EPackageReloadPhase::PostBatchPostGC)
{
// Tick some things that aren't processed while we're reloading packages and can result in excessive memory usage if not periodically updated.
if (GShaderCompilingManager)
{
GShaderCompilingManager->ProcessAsyncResults(true, false);
}
if (GDistanceFieldAsyncQueue)
{
GDistanceFieldAsyncQueue->ProcessAsyncTasks();
}
if (GCardRepresentationAsyncQueue)
{
GCardRepresentationAsyncQueue->ProcessAsyncTasks();
}
}
}
/**
* Wrapper method for multiple objects at once.
*
* @param TopLevelPackages the packages to be export
* @param LastExportPath the path that the user last exported assets to
* @param FilteredClasses if specified, set of classes that should be the only types exported if not exporting to single file
* @param bUseProvidedExportPath If true, use LastExportPath as the user's export path w/o prompting for a directory, where applicable
*
* @return the path that the user chose for the export.
*/
FString UPackageTools::DoBulkExport(const TArray<UPackage*>& TopLevelPackages, FString LastExportPath, const TSet<UClass*>* FilteredClasses /* = NULL */, bool bUseProvidedExportPath/* = false*/ )
{
// Disallow export if any packages are cooked.
if (HandleFullyLoadingPackages( TopLevelPackages, NSLOCTEXT("UnrealEd", "BulkExportE", "Bulk Export...") ) )
{
TArray<UObject*> ObjectsInPackages;
GetObjectsInPackages(&TopLevelPackages, ObjectsInPackages);
// See if any filtering has been requested. Objects can be filtered by class and/or localization filter.
TArray<UObject*> FilteredObjects;
if ( FilteredClasses )
{
// Present the user with a warning that only the filtered types are being exported
FSuppressableWarningDialog::FSetupInfo Info( NSLOCTEXT("UnrealEd", "BulkExport_FilteredWarning", "Asset types are currently filtered within the Content Browser. Only objects of the filtered types will be exported."),
LOCTEXT("BulkExport_FilteredWarning_Title", "Asset Filter in Effect"), "BulkExportFilterWarning" );
Info.ConfirmText = NSLOCTEXT("ModalDialogs", "BulkExport_FilteredWarningConfirm", "Close");
FSuppressableWarningDialog PromptAboutFiltering( Info );
PromptAboutFiltering.ShowModal();
for ( TArray<UObject*>::TConstIterator ObjIter(ObjectsInPackages); ObjIter; ++ObjIter )
{
UObject* CurObj = *ObjIter;
// Only add the object if it passes all of the specified filters
if ( CurObj && FilteredClasses->Contains( CurObj->GetClass() ) )
{
FilteredObjects.Add( CurObj );
}
}
}
// If a filtered set was provided, export the filtered objects array; otherwise, export all objects in the packages
TArray<UObject*>& ObjectsToExport = FilteredClasses ? FilteredObjects : ObjectsInPackages;
// Prompt the user about how many objects will be exported before proceeding.
const bool bProceed = EAppReturnType::Yes == FMessageDialog::Open( EAppMsgType::YesNo, EAppReturnType::Yes, FText::Format(
NSLOCTEXT("UnrealEd", "Prompt_AboutToBulkExportNItems_F", "About to bulk export {0} items. Proceed?"), FText::AsNumber(ObjectsToExport.Num()) ) );
if ( bProceed )
{
FAssetToolsModule& AssetToolsModule = FModuleManager::GetModuleChecked<FAssetToolsModule>("AssetTools");
AssetToolsModule.Get().ExportAssets(ObjectsToExport, LastExportPath);
}
}
return LastExportPath;
}
void UPackageTools::CheckOutRootPackages( const TArray<UPackage*>& Packages )
{
if (ISourceControlModule::Get().IsEnabled())
{
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
// Update to the latest source control state.
SourceControlProvider.Execute(ISourceControlOperation::Create<FUpdateStatus>(), Packages);
TArray<FString> TouchedPackageNames;
bool bCheckedSomethingOut = false;
for( int32 PackageIndex = 0 ; PackageIndex < Packages.Num() ; ++PackageIndex )
{
UPackage* Package = Packages[PackageIndex];
FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(Package, EStateCacheUsage::Use);
if( SourceControlState.IsValid() && SourceControlState->CanCheckout() )
{
// The package is still available, so do the check out.
bCheckedSomethingOut = true;
TouchedPackageNames.Add(Package->GetName());
}
else
{
// The status on the package has changed to something inaccessible, so we have to disallow the check out.
// Don't warn if the file isn't in the depot.
if (SourceControlState.IsValid() && SourceControlState->IsSourceControlled())
{
FMessageDialog::Open( EAppMsgType::Ok, NSLOCTEXT("UnrealEd", "Error_PackageStatusChanged", "Package can't be checked out - status has changed!") );
}
}
}
// Synchronize source control state if something was checked out.
SourceControlProvider.Execute(ISourceControlOperation::Create<FCheckOut>(), SourceControlHelpers::PackageFilenames(TouchedPackageNames));
}
}
/**
* Checks if the passed in path is in an external directory. I.E Ones not found automatically in the content directory
*
* @param PackagePath Path of the package to check, relative or absolute
* @return true if PackagePath points to an external location
*/
bool UPackageTools::IsPackagePathExternal( const FString& PackagePath )
{
bool bIsExternal = true;
TArray< FString > Paths;
GConfig->GetArray( TEXT("Core.System"), TEXT("Paths"), Paths, GEngineIni );
FString PackageFilename = FPaths::ConvertRelativePathToFull(PackagePath);
// absolute path of the package that was passed in, without the actual name of the package
FString PackageFullPath = FPaths::GetPath(PackageFilename);
for(int32 pathIdx = 0; pathIdx < Paths.Num(); ++pathIdx)
{
FString AbsolutePathName = FPaths::ConvertRelativePathToFull(Paths[ pathIdx ]);
// check if the package path is within the list of paths the engine searches.
if( PackageFullPath.Contains( AbsolutePathName ) )
{
bIsExternal = false;
break;
}
}
return bIsExternal;
}
/**
* Checks if the passed in package's filename is in an external directory. I.E Ones not found automatically in the content directory
*
* @param Package The package to check
* @return true if the package points to an external filename
*/
bool UPackageTools::IsPackageExternal(const UPackage& Package)
{
FString FileString;
FPackageName::DoesPackageExist(Package.GetName(), &FileString);
return IsPackagePathExternal( FileString );
}
bool UPackageTools::SavePackagesForObjects(const TArray<UObject*>& ObjectsToSave)
{
// Retrieve all dirty packages for the objects
TArray<UPackage*> PackagesToSave;
for (UObject* Object : ObjectsToSave)
{
if (Object->GetOutermost()->IsDirty())
{
PackagesToSave.AddUnique(Object->GetOutermost());
}
}
const bool bCheckDirty = false;
const bool bPromptToSave = false;
const FEditorFileUtils::EPromptReturnCode Return = FEditorFileUtils::PromptForCheckoutAndSave(PackagesToSave, bCheckDirty, bPromptToSave);
return (PackagesToSave.Num() > 0) && Return == FEditorFileUtils::EPromptReturnCode::PR_Success;
}
bool UPackageTools::IsSingleAssetPackage(const FString& PackageName)
{
FString PackageFileName;
if ( FPackageName::DoesPackageExist(PackageName, &PackageFileName) )
{
return FPaths::GetExtension(PackageFileName, /*bIncludeDot=*/true) == FPackageName::GetAssetPackageExtension();
}
// If it wasn't found in the package file cache, this package does not yet
// exist so it is assumed to be saved as a UAsset file.
return true;
}
FString UPackageTools::SanitizePackageName(const FString& InPackageName)
{
FString SanitizedName = ObjectTools::SanitizeInvalidChars(InPackageName, INVALID_LONGPACKAGE_CHARACTERS);
// Coalesce multiple contiguous slashes into a single slash
int32 CharIndex = 0;
while (CharIndex < SanitizedName.Len())
{
if (SanitizedName[CharIndex] == TEXT('/'))
{
int32 SlashCount = 1;
while (CharIndex + SlashCount < SanitizedName.Len() &&
SanitizedName[CharIndex + SlashCount] == TEXT('/'))
{
SlashCount++;
}
if (SlashCount > 1)
{
SanitizedName.RemoveAt(CharIndex + 1, SlashCount - 1, EAllowShrinking::No);
}
}
CharIndex++;
}
return SanitizedName;
}
FString UPackageTools::PackageNameToFilename(const FString& InPackageName, const FString& Extension)
{
FString Result;
FPackageName::TryConvertLongPackageNameToFilename(InPackageName, Result, Extension);
return Result;
}
FString UPackageTools::FilenameToPackageName(const FString& InFilename)
{
FString Result;
FPackageName::TryConvertFilenameToLongPackageName(InFilename, Result);
return Result;
}
UPackage* UPackageTools::FindOrCreatePackageForAssetType(const FName LongPackageName, UClass* AssetClass)
{
if (AssetClass)
{
// Test the asset registry first
// Only scan the disk cache since it's fast O(1) (otherwise the asset registry will iterate on all the objects in memory)
constexpr bool bIncludeOnlyOnDiskAssets = true;
TArray<FAssetData> OutAssets;
IAssetRegistry& AssetRegistry = FAssetRegistryModule::GetRegistry();
AssetRegistry.GetAssetsByPackageName(LongPackageName, OutAssets, bIncludeOnlyOnDiskAssets);
bool bShouldGenerateUniquePackageName = false;
int32 NumberOfAssets = OutAssets.Num();
if (NumberOfAssets == 1)
{
const FAssetData& AssetData = OutAssets[0];
if (AssetData.AssetClassPath != AssetClass->GetClassPathName())
{
bShouldGenerateUniquePackageName = true;
}
}
else if (NumberOfAssets > 1)
{
// this shouldn't happen
bShouldGenerateUniquePackageName = true;
}
UPackage* Package = nullptr;
if (!bShouldGenerateUniquePackageName)
{
// Create or return the existing package
FString PackageNameAsString = LongPackageName.ToString();
Package = LoadPackage(PackageNameAsString);
if (Package)
{
UObject* Object = FindObjectFast<UObject>(Package, Package->GetFName());
if (Object && Object->GetClass() != AssetClass)
{
bShouldGenerateUniquePackageName = true;
}
}
else
{
Package = NewObject<UPackage>(nullptr, LongPackageName, RF_Public);
}
}
if (bShouldGenerateUniquePackageName)
{
FString NewPackageName = MakeUniqueObjectName(nullptr, UPackage::StaticClass(), LongPackageName).ToString();
Package = NewObject<UPackage>(nullptr, *NewPackageName, RF_Public);
}
return Package;
}
return nullptr;
}
#if WITH_DEV_AUTOMATION_TESTS
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FPackageToolsAutomationTest, "Editor.PackageTools.UnitTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FPackageToolsAutomationTest::RunTest(const FString& Parameters)
{
struct FTest
{
FString Input;
FString Output;
};
TArray<FTest> TestStrings =
{
{ TEXT("/Game/Blah/Boo"), TEXT("/Game/Blah/Boo") },
{ TEXT("/Game/Blah//Double"), TEXT("/Game/Blah/Double") },
{ TEXT(""), TEXT("") },
{ TEXT("/Game/Trailing/"), TEXT("/Game/Trailing/") },
{ TEXT("/Game/TrailingDouble//"), TEXT("/Game/TrailingDouble/") },
{ TEXT("/Game/Blah///Multiple"), TEXT("/Game/Blah/Multiple") },
{ TEXT("/Game/Blah///Multiple///"), TEXT("/Game/Blah/Multiple/") }
};
bool bSuccess = true;
for (const FTest& TestString : TestStrings)
{
FString Result = UPackageTools::SanitizePackageName(TestString.Input);
if (Result != TestString.Output)
{
AddError(FString::Printf(TEXT("SanitizePackageName failed (result = %s, expected = %s)"), *Result, *TestString.Output));
bSuccess = false;
}
}
return bSuccess;
}
#endif // WITH_DEV_AUTOMATION_TESTS
#undef LOCTEXT_NAMESPACE
// EOF