// Copyright Epic Games, Inc. All Rights Reserved. #include "Commandlets/GatherTextFromAssetsCommandlet.h" #include "UObject/Class.h" #include "HAL/FileManager.h" #include "Misc/CommandLine.h" #include "Misc/ConfigCacheIni.h" #include "Misc/Paths.h" #include "Misc/OutputDeviceHelper.h" #include "Misc/FeedbackContext.h" #include "UObject/EditorObjectVersion.h" #include "UObject/UObjectIterator.h" #include "Modules/ModuleManager.h" #include "Serialization/PropertyLocalizationDataGathering.h" #include "Misc/PackageName.h" #include "Misc/FileHelper.h" #include "UObject/PackageFileSummary.h" #include "Framework/Commands/Commands.h" #include "Commandlets/GatherTextFromSourceCommandlet.h" #include "AssetData.h" #include "Sound/DialogueWave.h" #include "ARFilter.h" #include "AssetRegistryModule.h" #include "PackageHelperFunctions.h" #include "Templates/UniquePtr.h" #include "CollectionManagerModule.h" #include "ICollectionManager.h" DEFINE_LOG_CATEGORY_STATIC(LogGatherTextFromAssetsCommandlet, Log, All); /** Special feedback context used to stop the commandlet to reporting failure due to a package load error */ class FLoadPackageLogOutputRedirector: public FFeedbackContext { public: struct FScopedCapture { FScopedCapture(FLoadPackageLogOutputRedirector* InLogOutputRedirector, const FString& InPackageContext) : LogOutputRedirector(InLogOutputRedirector) { LogOutputRedirector->BeginCapturingLogData(InPackageContext); } ~FScopedCapture() { LogOutputRedirector->EndCapturingLogData(); } FLoadPackageLogOutputRedirector* LogOutputRedirector; }; FLoadPackageLogOutputRedirector() = default; virtual ~FLoadPackageLogOutputRedirector() = default; void BeginCapturingLogData(const FString& InPackageContext) { // Override GWarn so that we can capture any log data check(!OriginalWarningContext); OriginalWarningContext = GWarn; GWarn = this; PackageContext = InPackageContext; } void EndCapturingLogData() { // Restore the original GWarn now that we've finished capturing log data check(OriginalWarningContext); GWarn = OriginalWarningContext; OriginalWarningContext = nullptr; // Report any messages, and also report a warning if we silenced some warnings or errors when loading if (ErrorCount > 0 || WarningCount > 0) { static const FString LogIndentation = TEXT(" "); UE_LOG(LogGatherTextFromAssetsCommandlet, Display, TEXT("Package '%s' produced %d error(s) and %d warning(s) while loading (see below). Please verify that your text has gathered correctly."), *PackageContext, ErrorCount, WarningCount); for (const FString& FormattedOutput: FormattedErrorsAndWarningsList) { GWarn->Log(NAME_None, ELogVerbosity::Display, LogIndentation + FormattedOutput); } } PackageContext.Reset(); // Reset the counts and previous log output ErrorCount = 0; WarningCount = 0; FormattedErrorsAndWarningsList.Reset(); } virtual void Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const FName& Category) override { if (Verbosity == ELogVerbosity::Error) { ++ErrorCount; // Downgrade Error to Log while loading packages to avoid false positives from things searching for "Error:" tokens in the log file FormattedErrorsAndWarningsList.Add(FOutputDeviceHelper::FormatLogLine(ELogVerbosity::Log, Category, V)); } else if (Verbosity == ELogVerbosity::Warning) { ++WarningCount; // Downgrade Warning to Log while loading packages to avoid false positives from things searching for "Warning:" tokens in the log file FormattedErrorsAndWarningsList.Add(FOutputDeviceHelper::FormatLogLine(ELogVerbosity::Log, Category, V)); } else if (Verbosity == ELogVerbosity::Display) { // Downgrade Display to Log while loading packages OriginalWarningContext->Serialize(V, ELogVerbosity::Log, Category); } else { // Pass anything else on to GWarn so that it can handle them appropriately OriginalWarningContext->Serialize(V, Verbosity, Category); } } private: int32 ErrorCount = 0; int32 WarningCount = 0; TArray FormattedErrorsAndWarningsList; FString PackageContext; FFeedbackContext* OriginalWarningContext = nullptr; }; class FAssetGatherCacheMetrics { public: FAssetGatherCacheMetrics() : CachedAssetCount(0), UncachedAssetCount(0) { FMemory::Memzero(UncachedAssetBreakdown); } void CountCachedAsset() { ++CachedAssetCount; } void CountUncachedAsset(const UGatherTextFromAssetsCommandlet::EPackageLocCacheState InState) { check(InState != UGatherTextFromAssetsCommandlet::EPackageLocCacheState::Cached); ++UncachedAssetCount; ++UncachedAssetBreakdown[(int32)InState]; } void LogMetrics() const { UE_LOG(LogGatherTextFromAssetsCommandlet, Display, TEXT("%s"), *ToString()); } FString ToString() const { return FString::Printf( TEXT("Asset gather cache metrics: %d cached, %d uncached (%d too old, %d no cache or contained bytecode)"), CachedAssetCount, UncachedAssetCount, UncachedAssetBreakdown[(int32)UGatherTextFromAssetsCommandlet::EPackageLocCacheState::Uncached_TooOld], UncachedAssetBreakdown[(int32)UGatherTextFromAssetsCommandlet::EPackageLocCacheState::Uncached_NoCache]); } private: int32 CachedAssetCount; int32 UncachedAssetCount; int32 UncachedAssetBreakdown[(int32)UGatherTextFromAssetsCommandlet::EPackageLocCacheState::Cached]; }; #define LOC_DEFINE_REGION ////////////////////////////////////////////////////////////////////////// // UGatherTextFromAssetsCommandlet const FString UGatherTextFromAssetsCommandlet::UsageText( TEXT("GatherTextFromAssetsCommandlet usage...\r\n") TEXT(" UGatherTextFromAssetsCommandlet -root= -exclude=\r\n") TEXT(" \r\n") TEXT(" Paths to include. Delimited with ';'. Accepts wildcards. eg \"*Content/Developers/*;*/TestMaps/*\" OPTIONAL: If not present, everything will be included. \r\n") TEXT(" Paths to exclude. Delimited with ';'. Accepts wildcards. eg \"*Content/Developers/*;*/TestMaps/*\" OPTIONAL: If not present, nothing will be excluded.\r\n")); UGatherTextFromAssetsCommandlet::UGatherTextFromAssetsCommandlet(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer), PackagesPerBatchCount(100), MaxMemoryAllowanceBytes(0), bSkipGatherCache(false), ShouldGatherFromEditorOnlyData(false), ShouldExcludeDerivedClasses(false) { } void UGatherTextFromAssetsCommandlet::ProcessGatherableTextDataArray(const TArray& GatherableTextDataArray) { for (const FGatherableTextData& GatherableTextData: GatherableTextDataArray) { for (const FTextSourceSiteContext& TextSourceSiteContext: GatherableTextData.SourceSiteContexts) { if (!TextSourceSiteContext.IsEditorOnly || ShouldGatherFromEditorOnlyData) { if (TextSourceSiteContext.KeyName.IsEmpty()) { UE_LOG(LogGatherTextFromAssetsCommandlet, Warning, TEXT("Detected missing key on asset \"%s\"."), *TextSourceSiteContext.SiteDescription); continue; } static const FLocMetadataObject DefaultMetadataObject; FManifestContext Context; Context.Key = TextSourceSiteContext.KeyName; Context.KeyMetadataObj = !(FLocMetadataObject::IsMetadataExactMatch(&TextSourceSiteContext.KeyMetaData, &DefaultMetadataObject)) ? MakeShareable(new FLocMetadataObject(TextSourceSiteContext.KeyMetaData)) : nullptr; Context.InfoMetadataObj = !(FLocMetadataObject::IsMetadataExactMatch(&TextSourceSiteContext.InfoMetaData, &DefaultMetadataObject)) ? MakeShareable(new FLocMetadataObject(TextSourceSiteContext.InfoMetaData)) : nullptr; Context.bIsOptional = TextSourceSiteContext.IsOptional; Context.SourceLocation = TextSourceSiteContext.SiteDescription; Context.PlatformName = GetSplitPlatformNameFromPath(TextSourceSiteContext.SiteDescription); FLocItem Source(GatherableTextData.SourceData.SourceString); GatherManifestHelper->AddSourceText(GatherableTextData.NamespaceName, Source, Context); } } } } void CalculateDependenciesImpl(IAssetRegistry& InAssetRegistry, const FName& InPackageName, TSet& OutDependencies, TMap>& InOutPackageNameToDependencies) { const TSet* CachedDependencies = InOutPackageNameToDependencies.Find(InPackageName); if (!CachedDependencies) { // Add a dummy entry now to avoid any infinite recursion for this package as we build the dependencies list InOutPackageNameToDependencies.Add(InPackageName); // Build the complete list of dependencies for this package TSet LocalDependencies; { TArray LocalDependenciesArray; InAssetRegistry.GetDependencies(InPackageName, LocalDependenciesArray); LocalDependencies.Append(LocalDependenciesArray); for (const FName& LocalDependency: LocalDependenciesArray) { CalculateDependenciesImpl(InAssetRegistry, LocalDependency, LocalDependencies, InOutPackageNameToDependencies); } } // Add the real data now CachedDependencies = &InOutPackageNameToDependencies.Add(InPackageName, MoveTemp(LocalDependencies)); } check(CachedDependencies); OutDependencies.Append(*CachedDependencies); } void UGatherTextFromAssetsCommandlet::CalculateDependenciesForPackagesPendingGather() { FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); IAssetRegistry& AssetRegistry = AssetRegistryModule.Get(); TMap> PackageNameToDependencies; for (FPackagePendingGather& PackagePendingGather: PackagesPendingGather) { CalculateDependenciesImpl(AssetRegistry, PackagePendingGather.PackageName, PackagePendingGather.Dependencies, PackageNameToDependencies); } } bool UGatherTextFromAssetsCommandlet::HasExceededMemoryLimit() { const FPlatformMemoryStats MemStats = FPlatformMemory::GetStats(); const uint64 UsedMemory = MemStats.UsedPhysical; if (MaxMemoryAllowanceBytes > 0u && UsedMemory >= MaxMemoryAllowanceBytes) { UE_LOG(LogGatherTextFromAssetsCommandlet, Display, TEXT("Used memory %d kb exceeded max memory %d kb"), UsedMemory / 1024, MaxMemoryAllowanceBytes / 1024); return true; } return false; } void UGatherTextFromAssetsCommandlet::PurgeGarbage(const bool bPurgeReferencedPackages) { check(ObjectsToKeepAlive.Num() == 0); TSet LoadedPackageNames; TSet PackageNamesToKeepAlive; if (!bPurgeReferencedPackages) { // Build a complete list of packages that we still need to keep alive, either because we still // have to process them, or because they're a dependency for something we still have to process for (const FPackagePendingGather& PackagePendingGather: PackagesPendingGather) { PackageNamesToKeepAlive.Add(PackagePendingGather.PackageName); PackageNamesToKeepAlive.Append(PackagePendingGather.Dependencies); } for (TObjectIterator PackageIt; PackageIt; ++PackageIt) { UPackage* Package = *PackageIt; if (PackageNamesToKeepAlive.Contains(Package->GetFName())) { LoadedPackageNames.Add(Package->GetFName()); // Keep any requested packages (and their RF_Standalone inners) alive during a call to PurgeGarbage ObjectsToKeepAlive.Add(Package); ForEachObjectWithOuter(Package, [this](UObject* InPackageInner) { if (InPackageInner->HasAnyFlags(RF_Standalone)) { ObjectsToKeepAlive.Add(InPackageInner); } }, true, RF_NoFlags, EInternalObjectFlags::PendingKill); } } } CollectGarbage(RF_NoFlags); ObjectsToKeepAlive.Reset(); if (!bPurgeReferencedPackages) { // Sort the remaining packages to gather so that currently loaded packages are processed first, followed by those with the most dependencies // This aims to allow packages to be GC'd as soon as possible once nothing is no longer referencing them as a dependency PackagesPendingGather.Sort([&LoadedPackageNames](const FPackagePendingGather& PackagePendingGatherOne, const FPackagePendingGather& PackagePendingGatherTwo) { const bool bIsPackageOneLoaded = LoadedPackageNames.Contains(PackagePendingGatherOne.PackageName); const bool bIsPackageTwoLoaded = LoadedPackageNames.Contains(PackagePendingGatherTwo.PackageName); return (bIsPackageOneLoaded == bIsPackageTwoLoaded) ? PackagePendingGatherOne.Dependencies.Num() > PackagePendingGatherTwo.Dependencies.Num() : bIsPackageOneLoaded; }); } } void UGatherTextFromAssetsCommandlet::AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector) { Super::AddReferencedObjects(InThis, Collector); // Keep any requested objects alive during a call to PurgeGarbage UGatherTextFromAssetsCommandlet* This = CastChecked(InThis); Collector.AddReferencedObjects(This->ObjectsToKeepAlive); } bool IsGatherableTextDataIdentical(const TArray& GatherableTextDataArrayOne, const TArray& GatherableTextDataArrayTwo) { struct FSignificantGatherableTextData { FLocKey Identity; FString SourceString; }; auto ExtractSignificantGatherableTextData = [](const TArray& InGatherableTextDataArray) { TArray SignificantGatherableTextDataArray; for (const FGatherableTextData& GatherableTextData: InGatherableTextDataArray) { for (const FTextSourceSiteContext& TextSourceSiteContext: GatherableTextData.SourceSiteContexts) { SignificantGatherableTextDataArray.Add({FString::Printf(TEXT("%s:%s"), *GatherableTextData.NamespaceName, *TextSourceSiteContext.KeyName), GatherableTextData.SourceData.SourceString}); } } SignificantGatherableTextDataArray.Sort([](const FSignificantGatherableTextData& SignificantGatherableTextDataOne, const FSignificantGatherableTextData& SignificantGatherableTextDataTwo) { return SignificantGatherableTextDataOne.Identity < SignificantGatherableTextDataTwo.Identity; }); return SignificantGatherableTextDataArray; }; TArray SignificantGatherableTextDataArrayOne = ExtractSignificantGatherableTextData(GatherableTextDataArrayOne); TArray SignificantGatherableTextDataArrayTwo = ExtractSignificantGatherableTextData(GatherableTextDataArrayTwo); if (SignificantGatherableTextDataArrayOne.Num() != SignificantGatherableTextDataArrayTwo.Num()) { return false; } // These arrays are sorted by identity, so everything should match as we iterate through the array // If it doesn't, then these caches aren't identical for (int32 Idx = 0; Idx < SignificantGatherableTextDataArrayOne.Num(); ++Idx) { const FSignificantGatherableTextData& SignificantGatherableTextDataOne = SignificantGatherableTextDataArrayOne[Idx]; const FSignificantGatherableTextData& SignificantGatherableTextDataTwo = SignificantGatherableTextDataArrayTwo[Idx]; if (SignificantGatherableTextDataOne.Identity != SignificantGatherableTextDataTwo.Identity) { return false; } if (!SignificantGatherableTextDataOne.SourceString.Equals(SignificantGatherableTextDataTwo.SourceString, ESearchCase::CaseSensitive)) { return false; } } return true; } int32 UGatherTextFromAssetsCommandlet::Main(const FString& Params) { // Parse command line. TArray Tokens; TArray Switches; TMap ParamVals; UCommandlet::ParseCommandLine(*Params, Tokens, Switches, ParamVals); FString GatherTextConfigPath; FString SectionName; if (!GetConfigurationScript(ParamVals, GatherTextConfigPath, SectionName)) { return -1; } if (!ConfigureFromScript(GatherTextConfigPath, SectionName)) { return -1; } FGatherTextDelegates::GetAdditionalGatherPaths.Broadcast(GatherManifestHelper->GetTargetName(), IncludePathFilters, ExcludePathFilters); // Get destination path FString DestinationPath; if (!GetPathFromConfig(*SectionName, TEXT("DestinationPath"), DestinationPath, GatherTextConfigPath)) { UE_LOG(LogGatherTextFromAssetsCommandlet, Error, TEXT("No destination path specified.")); return -1; } // Add any manifest dependencies if they were provided { bool HasFailedToAddManifestDependency = false; for (const FString& ManifestDependency: ManifestDependenciesList) { FText OutError; if (!GatherManifestHelper->AddDependency(ManifestDependency, &OutError)) { UE_LOG(LogGatherTextFromAssetsCommandlet, Error, TEXT("The GatherTextFromAssets commandlet couldn't load the specified manifest dependency: '%'. %s"), *ManifestDependency, *OutError.ToString()); HasFailedToAddManifestDependency = true; } } if (HasFailedToAddManifestDependency) { return -1; } } // Preload necessary modules. { bool HasFailedToPreloadAnyModules = false; for (const FString& ModuleName: ModulesToPreload) { EModuleLoadResult ModuleLoadResult; FModuleManager::Get().LoadModuleWithFailureReason(*ModuleName, ModuleLoadResult); if (ModuleLoadResult != EModuleLoadResult::Success) { HasFailedToPreloadAnyModules = true; continue; } } if (HasFailedToPreloadAnyModules) { return -1; } } FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); IAssetRegistry& AssetRegistry = AssetRegistryModule.Get(); AssetRegistry.SearchAllAssets(true); TArray AssetDataArray; { FARFilter FirstPassFilter; // Filter object paths to only those in any of the specified collections. { bool HasFailedToGetACollection = false; FCollectionManagerModule& CollectionManagerModule = FCollectionManagerModule::GetModule(); ICollectionManager& CollectionManager = CollectionManagerModule.Get(); for (const FString& CollectionName: CollectionFilters) { if (!CollectionManager.GetObjectsInCollection(FName(*CollectionName), ECollectionShareType::CST_All, FirstPassFilter.ObjectPaths, ECollectionRecursionFlags::SelfAndChildren)) { UE_LOG(LogGatherTextFromAssetsCommandlet, Error, TEXT("Failed get objects in specified collection: %s"), *CollectionName); HasFailedToGetACollection = true; } } if (HasFailedToGetACollection) { return -1; } } // Filter out any objects of the specified classes and their children at this point. if (ShouldExcludeDerivedClasses) { FirstPassFilter.bRecursiveClasses = true; FirstPassFilter.ClassNames.Add(TEXT("Object")); for (const FString& ExcludeClassName: ExcludeClassNames) { // Note: Can't necessarily validate these class names here, as the class may be a generated blueprint class that hasn't been loaded yet. FirstPassFilter.RecursiveClassesExclusionSet.Add(*ExcludeClassName); } } // Apply filter if valid to do so, get all assets otherwise. if (FirstPassFilter.IsEmpty()) { AssetRegistry.GetAllAssets(AssetDataArray); } else { AssetRegistry.GetAssets(FirstPassFilter, AssetDataArray); } } if (!ShouldExcludeDerivedClasses) { // Filter out any objects of the specified classes. FARFilter ExcludeExactClassesFilter; ExcludeExactClassesFilter.bRecursiveClasses = false; for (const FString& ExcludeClassName: ExcludeClassNames) { // Note: Can't necessarily validate these class names here, as the class may be a generated blueprint class that hasn't been loaded yet. ExcludeExactClassesFilter.ClassNames.Add(*ExcludeClassName); } // Reapply filter over the current set of assets. if (!ExcludeExactClassesFilter.IsEmpty()) { // NOTE: The filter applied is actually the inverse, due to API limitations, so the resultant set must be removed from the current set. TArray AssetsToExclude = AssetDataArray; AssetRegistry.RunAssetsThroughFilter(AssetsToExclude, ExcludeExactClassesFilter); AssetDataArray.RemoveAll([&](const FAssetData& AssetData) { return AssetsToExclude.Contains(AssetData); }); } } // Note: AssetDataArray now contains all assets in the specified collections that are not instances of the specified excluded classes. const FFuzzyPathMatcher FuzzyPathMatcher = FFuzzyPathMatcher(IncludePathFilters, ExcludePathFilters); AssetDataArray.RemoveAll([&](const FAssetData& PartiallyFilteredAssetData) -> bool { FString PackageFilePath; if (!FPackageName::FindPackageFileWithoutExtension(FPackageName::LongPackageNameToFilename(PartiallyFilteredAssetData.PackageName.ToString()), PackageFilePath)) { return true; } PackageFilePath = FPaths::ConvertRelativePathToFull(PackageFilePath); const FString PackageFileName = FPaths::GetCleanFilename(PackageFilePath); // Filter out assets whose package file names DO NOT match any of the package file name filters. { bool HasPassedAnyFileNameFilter = false; for (const FString& PackageFileNameFilter: PackageFileNameFilters) { if (PackageFileName.MatchesWildcard(PackageFileNameFilter)) { HasPassedAnyFileNameFilter = true; break; } } if (!HasPassedAnyFileNameFilter) { return true; } } // Filter out assets whose package file paths do not pass the "fuzzy path" filters. if (FuzzyPathMatcher.TestPath(PackageFilePath) != FFuzzyPathMatcher::Included) { return true; } return false; }); if (AssetDataArray.Num() == 0) { UE_LOG(LogGatherTextFromAssetsCommandlet, Warning, TEXT("No assets matched the specified criteria.")); return 0; } // Collect the basic information about the packages that we're going to gather from { // Collapse the assets down to a set of packages TSet PackageNamesToGather; PackageNamesToGather.Reserve(AssetDataArray.Num()); for (const FAssetData& AssetData: AssetDataArray) { PackageNamesToGather.Add(AssetData.PackageName); } AssetDataArray.Empty(); // Build the basic information for the packages to gather (dependencies are filled in later once we've processed cached packages) PackagesPendingGather.Reserve(PackageNamesToGather.Num()); for (const FName& PackageNameToGather: PackageNamesToGather) { FString PackageFilename; if (!FPackageName::FindPackageFileWithoutExtension(FPackageName::LongPackageNameToFilename(PackageNameToGather.ToString()), PackageFilename)) { continue; } PackageFilename = FPaths::ConvertRelativePathToFull(PackageFilename); FPackagePendingGather& PackagePendingGather = PackagesPendingGather[PackagesPendingGather.AddDefaulted()]; PackagePendingGather.PackageName = PackageNameToGather; PackagePendingGather.PackageFilename = MoveTemp(PackageFilename); PackagePendingGather.PackageLocCacheState = EPackageLocCacheState::Cached; } } FAssetGatherCacheMetrics AssetGatherCacheMetrics; TMap AssignedPackageLocalizationIds; // Process all packages that do not need to be loaded. Remove processed packages from the list. PackagesPendingGather.RemoveAll([&](FPackagePendingGather& PackagePendingGather) -> bool { TUniquePtr FileReader(IFileManager::Get().CreateFileReader(*PackagePendingGather.PackageFilename)); if (!FileReader) { return false; } // Read package file summary from the file. FPackageFileSummary PackageFileSummary; *FileReader << PackageFileSummary; // Track the package localization ID of this package (if known) and detect duplicates if (!PackageFileSummary.LocalizationId.IsEmpty()) { if (const FName* ExistingLongPackageName = AssignedPackageLocalizationIds.Find(PackageFileSummary.LocalizationId)) { UE_LOG(LogGatherTextFromAssetsCommandlet, Warning, TEXT("Package '%s' and '%s' have the same localization ID (%s). Please reset one of these (Asset Localization -> Reset Localization ID) to avoid conflicts."), *PackagePendingGather.PackageName.ToString(), *ExistingLongPackageName->ToString(), *PackageFileSummary.LocalizationId); } else { AssignedPackageLocalizationIds.Add(PackageFileSummary.LocalizationId, PackagePendingGather.PackageName); } } PackagePendingGather.PackageLocCacheState = EPackageLocCacheState::Cached; // 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) { PackagePendingGather.PackageLocCacheState = EPackageLocCacheState::Uncached_NoCache; } } 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) { PackagePendingGather.PackageLocCacheState = EPackageLocCacheState::Uncached_TooOld; } // 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 || (!EditorVersion || EditorVersion->Version < FEditorObjectVersion::GatheredTextEditorOnlyPackageLocId)) { // Fallback on the old package flag check. if (PackageFileSummary.PackageFlags & PKG_RequiresLocalizationGather) { PackagePendingGather.PackageLocCacheState = EPackageLocCacheState::Uncached_TooOld; } } else if (PackageFileSummary.GetFileVersionUE4() < VER_UE4_DIALOGUE_WAVE_NAMESPACE_AND_CONTEXT_CHANGES) { TArray AllAssetDataInSamePackage; AssetRegistry.GetAssetsByPackageName(PackagePendingGather.PackageName, AllAssetDataInSamePackage); for (const FAssetData& AssetData: AllAssetDataInSamePackage) { if (AssetData.AssetClass == UDialogueWave::StaticClass()->GetFName()) { PackagePendingGather.PackageLocCacheState = EPackageLocCacheState::Uncached_TooOld; } } } // If this package doesn't have any cached data, then we have to load it for gather if (PackageFileSummary.GetFileVersionUE4() >= VER_UE4_SERIALIZE_TEXT_IN_PACKAGES && PackageFileSummary.GatherableTextDataOffset == 0 && (PackageFileSummary.PackageFlags & PKG_RequiresLocalizationGather)) { PackagePendingGather.PackageLocCacheState = EPackageLocCacheState::Uncached_NoCache; } if (PackagePendingGather.PackageLocCacheState != EPackageLocCacheState::Cached) { AssetGatherCacheMetrics.CountUncachedAsset(PackagePendingGather.PackageLocCacheState); return false; } // Process packages that don't require loading to process. if (PackageFileSummary.GatherableTextDataOffset > 0) { AssetGatherCacheMetrics.CountCachedAsset(); FileReader->Seek(PackageFileSummary.GatherableTextDataOffset); PackagePendingGather.GatherableTextDataArray.SetNum(PackageFileSummary.GatherableTextDataCount); for (int32 GatherableTextDataIndex = 0; GatherableTextDataIndex < PackageFileSummary.GatherableTextDataCount; ++GatherableTextDataIndex) { (*FileReader) << PackagePendingGather.GatherableTextDataArray[GatherableTextDataIndex]; } ProcessGatherableTextDataArray(PackagePendingGather.GatherableTextDataArray); } // If we're reporting or fixing assets with a stale gather cache then we still need to load this // package in order to do that, but the PackageLocCacheState prevents it being gathered again if (bReportStaleGatherCache || bFixStaleGatherCache) { check(PackagePendingGather.PackageLocCacheState == EPackageLocCacheState::Cached); return false; } return true; }); AssetGatherCacheMetrics.LogMetrics(); const int32 PackageCount = PackagesPendingGather.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, Display, TEXT("Loading %i packages in %i batches of %i."), PackageCount, BatchCount, PackagesPerBatchCount); } FLoadPackageLogOutputRedirector LogOutputRedirector; CalculateDependenciesForPackagesPendingGather(); TArray PackagesWithStaleGatherCache; // Process the packages in batches TArray GatherableTextDataArray; for (int32 BatchIndex = 0; BatchIndex < BatchCount; ++BatchIndex) { int32 PackagesInThisBatch = 0; int32 FailuresInThisBatch = 0; // Collect garbage before beginning to load packages for this batch // This also sorts the list of packages into the best processing order PurgeGarbage(/*bPurgeReferencedPackages*/ false); // Process this batch const int32 PackagesToProcessThisBatch = FMath::Min(PackagesPendingGather.Num(), PackagesPerBatchCount); for (int32 PackageIndex = 0; PackageIndex < PackagesToProcessThisBatch; ++PackageIndex) { const FPackagePendingGather& PackagePendingGather = PackagesPendingGather[PackageIndex]; const FString PackageNameStr = PackagePendingGather.PackageName.ToString(); UE_LOG(LogGatherTextFromAssetsCommandlet, Verbose, TEXT("Loading package: '%s'."), *PackageNameStr); UPackage* Package = nullptr; { FLoadPackageLogOutputRedirector::FScopedCapture ScopedCapture(&LogOutputRedirector, PackageNameStr); Package = LoadPackage(nullptr, *PackageNameStr, LOAD_NoWarn | LOAD_Quiet); } if (!Package) { ++FailuresInThisBatch; continue; } ++PackagesInThisBatch; // 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()) { // Gathers from the given package EPropertyLocalizationGathererResultFlags GatherableTextResultFlags = EPropertyLocalizationGathererResultFlags::Empty; FPropertyLocalizationDataGatherer(GatherableTextDataArray, Package, GatherableTextResultFlags); bool bSavePackage = false; // Optionally check to see whether the clean gather we did is in-sync with the gather cache and deal with it accordingly if ((bReportStaleGatherCache || bFixStaleGatherCache) && PackagePendingGather.PackageLocCacheState == EPackageLocCacheState::Cached) { // Look for any structurally significant changes (missing, added, or changed texts) in the cache // Ignore insignificant things (like source changes caused by assets moving or being renamed) if (EnumHasAnyFlags(GatherableTextResultFlags, EPropertyLocalizationGathererResultFlags::HasTextWithInvalidPackageLocalizationID) || !IsGatherableTextDataIdentical(GatherableTextDataArray, PackagePendingGather.GatherableTextDataArray)) { PackagesWithStaleGatherCache.Add(PackagePendingGather.PackageName); if (bFixStaleGatherCache) { bSavePackage = true; } } } // Optionally save the package if it is missing a gather cache if (bFixMissingGatherCache && PackagePendingGather.PackageLocCacheState == EPackageLocCacheState::Uncached_TooOld) { bSavePackage = true; } // Re-save the package to attempt to fix it? if (bSavePackage) { UE_LOG(LogGatherTextFromAssetsCommandlet, Display, TEXT("Resaving package: '%s'."), *PackageNameStr); bool bSavedPackage = false; { FLoadPackageLogOutputRedirector::FScopedCapture ScopedCapture(&LogOutputRedirector, PackageNameStr); bSavedPackage = FLocalizedAssetSCCUtil::SavePackageWithSCC(SourceControlInfo, Package, PackagePendingGather.PackageFilename); } if (!bSavedPackage) { UE_LOG(LogGatherTextFromAssetsCommandlet, Error, TEXT("Failed to resave package: '%s'."), *PackageNameStr); } } // This package may have already been cached in cases where we're reporting or fixing assets with a stale gather cache // This check prevents it being gathered a second time if (PackagePendingGather.PackageLocCacheState != EPackageLocCacheState::Cached) { ProcessGatherableTextDataArray(GatherableTextDataArray); } GatherableTextDataArray.Reset(); } if (HasExceededMemoryLimit()) { // Over the memory limit, perform a full purge PurgeGarbage(/*bPurgeReferencedPackages*/ true); } } UE_LOG(LogGatherTextFromAssetsCommandlet, Display, TEXT("Loaded %i packages in batch %i of %i. %i failed."), PackagesInThisBatch, BatchIndex + 1, BatchCount, FailuresInThisBatch); // Remove the processed packages PackagesPendingGather.RemoveAt(0, PackagesToProcessThisBatch, /*bAllowShrinking*/ false); } check(PackagesPendingGather.Num() == 0); PackagesWithStaleGatherCache.Sort(FNameLexicalLess()); if (bReportStaleGatherCache) { FString StaleGatherCacheReport; for (const FName& PackageWithStaleGatherCache: PackagesWithStaleGatherCache) { StaleGatherCacheReport += PackageWithStaleGatherCache.ToString(); StaleGatherCacheReport += TEXT("\n"); } const FString StaleGatherCacheReportFilename = DestinationPath / TEXT("StaleGatherCacheReport.txt"); const bool bStaleGatherCacheReportSaved = FLocalizedAssetSCCUtil::SaveFileWithSCC(SourceControlInfo, StaleGatherCacheReportFilename, [&StaleGatherCacheReport](const FString& InSaveFileName) -> bool { return FFileHelper::SaveStringToFile(StaleGatherCacheReport, *InSaveFileName, FFileHelper::EEncodingOptions::ForceUTF8); }); if (!bStaleGatherCacheReportSaved) { UE_LOG(LogGatherTextFromAssetsCommandlet, Error, TEXT("Failed to save report: '%s'."), *StaleGatherCacheReportFilename); } } return 0; } bool UGatherTextFromAssetsCommandlet::GetConfigurationScript(const TMap& InCommandLineParameters, FString& OutFilePath, FString& OutStepSectionName) { // Set config file const FString* ParamVal = InCommandLineParameters.Find(FString(TEXT("Config"))); if (ParamVal) { OutFilePath = *ParamVal; } else { UE_LOG(LogGatherTextFromAssetsCommandlet, Error, TEXT("No config specified.")); return false; } // Set config section ParamVal = InCommandLineParameters.Find(FString(TEXT("Section"))); if (ParamVal) { OutStepSectionName = *ParamVal; } else { UE_LOG(LogGatherTextFromAssetsCommandlet, Error, TEXT("No config section specified.")); return false; } return true; } bool UGatherTextFromAssetsCommandlet::ConfigureFromScript(const FString& GatherTextConfigPath, const FString& SectionName) { bool HasFatalError = false; // Modules to Preload GetStringArrayFromConfig(*SectionName, TEXT("ModulesToPreload"), ModulesToPreload, GatherTextConfigPath); // IncludePathFilters GetPathArrayFromConfig(*SectionName, TEXT("IncludePathFilters"), IncludePathFilters, GatherTextConfigPath); // IncludePaths (DEPRECATED) { TArray 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); HasFatalError = true; } // Collections GetStringArrayFromConfig(*SectionName, TEXT("CollectionFilters"), CollectionFilters, GatherTextConfigPath); for (const FString& CollectionName: CollectionFilters) { FCollectionManagerModule& CollectionManagerModule = FCollectionManagerModule::GetModule(); ICollectionManager& CollectionManager = CollectionManagerModule.Get(); const bool DoesCollectionExist = CollectionManager.CollectionExists(FName(*CollectionName), ECollectionShareType::CST_All); if (!DoesCollectionExist) { UE_LOG(LogGatherTextFromAssetsCommandlet, Error, TEXT("Failed to find a collection with name \"%s\", collection does not exist."), *CollectionName); HasFatalError = true; } } // ExcludePathFilters GetPathArrayFromConfig(*SectionName, TEXT("ExcludePathFilters"), ExcludePathFilters, GatherTextConfigPath); // ExcludePaths (DEPRECATED) { TArray 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 GetStringArrayFromConfig(*SectionName, TEXT("PackageFileNameFilters"), PackageFileNameFilters, GatherTextConfigPath); // PackageExtensions (DEPRECATED) { TArray 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); HasFatalError = true; } // Recursive asset class exclusion if (!GetBoolFromConfig(*SectionName, TEXT("ShouldExcludeDerivedClasses"), ShouldExcludeDerivedClasses, GatherTextConfigPath)) { ShouldExcludeDerivedClasses = false; } // Asset class exclude GetStringArrayFromConfig(*SectionName, TEXT("ExcludeClasses"), ExcludeClassNames, GatherTextConfigPath); GetPathArrayFromConfig(*SectionName, TEXT("ManifestDependencies"), ManifestDependenciesList, GatherTextConfigPath); // 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; } auto ReadBoolFlagWithFallback = [this, &SectionName, &GatherTextConfigPath](const TCHAR* FlagName, bool& OutValue) { OutValue = FParse::Param(FCommandLine::Get(), FlagName); if (!OutValue) { GetBoolFromConfig(*SectionName, FlagName, OutValue, GatherTextConfigPath); } UE_LOG(LogGatherTextFromAssetsCommandlet, Display, TEXT("%s: %s"), FlagName, OutValue ? TEXT("true") : TEXT("false")); }; ReadBoolFlagWithFallback(TEXT("SkipGatherCache"), bSkipGatherCache); ReadBoolFlagWithFallback(TEXT("ReportStaleGatherCache"), bReportStaleGatherCache); ReadBoolFlagWithFallback(TEXT("FixStaleGatherCache"), bFixStaleGatherCache); ReadBoolFlagWithFallback(TEXT("FixMissingGatherCache"), bFixMissingGatherCache); // Read some settings from the editor config { int32 MaxMemoryAllowanceInMB = 0; GConfig->GetInt(TEXT("GatherTextFromAssets"), TEXT("MaxMemoryAllowance"), MaxMemoryAllowanceInMB, GEditorIni); MaxMemoryAllowanceInMB = FMath::Max(MaxMemoryAllowanceInMB, 0); MaxMemoryAllowanceBytes = MaxMemoryAllowanceInMB * 1024LL * 1024LL; PackagesPerBatchCount = 100; GConfig->GetInt(TEXT("GatherTextFromAssets"), TEXT("BatchCount"), PackagesPerBatchCount, GEditorIni); } return !HasFatalError; } #undef LOC_DEFINE_REGION //////////////////////////////////////////////////////////////////////////