void FPathContextMenu::ExecuteSaveFolder() { // Get a list of package names in the selected paths TArray<FString> PackageNames; GetPackageNamesInSelectedPaths(PackageNames); // Form a list of packages from the assets TArray<UPackage*> Packages; for (int32 PackageIdx = 0; PackageIdx < PackageNames.Num(); ++PackageIdx) { UPackage* Package = FindPackage(NULL, *PackageNames[PackageIdx]); // Only save loaded and dirty packages if ( Package != NULL && Package->IsDirty() ) { Packages.Add(Package); } } // Save all packages that were found if ( Packages.Num() ) { ContentBrowserUtils::SavePackages(Packages); } }
bool FFrontendFilter_Modified::PassesFilter(FAssetFilterType InItem) const { UPackage* Package = FindPackage(NULL, *InItem.PackageName.ToString()); if ( Package != NULL ) { return Package->IsDirty(); } return false; }
/** * Finds the outermost package and marks it dirty */ bool UObjectBaseUtility::MarkPackageDirty() const { // since transient objects will never be saved into a package, there is no need to mark a package dirty // if we're transient if ( !HasAnyFlags(RF_Transient) ) { UPackage* Package = GetOutermost(); if( Package != NULL ) { // It is against policy to dirty a map or package during load in the Editor, to enforce this policy // we explicitly disable the ability to dirty a package or map during load. Commandlets can still // set the dirty state on load. if( IsRunningCommandlet() || (GIsEditor && !GIsEditorLoadingPackage && !GIsPlayInEditorWorld && !IsInAsyncLoadingThread() #if WITH_HOT_RELOAD && !GIsHotReload #endif // WITH_HOT_RELOAD #if WITH_EDITORONLY_DATA && !Package->bIsCookedForEditor // Cooked packages can't be modified nor marked as dirty #endif )) { const bool bIsDirty = Package->IsDirty(); // We prevent needless re-dirtying as this can be an expensive operation. if( !bIsDirty ) { Package->SetDirtyFlag(true); } // Always call PackageMarkedDirtyEvent, even when the package is already dirty Package->PackageMarkedDirtyEvent.Broadcast(Package, bIsDirty); return true; } else { // notify the caller that the request to mark the package as dirty was suppressed return false; } } } return true; }
void USoundClassGraph::RebuildGraph() { check(RootSoundClass); // Don't allow initial graph rebuild to affect package dirty state; remember current state... UPackage* Package = GetOutermost(); const bool bIsDirty = Package->IsDirty(); Modify(); RemoveAllNodes(); ConstructNodes(RootSoundClass, 0, 0); NotifyGraphChanged(); // ...and restore it Package->SetDirtyFlag(bIsDirty); }
/** * Finds the outermost package and marks it dirty */ void UObjectBaseUtility::MarkPackageDirty() const { // since transient objects will never be saved into a package, there is no need to mark a package dirty // if we're transient if ( !HasAnyFlags(RF_Transient) ) { UPackage* Package = GetOutermost(); if( Package != NULL ) { // It is against policy to dirty a map or package during load in the Editor, to enforce this policy // we explicitly disable the ability to dirty a package or map during load. Commandlets can still // set the dirty state on load. // We also prevent needless re-dirtying as this can be an expensive operation. if( !Package->IsDirty() && (!GIsEditor || IsRunningCommandlet() || (GIsEditor && !GIsEditorLoadingPackage))) { Package->SetDirtyFlag(true); } } } }
void UWorldComposition::RestoreDirtyTilesInfo(const FTilesList& TilesPrevState) { if (!TilesPrevState.Num()) { return; } for (FWorldCompositionTile& Tile : Tiles) { UPackage* LevelPackage = Cast<UPackage>(StaticFindObjectFast(UPackage::StaticClass(), NULL, Tile.PackageName)); if (LevelPackage && LevelPackage->IsDirty()) { auto* FoundTile = TilesPrevState.FindByPredicate([=](const FWorldCompositionTile& TilePrev) { return TilePrev.PackageName == Tile.PackageName; }); if (FoundTile) { Tile.Info = FoundTile->Info; } } } }
int32 UGatherTextFromAssetsCommandlet::Main(const FString& Params) { // Parse command line. TArray<FString> Tokens; TArray<FString> Switches; TMap<FString, FString> ParamVals; UCommandlet::ParseCommandLine(*Params, Tokens, Switches, ParamVals); //Set config file const FString* ParamVal = ParamVals.Find(FString(TEXT("Config"))); FString GatherTextConfigPath; if ( ParamVal ) { GatherTextConfigPath = *ParamVal; } else { UE_LOG(LogGatherTextFromAssetsCommandlet, Error, TEXT("No config specified.")); return -1; } //Set config section ParamVal = ParamVals.Find(FString(TEXT("Section"))); FString SectionName; if ( ParamVal ) { SectionName = *ParamVal; } else { UE_LOG(LogGatherTextFromAssetsCommandlet, Error, TEXT("No config section specified.")); return -1; } //Modules to Preload TArray<FString> ModulesToPreload; GetStringArrayFromConfig(*SectionName, TEXT("ModulesToPreload"), ModulesToPreload, GatherTextConfigPath); for (const FString& ModuleName : ModulesToPreload) { FModuleManager::Get().LoadModule(*ModuleName); } // IncludePathFilters TArray<FString> IncludePathFilters; GetPathArrayFromConfig(*SectionName, TEXT("IncludePathFilters"), IncludePathFilters, GatherTextConfigPath); // IncludePaths (DEPRECATED) { TArray<FString> IncludePaths; GetPathArrayFromConfig(*SectionName, TEXT("IncludePaths"), IncludePaths, GatherTextConfigPath); if (IncludePaths.Num()) { IncludePathFilters.Append(IncludePaths); UE_LOG(LogGatherTextFromAssetsCommandlet, Warning, TEXT("IncludePaths detected in section %s. IncludePaths is deprecated, please use IncludePathFilters."), *SectionName); } } if (IncludePathFilters.Num() == 0) { UE_LOG(LogGatherTextFromAssetsCommandlet, Error, TEXT("No include path filters in section %s."), *SectionName); return -1; } // ExcludePathFilters TArray<FString> ExcludePathFilters; GetPathArrayFromConfig(*SectionName, TEXT("ExcludePathFilters"), ExcludePathFilters, GatherTextConfigPath); // ExcludePaths (DEPRECATED) { TArray<FString> ExcludePaths; GetPathArrayFromConfig(*SectionName, TEXT("ExcludePaths"), ExcludePaths, GatherTextConfigPath); if (ExcludePaths.Num()) { ExcludePathFilters.Append(ExcludePaths); UE_LOG(LogGatherTextFromAssetsCommandlet, Warning, TEXT("ExcludePaths detected in section %s. ExcludePaths is deprecated, please use ExcludePathFilters."), *SectionName); } } // PackageNameFilters TArray<FString> PackageFileNameFilters; GetStringArrayFromConfig(*SectionName, TEXT("PackageFileNameFilters"), PackageFileNameFilters, GatherTextConfigPath); // PackageExtensions (DEPRECATED) { TArray<FString> PackageExtensions; GetStringArrayFromConfig(*SectionName, TEXT("PackageExtensions"), PackageExtensions, GatherTextConfigPath); if (PackageExtensions.Num()) { PackageFileNameFilters.Append(PackageExtensions); UE_LOG(LogGatherTextFromAssetsCommandlet, Warning, TEXT("PackageExtensions detected in section %s. PackageExtensions is deprecated, please use PackageFileNameFilters."), *SectionName); } } if (PackageFileNameFilters.Num() == 0) { UE_LOG(LogGatherTextFromAssetsCommandlet, Error, TEXT("No package file name filters in section %s."), *SectionName); return -1; } //asset class exclude TArray<FString> ExcludeClasses; GetStringArrayFromConfig(*SectionName, TEXT("ExcludeClasses"), ExcludeClasses, GatherTextConfigPath); FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")); AssetRegistryModule.Get().SearchAllAssets( true ); FARFilter Filter; for(const auto& ExcludeClass : ExcludeClasses) { UClass* FilterClass = FindObject<UClass>(ANY_PACKAGE, *ExcludeClass); if(FilterClass) { Filter.ClassNames.Add( FilterClass->GetFName() ); } else { UE_LOG(LogGatherTextFromAssetsCommandlet, Warning, TEXT("Invalid exclude class %s"), *ExcludeClass); } } TArray<FAssetData> AssetDataArray; AssetRegistryModule.Get().GetAssets(Filter, AssetDataArray); FString UAssetPackageExtension = FPackageName::GetAssetPackageExtension(); TSet< FString > LongPackageNamesToExclude; for (int Index = 0; Index < AssetDataArray.Num(); Index++) { LongPackageNamesToExclude.Add( FPackageName::LongPackageNameToFilename( AssetDataArray[Index].PackageName.ToString(), UAssetPackageExtension ) ); } //Get whether we should fix broken properties that we find. if (!GetBoolFromConfig(*SectionName, TEXT("bFixBroken"), bFixBroken, GatherTextConfigPath)) { bFixBroken = false; } // Get whether we should gather editor-only data. Typically only useful for the localization of UE4 itself. if (!GetBoolFromConfig(*SectionName, TEXT("ShouldGatherFromEditorOnlyData"), ShouldGatherFromEditorOnlyData, GatherTextConfigPath)) { ShouldGatherFromEditorOnlyData = false; } // Add any manifest dependencies if they were provided TArray<FString> ManifestDependenciesList; GetPathArrayFromConfig(*SectionName, TEXT("ManifestDependencies"), ManifestDependenciesList, GatherTextConfigPath); if( !ManifestInfo->AddManifestDependencies( ManifestDependenciesList ) ) { UE_LOG(LogGatherTextFromAssetsCommandlet, Error, TEXT("The GatherTextFromAssets commandlet couldn't find all the specified manifest dependencies.")); return -1; } //The main array of files to work from. TArray< FString > PackageFileNamesToProcess; TArray<FString> PackageFilesNotInIncludePath; TArray<FString> PackageFilesInExcludePath; TArray<FString> PackageFilesExcludedByClass; //Fill the list of packages to work from. uint8 PackageFilter = NORMALIZE_DefaultFlags; TArray<FString> Unused; for ( int32 PackageFilenameWildcardIdx = 0; PackageFilenameWildcardIdx < PackageFileNameFilters.Num(); PackageFilenameWildcardIdx++ ) { const bool IsAssetPackage = PackageFileNameFilters[PackageFilenameWildcardIdx] == ( FString( TEXT("*") )+ FPackageName::GetAssetPackageExtension() ); TArray<FString> PackageFiles; if ( !NormalizePackageNames( Unused, PackageFiles, PackageFileNameFilters[PackageFilenameWildcardIdx], PackageFilter) ) { UE_LOG(LogGatherTextFromAssetsCommandlet, Display, TEXT("No packages found with extension %i: '%s'"), PackageFilenameWildcardIdx, *PackageFileNameFilters[PackageFilenameWildcardIdx]); continue; } else { UE_LOG(LogGatherTextFromAssetsCommandlet, Display, TEXT("Found %i packages with extension %i: '%s'"), PackageFiles.Num(), PackageFilenameWildcardIdx, *PackageFileNameFilters[PackageFilenameWildcardIdx]); } //Run through all the files found and add any that pass the include, exclude and filter constraints to OrderedPackageFilesToLoad for (FString& PackageFile : PackageFiles) { PackageFile = FPaths::ConvertRelativePathToFull(PackageFile); bool bExclude = false; //Ensure it matches the include paths if there are some. for (FString& IncludePath : IncludePathFilters) { bExclude = true; if( PackageFile.MatchesWildcard(IncludePath) ) { bExclude = false; break; } } if ( bExclude ) { PackageFilesNotInIncludePath.Add(PackageFile); } //Ensure it does not match the exclude paths if there are some. for (const FString& ExcludePath : ExcludePathFilters) { if (PackageFile.MatchesWildcard(ExcludePath)) { bExclude = true; PackageFilesInExcludePath.Add(PackageFile); break; } } //Check that this is not on the list of packages that we don't care about e.g. textures. if ( !bExclude && IsAssetPackage && LongPackageNamesToExclude.Contains( PackageFile ) ) { bExclude = true; PackageFilesExcludedByClass.Add(PackageFile); } //If we haven't failed one of the above checks, add it to the array of packages to process. if(!bExclude) { PackageFileNamesToProcess.Add(PackageFile); } } } if ( PackageFileNamesToProcess.Num() == 0 ) { UE_LOG(LogGatherTextFromAssetsCommandlet, Warning, TEXT("No files found or none passed the include/exclude criteria.")); } bool bSkipGatherCache = FParse::Param(FCommandLine::Get(), TEXT("SkipGatherCache")); if (!bSkipGatherCache) { GetBoolFromConfig(*SectionName, TEXT("SkipGatherCache"), bSkipGatherCache, GatherTextConfigPath); } UE_LOG(LogGatherTextFromAssetsCommandlet, Log, TEXT("SkipGatherCache: %s"), bSkipGatherCache ? TEXT("true") : TEXT("false")); TArray< FString > PackageFileNamesToLoad; for (FString& PackageFile : PackageFileNamesToProcess) { TScopedPointer< FArchive > FileReader( IFileManager::Get().CreateFileReader( *PackageFile ) ); if( FileReader ) { // Read package file summary from the file FPackageFileSummary PackageFileSummary; (*FileReader) << PackageFileSummary; bool MustLoadForGather = false; // Have we been asked to skip the cache of text that exists in the header of newer packages? if (bSkipGatherCache && PackageFileSummary.GetFileVersionUE4() >= VER_UE4_SERIALIZE_TEXT_IN_PACKAGES) { // Fallback on the old package flag check. if (PackageFileSummary.PackageFlags & PKG_RequiresLocalizationGather) { MustLoadForGather = true; } } const FCustomVersion* const EditorVersion = PackageFileSummary.GetCustomVersionContainer().GetVersion(FEditorObjectVersion::GUID); // Packages not resaved since localization gathering flagging was added to packages must be loaded. if (PackageFileSummary.GetFileVersionUE4() < VER_UE4_PACKAGE_REQUIRES_LOCALIZATION_GATHER_FLAGGING) { MustLoadForGather = true; } // Package not resaved since gatherable text data was added to package headers must be loaded, since their package header won't contain pregathered text data. else if (PackageFileSummary.GetFileVersionUE4() < VER_UE4_SERIALIZE_TEXT_IN_PACKAGES) { // Fallback on the old package flag check. if (PackageFileSummary.PackageFlags & PKG_RequiresLocalizationGather) { MustLoadForGather = true; } } else if (PackageFileSummary.GetFileVersionUE4() < VER_UE4_DIALOGUE_WAVE_NAMESPACE_AND_CONTEXT_CHANGES) { IAssetRegistry& AssetRegistry = AssetRegistryModule.Get(); TArray<FAssetData> AssetDataInPackage; AssetRegistry.GetAssetsByPackageName(*FPackageName::FilenameToLongPackageName(PackageFile), AssetDataInPackage); for (const FAssetData& AssetData : AssetDataInPackage) { if (AssetData.AssetClass == UDialogueWave::StaticClass()->GetFName()) { MustLoadForGather = true; } } } // Add package to list of packages to load fully and process. if (MustLoadForGather) { PackageFileNamesToLoad.Add(PackageFile); } // Process immediately packages that don't require loading to process. else if (PackageFileSummary.GatherableTextDataOffset > 0) { FileReader->Seek(PackageFileSummary.GatherableTextDataOffset); TArray<FGatherableTextData> GatherableTextDataArray; GatherableTextDataArray.SetNum(PackageFileSummary.GatherableTextDataCount); for (int32 GatherableTextDataIndex = 0; GatherableTextDataIndex < PackageFileSummary.GatherableTextDataCount; ++GatherableTextDataIndex) { (*FileReader) << GatherableTextDataArray[GatherableTextDataIndex]; } ProcessGatherableTextDataArray(PackageFile, GatherableTextDataArray); } } } CollectGarbage(RF_NoFlags); //Now go through the remaining packages in the main array and process them in batches. int32 PackagesPerBatchCount = 100; TArray< UPackage* > LoadedPackages; TArray< FString > LoadedPackageFileNames; TArray< FString > FailedPackageFileNames; TArray< UPackage* > LoadedPackagesToProcess; const int32 PackageCount = PackageFileNamesToLoad.Num(); const int32 BatchCount = PackageCount / PackagesPerBatchCount + (PackageCount % PackagesPerBatchCount > 0 ? 1 : 0); // Add an extra batch for any remainder if necessary if(PackageCount > 0) { UE_LOG(LogGatherTextFromAssetsCommandlet, Log, TEXT("Loading %i packages in %i batches of %i."), PackageCount, BatchCount, PackagesPerBatchCount); } FLoadPackageLogOutputRedirector LogOutputRedirector; //Load the packages in batches int32 PackageIndex = 0; for( int32 BatchIndex = 0; BatchIndex < BatchCount; ++BatchIndex ) { int32 PackagesInThisBatch = 0; int32 FailuresInThisBatch = 0; for( ; PackageIndex < PackageCount && PackagesInThisBatch < PackagesPerBatchCount; ++PackageIndex ) { FString PackageFileName = PackageFileNamesToLoad[PackageIndex]; UE_LOG(LogGatherTextFromAssetsCommandlet, Verbose, TEXT("Loading package: '%s'."), *PackageFileName); UPackage *Package = nullptr; { FString LongPackageName; if (!FPackageName::TryConvertFilenameToLongPackageName(PackageFileName, LongPackageName)) { LongPackageName = FPaths::GetCleanFilename(PackageFileName); } FLoadPackageLogOutputRedirector::FScopedCapture ScopedCapture(&LogOutputRedirector, LongPackageName); Package = LoadPackage( NULL, *PackageFileName, LOAD_NoWarn | LOAD_Quiet ); } if( Package ) { LoadedPackages.Add(Package); LoadedPackageFileNames.Add(PackageFileName); // Because packages may not have been resaved after this flagging was implemented, we may have added packages to load that weren't flagged - potential false positives. // The loading process should have reflagged said packages so that only true positives will have this flag. if( Package->RequiresLocalizationGather() ) { LoadedPackagesToProcess.Add( Package ); } } else { FailedPackageFileNames.Add( PackageFileName ); ++FailuresInThisBatch; continue; } ++PackagesInThisBatch; } UE_LOG(LogGatherTextFromAssetsCommandlet, Log, TEXT("Loaded %i packages in batch %i of %i. %i failed."), PackagesInThisBatch, BatchIndex + 1, BatchCount, FailuresInThisBatch); ProcessPackages(LoadedPackagesToProcess); LoadedPackagesToProcess.Empty(PackagesPerBatchCount); if( bFixBroken ) { for( int32 LoadedPackageIndex=0; LoadedPackageIndex < LoadedPackages.Num() ; ++LoadedPackageIndex ) { UPackage *Package = LoadedPackages[LoadedPackageIndex]; const FString PackageName = LoadedPackageFileNames[LoadedPackageIndex]; //Todo - link with source control. if( Package ) { if( Package->IsDirty() ) { if( SavePackageHelper( Package, *PackageName ) ) { UE_LOG(LogGatherTextFromAssetsCommandlet, Log, TEXT("Saved Package %s."),*PackageName); } else { //TODO - Work out how to integrate with source control. The code from the source gatherer doesn't work. UE_LOG(LogGatherTextFromAssetsCommandlet, Log, TEXT("Could not save package %s. Probably due to source control. "),*PackageName); } } } else { UE_LOG(LogGatherTextFromAssetsCommandlet, Warning, TEXT("Failed to find one of the loaded packages.")); } } } CollectGarbage(RF_NoFlags); LoadedPackages.Empty(PackagesPerBatchCount); LoadedPackageFileNames.Empty(PackagesPerBatchCount); } return 0; }
void UUnrealEdEngine::OnPackageDirtyStateUpdated( UPackage* Pkg) { // The passed in object should never be NULL check(Pkg); UPackage* Package = Pkg->GetOutermost(); const FString PackageName = Package->GetName(); // Alert the user if they have modified a package that won't be able to be saved because // it's already been saved with an engine version that is newer than the current one. if (!FUObjectThreadContext::Get().IsRoutingPostLoad && Package->IsDirty() && !PackagesCheckedForEngineVersion.Contains(PackageName)) { EWriteDisallowedWarningState WarningStateToSet = WDWS_WarningUnnecessary; FString PackageFileName; if ( FPackageName::DoesPackageExist( Package->GetName(), NULL, &PackageFileName ) ) { // If a package has never been loaded, a file reader is necessary to find the package file summary for its saved engine version. FArchive* PackageReader = IFileManager::Get().CreateFileReader( *PackageFileName ); if ( PackageReader ) { FPackageFileSummary Summary; *PackageReader << Summary; if ( Summary.GetFileVersionUE4() > GPackageFileUE4Version || !GEngineVersion.IsCompatibleWith(Summary.CompatibleWithEngineVersion) ) { WarningStateToSet = WDWS_PendingWarn; bNeedWarningForPkgEngineVer = true; } } delete PackageReader; } PackagesCheckedForEngineVersion.Add( PackageName, WarningStateToSet ); } // Alert the user if they have modified a package that they do not have sufficient permission to write to disk. // This can be due to the content being in the "Program Files" folder and the user does not have admin privileges. if (!FUObjectThreadContext::Get().IsRoutingPostLoad && Package->IsDirty() && !PackagesCheckedForWritePermission.Contains(PackageName)) { EWriteDisallowedWarningState WarningStateToSet = GetWarningStateForWritePermission(PackageName); if ( WarningStateToSet == WDWS_PendingWarn ) { bNeedWarningForWritePermission = true; } PackagesCheckedForWritePermission.Add( PackageName, WarningStateToSet ); } if( Package->IsDirty() ) { // Find out if we have already asked the user to modify this package const uint8* PromptState = PackageToNotifyState.Find( Package ); const bool bAlreadyAsked = PromptState != NULL; // During an autosave, packages are saved in the autosave directory which switches off their dirty flags. // To preserve the pre-autosave state, any saved package is then remarked as dirty because it wasn't saved in the normal location where it would be picked up by source control. // Any callback that happens during an autosave is bogus since a package wasn't marked dirty due to a user modification. const bool bIsAutoSaving = PackageAutoSaver.Get() && PackageAutoSaver->IsAutoSaving(); const UEditorLoadingSavingSettings* Settings = GetDefault<UEditorLoadingSavingSettings>(); if( !bIsAutoSaving && !GIsEditorLoadingPackage && // Don't ask if the package was modified as a result of a load !bAlreadyAsked && // Don't ask if we already asked once! (Settings->bPromptForCheckoutOnAssetModification || Settings->bAutomaticallyCheckoutOnAssetModification) ) { // Force source control state to be updated ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); TArray<FString> Files; Files.Add(SourceControlHelpers::PackageFilename(Package)); SourceControlProvider.Execute(ISourceControlOperation::Create<FUpdateStatus>(), SourceControlHelpers::AbsoluteFilenames(Files), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateUObject(this, &UUnrealEdEngine::OnSourceControlStateUpdated, TWeakObjectPtr<UPackage>(Package))); } } else { // This package was saved, the user should be prompted again if they checked in the package PackageToNotifyState.Remove( Package ); } }
int32 UGatherTextFromAssetsCommandlet::Main(const FString& Params) { // Parse command line. TArray<FString> Tokens; TArray<FString> Switches; TMap<FString, FString> ParamVals; UCommandlet::ParseCommandLine(*Params, Tokens, Switches, ParamVals); //Set config file const FString* ParamVal = ParamVals.Find(FString(TEXT("Config"))); FString GatherTextConfigPath; if ( ParamVal ) { GatherTextConfigPath = *ParamVal; } else { UE_LOG(LogGatherTextFromAssetsCommandlet, Error, TEXT("No config specified.")); return -1; } //Set config section ParamVal = ParamVals.Find(FString(TEXT("Section"))); FString SectionName; if ( ParamVal ) { SectionName = *ParamVal; } else { UE_LOG(LogGatherTextFromAssetsCommandlet, Error, TEXT("No config section specified.")); return -1; } //Include paths TArray<FString> IncludePaths; GetConfigArray(*SectionName, TEXT("IncludePaths"), IncludePaths, GatherTextConfigPath); if (IncludePaths.Num() == 0) { UE_LOG(LogGatherTextFromAssetsCommandlet, Error, TEXT("No include paths in section %s"), *SectionName); return -1; } //Exclude paths TArray<FString> ExcludePaths; GetConfigArray(*SectionName, TEXT("ExcludePaths"), ExcludePaths, GatherTextConfigPath); //package extensions TArray<FString> PackageExts; GetConfigArray(*SectionName, TEXT("PackageExtensions"), PackageExts, GatherTextConfigPath); if (PackageExts.Num() == 0) { UE_LOG(LogGatherTextFromAssetsCommandlet, Warning, TEXT("No package extensions specified in section %s, using defaults"), *SectionName); PackageExts.Add(FString("*") + FPackageName::GetAssetPackageExtension()); PackageExts.Add(FString("*") + FPackageName::GetMapPackageExtension()); } //asset class exclude TArray<FString> ExcludeClasses; GetConfigArray(*SectionName, TEXT("ExcludeClasses"), ExcludeClasses, GatherTextConfigPath); FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")); AssetRegistryModule.Get().SearchAllAssets( true ); FARFilter Filter; for(int32 i = 0; i < ExcludeClasses.Num(); i++) { UClass* FilterClass = FindObject<UClass>(ANY_PACKAGE, *ExcludeClasses[i]); if(FilterClass) { Filter.ClassNames.Add( FilterClass->GetFName() ); } else { UE_LOG(LogGatherTextFromAssetsCommandlet, Warning, TEXT("Invalid exclude class %s"), *ExcludeClasses[i]); } } TArray<FAssetData> AssetData; AssetRegistryModule.Get().GetAssets(Filter, AssetData); FString UAssetPackageExtension = FPackageName::GetAssetPackageExtension(); TSet< FString > LongPackageNamesToExclude; for (int Index = 0; Index < AssetData.Num(); Index++) { LongPackageNamesToExclude.Add( FPackageName::LongPackageNameToFilename( AssetData[Index].PackageName.ToString(), UAssetPackageExtension ) ); } //Get whether we should fix broken properties that we find. GetConfigBool(*SectionName, TEXT("bFixBroken"), bFixBroken, GatherTextConfigPath); // Add any manifest dependencies if they were provided TArray<FString> ManifestDependenciesList; GetConfigArray(*SectionName, TEXT("ManifestDependencies"), ManifestDependenciesList, GatherTextConfigPath); if( !ManifestInfo->AddManifestDependencies( ManifestDependenciesList ) ) { UE_LOG(LogGatherTextFromAssetsCommandlet, Error, TEXT("The GatherTextFromAssets commandlet couldn't find all the specified manifest dependencies.")); return -1; } //The main array of files to work from. TArray< FString > PackageFileNamesToLoad; TSet< FString > LongPackageNamesToProcess; TArray<FString> PackageFilesNotInIncludePath; TArray<FString> PackageFilesInExcludePath; TArray<FString> PackageFilesExcludedByClass; //Fill the list of packages to work from. uint8 PackageFilter = NORMALIZE_DefaultFlags; TArray<FString> Unused; for ( int32 PackageFilenameWildcardIdx = 0; PackageFilenameWildcardIdx < PackageExts.Num(); PackageFilenameWildcardIdx++ ) { const bool IsAssetPackage = PackageExts[PackageFilenameWildcardIdx] == ( FString( TEXT("*") )+ FPackageName::GetAssetPackageExtension() ); TArray<FString> PackageFiles; if ( !NormalizePackageNames( Unused, PackageFiles, PackageExts[PackageFilenameWildcardIdx], PackageFilter) ) { UE_LOG(LogGatherTextFromAssetsCommandlet, Display, TEXT("No packages found with extension %i: '%s'"), PackageFilenameWildcardIdx, *PackageExts[PackageFilenameWildcardIdx]); continue; } else { UE_LOG(LogGatherTextFromAssetsCommandlet, Display, TEXT("Found %i packages with extension %i: '%s'"), PackageFiles.Num(), PackageFilenameWildcardIdx, *PackageExts[PackageFilenameWildcardIdx]); } //Run through all the files found and add any that pass the include, exclude and filter constraints to OrderedPackageFilesToLoad for( int32 PackageFileIdx=0; PackageFileIdx<PackageFiles.Num(); ++PackageFileIdx ) { bool bExclude = false; //Ensure it matches the include paths if there are some. for( int32 IncludePathIdx=0; IncludePathIdx<IncludePaths.Num() ; ++IncludePathIdx ) { bExclude = true; if( PackageFiles[PackageFileIdx].MatchesWildcard(IncludePaths[IncludePathIdx]) ) { bExclude = false; break; } } if ( bExclude ) { PackageFilesNotInIncludePath.Add(PackageFiles[PackageFileIdx]); } //Ensure it does not match the exclude paths if there are some. for( int32 ExcludePathIdx=0; !bExclude && ExcludePathIdx<ExcludePaths.Num() ; ++ExcludePathIdx ) { if( PackageFiles[PackageFileIdx].MatchesWildcard(ExcludePaths[ExcludePathIdx]) ) { bExclude = true; PackageFilesInExcludePath.Add(PackageFiles[PackageFileIdx]); break; } } //Check that this is not on the list of packages that we don't care about e.g. textures. if ( !bExclude && IsAssetPackage && LongPackageNamesToExclude.Contains( PackageFiles[PackageFileIdx] ) ) { bExclude = true; PackageFilesExcludedByClass.Add(PackageFiles[PackageFileIdx]); } //If we haven't failed one of the above checks, add it to the array of packages to process. if(!bExclude) { TScopedPointer< FArchive > FileReader( IFileManager::Get().CreateFileReader( *PackageFiles[PackageFileIdx] ) ); if( FileReader ) { // Read package file summary from the file FPackageFileSummary PackageSummary; (*FileReader) << PackageSummary; // Early out check if the package has been flagged as needing localization gathering if( PackageSummary.PackageFlags & PKG_RequiresLocalizationGather || PackageSummary.GetFileVersionUE4() < VER_UE4_PACKAGE_REQUIRES_LOCALIZATION_GATHER_FLAGGING ) { PackageFileNamesToLoad.Add( PackageFiles[PackageFileIdx] ); } } } } } if ( PackageFileNamesToLoad.Num() == 0 ) { UE_LOG(LogGatherTextFromAssetsCommandlet, Warning, TEXT("No files found. Or none passed the include/exclude criteria.")); } CollectGarbage( RF_Native ); //Now go through the remaining packages in the main array and process them in batches. int32 PackagesPerBatchCount = 100; TArray< UPackage* > LoadedPackages; TArray< FString > LoadedPackageFileNames; TArray< FString > FailedPackageFileNames; TArray< UPackage* > PackagesToProcess; const int32 PackageCount = PackageFileNamesToLoad.Num(); const int32 BatchCount = PackageCount / PackagesPerBatchCount + (PackageCount % PackagesPerBatchCount > 0 ? 1 : 0); // Add an extra batch for any remainder if necessary if(PackageCount > 0) { UE_LOG(LogGatherTextFromAssetsCommandlet, Log, TEXT("Loading %i packages in %i batches of %i."), PackageCount, BatchCount, PackagesPerBatchCount); } //Load the packages in batches int32 PackageIndex = 0; for( int32 BatchIndex = 0; BatchIndex < BatchCount; ++BatchIndex ) { int32 PackagesInThisBatch = 0; for( PackageIndex; PackageIndex < PackageCount && PackagesInThisBatch < PackagesPerBatchCount; ++PackageIndex ) { FString PackageFileName = PackageFileNamesToLoad[PackageIndex]; UPackage *Package = LoadPackage( NULL, *PackageFileName, LOAD_None ); if( Package ) { LoadedPackages.Add(Package); LoadedPackageFileNames.Add(PackageFileName); // Because packages may not have been resaved after this flagging was implemented, we may have added packages to load that weren't flagged - potential false positives. // The loading process should have reflagged said packages so that only true positives will have this flag. if( Package->RequiresLocalizationGather() ) { PackagesToProcess.Add( Package ); } } else { FailedPackageFileNames.Add( PackageFileName ); continue; } ++PackagesInThisBatch; } UE_LOG(LogGatherTextFromAssetsCommandlet, Log, TEXT("Loaded %i packages in batch %i of %i."), PackagesInThisBatch, BatchIndex + 1, BatchCount); ProcessPackages(PackagesToProcess); PackagesToProcess.Empty(PackagesPerBatchCount); if( bFixBroken ) { for( int32 LoadedPackageIndex=0; LoadedPackageIndex < LoadedPackages.Num() ; ++LoadedPackageIndex ) { UPackage *Package = LoadedPackages[LoadedPackageIndex]; const FString PackageName = LoadedPackageFileNames[LoadedPackageIndex]; //Todo - link with source control. if( Package ) { if( Package->IsDirty() ) { if( SavePackageHelper( Package, *PackageName ) ) { UE_LOG(LogGatherTextFromAssetsCommandlet, Log, TEXT("Saved Package %s."),*PackageName); } else { //TODO - Work out how to integrate with source control. The code from the source gatherer doesn't work. UE_LOG(LogGatherTextFromAssetsCommandlet, Log, TEXT("Could not save package %s. Probably due to source control. "),*PackageName); } } } else { UE_LOG(LogGatherTextFromAssetsCommandlet, Warning, TEXT("Failed to find one of the loaded packages.")); } } } CollectGarbage( RF_Native ); LoadedPackages.Empty(PackagesPerBatchCount); LoadedPackageFileNames.Empty(PackagesPerBatchCount); } for(auto i = ConflictTracker.Namespaces.CreateConstIterator(); i; ++i) { const FString& NamespaceName = i.Key(); const FConflictTracker::FKeyTable& KeyTable = i.Value(); for(auto j = KeyTable.CreateConstIterator(); j; ++j) { const FString& KeyName = j.Key(); const FConflictTracker::FEntryArray& EntryArray = j.Value(); for(int k = 0; k < EntryArray.Num(); ++k) { const FConflictTracker::FEntry& Entry = EntryArray[k]; switch(Entry.Status) { case EAssetTextGatherStatus::MissingKey: { UE_LOG(LogGatherTextFromAssetsCommandlet, Warning, TEXT("Detected missing key on asset \"%s\"."), *Entry.ObjectPath); } break; case EAssetTextGatherStatus::MissingKey_Resolved: { UE_LOG(LogGatherTextFromAssetsCommandlet, Warning, TEXT("Fixed missing key on asset \"%s\"."), *Entry.ObjectPath); } break; case EAssetTextGatherStatus::IdentityConflict: { UE_LOG(LogGatherTextFromAssetsCommandlet, Warning, TEXT("Detected duplicate identity with differing source on asset \"%s\"."), *Entry.ObjectPath); } break; case EAssetTextGatherStatus::IdentityConflict_Resolved: { UE_LOG(LogGatherTextFromAssetsCommandlet, Warning, TEXT("Fixed duplicate identity with differing source on asset \"%s\"."), *Entry.ObjectPath); } break; } } } } return 0; }
bool ULevelStreaming::RequestLevel(UWorld* PersistentWorld, bool bAllowLevelLoadRequests, bool bBlockOnLoad) { // Quit early in case load request already issued if (bHasLoadRequestPending) { return true; } // Previous attempts have failed, no reason to try again if (bFailedToLoad) { return false; } QUICK_SCOPE_CYCLE_COUNTER(STAT_ULevelStreaming_RequestLevel); FScopeCycleCounterUObject Context(PersistentWorld); // Package name we want to load FName DesiredPackageName = PersistentWorld->IsGameWorld() ? GetLODPackageName() : GetWorldAssetPackageFName(); FName DesiredPackageNameToLoad = PersistentWorld->IsGameWorld() ? GetLODPackageNameToLoad() : PackageNameToLoad; // Check if currently loaded level is what we want right now if (LoadedLevel != NULL && LoadedLevel->GetOutermost()->GetFName() == DesiredPackageName) { return true; } // Can not load new level now, there is still level pending unload if (PendingUnloadLevel != NULL) { return false; } // Try to find the [to be] loaded package. UPackage* LevelPackage = (UPackage*) StaticFindObjectFast(UPackage::StaticClass(), NULL, DesiredPackageName, 0, 0, RF_PendingKill); // Package is already or still loaded. if (LevelPackage) { // Find world object and use its PersistentLevel pointer. UWorld* World = UWorld::FindWorldInPackage(LevelPackage); // Check for a redirector. Follow it, if found. if ( !World ) { World = UWorld::FollowWorldRedirectorInPackage(LevelPackage); if ( World ) { LevelPackage = World->GetOutermost(); } } if (World != NULL) { #if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) if (World->PersistentLevel == NULL) { UE_LOG(LogLevelStreaming, Log, TEXT("World exists but PersistentLevel doesn't for %s, most likely caused by reference to world of unloaded level and GC setting reference to NULL while keeping world object"), *World->GetOutermost()->GetName()); // print out some debug information... StaticExec(World, *FString::Printf(TEXT("OBJ REFS CLASS=WORLD NAME=%s shortest"), *World->GetPathName())); TMap<UObject*,UProperty*> Route = FArchiveTraceRoute::FindShortestRootPath( World, true, GARBAGE_COLLECTION_KEEPFLAGS ); FString ErrorString = FArchiveTraceRoute::PrintRootPath( Route, World ); UE_LOG(LogLevelStreaming, Log, TEXT("%s"), *ErrorString); // before asserting checkf(World->PersistentLevel,TEXT("Most likely caused by reference to world of unloaded level and GC setting reference to NULL while keeping world object")); return false; } #endif if (World->PersistentLevel != LoadedLevel) { SetLoadedLevel(World->PersistentLevel); // Broadcast level loaded event to blueprints OnLevelLoaded.Broadcast(this); } return true; } } EPackageFlags PackageFlags = PKG_None; int32 PIEInstanceID = INDEX_NONE; // copy streaming level on demand if we are in PIE // (the world is already loaded for the editor, just find it and copy it) if ( PersistentWorld->IsPlayInEditor() ) { #if WITH_EDITOR if (PersistentWorld->GetOutermost()->HasAnyPackageFlags(PKG_PlayInEditor)) { PackageFlags |= PKG_PlayInEditor; } PIEInstanceID = PersistentWorld->GetOutermost()->PIEInstanceID; #endif const FString NonPrefixedLevelName = UWorld::StripPIEPrefixFromPackageName(DesiredPackageName.ToString(), PersistentWorld->StreamingLevelsPrefix); UPackage* EditorLevelPackage = FindObjectFast<UPackage>(nullptr, FName(*NonPrefixedLevelName)); bool bShouldDuplicate = EditorLevelPackage && (bBlockOnLoad || EditorLevelPackage->IsDirty() || !GEngine->PreferToStreamLevelsInPIE()); if (bShouldDuplicate) { // Do the duplication UWorld* PIELevelWorld = UWorld::DuplicateWorldForPIE(NonPrefixedLevelName, PersistentWorld); if (PIELevelWorld) { PIELevelWorld->PersistentLevel->bAlreadyMovedActors = true; // As we have duplicated the world, the actors will already have been transformed check(PendingUnloadLevel == NULL); SetLoadedLevel(PIELevelWorld->PersistentLevel); // Broadcast level loaded event to blueprints { QUICK_SCOPE_CYCLE_COUNTER(STAT_OnLevelLoaded_Broadcast); OnLevelLoaded.Broadcast(this); } return true; } else if (PersistentWorld->WorldComposition == NULL) // In world composition streaming levels are not loaded by default { if ( bAllowLevelLoadRequests ) { UE_LOG(LogLevelStreaming, Log, TEXT("World to duplicate for PIE '%s' not found. Attempting load."), *NonPrefixedLevelName); } else { UE_LOG(LogLevelStreaming, Warning, TEXT("Unable to duplicate PIE World: '%s'"), *NonPrefixedLevelName); } } } } // Async load package if world object couldn't be found and we are allowed to request a load. if (bAllowLevelLoadRequests) { FString PackageNameToLoadFrom = DesiredPackageName.ToString(); if (DesiredPackageNameToLoad != NAME_None) { PackageNameToLoadFrom = DesiredPackageNameToLoad.ToString(); } if (GUseSeekFreeLoading) { // Only load localized package if it exists as async package loading doesn't handle errors gracefully. FString LocalizedPackageName = PackageNameToLoadFrom + LOCALIZED_SEEKFREE_SUFFIX; FString LocalizedFileName; if (FPackageName::DoesPackageExist(LocalizedPackageName, NULL, &LocalizedFileName)) { // Load localized part of level first in case it exists. We don't need to worry about GC or completion // callback as we always kick off another async IO for the level below. LoadPackageAsync(*(GetWorldAssetPackageName() + LOCALIZED_SEEKFREE_SUFFIX), nullptr, *LocalizedPackageName, FLoadPackageAsyncDelegate(), PackageFlags, PIEInstanceID); } } if (FPackageName::DoesPackageExist(PackageNameToLoadFrom, NULL, NULL)) { bHasLoadRequestPending = true; ULevel::StreamedLevelsOwningWorld.Add(DesiredPackageName, PersistentWorld); UWorld::WorldTypePreLoadMap.FindOrAdd(DesiredPackageName) = PersistentWorld->WorldType; // Kick off async load request. STAT_ADD_CUSTOMMESSAGE_NAME( STAT_NamedMarker, *(FString( TEXT( "RequestLevel - " ) + DesiredPackageName.ToString() )) ); LoadPackageAsync(DesiredPackageName.ToString(), nullptr, *PackageNameToLoadFrom, FLoadPackageAsyncDelegate::CreateUObject(this, &ULevelStreaming::AsyncLevelLoadComplete), PackageFlags, PIEInstanceID); // streamingServer: server loads everything? // Editor immediately blocks on load and we also block if background level streaming is disabled. if (bBlockOnLoad || ShouldBeAlwaysLoaded()) { // Finish all async loading. FlushAsyncLoading(); } } else { UE_LOG(LogStreaming, Error,TEXT("Couldn't find file for package %s."), *PackageNameToLoadFrom); bFailedToLoad = true; return false; } } return true; }
bool IsAssetDirty(UObject* Asset) { UPackage* Package = Asset ? Asset->GetOutermost() : nullptr; return Package ? Package->IsDirty() : false; }
/** * Runs compile-on-load test against all unloaded, and optionally loaded, blueprints * See the TestAllBlueprints config key in the [Automation.Blueprint] config sections */ bool FBlueprintCompileOnLoadTest::RunTest(const FString& BlueprintAssetPath) { FCompilerResultsLog Results; UBlueprint* ExistingBP = nullptr; // if this blueprint was already loaded, then these tests are invalidated // (because dependencies have already been loaded) if (FBlueprintAutomationTestUtilities::IsBlueprintLoaded(BlueprintAssetPath, &ExistingBP)) { if (FBlueprintAutomationTestUtilities::IsAssetUnsaved(BlueprintAssetPath)) { AddError(FString::Printf(TEXT("You have unsaved changes made to '%s', please save them before running this test."), *BlueprintAssetPath)); return false; } else { AddWarning(FString::Printf(TEXT("Test may be invalid (the blueprint is already loaded): '%s'"), *BlueprintAssetPath)); FBlueprintAutomationTestUtilities::UnloadBlueprint(ExistingBP); } } // tracks blueprints that were already loaded (and cleans up any that were // loaded in its lifetime, once it is destroyed) FScopedBlueprintUnloader NewBlueprintUnloader(/*bAutoOpenScope =*/true, /*bRunGCOnCloseIn =*/true); // We load the blueprint twice and compare the two for discrepancies. This is // to bring dependency load issues to light (among other things). If a blueprint's // dependencies are loaded too late, then this first object is the degenerate one. UBlueprint* InitialBlueprint = Cast<UBlueprint>(StaticLoadObject(UBlueprint::StaticClass(), NULL, *BlueprintAssetPath)); // if we failed to load it the first time, then there is no need to make a // second attempt, leave them to fix up this issue first if (InitialBlueprint == NULL) { AddError(*FString::Printf(TEXT("Unable to load blueprint for: '%s'"), *BlueprintAssetPath)); return false; } if (!InitialBlueprint->SkeletonGeneratedClass || !InitialBlueprint->GeneratedClass) { AddError(*FString::Printf(TEXT("Unable to load blueprint for: '%s'. Probably it derives from an invalid class."), *BlueprintAssetPath)); return false; } // GATHER SUBOBJECTS TArray<TWeakObjectPtr<UObject>> InitialBlueprintSubobjects; { TArray<UObject*> InitialBlueprintSubobjectsPtr; GetObjectsWithOuter(InitialBlueprint, InitialBlueprintSubobjectsPtr); for (auto Obj : InitialBlueprintSubobjectsPtr) { InitialBlueprintSubobjects.Add(Obj); } } // GATHER DEPENDENCIES TSet<TWeakObjectPtr<UBlueprint>> BlueprintDependencies; { TArray<UBlueprint*> DependentBlueprints; FBlueprintEditorUtils::GetDependentBlueprints(InitialBlueprint, DependentBlueprints); for (auto BP : DependentBlueprints) { BlueprintDependencies.Add(BP); } } BlueprintDependencies.Add(InitialBlueprint); // GATHER DEPENDENCIES PERSISTENT DATA struct FReplaceInnerData { TWeakObjectPtr<UClass> Class; FStringAssetReference BlueprintAsset; }; TArray<FReplaceInnerData> ReplaceInnerData; for (auto BPToUnloadWP : BlueprintDependencies) { auto BPToUnload = BPToUnloadWP.Get(); auto OldClass = BPToUnload ? *BPToUnload->GeneratedClass : NULL; if (OldClass) { FReplaceInnerData Data; Data.Class = OldClass; Data.BlueprintAsset = FStringAssetReference(BPToUnload); ReplaceInnerData.Add(Data); } } // store off data for the initial blueprint so we can unload it (and reconstruct // later to compare it with a second one) TArray<uint8> InitialLoadData; FObjectWriter(InitialBlueprint, InitialLoadData); // grab the name before we unload the blueprint FName const BlueprintName = InitialBlueprint->GetFName(); // unload the blueprint so we can reload it (to catch any differences, now // that all its dependencies should be loaded as well) //UNLOAD DEPENDENCIES, all circular dependencies will be loaded again // unload the blueprint so we can reload it (to catch any differences, now // that all its dependencies should be loaded as well) for (auto BPToUnloadWP : BlueprintDependencies) { if (auto BPToUnload = BPToUnloadWP.Get()) { FBlueprintAutomationTestUtilities::UnloadBlueprint(BPToUnload); } } // this blueprint is now dead (will be destroyed next garbage-collection pass) UBlueprint* UnloadedBlueprint = InitialBlueprint; InitialBlueprint = NULL; // load the blueprint a second time; if the two separately loaded blueprints // are different, then this one is most likely the choice one (it has all its // dependencies loaded) UBlueprint* ReloadedBlueprint = Cast<UBlueprint>(StaticLoadObject(UBlueprint::StaticClass(), NULL, *BlueprintAssetPath)); UPackage* TransientPackage = GetTransientPackage(); FName ReconstructedName = MakeUniqueObjectName(TransientPackage, UBlueprint::StaticClass(), BlueprintName); // reconstruct the initial blueprint (using the serialized data from its initial load) EObjectFlags const StandardBlueprintFlags = RF_Public | RF_Standalone | RF_Transactional; InitialBlueprint = ConstructObject<UBlueprint>(UBlueprint::StaticClass(), TransientPackage, ReconstructedName, StandardBlueprintFlags | RF_Transient); FObjectReader(InitialBlueprint, InitialLoadData); { TMap<UObject*, UObject*> ClassRedirects; for (auto& Data : ReplaceInnerData) { UClass* OriginalClass = Data.Class.Get(); UBlueprint* NewBlueprint = Cast<UBlueprint>(Data.BlueprintAsset.ResolveObject()); UClass* NewClass = NewBlueprint ? *NewBlueprint->GeneratedClass : NULL; if (OriginalClass && NewClass) { ClassRedirects.Add(OriginalClass, NewClass); } } // REPLACE OLD DATA FArchiveReplaceObjectRef<UObject>(InitialBlueprint, ClassRedirects, /*bNullPrivateRefs=*/false, /*bIgnoreOuterRef=*/true, /*bIgnoreArchetypeRef=*/false); for (auto SubobjWP : InitialBlueprintSubobjects) { if (auto Subobj = SubobjWP.Get()) { FArchiveReplaceObjectRef<UObject>(Subobj, ClassRedirects, /*bNullPrivateRefs=*/false, /*bIgnoreOuterRef=*/true, /*bIgnoreArchetypeRef=*/false); } } UPackage* AssetPackage = ReloadedBlueprint->GetOutermost(); bool bHasUnsavedChanges = AssetPackage->IsDirty(); FBlueprintEditorUtils::RefreshAllNodes(ReloadedBlueprint); AssetPackage->SetDirtyFlag(bHasUnsavedChanges); } // look for diffs between subsequent loads and log them as errors TArray<FDiffSingleResult> BlueprintDiffs; bool bDiffsFound = FBlueprintAutomationTestUtilities::DiffBlueprints(InitialBlueprint, ReloadedBlueprint, BlueprintDiffs); if (bDiffsFound) { FBlueprintAutomationTestUtilities::ResolveCircularDependencyDiffs(ReloadedBlueprint, BlueprintDiffs); // if there are still diffs after resolving any the could have been from unloaded circular dependencies if (BlueprintDiffs.Num() > 0) { AddError(FString::Printf(TEXT("Inconsistencies between subsequent blueprint loads for: '%s' (was a dependency not preloaded?)"), *BlueprintAssetPath)); } else { bDiffsFound = false; } // list all the differences (so as to help identify what dependency was missing) for (auto DiffIt(BlueprintDiffs.CreateIterator()); DiffIt; ++DiffIt) { // will be presented in the context of "what changed between the initial load and the second?" FString DiffDescription = DiffIt->ToolTip; if (DiffDescription != DiffIt->DisplayString) { DiffDescription = FString::Printf(TEXT("%s (%s)"), *DiffDescription, *DiffIt->DisplayString); } const UEdGraphNode* NodeFromPin = DiffIt->Pin1 ? Cast<const UEdGraphNode>(DiffIt->Pin1->GetOuter()) : NULL; const UEdGraphNode* Node = DiffIt->Node1 ? DiffIt->Node1 : NodeFromPin; const UEdGraph* Graph = Node ? Node->GetGraph() : NULL; const FString GraphName = Graph ? Graph->GetName() : FString(TEXT("Unknown Graph")); AddError(FString::Printf(TEXT("%s.%s differs between subsequent loads: %s"), *BlueprintName.ToString(), *GraphName, *DiffDescription)); } } // At the close of this function, the FScopedBlueprintUnloader should prep // for following tests by unloading any blueprint dependencies that were // loaded for this one (should catch InitialBlueprint and ReloadedBlueprint) // // The FScopedBlueprintUnloader should also run garbage-collection after, // in hopes that the imports for this blueprint get destroyed so that they // don't invalidate other tests that share the same dependencies return !bDiffsFound; }