// 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* UPackageTools::PackagesBeingUnloaded = nullptr; TSet UPackageTools::ObjectsThatHadFlagsCleared; FDelegateHandle UPackageTools::ReachabilityCallbackHandle; void UPackageTools::FlushAsyncCompilation(TArrayView InPackages) { TArray ObjectsToFinish; for (const UPackage* Package : InPackages) { ForEachObjectWithPackage(Package, [&ObjectsToFinish](UObject* Object) { if (const IInterface_AsyncCompilation* AsyncCompilationIF = Cast(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& Params) { TArray 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& 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()) { // 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* InPackages, TArray& OutObjects ) { if (InPackages) { for (UPackage* Package : *InPackages) { ForEachObjectWithPackage(Package,[&OutObjects](UObject* Obj) { if (ObjectTools::IsObjectBrowsable(Obj)) { OutObjects.Add(Obj); } return true; }); } } else { for (TObjectIterator It; It; ++It) { UObject* Obj = *It; if (ObjectTools::IsObjectBrowsable(Obj)) { OutObjects.Add(Obj); } } } } bool UPackageTools::HandleFullyLoadingPackages( const TArray& TopLevelPackages, const FText& OperationText ) { bool bSuccessfullyCompleted = true; // whether or not to suppress the ask to fully load message bool bSuppress = GetDefault()->bSuppressFullyLoadPrompt; // Make sure they are all fully loaded. bool bNeedsUpdate = false; for( int32 PackageIndex=0; PackageIndexGetOuter() == 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& TopLevelPackages ) { FText ErrorMessage; const bool bResult = UnloadPackages(TopLevelPackages, ErrorMessage); if(!ErrorMessage.IsEmpty()) { FMessageDialog::Open( EAppMsgType::Ok, ErrorMessage ); } return bResult; } bool UPackageTools::UnloadPackages(const TArray& 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 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 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> 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& 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 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 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()->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(Obj)) { BP->ClearEditorReferences(); // Remove from cached dependent lists. for (const TWeakObjectPtr Dependency : BP->CachedDependencies) { if (UBlueprint* ResolvedDependency = Dependency.Get()) { ResolvedDependency->CachedDependents.Remove(BP); } } BP->CachedDependencies.Reset(); // Remove from cached dependency lists. for (const TWeakObjectPtr Dependent : BP->CachedDependents) { if (UBlueprint* ResolvedDependent = Dependent.Get()) { ResolvedDependent->CachedDependencies.Remove(BP); } } BP->CachedDependents.Reset(); } else if (UBlueprintGeneratedClass* BPGC = Cast(Obj)) { FKismetEditorUtilities::OnBlueprintGeneratedClassUnloaded.Broadcast(BPGC); } else if (UWorld* World = Cast(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(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& 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& 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 PackagesToReload; TArray DirtyPackages; TArray InMemoryPackages; }; FFilteredPackages GetPackagesToReload(const TArray& 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 { return InPackage; }); } ~FScopedTrackFilteredPackages() { Filtered.PackagesToReload.Reset(); Algo::TransformIf(WeakPackagesToReload, Filtered.PackagesToReload, [](TWeakObjectPtr InPackage) {return InPackage.IsValid();}, [](TWeakObjectPtr InPackage) -> UPackage* {return InPackage.Get();}); } TArray> WeakPackagesToReload; FFilteredPackages& Filtered; }; TWeakObjectPtr GetCurrentWorld() { if (GIsEditor) { if (UWorld* EditorWorld = GEditor->GetEditorWorldContext().World()) { return EditorWorld; } } else if (UGameEngine* GameEngine = Cast(GEngine)) { if (UWorld* GameWorld = GameEngine->GetGameWorld()) { return GameWorld; } } return nullptr; } } bool UPackageTools::ReloadPackages( const TArray& TopLevelPackages, FText& OutErrorMessage, const EReloadPackagesInteractionMode InteractionMode ) { bool bResult = false; TGuardValueAccessors 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 CurrentWorld = GetCurrentWorld(); // Check to see if we need to reload the current world. FName WorldNameToReload; FName CurrentWorldPackageName; TArray 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(GEngine)) { // Outside of the editor we need to keep the packages alive to stop the world transition from GC'ing them TGCObjectsScopeGuard 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& 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& 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 PackagesToReloadData; PackagesToReloadData.Reserve(PackagesToReload.Num()); for (UPackage* PackageToReload : PackagesToReload) { bScriptPackageWasReloaded |= PackageToReload->HasAnyPackageFlags(PKG_ContainsScript); PackagesToReloadData.Emplace(PackageToReload, LOAD_None); } TArray ReloadedPackages; ::ReloadPackages(PackagesToReloadData, ReloadedPackages, 500); TArray 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 WorldNamesToReload; WorldNamesToReload.Add(WorldNameToReload); GEditor->GetEditorSubsystem()->OpenEditorsForAssets(WorldNamesToReload); UWorld::WorldTypePreLoadMap.Remove(CurrentWorldPackageName); } else if (UGameEngine* GameEngine = Cast(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 BlueprintsToRecompileThisBatch; if (InPackageReloadPhase == EPackageReloadPhase::PrePackageFixup) { GEngine->NotifyToolsOfObjectReplacement(InPackageReloadedEvent->GetRepointedObjects()); // Notify any Blueprints that are about to be unloaded, and destroy any leftover worlds. TArray Objects; GetObjectsWithPackage(InPackageReloadedEvent->GetOldPackage(), Objects, true, RF_Transient, EInternalObjectFlags::Garbage); for (UObject* InObject : Objects) { if (UBlueprint* BP = Cast(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 Dependency : BP->CachedDependencies) { if (UBlueprint* ResolvedDependency = Dependency.Get()) { ResolvedDependency->CachedDependents.Remove(BP); } } } if (UWorld* World = Cast(InObject)) { if (World->bIsWorldInitialized) { World->CleanupWorld(); } } } } if (InPackageReloadPhase == EPackageReloadPhase::OnPackageFixup) { TMap 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(OldObject); if(OldObjectAsClass) { UClass* NewObjectAsClass = Cast(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(OldObject)) { if (NewObject && CastChecked(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 OldInstances; GetObjectsOfClass(OldBlueprint->GeneratedClass, OldInstances, false); OldInstances.RemoveAllSwap( [](UObject* Obj){ return !Obj->HasAnyFlags(RF_NewerVersionExists); } ); TSet InstancesToLeaveAlone(OldInstances); FReplaceInstancesOfClassParameters ReplaceInstancesParameters; ReplaceInstancesParameters.InstancesThatShouldUseOldClass = &InstancesToLeaveAlone; FBlueprintCompileReinstancer::ReplaceInstancesOfClass(OldBlueprint->GeneratedClass, CastChecked(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 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(ObjectReferencerPtr)) { BlueprintToRecompile = BlueprintReferencer; } else if (UClass* ClassReferencer = Cast(ObjectReferencerPtr)) { BlueprintToRecompile = Cast(ClassReferencer->ClassGeneratedBy); } else { BlueprintToRecompile = ObjectReferencerPtr->GetTypedOuter(); } 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(BlueprintsToRecompileThisBatch.Num()), NSLOCTEXT("UnrealEd", "CompilingBlueprints", "Compiling Blueprints")); // Gather up all loaded BP assets. TArray BPs; GetObjectsOfClass(UBlueprint::StaticClass(), BPs); // Keeps track of BPs with a dependent cache set that's ready to be repopulated. TSet BPsWithResetDependentCache; BPsWithResetDependentCache.Reserve(BPs.Num()); // Rebuild the dependency/dependent cache sets for each loaded BP. for (UObject* BP : BPs) { UBlueprint* AsBP = CastChecked(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> LocalCopy_CachedDependencies = AsBP->CachedDependencies; for (const TWeakObjectPtr& 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(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& TopLevelPackages, FString LastExportPath, const TSet* FilteredClasses /* = NULL */, bool bUseProvidedExportPath/* = false*/ ) { // Disallow export if any packages are cooked. if (HandleFullyLoadingPackages( TopLevelPackages, NSLOCTEXT("UnrealEd", "BulkExportE", "Bulk Export...") ) ) { TArray ObjectsInPackages; GetObjectsInPackages(&TopLevelPackages, ObjectsInPackages); // See if any filtering has been requested. Objects can be filtered by class and/or localization filter. TArray 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::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& 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("AssetTools"); AssetToolsModule.Get().ExportAssets(ObjectsToExport, LastExportPath); } } return LastExportPath; } void UPackageTools::CheckOutRootPackages( const TArray& Packages ) { if (ISourceControlModule::Get().IsEnabled()) { ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); // Update to the latest source control state. SourceControlProvider.Execute(ISourceControlOperation::Create(), Packages); TArray 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(), 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& ObjectsToSave) { // Retrieve all dirty packages for the objects TArray 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 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(Package, Package->GetFName()); if (Object && Object->GetClass() != AssetClass) { bShouldGenerateUniquePackageName = true; } } else { Package = NewObject(nullptr, LongPackageName, RF_Public); } } if (bShouldGenerateUniquePackageName) { FString NewPackageName = MakeUniqueObjectName(nullptr, UPackage::StaticClass(), LongPackageName).ToString(); Package = NewObject(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 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