// Copyright Epic Games, Inc. All Rights Reserved. #include "PackageTools.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/EditorPerProjectUserSettings.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 "Widgets/Notifications/SNotificationList.h" #include "AssetData.h" #include "AssetRegistryModule.h" #include "ComponentReregisterContext.h" #include "Engine/BlueprintGeneratedClass.h" #include "Engine/GameEngine.h" #include "Engine/LevelStreaming.h" #include "Engine/MapBuildDataRegistry.h" #include "Engine/Selection.h" #include "IAssetRegistry.h" #include "Logging/MessageLog.h" #include "UObject/UObjectGlobals.h" #include "UObject/UObjectIterator.h" #include "ShaderCompiler.h" #include "DistanceFieldAtlas.h" #include "AssetToolsModule.h" #include "Subsystems/AssetEditorSubsystem.h" #define LOCTEXT_NAMESPACE "PackageTools" DEFINE_LOG_CATEGORY_STATIC(LogPackageTools, Log, All); /** State passed to RestoreStandaloneOnReachableObjects. */ UPackage* UPackageTools::PackageBeingUnloaded = nullptr; TMap UPackageTools::ObjectsThatHadFlagsCleared; FDelegateHandle UPackageTools::ReachabilityCallbackHandle; 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); ForEachObjectWithPackage(PackageBeingUnloaded, [](UObject* Object) { if (ObjectsThatHadFlagsCleared.Find(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; 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", "Package {0} is not fully loaded. Do you want to fully load it? Not doing so will abort the '{1}' operation."), FText::FromString(TopLevelPackage->GetName()), OperationText))) { // Fully load package. const FScopedBusyCursor BusyCursor; GWarn->BeginSlowTask(NSLOCTEXT("UnrealEd", "FullyLoadingPackages", "Fully loading packages"), 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; bool bResult = UnloadPackages(TopLevelPackages, ErrorMessage); if (!ErrorMessage.IsEmpty()) { FMessageDialog::Open(EAppMsgType::Ok, ErrorMessage); } return bResult; } bool UPackageTools::UnloadPackages(const TArray& TopLevelPackages, FText& OutErrorMessage) { bool bResult = false; // Get outermost packages, in case groups were selected. TArray 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 (int32 PackageIndex = 0; PackageIndex < TopLevelPackages.Num(); ++PackageIndex) { UPackage* Package = TopLevelPackages[PackageIndex]; if (Package != NULL) { if (Package->IsDirty()) { DirtyPackages.Add(Package); } else { PackagesToUnload.AddUnique(Package->GetOutermost() ? Package->GetOutermost() : Package); } } } // Inform the user that dirty packages won't be unloaded. if (DirtyPackages.Num() > 0) { FString DirtyPackagesList; for (int32 PackageIndex = 0; PackageIndex < DirtyPackages.Num(); ++PackageIndex) { DirtyPackagesList += FString::Printf(TEXT("\n %s"), *DirtyPackages[PackageIndex]->GetName()); } FFormatNamedArguments Args; Args.Add(TEXT("DirtyPackages"), FText::FromString(DirtyPackagesList)); 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 (UWorld* EditorWorld = GEditor->GetEditorWorldContext().World()) { // Is the currently loaded world being unloaded? If so, we just reset the current world. // We also need to skip the build data package as that will also be destroyed by the call to CreateNewMapForEditing. if (PackagesToUnload.Contains(EditorWorld->GetOutermost())) { // Remove the world package from the unload list PackagesToUnload.Remove(EditorWorld->GetOutermost()); // Remove the level build data package from the unload list as creating a new map will unload build data for the current world for (int32 LevelIndex = 0; LevelIndex < EditorWorld->GetNumLevels(); ++LevelIndex) { ULevel* Level = EditorWorld->GetLevel(LevelIndex); if (Level->MapBuildData) { PackagesToUnload.Remove(Level->MapBuildData->GetOutermost()); } } // Remove any streaming levels from the unload list as creating a new map will unload streaming levels for the current world for (ULevelStreaming* EditorStreamingLevel: EditorWorld->GetStreamingLevels()) { if (EditorStreamingLevel->IsLevelLoaded()) { UPackage* EditorStreamingLevelPackage = EditorStreamingLevel->GetLoadedLevel()->GetOutermost(); PackagesToUnload.Remove(EditorStreamingLevelPackage); } } // Unload the current world GEditor->CreateNewMapForEditing(); } } if (PackagesToUnload.Num() > 0) { const FScopedBusyCursor BusyCursor; // Complete any load/streaming requests, then lock IO. FlushAsyncLoading(); (*GFlushStreamingFunc)(); // Remove potential references to to-be deleted objects from the GB selection set. GEditor->GetSelectedObjects()->DeselectAll(); // 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); bool bScriptPackageWasUnloaded = false; GWarn->BeginSlowTask(NSLOCTEXT("UnrealEd", "Unloading", "Unloading"), true); // 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 (int32 PackageIndex = 0; PackageIndex < PackagesToUnload.Num(); ++PackageIndex) { UPackage* Pkg = PackagesToUnload[PackageIndex]; if (!Pkg->IsRooted()) { Pkg->AddToRoot(); PackagesAddedToRoot.Add(Pkg); } } // Now try to clean up assets in all packages to unload. for (int32 PackageIndex = 0; PackageIndex < PackagesToUnload.Num(); ++PackageIndex) { PackageBeingUnloaded = PackagesToUnload[PackageIndex]; GWarn->StatusUpdate(PackageIndex, PackagesToUnload.Num(), FText::Format(NSLOCTEXT("UnrealEd", "Unloadingf", "Unloading {0}..."), FText::FromString(PackageBeingUnloaded->GetName()))); // Flush all pending render commands, as unloading the package may invalidate render resources. FlushRenderingCommands(); // Close any open asset editors ForEachObjectWithPackage(PackageBeingUnloaded, [](UObject* Obj) { if (Obj->IsAsset()) { GEditor->GetEditorSubsystem()->CloseAllEditorsForAsset(Obj); } return true; }, false); PackageBeingUnloaded->bHasBeenFullyLoaded = false; PackageBeingUnloaded->ClearFlags(RF_WasLoaded); if (PackageBeingUnloaded->HasAnyPackageFlags(PKG_ContainsScript)) { bScriptPackageWasUnloaded = true; } // Clear RF_Standalone flag from objects in the package to be unloaded so they get GC'd. { TArray ObjectsInPackage; GetObjectsWithPackage(PackageBeingUnloaded, ObjectsInPackage); for (UObject* Object: ObjectsInPackage) { if (Object->HasAnyFlags(RF_Standalone)) { Object->ClearFlags(RF_Standalone); ObjectsThatHadFlagsCleared.Add(Object, Object); } } } // Reset loaders ResetLoaders(PackageBeingUnloaded); // Collect garbage. CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); 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); } // Cleanup. ObjectsThatHadFlagsCleared.Empty(); PackageBeingUnloaded = NULL; bResult = true; } // Now remove from root all the packages we added earlier so they may be GCed if possible for (int32 PackageIndex = 0; PackageIndex < PackagesAddedToRoot.Num(); ++PackageIndex) { PackagesAddedToRoot[PackageIndex]->RemoveFromRoot(); } PackagesAddedToRoot.Empty(); GWarn->EndSlowTask(); // Remove the post reachability callback. FCoreUObjectDelegates::PostReachabilityAnalysis.Remove(ReachabilityCallbackHandle); // Clear the standalone flag on metadata objects that are going to be GC'd below. // This resolves the circular dependency between metadata and packages. TArray> PackageMetaDataWithClearedStandaloneFlag; for (UPackage* PackageToUnload: PackagesToUnload) { UMetaData* PackageMetaData = PackageToUnload ? PackageToUnload->MetaData : nullptr; if (PackageMetaData && PackageMetaData->HasAnyFlags(RF_Standalone)) { PackageMetaData->ClearFlags(RF_Standalone); PackageMetaDataWithClearedStandaloneFlag.Add(PackageMetaData); } } CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); // Restore the standalone flag on any metadata objects that survived the GC for (const TWeakObjectPtr& WeakPackageMetaData: PackageMetaDataWithClearedStandaloneFlag) { UMetaData* MetaData = WeakPackageMetaData.Get(); if (MetaData) { MetaData->SetFlags(RF_Standalone); } } // 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); } bool UPackageTools::ReloadPackages(const TArray& TopLevelPackages, FText& OutErrorMessage, const EReloadPackagesInteractionMode InteractionMode) { bool bResult = false; FTextBuilder ErrorMessageBuilder; // Split the set of selected top level packages into packages which are dirty or in-memory (and thus cannot be reloaded) and packages that are not dirty (and thus can be reloaded). TArray PackagesToReload; { TArray DirtyPackages; TArray InMemoryPackages; for (UPackage* TopLevelPackage: TopLevelPackages) { if (TopLevelPackage) { // Get outermost packages, in case groups were selected. UPackage* RealPackage = TopLevelPackage->GetOutermost() ? TopLevelPackage->GetOutermost() : TopLevelPackage; if (RealPackage->HasAnyPackageFlags(PKG_InMemoryOnly)) { InMemoryPackages.AddUnique(RealPackage); } else if (RealPackage->IsDirty()) { DirtyPackages.AddUnique(RealPackage); } else { PackagesToReload.AddUnique(RealPackage); } } // How should we handle locally dirty packages? if (DirtyPackages.Num() > 0) { 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 packages have been modified:")); { ReloadDirtyPackagesMsgBuilder.Indent(); for (UPackage* DirtyPackage: DirtyPackages) { ReloadDirtyPackagesMsgBuilder.AppendLine(DirtyPackage->GetFName()); } ReloadDirtyPackagesMsgBuilder.Unindent(); } ReloadDirtyPackagesMsgBuilder.AppendLine(NSLOCTEXT("UnrealEd", "ShouldReloadDirtyPackagesFooter", "Would you like to reload these packages? 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: DirtyPackages) { DirtyPackage->SetDirtyFlag(false); PackagesToReload.AddUnique(DirtyPackage); } DirtyPackages.Reset(); } } } // Inform the user that dirty packages won't be reloaded. if (DirtyPackages.Num() > 0) { if (!ErrorMessageBuilder.IsEmpty()) { ErrorMessageBuilder.AppendLine(); } ErrorMessageBuilder.AppendLine(NSLOCTEXT("UnrealEd", "Error_ReloadDirtyPackagesHeader", "The following packages have been modified and cannot be reloaded:")); { ErrorMessageBuilder.Indent(); for (UPackage* DirtyPackage: DirtyPackages) { ErrorMessageBuilder.AppendLine(DirtyPackage->GetFName()); } ErrorMessageBuilder.Unindent(); } ErrorMessageBuilder.AppendLine(NSLOCTEXT("UnrealEd", "Error_ReloadDirtyPackagesFooter", "Saving these packages will allow them to be reloaded.")); } // Inform the user that in-memory packages won't be reloaded. if (InMemoryPackages.Num() > 0) { if (!ErrorMessageBuilder.IsEmpty()) { ErrorMessageBuilder.AppendLine(); } ErrorMessageBuilder.AppendLine(NSLOCTEXT("UnrealEd", "Error_ReloadInMemoryPackagesHeader", "The following packages are in-memory only and cannot be reloaded:")); { ErrorMessageBuilder.Indent(); for (UPackage* InMemoryPackage: InMemoryPackages) { ErrorMessageBuilder.AppendLine(InMemoryPackage->GetFName()); } ErrorMessageBuilder.Unindent(); } } } // Get the current world. TWeakObjectPtr CurrentWorld; if (GIsEditor) { if (UWorld* EditorWorld = GEditor->GetEditorWorldContext().World()) { CurrentWorld = EditorWorld; } } else if (UGameEngine* GameEngine = Cast(GEngine)) { if (UWorld* GameWorld = GameEngine->GetGameWorld()) { CurrentWorld = GameWorld; } } // Check to see if we need to reload the current world. FName WorldNameToReload; 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 (PackagesToReload.Contains(CurrentWorldPtr->GetOutermost())) { // Cache this so we can reload the world later WorldNameToReload = *CurrentWorldPtr->GetPathName(); // Remove the world package from the reload list PackagesToReload.Remove(CurrentWorldPtr->GetOutermost()); // Remove the level build data package from the reload list as creating a new map will unload build data for the current world for (int32 LevelIndex = 0; LevelIndex < CurrentWorldPtr->GetNumLevels(); ++LevelIndex) { ULevel* Level = CurrentWorldPtr->GetLevel(LevelIndex); if (Level->MapBuildData) { PackagesToReload.Remove(Level->MapBuildData->GetOutermost()); } } // Remove any streaming levels from the reload list as creating a new map will unload streaming levels for the current world for (ULevelStreaming* StreamingLevel: CurrentWorldPtr->GetStreamingLevels()) { if (StreamingLevel->IsLevelLoaded()) { UPackage* StreamingLevelPackage = StreamingLevel->GetLoadedLevel()->GetOutermost(); PackagesToReload.Remove(StreamingLevelPackage); } } // Unload the current world if (GIsEditor) { 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(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 (PackagesToReload.Contains(Level->GetOutermost())) { for (ULevelStreaming* StreamingLevel: CurrentWorldPtr->GetStreamingLevels()) { if (StreamingLevel->GetLoadedLevel() == Level) { CurrentWorldPtr->RemoveFromWorld(Level); StreamingLevel->RemoveLevelFromCollectionForReload(); RemovedStreamingLevels.Add(StreamingLevel); break; } } } } } } 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 packages failed to reload:")); { ErrorMessageBuilder.Indent(); for (UPackage* FailedPackage: FailedPackages) { ErrorMessageBuilder.AppendLine(FailedPackage->GetFName()); } 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) { TArray WorldNamesToReload; WorldNamesToReload.Add(WorldNameToReload); GEditor->GetEditorSubsystem()->OpenEditorsForAssets(WorldNamesToReload); } 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(); 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. ForEachObjectWithPackage(InPackageReloadedEvent->GetOldPackage(), [&](UObject* InObject) { if (UBlueprint* BP = Cast(InObject)) { BP->ClearEditorReferences(); } return true; }, false, RF_Transient, EInternalObjectFlags::PendingKill); } 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(OldBlueprint->GeneratedClass, CastChecked(NewObject)->GeneratedClass); ReplaceInstancesParameters.InstancesThatShouldUseOldClass = &InstancesToLeaveAlone; FBlueprintCompileReinstancer::ReplaceInstancesOfClassEx(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; } 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(BlueprintsToRecompileThisBatch.Num(), NSLOCTEXT("UnrealEd", "CompilingBlueprints", "Compiling Blueprints")); TArray BPs; GetObjectsOfClass(UBlueprint::StaticClass(), BPs); for (UObject* BP: BPs) { UBlueprint* AsBP = CastChecked(BP); AsBP->bCachedDependenciesUpToDate = false; FBlueprintEditorUtils::EnsureCachedDependenciesUpToDate(AsBP); for (TWeakObjectPtr Dependent: AsBP->CachedDependents) { if (UBlueprint* StillAlive = Dependent.Get()) { StillAlive->CachedDependencies.Add(AsBP); } } for (TWeakObjectPtr Dependent: AsBP->CachedDependencies) { if (UBlueprint* StillAlive = Dependent.Get()) { StillAlive->CachedDependents.Add(AsBP); } } } 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(); } } } /** * 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 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(), NULL, &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, NULL, &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); // Remove double-slashes SanitizedName.ReplaceInline(TEXT("//"), TEXT("/")); return SanitizedName; } 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.AssetClass != AssetClass->GetFName()) { bShouldGenerateUniquePackageName = true; } } else if (NumberOfAssets > 1) { // this shouldn't happen in UE4 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; } #undef LOCTEXT_NAMESPACE // EOF