2448 lines
85 KiB
C++
2448 lines
85 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
/*=============================================================================
|
|
PackageUtilities.cpp: Commandlets for viewing information about package files
|
|
=============================================================================*/
|
|
|
|
#include "CoreMinimal.h"
|
|
#include "Animation/Skeleton.h"
|
|
#include "HAL/FileManager.h"
|
|
#include "Misc/CommandLine.h"
|
|
#include "Misc/FileHelper.h"
|
|
#include "Misc/Paths.h"
|
|
#include "Misc/ObjectThumbnail.h"
|
|
#include "UObject/ObjectMacros.h"
|
|
#include "UObject/Class.h"
|
|
#include "UObject/UObjectIterator.h"
|
|
#include "UObject/Package.h"
|
|
#include "Serialization/ArchiveCountMem.h"
|
|
#include "Misc/PackageName.h"
|
|
#include "UObject/ObjectResource.h"
|
|
#include "UObject/LinkerLoad.h"
|
|
#include "UObject/SavePackage.h"
|
|
#include "Engine/EngineTypes.h"
|
|
#include "GameFramework/Actor.h"
|
|
#include "Engine/World.h"
|
|
#include "Commandlets/Commandlet.h"
|
|
#include "Commandlets/CompressAnimationsCommandlet.h"
|
|
#include "Engine/SkeletalMesh.h"
|
|
#include "Animation/AnimSequence.h"
|
|
#include "ISourceControlOperation.h"
|
|
#include "SourceControlOperations.h"
|
|
#include "SourceControlHelpers.h"
|
|
#include "Commandlets/LoadPackageCommandlet.h"
|
|
#include "Commandlets/PkgInfoCommandlet.h"
|
|
#include "Commandlets/ReplaceActorCommandlet.h"
|
|
#include "Misc/ConfigCacheIni.h"
|
|
#include "Serialization/ArchiveReplaceObjectRef.h"
|
|
#include "GameFramework/WorldSettings.h"
|
|
#include "Editor.h"
|
|
#include "FileHelpers.h"
|
|
#include "AssetRegistry/IAssetRegistry.h"
|
|
|
|
#include "CollectionManagerTypes.h"
|
|
#include "ICollectionContainer.h"
|
|
#include "ICollectionManager.h"
|
|
#include "CollectionManagerModule.h"
|
|
#include "PackageHelperFunctions.h"
|
|
#include "PackageUtilityWorkers.h"
|
|
|
|
#include "AnimationCompression.h"
|
|
#include "Animation/AnimationSettings.h"
|
|
|
|
#include "EngineUtils.h"
|
|
#include "Animation/IAnimationSequenceCompiler.h"
|
|
#include "Materials/Material.h"
|
|
#include "Serialization/ArchiveStackTrace.h"
|
|
#include "Misc/OutputDeviceHelper.h"
|
|
#include "Misc/OutputDeviceFile.h"
|
|
#include "UObject/UObjectThreadContext.h"
|
|
#include "Internationalization/GatherableTextData.h"
|
|
|
|
DEFINE_LOG_CATEGORY(LogPackageHelperFunctions);
|
|
DEFINE_LOG_CATEGORY_STATIC(LogPackageUtilities, Log, All);
|
|
|
|
/*-----------------------------------------------------------------------------
|
|
Package Helper Functions (defined in PackageHelperFunctions.h
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
void SearchDirectoryRecursive( const FString& SearchPathMask, TArray<FString>& out_PackageNames, TArray<FString>& out_PackageFilenames )
|
|
{
|
|
const FString SearchPath = FPaths::GetPath(SearchPathMask);
|
|
TArray<FString> PackageNames;
|
|
IFileManager::Get().FindFiles( PackageNames, *SearchPathMask, true, false );
|
|
if ( PackageNames.Num() > 0 )
|
|
{
|
|
for ( int32 PkgIndex = 0; PkgIndex < PackageNames.Num(); PkgIndex++ )
|
|
{
|
|
out_PackageFilenames.Add( SearchPath / PackageNames[PkgIndex] );
|
|
}
|
|
|
|
out_PackageNames += PackageNames;
|
|
}
|
|
|
|
// now search all subdirectories
|
|
TArray<FString> Subdirectories;
|
|
IFileManager::Get().FindFiles( Subdirectories, *(SearchPath / TEXT("*")), false, true );
|
|
for ( int32 DirIndex = 0; DirIndex < Subdirectories.Num(); DirIndex++ )
|
|
{
|
|
SearchDirectoryRecursive( SearchPath / Subdirectories[DirIndex] / FPaths::GetCleanFilename(SearchPathMask), out_PackageNames, out_PackageFilenames);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Takes an array of package names (in any format) and converts them into relative pathnames for each package.
|
|
*
|
|
* @param PackageNames the array of package names to normalize. If this array is empty, the complete package list will be used.
|
|
* @param PackagePathNames will be filled with the complete relative path name for each package name in the input array
|
|
* @param PackageWildcard if specified, allows the caller to specify a wildcard to use for finding package files
|
|
* @param PackageFilter allows the caller to limit the types of packages returned.
|
|
*
|
|
* @return true if packages were found successfully, false otherwise.
|
|
*/
|
|
bool NormalizePackageNames( TArray<FString> PackageNames, TArray<FString>& PackagePathNames, const FString& PackageWildcard, uint8 PackageFilter )
|
|
{
|
|
if ( PackageNames.Num() == 0 )
|
|
{
|
|
IFileManager::Get().FindFiles( PackageNames, *PackageWildcard, true, false );
|
|
}
|
|
|
|
FString ProjectContentDir = FPaths::ProjectContentDir();
|
|
FStringView DevelopersFolderName = FPaths::DevelopersFolderName();
|
|
const FString DeveloperFolder = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(
|
|
*FPaths::Combine(ProjectContentDir, DevelopersFolderName));
|
|
const FString DeveloperExternalActorsFolder = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(
|
|
*FPaths::Combine(ProjectContentDir, FPackagePath::GetExternalActorsFolderName(), DevelopersFolderName));
|
|
const FString DeveloperExternalObjectsFolder = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(
|
|
*FPaths::Combine(ProjectContentDir, FPackagePath::GetExternalObjectsFolderName(), DevelopersFolderName));
|
|
|
|
if( PackageNames.Num() == 0 )
|
|
{
|
|
TArray<FString> Paths;
|
|
if ( GConfig->GetArray( TEXT("Core.System"), TEXT("Paths"), Paths, GEngineIni ) > 0 )
|
|
{
|
|
TStringBuilder<256> UnusedPackagePath;
|
|
TStringBuilder<256> UnusedFilePath;
|
|
TStringBuilder<256> UnusedRelPath;
|
|
for ( const FString& Path : Paths)
|
|
{
|
|
if (!FPackageName::TryGetMountPointForPath(Path, UnusedPackagePath, UnusedFilePath, UnusedRelPath))
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning,
|
|
TEXT("Engine.ini:[Core.System]:Paths entry '%s' is not mounted. Skipping it."), *Path);
|
|
continue;
|
|
}
|
|
FString SearchWildcard = Path / PackageWildcard;
|
|
UE_LOG(LogPackageUtilities, Log, TEXT("Searching using wildcard: '%s'"), *SearchWildcard);
|
|
SearchDirectoryRecursive(SearchWildcard, PackageNames, PackagePathNames);
|
|
}
|
|
}
|
|
|
|
if ( PackageNames.Num() == 0 )
|
|
{
|
|
// Check if long package name is provided and if it exists on disk.
|
|
FString Filename;
|
|
if ( FPackageName::IsValidLongPackageName(PackageWildcard, true) && FPackageName::DoesPackageExist(PackageWildcard, &Filename) )
|
|
{
|
|
PackagePathNames.Add(Filename);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// re-add the path information so that GetPackageLinker finds the correct version of the file.
|
|
const FString WildcardPath = FPaths::GetPath(PackageWildcard);
|
|
for ( int32 FileIndex = 0; FileIndex < PackageNames.Num(); FileIndex++ )
|
|
{
|
|
PackagePathNames.Add(WildcardPath / PackageNames[FileIndex]);
|
|
}
|
|
}
|
|
|
|
if ( PackagePathNames.Num() == 0 )
|
|
{
|
|
UE_LOG(LogPackageUtilities, Log, TEXT("No packages found using '%s'!"), *PackageWildcard);
|
|
return false;
|
|
}
|
|
|
|
// now apply any filters to the list of packages
|
|
for ( int32 PackageIndex = PackagePathNames.Num() - 1; PackageIndex >= 0; PackageIndex-- )
|
|
{
|
|
FString PackageExtension = FPaths::GetExtension(PackagePathNames[PackageIndex], true);
|
|
if ( !FPackageName::IsPackageExtension(*PackageExtension) )
|
|
{
|
|
// not a valid package file - remove it
|
|
PackagePathNames.RemoveAt(PackageIndex);
|
|
}
|
|
else
|
|
{
|
|
if ( (PackageFilter&NORMALIZE_ExcludeMapPackages) != 0 )
|
|
{
|
|
if ( PackageExtension == FPackageName::GetMapPackageExtension() )
|
|
{
|
|
PackagePathNames.RemoveAt(PackageIndex);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if ( (PackageFilter&NORMALIZE_ExcludeContentPackages) != 0 )
|
|
{
|
|
if ( PackageExtension == FPackageName::GetAssetPackageExtension() )
|
|
{
|
|
PackagePathNames.RemoveAt(PackageIndex);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if ( (PackageFilter&NORMALIZE_ExcludeEnginePackages) != 0 )
|
|
{
|
|
if (PackagePathNames[PackageIndex].StartsWith(FPaths::EngineDir()))
|
|
{
|
|
PackagePathNames.RemoveAt(PackageIndex);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
FString Filename = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*PackagePathNames[PackageIndex]);
|
|
|
|
if ( (PackageFilter & NORMALIZE_ExcludeDeveloperPackages) != 0 ||
|
|
(PackageFilter & NORMALIZE_ExcludeNonDeveloperPackages) != 0)
|
|
{
|
|
// Technically both flags present should mean exclude everything, but legacy behavior is to have
|
|
// excludedevelopers override excludenondevelopers.
|
|
bool bDevelopersFolderIncluded = !((PackageFilter & NORMALIZE_ExcludeDeveloperPackages) != 0);
|
|
bool bIsDeveloperFolder = Filename.StartsWith(DeveloperFolder) ||
|
|
Filename.StartsWith(DeveloperExternalActorsFolder) ||
|
|
Filename.StartsWith(DeveloperExternalObjectsFolder);
|
|
if (bDevelopersFolderIncluded != bIsDeveloperFolder)
|
|
{
|
|
PackagePathNames.RemoveAt(PackageIndex);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if ( (PackageFilter&NORMALIZE_ExcludeNoRedistPackages) != 0 )
|
|
{
|
|
if (Filename.Contains(TEXT("/NoRedist/")) || Filename.Contains(TEXT("/NotForLicensees/")) || Filename.Contains(TEXT("/LimitedAccess/")) || Filename.Contains(TEXT("/EpicInternal/")))
|
|
{
|
|
PackagePathNames.RemoveAt(PackageIndex);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if ( (PackageFilter&NORMALIZE_ExcludeLocalizedPackages) != 0 )
|
|
{
|
|
FString PackageName;
|
|
if (FPackageName::TryConvertFilenameToLongPackageName(Filename, PackageName) && FPackageName::IsLocalizedPackage(PackageName))
|
|
{
|
|
PackagePathNames.RemoveAt(PackageIndex);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( (PackageFilter&NORMALIZE_ResetExistingLoaders) != 0 )
|
|
{
|
|
// reset the loaders for the packages we want to load so that we don't find the wrong version of the file
|
|
for ( int32 PackageIndex = 0; PackageIndex < PackagePathNames.Num(); PackageIndex++ )
|
|
{
|
|
// (otherwise, attempting to run a commandlet on e.g. Engine.xxx will always return results for Engine.u instead)
|
|
FString PackageName;
|
|
if (!FPackageName::TryConvertFilenameToLongPackageName(PackagePathNames[PackageIndex], PackageName))
|
|
{
|
|
PackageName = PackagePathNames[PackageIndex];
|
|
}
|
|
UPackage* ExistingPackage = FindObject<UPackage>(NULL, *PackageName, true);
|
|
if ( ExistingPackage != NULL )
|
|
{
|
|
// skip resetting loaders on default materials since they are expected to be post-loaded at that point
|
|
bool bContainsDefaultMaterial = false;
|
|
ForEachObjectWithOuter(ExistingPackage,
|
|
[&bContainsDefaultMaterial](UObject* Obj)
|
|
{
|
|
if (!bContainsDefaultMaterial)
|
|
{
|
|
UMaterial* Material = Cast<UMaterial>(Obj);
|
|
if (Material && Material->IsDefaultMaterial())
|
|
{
|
|
bContainsDefaultMaterial = true;
|
|
}
|
|
}
|
|
}
|
|
);
|
|
|
|
if (!bContainsDefaultMaterial)
|
|
{
|
|
ResetLoaders(ExistingPackage);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SavePackageHelper(UPackage* Package, FString Filename, EObjectFlags KeepObjectFlags, FOutputDevice* ErrorDevice, FLinkerNull* LinkerToConformAgainst, ESaveFlags SaveFlags)
|
|
{
|
|
return SavePackageHelper(Package, Filename, KeepObjectFlags, ErrorDevice, SaveFlags);
|
|
}
|
|
|
|
bool SavePackageHelper(UPackage* Package, FString Filename, EObjectFlags KeepObjectFlags, FOutputDevice* ErrorDevice, ESaveFlags SaveFlags)
|
|
{
|
|
FSavePackageArgs SaveArgs;
|
|
SaveArgs.TopLevelFlags = KeepObjectFlags;
|
|
SaveArgs.Error = ErrorDevice;
|
|
SaveArgs.SaveFlags = SaveFlags;
|
|
return GEditor->SavePackage(Package, nullptr, *Filename, SaveArgs);
|
|
}
|
|
|
|
/**
|
|
* Policy that marks Asset Sets via the CollectionManager module
|
|
*/
|
|
class FCollectionPolicy
|
|
{
|
|
public:
|
|
static bool CreateAssetSet(ICollectionContainer& CollectionContainer, FName InSetName, ECollectionShareType::Type InSetType)
|
|
{
|
|
return CollectionContainer.CreateCollection(InSetName, InSetType, ECollectionStorageMode::Static);
|
|
}
|
|
|
|
static bool DestroyAssetSet(ICollectionContainer& CollectionContainer, FName InSetName, ECollectionShareType::Type InSetType )
|
|
{
|
|
return CollectionContainer.DestroyCollection(InSetName, InSetType);
|
|
}
|
|
|
|
static bool RemoveAssetsFromSet(ICollectionContainer& CollectionContainer, FName InSetName, ECollectionShareType::Type InSetType, const TArray<FSoftObjectPath>& InAssetPathNames )
|
|
{
|
|
return CollectionContainer.RemoveFromCollection(InSetName, InSetType, InAssetPathNames);
|
|
}
|
|
|
|
static bool AddAssetsToSet(ICollectionContainer& CollectionContainer, FName InSetName, ECollectionShareType::Type InSetType, const TArray<FSoftObjectPath>& InAssetPathNames )
|
|
{
|
|
return CollectionContainer.AddToCollection(InSetName, InSetType, InAssetPathNames);
|
|
}
|
|
|
|
static bool QueryAssetsInSet(ICollectionContainer& CollectionContainer, FName InSetName, ECollectionShareType::Type InSetType, TArray<FSoftObjectPath>& OutAssetPathNames )
|
|
{
|
|
return CollectionContainer.GetAssetsInCollection(InSetName, InSetType, OutAssetPathNames);
|
|
}
|
|
};
|
|
|
|
template <class AssetSetPolicy>
|
|
bool FContentHelper::CreateAssetSet(FName InSetName, ECollectionShareType::Type InSetType )
|
|
{
|
|
return AssetSetPolicy::CreateAssetSet(*CollectionContainer, InSetName, InSetType);
|
|
}
|
|
|
|
/** Clears the content of a Tag or Collection */
|
|
template <class AssetSetPolicy>
|
|
bool FContentHelper::ClearAssetSet(FName InSetName, ECollectionShareType::Type InSetType )
|
|
{
|
|
if (bInitialized == false)
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Collection Helper is not initialized."));
|
|
return false;
|
|
}
|
|
|
|
if ( AssetSetPolicy::DestroyAssetSet( *CollectionContainer, InSetName, InSetType ) == false)
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Collection Helper failed to destroy collection %s."), *InSetName.ToString());
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/** Sets the contents of a Tag or Collection to be the InAssetList. Assets not mentioned in the list will be untagged. */
|
|
template <class AssetSetPolicy>
|
|
bool FContentHelper::AssignSetContent(FName InSetName, ECollectionShareType::Type InType, const TArray<FSoftObjectPath>& InAssetList )
|
|
{
|
|
bool bResult = true;
|
|
|
|
if (bInitialized == false)
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Collection Helper is not initialized."));
|
|
return false;
|
|
}
|
|
|
|
// We ALWAYS want to create the collection.
|
|
// Even when there is nothing to add, it will indicate the operation was a success.
|
|
// For example, if a commandlet is run and a collection isn't generated, it would
|
|
// not be clear whether the commandlet actually completed successfully.
|
|
if (AssetSetPolicy::CreateAssetSet(*CollectionContainer, InSetName, InType) == true)
|
|
{
|
|
// If there is nothing to update, we are done.
|
|
if (InAssetList.Num() >= 0)
|
|
{
|
|
bool bAddCompleteInAssetList = true;
|
|
|
|
TArray<FSoftObjectPath> AssetsInCollection;
|
|
AssetSetPolicy::QueryAssetsInSet(*CollectionContainer, InSetName, InType, AssetsInCollection);
|
|
int32 CurrentAssetCount = AssetsInCollection.Num();
|
|
if (CurrentAssetCount != 0)
|
|
{
|
|
// Generate the lists
|
|
TArray<FSoftObjectPath> TrueAddList;
|
|
TArray<FSoftObjectPath> TrueRemoveList;
|
|
|
|
// See how many items are really being added/removed
|
|
for (int32 CheckIdx = 0; CheckIdx < AssetsInCollection.Num(); CheckIdx++)
|
|
{
|
|
FSoftObjectPath CheckAsset = FSoftObjectPath(AssetsInCollection[CheckIdx]);
|
|
if (InAssetList.Find(CheckAsset) != INDEX_NONE)
|
|
{
|
|
TrueAddList.AddUnique(CheckAsset);
|
|
}
|
|
else
|
|
{
|
|
TrueRemoveList.AddUnique(CheckAsset);
|
|
}
|
|
}
|
|
|
|
if ((TrueRemoveList.Num() + TrueAddList.Num()) < CurrentAssetCount)
|
|
{
|
|
// Remove and add only the required assets.
|
|
bAddCompleteInAssetList = false;
|
|
if (TrueRemoveList.Num() > 0)
|
|
{
|
|
if (AssetSetPolicy::RemoveAssetsFromSet(*CollectionContainer, InSetName, InType, TrueRemoveList) == false)
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Collection Helper failed to remove assets from collection %s."), *InSetName.ToString());
|
|
bResult = false;
|
|
}
|
|
}
|
|
if (TrueAddList.Num() > 0)
|
|
{
|
|
if (AssetSetPolicy::AddAssetsToSet(*CollectionContainer, InSetName, InType, TrueAddList) == false)
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Collection Helper failed to add assets to collection %s."), *InSetName.ToString());
|
|
bResult = false;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Clear the collection and fall into the add all case
|
|
bAddCompleteInAssetList = ClearAssetSet<AssetSetPolicy>(InSetName, InType);
|
|
if (bAddCompleteInAssetList == false)
|
|
{
|
|
// this is a problem!!!
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Collection Helper failed to clear assets for collection %s."), *InSetName.ToString());
|
|
bResult = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bAddCompleteInAssetList == true)
|
|
{
|
|
// Just add 'em all...
|
|
if (AssetSetPolicy::AddAssetsToSet(*CollectionContainer, InSetName, InType, InAssetList) == false)
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Collection Helper failed to add assets to collection %s."), *InSetName.ToString());
|
|
bResult = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Collection Helper failed to create collection %s."), *InSetName.ToString());
|
|
bResult = false;
|
|
}
|
|
|
|
return bResult;
|
|
}
|
|
|
|
/** Add and remove assets for the specified Tag or Connection. Assets from InAddList are added; assets from InRemoveList are removed. */
|
|
template <class AssetSetPolicy>
|
|
bool FContentHelper::UpdateSetContent(FName InSetName, ECollectionShareType::Type InType, const TArray<FSoftObjectPath>& InAddList, const TArray<FSoftObjectPath>& InRemoveList )
|
|
{
|
|
bool bResult = true;
|
|
|
|
if (bInitialized == false)
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Collection Helper is not initialized."));
|
|
return false;
|
|
}
|
|
|
|
// We ALWAYS want to create the collection.
|
|
// Even when there is nothing to add, it will indicate the operation was a success.
|
|
// For example, if a commandlet is run and a collection isn't generated, it would
|
|
// not be clear whether the commandlet actually completed successfully.
|
|
if (AssetSetPolicy::CreateAssetSet(*CollectionContainer, InSetName, InType) == true)
|
|
{
|
|
// If there is nothing to update, we are done.
|
|
if ((InAddList.Num() >= 0) || (InRemoveList.Num() >= 0))
|
|
{
|
|
TArray<FSoftObjectPath> AssetsInCollection;
|
|
AssetSetPolicy::QueryAssetsInSet(*CollectionContainer, InSetName, InType, AssetsInCollection);
|
|
if (AssetsInCollection.Num() != 0)
|
|
{
|
|
// Clean up the lists
|
|
TArray<FSoftObjectPath> TrueAddList;
|
|
TArray<FSoftObjectPath> TrueRemoveList;
|
|
|
|
// Generate the true Remove list, only removing items that are actually in the collection.
|
|
for (int32 RemoveIdx = 0; RemoveIdx < InRemoveList.Num(); RemoveIdx++)
|
|
{
|
|
if (AssetsInCollection.Contains(InRemoveList[RemoveIdx]) == true)
|
|
{
|
|
TrueRemoveList.AddUnique(InRemoveList[RemoveIdx]);
|
|
}
|
|
}
|
|
|
|
if (TrueRemoveList.Num() > 0)
|
|
{
|
|
if (AssetSetPolicy::RemoveAssetsFromSet(*CollectionContainer, InSetName, InType, TrueRemoveList) == false)
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Collection Helper failed to remove assets from collection %s."), *InSetName.ToString());
|
|
bResult = false;
|
|
}
|
|
}
|
|
|
|
// Generate the true Add list, only adding items that are not already in the collection.
|
|
for (int32 AddIdx = 0; AddIdx < InAddList.Num(); AddIdx++)
|
|
{
|
|
if (AssetsInCollection.Contains(InAddList[AddIdx]) == false)
|
|
{
|
|
TrueAddList.AddUnique(InAddList[AddIdx]);
|
|
}
|
|
}
|
|
|
|
if (TrueAddList.Num() > 0)
|
|
{
|
|
if (AssetSetPolicy::AddAssetsToSet(*CollectionContainer, InSetName, InType, TrueAddList) == false)
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Collection Helper failed to add assets to collection %s."), *InSetName.ToString());
|
|
bResult = false;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Just add 'em all...
|
|
if (AssetSetPolicy::AddAssetsToSet(*CollectionContainer, InSetName, InType, InAddList) == false)
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Collection Helper failed to add assets to collection %s."), *InSetName.ToString());
|
|
bResult = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Collection Helper failed to create collection %s."), *InSetName.ToString());
|
|
bResult = false;
|
|
}
|
|
|
|
return bResult;
|
|
}
|
|
|
|
/** Get the list of all assets in the specified Collection or Tag */
|
|
template <class AssetSetPolicy>
|
|
bool FContentHelper::QuerySetContent(FName InSetName, ECollectionShareType::Type InType, TArray<FSoftObjectPath>& OutAssetPathNames)
|
|
{
|
|
if (bInitialized == false)
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Collection Helper is not initialized."));
|
|
return false;
|
|
}
|
|
|
|
return AssetSetPolicy::QueryAssetsInSet(*CollectionContainer, InSetName, InType, OutAssetPathNames);
|
|
}
|
|
|
|
FContentHelper::FContentHelper()
|
|
: FContentHelper(FCollectionManagerModule::GetModule().Get().GetProjectCollectionContainer())
|
|
{
|
|
}
|
|
|
|
FContentHelper::FContentHelper(const TSharedRef<ICollectionContainer>& InCollectionContainer)
|
|
: bInitialized(false)
|
|
, CollectionContainer(InCollectionContainer)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Initialize the Collection helper
|
|
*
|
|
* @return bool true if successful, false if failed
|
|
*/
|
|
bool FContentHelper::Initialize()
|
|
{
|
|
// We no longer need to initialize anything. Keep this here in case we need to in the future.
|
|
bInitialized = true;
|
|
return bInitialized;
|
|
}
|
|
|
|
/**
|
|
* Shutdown the collection helper
|
|
*/
|
|
void FContentHelper::Shutdown()
|
|
{
|
|
// We no longer need to shut down anything. Keep this here in case we need to in the future.
|
|
bInitialized = false;
|
|
}
|
|
|
|
bool FContentHelper::CreateCollection(FName CollectionName, ECollectionShareType::Type InType)
|
|
{
|
|
return this->CreateAssetSet<FCollectionPolicy>(CollectionName, InType);
|
|
}
|
|
|
|
/**
|
|
* Clear the given collection
|
|
*
|
|
* @param InCollectionName The name of the collection to create
|
|
* @param InType Type of collection
|
|
*
|
|
* @return bool true if successful, false if failed
|
|
*/
|
|
bool FContentHelper::ClearCollection(FName InCollectionName, ECollectionShareType::Type InType)
|
|
{
|
|
return this->ClearAssetSet<FCollectionPolicy>( InCollectionName, InType );
|
|
}
|
|
|
|
/**
|
|
* Fill the given collection with the given list of assets
|
|
*
|
|
* @param InCollectionName The name of the collection to fill
|
|
* @param InType Type of collection
|
|
* @param InAssetList The list of items to fill the collection with (can be empty)
|
|
*
|
|
* @return bool true if successful, false if not.
|
|
*/
|
|
bool FContentHelper::SetCollection(FName InCollectionName, ECollectionShareType::Type InType, const TArray<FSoftObjectPath>& InAssetList)
|
|
{
|
|
return this->AssignSetContent<FCollectionPolicy>(InCollectionName, InType, InAssetList);
|
|
}
|
|
|
|
/**
|
|
* Update the given collection with the lists of adds/removes
|
|
*
|
|
* @param InCollectionName The name of the collection to update
|
|
* @param InType Type of collection
|
|
* @param InAddList The list of items to ADD to the collection (can be empty)
|
|
* @param InRemoveList The list of items to REMOVE from the collection (can be empty)
|
|
*
|
|
* @return bool true if successful, false if not.
|
|
*/
|
|
bool FContentHelper::UpdateCollection(FName InCollectionName, ECollectionShareType::Type InType, const TArray<FSoftObjectPath>& InAddList, const TArray<FSoftObjectPath>& InRemoveList)
|
|
{
|
|
return this->UpdateSetContent<FCollectionPolicy>( InCollectionName, InType, InAddList, InRemoveList );
|
|
}
|
|
|
|
/**
|
|
* Retrieve the assets contained in the given collection.
|
|
*
|
|
* @param InCollectionName Name of collection to query
|
|
* @param InType Type of collection
|
|
* @param OutAssetPaths The assets contained in the collection
|
|
*
|
|
* @return True if collection was created successfully
|
|
*/
|
|
bool FContentHelper::QueryAssetsInCollection(FName InCollectionName, ECollectionShareType::Type InType, TArray<FSoftObjectPath>& OutAssetPaths)
|
|
{
|
|
return this->QuerySetContent<FCollectionPolicy>(InCollectionName, InType, OutAssetPaths);
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------
|
|
ULoadPackageCommandlet
|
|
-----------------------------------------------------------------------------*/
|
|
ULoadPackageCommandlet::ULoadPackageCommandlet(const FObjectInitializer& ObjectInitializer)
|
|
: Super(ObjectInitializer)
|
|
{
|
|
LogToConsole = false;
|
|
}
|
|
|
|
|
|
bool ULoadPackageCommandlet::ParseLoadListFile(FString& LoadListFilename, TArray<FString>& Tokens)
|
|
{
|
|
//Open file
|
|
FString Data;
|
|
if (FFileHelper::LoadFileToString(Data, *LoadListFilename) == true)
|
|
{
|
|
const TCHAR* Ptr = *Data;
|
|
FString StrLine;
|
|
|
|
while (FParse::Line(&Ptr, StrLine))
|
|
{
|
|
//UE_LOG(LogPackageUtilities, Log, TEXT("Read in: %s"), *StrLine);
|
|
Tokens.AddUnique(StrLine);
|
|
}
|
|
|
|
// debugging...
|
|
//UE_LOG(LogPackageUtilities, Log, TEXT("\nPACKAGES TO LOAD:"));
|
|
for (int32 TokenIdx = 0; TokenIdx < Tokens.Num(); TokenIdx++)
|
|
{
|
|
//UE_LOG(LogPackageUtilities, Log, TEXT("\t%s"), *(Tokens(TokenIdx)));
|
|
}
|
|
return (Tokens.Num() > 0);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
int32 ULoadPackageCommandlet::Main( const FString& Params )
|
|
{
|
|
TArray<FString> Tokens, Switches;
|
|
ParseCommandLine(*Params, Tokens, Switches);
|
|
|
|
bool bLoadAllPackages = Switches.Contains(TEXT("ALL"));
|
|
bool bCheckForLegacyPackages = Switches.Contains(TEXT("CheckForLegacyPackages"));
|
|
bool bFast = Switches.Contains(TEXT("FAST"));
|
|
|
|
int32 MinVersion = MAX_int32;
|
|
|
|
// Check for a load list file...
|
|
for (int32 TokenIdx = 0; TokenIdx < Tokens.Num(); TokenIdx++)
|
|
{
|
|
FString LoadListFilename = TEXT("");
|
|
if (FParse::Value(*(Tokens[TokenIdx]), TEXT("LOADLIST="), LoadListFilename))
|
|
{
|
|
// Found one - this will be a list of packages to load
|
|
//UE_LOG(LogPackageUtilities, Log, TEXT("LoadList in file %s"), *LoadListFilename);
|
|
|
|
TArray<FString> TempTokens;
|
|
if (ParseLoadListFile(LoadListFilename, TempTokens) == true)
|
|
{
|
|
bLoadAllPackages = false;
|
|
|
|
Tokens.Empty(TempTokens.Num());
|
|
Tokens = TempTokens;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
TArray<FString> FilesInPath;
|
|
if ( bLoadAllPackages )
|
|
{
|
|
Tokens.Empty(2);
|
|
Tokens.Add(FString("*") + FPackageName::GetAssetPackageExtension());
|
|
Tokens.Add(FString("*") + FPackageName::GetMapPackageExtension());
|
|
}
|
|
|
|
if ( Tokens.Num() == 0 )
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("You must specify a package name (multiple files can be delimited by spaces) or wild-card, or specify -all to include all registered packages"));
|
|
return 1;
|
|
}
|
|
|
|
uint8 PackageFilter = NORMALIZE_DefaultFlags;
|
|
if (Switches.Contains(TEXT("SKIPMAPS")))
|
|
{
|
|
PackageFilter |= NORMALIZE_ExcludeMapPackages;
|
|
}
|
|
else if (Switches.Contains(TEXT("MAPSONLY")))
|
|
{
|
|
PackageFilter |= NORMALIZE_ExcludeContentPackages;
|
|
}
|
|
|
|
if (Switches.Contains(TEXT("PROJECTONLY")))
|
|
{
|
|
PackageFilter |= NORMALIZE_ExcludeEnginePackages;
|
|
}
|
|
|
|
if (Switches.Contains(TEXT("SkipDeveloperFolders")) || Switches.Contains(TEXT("NODEV")))
|
|
{
|
|
PackageFilter |= NORMALIZE_ExcludeDeveloperPackages;
|
|
}
|
|
else if (Switches.Contains(TEXT("OnlyDeveloperFolders")))
|
|
{
|
|
PackageFilter |= NORMALIZE_ExcludeNonDeveloperPackages;
|
|
}
|
|
|
|
// assume the first token is the map wildcard/pathname
|
|
TArray<FString> Unused;
|
|
for ( int32 TokenIndex = 0; TokenIndex < Tokens.Num(); TokenIndex++ )
|
|
{
|
|
TArray<FString> TokenFiles;
|
|
if ( !NormalizePackageNames( Unused, TokenFiles, Tokens[TokenIndex], PackageFilter) )
|
|
{
|
|
UE_LOG(LogPackageUtilities, Display, TEXT("No packages found for parameter %i: '%s'"), TokenIndex, *Tokens[TokenIndex]);
|
|
continue;
|
|
}
|
|
|
|
FilesInPath += TokenFiles;
|
|
}
|
|
|
|
if ( FilesInPath.Num() == 0 )
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("No files found."));
|
|
return 1;
|
|
}
|
|
|
|
GIsClient = !Switches.Contains(TEXT("NOCLIENT"));
|
|
GIsServer = !Switches.Contains(TEXT("NOSERVER"));
|
|
GIsEditor = !Switches.Contains(TEXT("NOEDITOR"));
|
|
|
|
for( int32 FileIndex = 0; FileIndex < FilesInPath.Num(); FileIndex++ )
|
|
{
|
|
const FString& Filename = FilesInPath[FileIndex];
|
|
|
|
UE_LOG(LogPackageUtilities, Display, TEXT("Loading %s"), *Filename );
|
|
|
|
FPackagePath PackagePath = FPackagePath::FromLocalPath(Filename);
|
|
FString PackageName = PackagePath.GetPackageName();
|
|
if (!PackageName.IsEmpty())
|
|
{
|
|
UPackage* Package = FindObject<UPackage>(nullptr, *PackageName, true);
|
|
if (Package != NULL && !bLoadAllPackages)
|
|
{
|
|
ResetLoaders(Package);
|
|
}
|
|
}
|
|
|
|
if (bCheckForLegacyPackages)
|
|
{
|
|
FLinkerLoad* Linker = GetPackageLinker(nullptr, PackagePath, LOAD_NoVerify, nullptr);
|
|
MinVersion = FMath::Min<int32>(MinVersion, Linker->Summary.GetFileVersionUE().ToValue());
|
|
}
|
|
else
|
|
{
|
|
UPackage* Package = LoadPackage(nullptr, PackagePath, LOAD_None );
|
|
if(Package == nullptr)
|
|
{
|
|
UE_LOG(LogPackageUtilities, Error, TEXT("Error loading %s!"), *Filename );
|
|
}
|
|
}
|
|
if (!bFast || FileIndex % 100 == 99)
|
|
{
|
|
CollectGarbage(RF_NoFlags);
|
|
}
|
|
}
|
|
GIsEditor = GIsServer = GIsClient = true;
|
|
if (bCheckForLegacyPackages)
|
|
{
|
|
UE_LOG(LogPackageUtilities, Log, TEXT("%d minimum UE version number."), MinVersion );
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------
|
|
UPkgInfo commandlet.
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
struct FExportInfo
|
|
{
|
|
FObjectExport Export;
|
|
int32 ExportIndex;
|
|
FString PathName;
|
|
FString OuterPathName;
|
|
|
|
FExportInfo( FLinkerLoad* Linker, int32 InIndex )
|
|
: Export(Linker->ExportMap[InIndex]), ExportIndex(InIndex)
|
|
, OuterPathName(TEXT("NULL"))
|
|
{
|
|
PathName = Linker->GetExportPathName(ExportIndex);
|
|
SetOuterPathName(Linker);
|
|
}
|
|
|
|
void SetOuterPathName( FLinkerLoad* Linker )
|
|
{
|
|
if ( !Export.OuterIndex.IsNull() )
|
|
{
|
|
OuterPathName = Linker->GetPathName(Export.OuterIndex);
|
|
}
|
|
}
|
|
};
|
|
|
|
namespace
|
|
{
|
|
enum EExportSortType
|
|
{
|
|
EXPORTSORT_ExportSize,
|
|
EXPORTSORT_ExportIndex,
|
|
EXPORTSORT_ObjectPathname,
|
|
EXPORTSORT_OuterPathname,
|
|
EXPORTSORT_MAX
|
|
};
|
|
|
|
struct FObjectExport_Sorter
|
|
{
|
|
static EExportSortType SortPriority[EXPORTSORT_MAX];
|
|
|
|
// Comparison method
|
|
bool operator()( const FExportInfo& A, const FExportInfo& B ) const
|
|
{
|
|
int64 Result = 0;
|
|
|
|
for ( int32 PriorityType = 0; PriorityType < EXPORTSORT_MAX; PriorityType++ )
|
|
{
|
|
switch ( SortPriority[PriorityType] )
|
|
{
|
|
case EXPORTSORT_ExportSize:
|
|
Result = B.Export.SerialSize - A.Export.SerialSize;
|
|
break;
|
|
|
|
case EXPORTSORT_ExportIndex:
|
|
Result = A.ExportIndex - B.ExportIndex;
|
|
break;
|
|
|
|
case EXPORTSORT_ObjectPathname:
|
|
Result = A.PathName.Len() - B.PathName.Len();
|
|
if ( Result == 0 )
|
|
{
|
|
Result = FCString::Stricmp(*A.PathName, *B.PathName);
|
|
}
|
|
break;
|
|
|
|
case EXPORTSORT_OuterPathname:
|
|
Result = A.OuterPathName.Len() - B.OuterPathName.Len();
|
|
if ( Result == 0 )
|
|
{
|
|
Result = FCString::Stricmp(*A.OuterPathName, *B.OuterPathName);
|
|
}
|
|
break;
|
|
|
|
case EXPORTSORT_MAX:
|
|
return !!Result;
|
|
}
|
|
|
|
if ( Result != 0 )
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
return Result < 0;
|
|
}
|
|
};
|
|
|
|
EExportSortType FObjectExport_Sorter::SortPriority[EXPORTSORT_MAX] =
|
|
{ EXPORTSORT_ExportIndex, EXPORTSORT_ExportSize, EXPORTSORT_OuterPathname, EXPORTSORT_ObjectPathname };
|
|
}
|
|
|
|
/** Given a package filename, creates a linker and a temporary package. The filename does not need to point to a package under the current project content folder */
|
|
FLinkerLoad* CreateLinkerForFilename(FUObjectSerializeContext* LoadContext, const FString& InFilename)
|
|
{
|
|
FString TempPackageName;
|
|
TempPackageName = FPaths::Combine(TEXT("/Temp"), *FPaths::GetPath(InFilename.Mid(InFilename.Find(TEXT(":"), ESearchCase::CaseSensitive) + 1)), *FPaths::GetBaseFilename(InFilename));
|
|
UPackage* Package = FindObjectFast<UPackage>(nullptr, *TempPackageName);
|
|
if (!Package)
|
|
{
|
|
Package = CreatePackage( *TempPackageName);
|
|
}
|
|
FLinkerLoad* Linker = FLinkerLoad::CreateLinker(LoadContext, Package, FPackagePath::FromLocalPath(InFilename), LOAD_NoVerify);
|
|
return Linker;
|
|
}
|
|
|
|
/**
|
|
* Writes information about the linker to the log.
|
|
*
|
|
* @param InLinker if specified, changes this reporter's Linker before generating the report.
|
|
*/
|
|
void FPkgInfoReporter_Log::GeneratePackageReport( FLinkerLoad* InLinker /*=nullptr*/, FOutputDevice& Out /*=*GWarn*/)
|
|
{
|
|
check(InLinker);
|
|
|
|
if ( InLinker != NULL )
|
|
{
|
|
SetLinker(InLinker);
|
|
}
|
|
|
|
if ( PackageCount++ > 0 )
|
|
{
|
|
Out.Logf(ELogVerbosity::Display, TEXT(""));
|
|
}
|
|
|
|
if (InLinker->IsTextFormat())
|
|
{
|
|
Out.Logf(ELogVerbosity::Warning, TEXT("\tPackageReports are not currently supported for text based assets"));
|
|
return;
|
|
}
|
|
|
|
// Display information about the package.
|
|
FName LinkerName = Linker->LinkerRoot->GetFName();
|
|
// Display summary info.
|
|
Out.Logf(ELogVerbosity::Display, TEXT("********************************************") );
|
|
Out.Logf(ELogVerbosity::Display, TEXT("Package '%s' Summary"), *LinkerName.ToString() );
|
|
Out.Logf(ELogVerbosity::Display, TEXT("--------------------------------------------") );
|
|
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t Filename: %s"), *Linker->GetPackagePath().GetLocalFullPath());
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t File Version: %i"), Linker->UEVer().ToValue());
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t Engine Version: %s"), *Linker->Summary.SavedByEngineVersion.ToString());
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t Compat Version: %s"), *Linker->Summary.CompatibleWithEngineVersion.ToString());
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t PackageFlags: %X"), Linker->Summary.GetPackageFlags() );
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t NameCount: %d"), Linker->Summary.NameCount );
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t NameOffset: %d"), Linker->Summary.NameOffset );
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t ImportCount: %d"), Linker->Summary.ImportCount );
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t ImportOffset: %d"), Linker->Summary.ImportOffset );
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t ExportCount: %d"), Linker->Summary.ExportCount );
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t ExportOffset: %d"), Linker->Summary.ExportOffset );
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\tCompression Flags: %X"), Linker->Summary.CompressionFlags);
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t Custom Versions:\n%s"), *Linker->Summary.GetCustomVersionContainer().ToString("\t\t"));
|
|
|
|
|
|
if (!IsHideSaveUnstable())
|
|
{
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t SavedHash: %s"), *WriteToString<40>(Linker->Summary.GetSavedHash()));
|
|
}
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t PersistentGuid: %s"), *Linker->Summary.PersistentGuid.ToString());
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t Generations:"));
|
|
for( int32 i = 0; i < Linker->Summary.Generations.Num(); ++i )
|
|
{
|
|
const FGenerationInfo& generationInfo = Linker->Summary.Generations[ i ];
|
|
Out.Logf(ELogVerbosity::Display,TEXT("\t\t\t%d) ExportCount=%d, NameCount=%d "), i, generationInfo.ExportCount, generationInfo.NameCount );
|
|
}
|
|
|
|
if( (InfoFlags&PKGINFO_Names) != 0 )
|
|
{
|
|
Out.Logf(ELogVerbosity::Display, TEXT("--------------------------------------------") );
|
|
Out.Logf(ELogVerbosity::Display, TEXT("Name Map"));
|
|
Out.Logf(ELogVerbosity::Display, TEXT("========"));
|
|
for( int32 i = 0; i < Linker->NameMap.Num(); ++i )
|
|
{
|
|
FName name = FName::CreateFromDisplayId(Linker->NameMap[ i ], 0);
|
|
if (IsHideProcessUnstable())
|
|
{
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t%d: Name '%s' [Internal: %s, %d]"), i, *name.ToString(), *name.GetPlainNameString(), name.GetNumber());
|
|
}
|
|
else
|
|
{
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t%d: Name '%s' Comparison Index %d Display Index %d [Internal: %s, %d]"), i, *name.ToString(), name.GetComparisonIndex().ToUnstableInt(), name.GetDisplayIndex().ToUnstableInt(), *name.GetPlainNameString(), name.GetNumber());
|
|
}
|
|
}
|
|
}
|
|
|
|
// if we _only_ want name info, skip this part completely
|
|
if ( InfoFlags != PKGINFO_Names )
|
|
{
|
|
if( (InfoFlags&PKGINFO_Imports) != 0 )
|
|
{
|
|
Out.Logf(ELogVerbosity::Display, TEXT("--------------------------------------------") );
|
|
Out.Logf(ELogVerbosity::Display, TEXT("Import Map"));
|
|
Out.Logf(ELogVerbosity::Display, TEXT("=========="));
|
|
}
|
|
|
|
TArray<FName> DependentPackages;
|
|
for( int32 i = 0; i < Linker->ImportMap.Num(); ++i )
|
|
{
|
|
FObjectImport& import = Linker->ImportMap[ i ];
|
|
|
|
FName PackageName = NAME_None;
|
|
FName OuterName = NAME_None;
|
|
if ( !import.OuterIndex.IsNull() )
|
|
{
|
|
if ( (InfoFlags&PKGINFO_Paths) != 0 )
|
|
{
|
|
OuterName = *Linker->GetPathName(import.OuterIndex);
|
|
}
|
|
else
|
|
{
|
|
OuterName = Linker->ImpExp(import.OuterIndex).ObjectName;
|
|
}
|
|
|
|
// Find the package which contains this import. import.SourceLinker is cleared in EndLoad, so we'll need to do this manually now.
|
|
FPackageIndex OutermostLinkerIndex = import.OuterIndex;
|
|
for ( FPackageIndex LinkerIndex = import.OuterIndex; !LinkerIndex.IsNull(); )
|
|
{
|
|
OutermostLinkerIndex = LinkerIndex;
|
|
LinkerIndex = Linker->ImpExp(LinkerIndex).OuterIndex;
|
|
}
|
|
check(!OutermostLinkerIndex.IsNull());
|
|
PackageName = Linker->ImpExp(OutermostLinkerIndex).ObjectName;
|
|
}
|
|
|
|
if ( (InfoFlags&PKGINFO_Imports) != 0 )
|
|
{
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t*************************"));
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\tImport %d: '%s'"), i, *import.ObjectName.ToString() );
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t Outer: '%s' (%d)"), *OuterName.ToString(), import.OuterIndex.ForDebugging());
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t Package: '%s'"), *PackageName.ToString());
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t Class: '%s'"), *import.ClassName.ToString() );
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\tClassPackage: '%s'"), *import.ClassPackage.ToString() );
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t XObject: %s"), import.XObject ? TEXT("VALID") : TEXT("NULL"));
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t SourceIndex: %d"), import.SourceIndex );
|
|
|
|
// dump depends info
|
|
if (InfoFlags & PKGINFO_Depends)
|
|
{
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t All Depends:"));
|
|
|
|
TSet<FDependencyRef> AllDepends;
|
|
Linker->GatherImportDependencies(i, AllDepends);
|
|
int32 DependsIndex = 0;
|
|
for(TSet<FDependencyRef>::TConstIterator It(AllDepends);It;++It)
|
|
{
|
|
const FDependencyRef& Ref = *It;
|
|
if (Ref.Linker)
|
|
{
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t\t%i) %s"), DependsIndex++, *Ref.Linker->GetExportFullName(Ref.ExportIndex));
|
|
}
|
|
else
|
|
{
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t\t%i) NULL"), DependsIndex++);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( PackageName == NAME_None && import.ClassName == NAME_Package )
|
|
{
|
|
PackageName = import.ObjectName;
|
|
}
|
|
|
|
if ( PackageName != NAME_None && PackageName != LinkerName )
|
|
{
|
|
DependentPackages.AddUnique(PackageName);
|
|
}
|
|
|
|
if ( import.ClassPackage != NAME_None && import.ClassPackage != LinkerName )
|
|
{
|
|
DependentPackages.AddUnique(import.ClassPackage);
|
|
}
|
|
}
|
|
|
|
if ( DependentPackages.Num() )
|
|
{
|
|
Out.Logf(ELogVerbosity::Display, TEXT("--------------------------------------------") );
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\tPackages referenced by %s:"), *LinkerName.ToString());
|
|
for ( int32 i = 0; i < DependentPackages.Num(); i++ )
|
|
{
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t%i) %s"), i, *DependentPackages[i].ToString());
|
|
}
|
|
}
|
|
}
|
|
|
|
if( (InfoFlags&PKGINFO_Exports) != 0 )
|
|
{
|
|
Out.Logf(ELogVerbosity::Display, TEXT("--------------------------------------------") );
|
|
Out.Logf(ELogVerbosity::Display, TEXT("Export Map"));
|
|
Out.Logf(ELogVerbosity::Display, TEXT("=========="));
|
|
|
|
TArray<FExportInfo> SortedExportMap;
|
|
SortedExportMap.Empty(Linker->ExportMap.Num());
|
|
for( int32 i = 0; i < Linker->ExportMap.Num(); ++i )
|
|
{
|
|
SortedExportMap.Emplace(Linker, i);
|
|
}
|
|
|
|
FString SortingParms;
|
|
if ( FParse::Value(FCommandLine::Get(), TEXT("SORT="), SortingParms) )
|
|
{
|
|
TArray<FString> SortValues;
|
|
SortingParms.ParseIntoArray(SortValues, TEXT(","), true);
|
|
|
|
for ( int32 i = 0; i < EXPORTSORT_MAX; i++ )
|
|
{
|
|
if ( i < SortValues.Num() )
|
|
{
|
|
const FString Value = SortValues[i];
|
|
if ( Value == TEXT("index") )
|
|
{
|
|
FObjectExport_Sorter::SortPriority[i] = EXPORTSORT_ExportIndex;
|
|
}
|
|
else if ( Value == TEXT("size") )
|
|
{
|
|
FObjectExport_Sorter::SortPriority[i] = EXPORTSORT_ExportSize;
|
|
}
|
|
else if ( Value == TEXT("name") )
|
|
{
|
|
FObjectExport_Sorter::SortPriority[i] = EXPORTSORT_ObjectPathname;
|
|
}
|
|
else if ( Value == TEXT("outer") )
|
|
{
|
|
FObjectExport_Sorter::SortPriority[i] = EXPORTSORT_OuterPathname;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FObjectExport_Sorter::SortPriority[i] = EXPORTSORT_MAX;
|
|
}
|
|
}
|
|
}
|
|
|
|
SortedExportMap.Sort( FObjectExport_Sorter() );
|
|
|
|
if ( (InfoFlags&PKGINFO_Compact) == 0 )
|
|
{
|
|
for( const auto& ExportInfo : SortedExportMap )
|
|
{
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t*************************"));
|
|
const FObjectExport& Export = ExportInfo.Export;
|
|
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\tExport %d: '%s'"), ExportInfo.ExportIndex, *Export.ObjectName.ToString() );
|
|
|
|
// find the name of this object's class
|
|
FPackageIndex ClassIndex = Export.ClassIndex;
|
|
FName ClassName = ClassIndex.IsNull() ? FName(NAME_Class) : Linker->ImpExp(ClassIndex).ObjectName;
|
|
|
|
// find the name of this object's parent...for UClasses, this will be the parent class
|
|
// for UFunctions, this will be the SuperFunction, if it exists, etc.
|
|
FString ParentName;
|
|
if ( !Export.SuperIndex.IsNull() )
|
|
{
|
|
if ( (InfoFlags&PKGINFO_Paths) != 0 )
|
|
{
|
|
ParentName = *Linker->GetPathName(Export.SuperIndex);
|
|
}
|
|
else
|
|
{
|
|
ParentName = Linker->ImpExp(Export.SuperIndex).ObjectName.ToString();
|
|
}
|
|
}
|
|
|
|
// find the name of this object's parent...for UClasses, this will be the parent class
|
|
// for UFunctions, this will be the SuperFunction, if it exists, etc.
|
|
FString TemplateName;
|
|
if (!Export.TemplateIndex.IsNull())
|
|
{
|
|
if ((InfoFlags&PKGINFO_Paths) != 0)
|
|
{
|
|
TemplateName = *Linker->GetPathName(Export.TemplateIndex);
|
|
}
|
|
else
|
|
{
|
|
TemplateName = Linker->ImpExp(Export.TemplateIndex).ObjectName.ToString();
|
|
}
|
|
}
|
|
|
|
// find the name of this object's Outer. For UClasses, this will generally be the
|
|
// top-level package itself. For properties, a UClass, etc.
|
|
FString OuterName;
|
|
if ( !Export.OuterIndex.IsNull() )
|
|
{
|
|
if ( (InfoFlags&PKGINFO_Paths) != 0 )
|
|
{
|
|
OuterName = *Linker->GetPathName(Export.OuterIndex);
|
|
}
|
|
else
|
|
{
|
|
OuterName = Linker->ImpExp(Export.OuterIndex).ObjectName.ToString();
|
|
}
|
|
}
|
|
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t Class: '%s' (%i)"), *ClassName.ToString(), ClassIndex.ForDebugging() );
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t Parent: '%s' (%d)"), *ParentName, Export.SuperIndex.ForDebugging());
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t Template: '%s' (%d)"), *TemplateName, Export.TemplateIndex.ForDebugging());
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t Outer: '%s' (%d)"), *OuterName, Export.OuterIndex.ForDebugging() );
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t ObjectFlags: 0x%08X"), (uint32)Export.ObjectFlags );
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t Size: %d"), Export.SerialSize );
|
|
if ( !IsHideOffsets())
|
|
{
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t Offset: %d"), Export.SerialOffset );
|
|
}
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t Object: %s"), Export.Object ? TEXT("VALID") : TEXT("NULL"));
|
|
if ( !IsHideOffsets() )
|
|
{
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t HashNext: %d"), Export.HashNext );
|
|
}
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t bNotForClient: %d"), Export.bNotForClient );
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t bNotForServer: %d"), Export.bNotForServer );
|
|
|
|
// dump depends info
|
|
if (InfoFlags & PKGINFO_Depends)
|
|
{
|
|
if (ExportInfo.ExportIndex < Linker->DependsMap.Num())
|
|
{
|
|
TArray<FPackageIndex>& Depends = Linker->DependsMap[ExportInfo.ExportIndex];
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t DependsMap:"));
|
|
|
|
for (int32 DependsIndex = 0; DependsIndex < Depends.Num(); DependsIndex++)
|
|
{
|
|
Out.Logf(ELogVerbosity::Display,TEXT("\t\t\t%i) %s (%i)"),
|
|
DependsIndex,
|
|
*Linker->GetFullImpExpName(Depends[DependsIndex]),
|
|
Depends[DependsIndex].ForDebugging()
|
|
);
|
|
}
|
|
|
|
TSet<FDependencyRef> AllDepends;
|
|
Linker->GatherExportDependencies(ExportInfo.ExportIndex, AllDepends);
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t All Depends:"));
|
|
int32 DependsIndex = 0;
|
|
for(TSet<FDependencyRef>::TConstIterator It(AllDepends);It;++It)
|
|
{
|
|
const FDependencyRef& Ref = *It;
|
|
if (Ref.Linker)
|
|
{
|
|
Out.Logf(ELogVerbosity::Display,TEXT("\t\t\t%i) %s (%i)"),
|
|
DependsIndex++,
|
|
*Ref.Linker->GetExportFullName(Ref.ExportIndex),
|
|
Ref.ExportIndex);
|
|
}
|
|
else
|
|
{
|
|
Out.Logf(ELogVerbosity::Display,TEXT("\t\t\t%i) NULL (%i)"),
|
|
DependsIndex++,
|
|
Ref.ExportIndex);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for( const auto& ExportInfo : SortedExportMap )
|
|
{
|
|
const FObjectExport& Export = ExportInfo.Export;
|
|
Out.Logf(ELogVerbosity::Display, TEXT(" %8i %10i %32s %s"), ExportInfo.ExportIndex, Export.SerialSize,
|
|
*(Linker->GetExportClassName(ExportInfo.ExportIndex).ToString()),
|
|
(InfoFlags&PKGINFO_Paths) != 0 ? *Linker->GetExportPathName(ExportInfo.ExportIndex) : *Export.ObjectName.ToString());
|
|
}
|
|
}
|
|
}
|
|
|
|
if( (InfoFlags&PKGINFO_Text) != 0 )
|
|
{
|
|
Out.Logf(ELogVerbosity::Display, TEXT("--------------------------------------------") );
|
|
Out.Logf(ELogVerbosity::Display, TEXT("Gatherable Text Data Map"));
|
|
Out.Logf(ELogVerbosity::Display, TEXT("=========="));
|
|
|
|
if (Linker->SerializeGatherableTextDataMap(true))
|
|
{
|
|
Out.Logf(ELogVerbosity::Display, TEXT("Number of Text Data Entries: %d"), Linker->GatherableTextDataMap.Num());
|
|
|
|
for (int32 i = 0; i < Linker->GatherableTextDataMap.Num(); ++i)
|
|
{
|
|
const FGatherableTextData& GatherableTextData = Linker->GatherableTextDataMap[i];
|
|
Out.Logf(ELogVerbosity::Display, TEXT("Entry %d:"), 1 + i);
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t String: %s"), *GatherableTextData.SourceData.SourceString.ReplaceCharWithEscapedChar());
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\tNamespace: %s"), *GatherableTextData.NamespaceName);
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t Key(s): %d"), GatherableTextData.SourceSiteContexts.Num());
|
|
for (const FTextSourceSiteContext& TextSourceSiteContext : GatherableTextData.SourceSiteContexts)
|
|
{
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t%s from %s"), *TextSourceSiteContext.KeyName, *TextSourceSiteContext.SiteDescription);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( Linker->Summary.GatherableTextDataOffset > 0 )
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning,TEXT("Failed to load gatherable text data for package %s!"), *LinkerName.ToString());
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if( (InfoFlags&PKGINFO_Thumbs) != 0 )
|
|
{
|
|
Out.Logf(ELogVerbosity::Display, TEXT("--------------------------------------------") );
|
|
Out.Logf(ELogVerbosity::Display, TEXT("Thumbnail Data"));
|
|
Out.Logf(ELogVerbosity::Display, TEXT("=========="));
|
|
|
|
if ( Linker->SerializeThumbnails(true) )
|
|
{
|
|
if ( Linker->LinkerRoot->HasThumbnailMap() )
|
|
{
|
|
FThumbnailMap& LinkerThumbnails = Linker->LinkerRoot->AccessThumbnailMap();
|
|
|
|
int32 MaxObjectNameSize = 0;
|
|
for ( TMap<FName, FObjectThumbnail>::TIterator It(LinkerThumbnails); It; ++It )
|
|
{
|
|
FName& ObjectPathName = It.Key();
|
|
MaxObjectNameSize = FMath::Max(MaxObjectNameSize, ObjectPathName.ToString().Len());
|
|
}
|
|
|
|
int32 ThumbIdx=0;
|
|
for ( TMap<FName, FObjectThumbnail>::TIterator It(LinkerThumbnails); It; ++It )
|
|
{
|
|
FName& ObjectFullName = It.Key();
|
|
FObjectThumbnail& Thumb = It.Value();
|
|
|
|
Out.Logf(ELogVerbosity::Display,TEXT("\t\t%i) %*s: %ix%i\t\tImage Data:%i bytes"), ThumbIdx++, MaxObjectNameSize, *ObjectFullName.ToString(), Thumb.GetImageWidth(), Thumb.GetImageHeight(), Thumb.GetCompressedDataSize());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning,TEXT("%s has no thumbnail map!"), *LinkerName.ToString());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( Linker->Summary.ThumbnailTableOffset > 0 )
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning,TEXT("Failed to load thumbnails for package %s!"), *LinkerName.ToString());
|
|
}
|
|
}
|
|
}
|
|
|
|
if( (InfoFlags&PKGINFO_Lazy) != 0 )
|
|
{
|
|
Out.Logf(ELogVerbosity::Display, TEXT("--------------------------------------------") );
|
|
Out.Logf(ELogVerbosity::Display, TEXT("Lazy Pointer Data"));
|
|
Out.Logf(ELogVerbosity::Display, TEXT("==============="));
|
|
}
|
|
|
|
if( (InfoFlags&PKGINFO_AssetRegistry) != 0 )
|
|
{
|
|
Out.Logf(ELogVerbosity::Display, TEXT("--------------------------------------------"));
|
|
|
|
{
|
|
const int32 NextOffset = Linker->Summary.WorldTileInfoDataOffset ? Linker->Summary.WorldTileInfoDataOffset : Linker->Summary.TotalHeaderSize;
|
|
const int32 AssetRegistrySize = NextOffset - Linker->Summary.AssetRegistryDataOffset;
|
|
Out.Logf(ELogVerbosity::Display, TEXT("Asset Registry Size: %10i"), AssetRegistrySize);
|
|
}
|
|
|
|
Out.Logf(ELogVerbosity::Display, TEXT("Asset Registry Data"));
|
|
Out.Logf(ELogVerbosity::Display, TEXT("=========="));
|
|
|
|
if( Linker->Summary.AssetRegistryDataOffset > 0 )
|
|
{
|
|
// Seek to the AssetRegistry table of contents
|
|
Linker->GetLoader_Unsafe()->Seek( Linker->Summary.AssetRegistryDataOffset );
|
|
TArray<FAssetData*> AssetDatas;
|
|
UE::AssetRegistry::EReadPackageDataMainErrorCode ErrorCode;
|
|
int64 DependencyDataOffset;
|
|
UE::AssetRegistry::ReadPackageDataMain(*Linker->GetLoader_Unsafe(), LinkerName.ToString(), Linker->Summary, DependencyDataOffset, AssetDatas, ErrorCode);
|
|
|
|
Out.Logf(ELogVerbosity::Display, TEXT("Number of assets with Asset Registry data: %d"), AssetDatas.Num() );
|
|
|
|
// If there are any Asset Registry tags, print them
|
|
int AssetIdx = 0;
|
|
for (FAssetData* AssetData : AssetDatas)
|
|
{
|
|
// Display the asset class and path
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t%d) %s'%s' (%d Tags)"), AssetIdx++, *AssetData->AssetClassPath.ToString(), *AssetData->GetObjectPathString(), AssetData->TagsAndValues.Num());
|
|
|
|
// Display all tags on the asset
|
|
for (const TPair<FName, FAssetTagValueRef>& Pair : AssetData->TagsAndValues)
|
|
{
|
|
Out.Logf(ELogVerbosity::Display, TEXT("\t\t\t\"%s\": \"%s\""), *Pair.Key.ToString(), *Pair.Value.AsString() );
|
|
}
|
|
|
|
delete AssetData;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
UPkgInfoCommandlet::UPkgInfoCommandlet(const FObjectInitializer& ObjectInitializer)
|
|
: Super(ObjectInitializer)
|
|
{
|
|
LogToConsole = false;
|
|
}
|
|
|
|
|
|
int32 UPkgInfoCommandlet::Main( const FString& Params )
|
|
{
|
|
const TCHAR* Parms = *Params;
|
|
|
|
TArray<FString> Tokens, Switches;
|
|
ParseCommandLine(Parms, Tokens, Switches);
|
|
|
|
// find out which type of info we're looking for
|
|
bool bDumpProperties = false;
|
|
uint32 InfoFlags = PKGINFO_None;
|
|
if ( Switches.Contains(TEXT("names")) )
|
|
{
|
|
InfoFlags |= PKGINFO_Names;
|
|
}
|
|
if ( Switches.Contains(TEXT("imports")) )
|
|
{
|
|
InfoFlags |= PKGINFO_Imports;
|
|
}
|
|
if ( Switches.Contains(TEXT("exports")) )
|
|
{
|
|
InfoFlags |= PKGINFO_Exports;
|
|
}
|
|
if ( Switches.Contains(TEXT("simple")) )
|
|
{
|
|
InfoFlags |= PKGINFO_Compact;
|
|
}
|
|
if ( Switches.Contains(TEXT("depends")) )
|
|
{
|
|
InfoFlags |= PKGINFO_Depends;
|
|
}
|
|
if ( Switches.Contains(TEXT("paths")) )
|
|
{
|
|
InfoFlags |= PKGINFO_Paths;
|
|
}
|
|
if ( Switches.Contains(TEXT("thumbnails")) )
|
|
{
|
|
InfoFlags |= PKGINFO_Thumbs;
|
|
}
|
|
if ( Switches.Contains(TEXT("lazy")) )
|
|
{
|
|
InfoFlags |= PKGINFO_Lazy;
|
|
}
|
|
if ( Switches.Contains(TEXT("assetregistry")) )
|
|
{
|
|
InfoFlags |= PKGINFO_AssetRegistry;
|
|
}
|
|
if (Switches.Contains(TEXT("properties")))
|
|
{
|
|
bDumpProperties = true;
|
|
}
|
|
if ( Switches.Contains(TEXT("all")) )
|
|
{
|
|
bDumpProperties = true;
|
|
InfoFlags |= PKGINFO_All;
|
|
}
|
|
|
|
// Create a file writer to dump the info to
|
|
FOutputDevice* OutputOverride = GWarn;
|
|
FString OutputFilename;
|
|
TUniquePtr<FOutputDeviceFile> OutputBuffer;
|
|
if (FParse::Value(*Params, TEXT("dumptofile="), OutputFilename))
|
|
{
|
|
OutputBuffer = MakeUnique<FOutputDeviceFile>(*OutputFilename, true);
|
|
OutputBuffer->SetSuppressEventTag(true);
|
|
OutputOverride = OutputBuffer.Get();
|
|
}
|
|
|
|
|
|
uint32 DisplayFlags = PKGINFODISPLAY_None;
|
|
DisplayFlags |= Switches.Contains(TEXT("HideUnstable")) ? PKGINFODISPLAY_HideAllUnstable : 0;
|
|
DisplayFlags |= Switches.Contains(TEXT("HideProcessUnstable")) ? PKGINFODISPLAY_HideProcessUnstable : 0;
|
|
DisplayFlags |= Switches.Contains(TEXT("HideSaveUnstable")) ? PKGINFODISPLAY_HideSaveUnstable : 0;
|
|
DisplayFlags |= Switches.Contains(TEXT("HideOffsets")) ? PKGINFODISPLAY_HideOffsets : 0;
|
|
|
|
FPkgInfoReporter* Reporter = new FPkgInfoReporter_Log(InfoFlags, (EPackageInfoDisplayFlags)DisplayFlags);
|
|
|
|
TArray<FString> FilesInPath;
|
|
FString PathWithPackages;
|
|
FString RelPathSibling;
|
|
if (FParse::Value(*Params, TEXT("AllPackagesIn="), PathWithPackages))
|
|
{
|
|
FPackageName::FindPackagesInDirectory(FilesInPath, PathWithPackages);
|
|
RelPathSibling = FPaths::ConvertRelativePathToFull(PathWithPackages);
|
|
RelPathSibling = FPaths::Combine(RelPathSibling, TEXT("Placeholder"));
|
|
}
|
|
else if( Switches.Contains(TEXT("AllPackages")) )
|
|
{
|
|
FEditorFileUtils::FindAllPackageFiles(FilesInPath);
|
|
}
|
|
else
|
|
{
|
|
for ( int32 TokenIndex = 0; TokenIndex < Tokens.Num(); TokenIndex++ )
|
|
{
|
|
FString& PackageWildcard = Tokens[TokenIndex];
|
|
|
|
TArray<FString> PerTokenFilesInPath;
|
|
IFileManager::Get().FindFiles( PerTokenFilesInPath, *PackageWildcard, true, false );
|
|
|
|
if( PerTokenFilesInPath.Num() == 0 )
|
|
{
|
|
TArray<FString> Paths;
|
|
if ( GConfig->GetArray( TEXT("Core.System"), TEXT("Paths"), Paths, GEngineIni ) > 0 )
|
|
{
|
|
for ( int32 i = 0; i < Paths.Num(); i++ )
|
|
{
|
|
IFileManager::Get().FindFiles( PerTokenFilesInPath, *(Paths[i] / PackageWildcard), 1, 0 );
|
|
}
|
|
}
|
|
|
|
if ( PerTokenFilesInPath.Num() == 0 )
|
|
{
|
|
// Check if long package name is provided and if it exists on disk.
|
|
FString Filename;
|
|
if ( FPackageName::IsValidLongPackageName(PackageWildcard, true) && FPackageName::DoesPackageExist(PackageWildcard, &Filename) )
|
|
{
|
|
PerTokenFilesInPath.Add(Filename);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// re-add the path information so that GetPackageLinker finds the correct version of the file.
|
|
FString WildcardPath = PackageWildcard;
|
|
for ( int32 FileIndex = 0; FileIndex < PerTokenFilesInPath.Num(); FileIndex++ )
|
|
{
|
|
PerTokenFilesInPath[FileIndex] = FPaths::GetPath(WildcardPath) / PerTokenFilesInPath[FileIndex];
|
|
FPaths::NormalizeFilename(PerTokenFilesInPath[FileIndex]);
|
|
}
|
|
}
|
|
|
|
if ( PerTokenFilesInPath.Num() == 0 )
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("No packages found using '%s'!"), *PackageWildcard);
|
|
continue;
|
|
}
|
|
|
|
FilesInPath += PerTokenFilesInPath;
|
|
}
|
|
}
|
|
|
|
FString OutputPath;
|
|
if (FParse::Value(*Params, TEXT("dumptopath="), OutputPath))
|
|
{
|
|
if (!OutputFilename.IsEmpty())
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("-dumptopath is not supported with -dumptofile, ignoring -dumptopath."));
|
|
OutputPath.Empty();
|
|
}
|
|
else if (RelPathSibling.IsEmpty())
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("-dumptopath is only supported with -AllPackagesIn, ignoring -dumptopath."));
|
|
OutputPath.Empty();
|
|
}
|
|
else
|
|
{
|
|
OutputPath = FPaths::ConvertRelativePathToFull(OutputPath);
|
|
}
|
|
}
|
|
|
|
for( int32 FileIndex = 0; FileIndex < FilesInPath.Num(); FileIndex++ )
|
|
{
|
|
FString Filename = FPaths::ConvertRelativePathToFull(FilesInPath[FileIndex]);
|
|
FString PackageOutputFilename;
|
|
|
|
if (!OutputPath.IsEmpty())
|
|
{
|
|
PackageOutputFilename = Filename;
|
|
if (!FPaths::MakePathRelativeTo(PackageOutputFilename, *RelPathSibling))
|
|
{
|
|
UE_LOG(LogPackageUtilities, Error, TEXT("Package filename '%s' is not a child path of root content path '%s', unable to create Outputfile, skipping the file."),
|
|
*Filename, *FPaths::GetPath(RelPathSibling));
|
|
continue;
|
|
}
|
|
PackageOutputFilename = FPaths::Combine(OutputPath, PackageOutputFilename) + TEXT(".txt");
|
|
}
|
|
|
|
{
|
|
// reset the loaders for the packages we want to load so that we don't find the wrong version of the file
|
|
// (otherwise, attempting to run pkginfo on e.g. Engine.xxx will always return results for Engine.u instead)
|
|
FString PackageName;
|
|
if (FPackageName::TryConvertFilenameToLongPackageName(Filename, PackageName))
|
|
{
|
|
UPackage* ExistingPackage = FindObject<UPackage>(nullptr, *PackageName, true);
|
|
if (ExistingPackage != nullptr)
|
|
{
|
|
ResetLoaders(ExistingPackage);
|
|
}
|
|
}
|
|
}
|
|
|
|
FLinkerLoad* Linker = nullptr;
|
|
UPackage* Package = nullptr;
|
|
FArchiveStackTraceReader* Reader = nullptr;
|
|
|
|
if (!bDumpProperties)
|
|
{
|
|
TGuardValue<int32> GuardAllowUnversionedContentInEditor(GAllowUnversionedContentInEditor, 1);
|
|
TGuardValue<int32> GuardAllowCookedContentInEditor(GAllowCookedDataInEditorBuilds, 1);
|
|
TRefCountPtr<FUObjectSerializeContext> LoadContext(FUObjectThreadContext::Get().GetSerializeContext());
|
|
BeginLoad(LoadContext);
|
|
Linker = CreateLinkerForFilename(LoadContext, Filename);
|
|
EndLoad(LoadContext);
|
|
}
|
|
else
|
|
{
|
|
FString TempPackageName = Filename;
|
|
const TCHAR* ContentFolderString = TEXT("/Content/");
|
|
int32 ContentFolderIndex = TempPackageName.Find(ContentFolderString);
|
|
if (ContentFolderIndex >= 0)
|
|
{
|
|
TempPackageName = Filename.Mid(ContentFolderIndex + FCString::Strlen(ContentFolderString));
|
|
}
|
|
TempPackageName = FPaths::Combine(TEXT("/Temp"), *FPaths::GetPath(TempPackageName.Mid(TempPackageName.Find(TEXT(":"), ESearchCase::CaseSensitive) + 1)), *FPaths::GetBaseFilename(TempPackageName));
|
|
Package = FindObjectFast<UPackage>(nullptr, *TempPackageName);
|
|
if (!Package)
|
|
{
|
|
Package = CreatePackage( *TempPackageName);
|
|
}
|
|
|
|
Reader = FArchiveStackTraceReader::CreateFromFile(*Filename);
|
|
if (Reader)
|
|
{
|
|
TGuardValue<int32> GuardAllowUnversionedContentInEditor(GAllowUnversionedContentInEditor, 1);
|
|
TGuardValue<int32> GuardAllowCookedContentInEditor(GAllowCookedDataInEditorBuilds, 1);
|
|
UPackage* LoadedPackage = LoadPackage(Package, *Filename, LOAD_NoVerify, Reader);
|
|
if (LoadedPackage)
|
|
{
|
|
check(LoadedPackage == Package);
|
|
Linker = Package->GetLinker();
|
|
check(Linker);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogPackageUtilities, Error, TEXT("Unable to fully load package %s"), *Filename);
|
|
bDumpProperties = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogPackageUtilities, Error, TEXT("Unable to create archive for package %s"), *Filename);
|
|
bDumpProperties = false;
|
|
}
|
|
}
|
|
|
|
if (!PackageOutputFilename.IsEmpty())
|
|
{
|
|
OutputBuffer = MakeUnique<FOutputDeviceFile>(*PackageOutputFilename, true);
|
|
OutputBuffer->SetSuppressEventTag(true);
|
|
OutputOverride = OutputBuffer.Get();
|
|
}
|
|
|
|
{
|
|
// Turn off log categories etc as it makes diffing hard
|
|
TGuardValue GuardPrintLogTimes(GPrintLogTimes, ELogTimes::None);
|
|
TGuardValue GuardPrintLogCategory(GPrintLogCategory, false);
|
|
TGuardValue GuardPrintLogVerbosity(GPrintLogVerbosity, false);
|
|
|
|
if (Linker)
|
|
{
|
|
Reporter->GeneratePackageReport(Linker, *OutputOverride);
|
|
}
|
|
#if !NO_LOGGING
|
|
if (bDumpProperties)
|
|
{
|
|
check(Reader);
|
|
const int32 BaseIndent = FOutputDeviceHelper::FormatLogLine(ELogVerbosity::Display, LogPackageUtilities.GetCategoryName(), TEXT(""), GPrintLogTimes).Len();
|
|
FOutputDevice& Out = *OutputOverride;
|
|
|
|
Out.Logf(ELogVerbosity::Display, TEXT("--------------------------------------------"));
|
|
Out.Logf(ELogVerbosity::Display, TEXT("Serialize calls for exports"));
|
|
Out.Logf(ELogVerbosity::Display, TEXT("==========================="));
|
|
|
|
int64 TotalSerializeCalls = 0;
|
|
for (int32 SerializeCallIndex = 0; SerializeCallIndex < Reader->GetSerializeTrace().Num(); ++SerializeCallIndex)
|
|
{
|
|
FString IndexString = FString::FromInt(SerializeCallIndex);
|
|
const TCHAR* Indent = FCString::Spc(BaseIndent + IndexString.Len() + 2);
|
|
const FArchiveStackTraceReader::FSerializeData& SerializeData = Reader->GetSerializeTrace()[SerializeCallIndex];
|
|
FString DisplayText = FString::Printf(TEXT("[%s] Offset: %lld\n%s Object: %s\n%s Property: %s\n%s Size: %lld\n%s Count: %lld"),
|
|
*IndexString,
|
|
SerializeData.Offset,
|
|
Indent, *GetFullNameSafe(SerializeData.Object),
|
|
Indent, *SerializeData.FullPropertyName,
|
|
Indent, SerializeData.Size,
|
|
Indent, SerializeData.Count);
|
|
Out.Logf(ELogVerbosity::Display, TEXT("%s"), *DisplayText);
|
|
TotalSerializeCalls += SerializeData.Count;
|
|
}
|
|
Out.Logf(ELogVerbosity::Display, TEXT("Total number of Serialize calls: %lld"), TotalSerializeCalls);
|
|
}
|
|
#endif // !NO_LOGGING
|
|
|
|
// Flush logs while the disabled times, category, and verbosity are in scope.
|
|
if (GLog)
|
|
{
|
|
GLog->Flush();
|
|
}
|
|
}
|
|
CollectGarbage(RF_NoFlags);
|
|
}
|
|
|
|
delete Reporter;
|
|
Reporter = NULL;
|
|
return 0;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------------
|
|
CompressAnimations Commandlet
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
static int32 AnalyzeCompressionCandidates = 0;
|
|
static TArray<FString> PackagesThatCouldNotBeSavedList;
|
|
|
|
struct AddAllSkeletalMeshesToListFunctor
|
|
{
|
|
template< typename OBJECTYPE >
|
|
void DoIt( UCommandlet* Commandlet, UPackage* Package, TArray<FString>& Tokens, TArray<FString>& Switches )
|
|
{
|
|
for( TObjectIterator<OBJECTYPE> It; It; ++It )
|
|
{
|
|
OBJECTYPE* SkelMesh = *It;
|
|
SkelMesh->AddToRoot();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
*
|
|
*/
|
|
struct CompressAnimationsFunctor
|
|
{
|
|
template< typename OBJECTYPE >
|
|
void DoIt( UCommandlet* Commandlet, UPackage* Package, TArray<FString>& Tokens, TArray<FString>& Switches )
|
|
{
|
|
// Count the number of animations to provide some limited progress indication
|
|
int32 NumAnimationsInPackage = 0;
|
|
for (TObjectIterator<OBJECTYPE> It; It; ++It)
|
|
{
|
|
OBJECTYPE* AnimSeq = *It;
|
|
if (!AnimSeq->IsIn(Package))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
++NumAnimationsInPackage;
|
|
}
|
|
|
|
// Skip packages that contain no Animations.
|
|
if (NumAnimationsInPackage == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// @todoanim: we expect this won't work properly since it won't have any skeletalmesh,
|
|
// but soon, the compression will changed based on skeleton.
|
|
// when that happens, this doesn't have to worry about skeletalmesh not loaded
|
|
double LastSaveTime = FPlatformTime::Seconds();
|
|
bool bDirtyPackage = false;
|
|
const FName& PackageName = Package->GetFName();
|
|
FString PackageFileName;
|
|
FPackageName::DoesPackageExist( PackageName.ToString(), &PackageFileName );
|
|
|
|
// Ensure source control is initialized and shut down properly
|
|
FScopedSourceControl SourceControl;
|
|
|
|
const bool bSkipCinematicPackages = Switches.Contains(TEXT("SKIPCINES"));
|
|
const bool bSkipLongAnimations = Switches.Contains(TEXT("SKIPLONGANIMS"));
|
|
/** Clear bDoNotOverrideCompression flag in animations */
|
|
const bool bClearNoCompressionOverride = Switches.Contains(TEXT("CLEARNOCOMPRESSIONOVERRIDE"));
|
|
// See if we can save this package. If we can't, don't bother...
|
|
/** if we should auto checkout packages that need to be saved **/
|
|
const bool bAutoCheckOut = Switches.Contains(TEXT("AUTOCHECKOUTPACKAGES"));
|
|
|
|
FSourceControlStatePtr SourceControlState = SourceControl.GetProvider().GetState(PackageFileName, EStateCacheUsage::ForceUpdate);
|
|
|
|
// check to see if we need to check this package out
|
|
if( SourceControlState.IsValid() && SourceControlState->CanCheckout() )
|
|
{
|
|
// Cant check out, check to see why
|
|
if (bAutoCheckOut == true)
|
|
{
|
|
// Checked out by other.. fail :(
|
|
if( SourceControlState->IsCheckedOutOther() )
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Package (%s) checked out by other, skipping."), *PackageFileName);
|
|
PackagesThatCouldNotBeSavedList.Add( PackageFileName );
|
|
return;
|
|
}
|
|
// Package not at head revision
|
|
else if ( !SourceControlState->IsCurrent() )
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Package (%s) is not at head revision, skipping."), *PackageFileName );
|
|
PackagesThatCouldNotBeSavedList.Add( PackageFileName );
|
|
return;
|
|
}
|
|
// Package marked for delete
|
|
else if ( SourceControlState->IsDeleted() )
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Package (%s) is marked for delete, skipping."), *PackageFileName );
|
|
PackagesThatCouldNotBeSavedList.Add( PackageFileName );
|
|
return;
|
|
}
|
|
}
|
|
// not allowed to auto check out :(
|
|
else
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Package (%s) cannot be checked out. Switch AUTOCHECKOUTPACKAGES not set. Skip."), *PackageFileName);
|
|
PackagesThatCouldNotBeSavedList.AddUnique( PackageFileName );
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (bSkipCinematicPackages && (PackageFileName.Contains(TEXT("CINE"))))
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Package (%s) name contains 'cine' and switch SKIPCINES is set. Skip."), *PackageFileName);
|
|
PackagesThatCouldNotBeSavedList.AddUnique( PackageFileName );
|
|
return;
|
|
}
|
|
|
|
// Get version number. Bump this up every time you want to recompress all animations.
|
|
const int32 CompressCommandletVersion = UAnimationSettings::Get()->CompressCommandletVersion;
|
|
|
|
int32 ActiveAnimationIndex = 0;
|
|
for (TObjectIterator<OBJECTYPE> It; It; ++It)
|
|
{
|
|
OBJECTYPE* AnimSeq = *It;
|
|
if (!AnimSeq->IsIn(Package))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
++ActiveAnimationIndex;
|
|
|
|
// If animation hasn't been compressed, force it.
|
|
bool bForceCompression = !AnimSeq->IsCompressedDataValid();
|
|
|
|
// If animation has already been compressed with the commandlet and version is the same. then skip.
|
|
// We're only interested in new animations.
|
|
if( !bForceCompression && AnimSeq->CompressCommandletVersion == CompressCommandletVersion )
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Same CompressCommandletVersion (%i) skip animation: %s (%s)"), CompressCommandletVersion, *AnimSeq->GetName(), *AnimSeq->GetFullName());
|
|
continue;
|
|
}
|
|
|
|
if( !bForceCompression && bSkipLongAnimations && (AnimSeq->GetNumberOfSampledKeys() > 300) )
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Animation (%s) has more than 300 keys (%i keys) and SKIPLONGANIMS switch is set. Skipping."), *AnimSeq->GetName(), AnimSeq->GetNumberOfSampledKeys());
|
|
continue;
|
|
}
|
|
|
|
USkeleton* Skeleton = AnimSeq->GetSkeleton();
|
|
|
|
if (Skeleton == nullptr)
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Animation (%s) is missing its skeleton. Skipping."), *AnimSeq->GetName());
|
|
continue;
|
|
}
|
|
|
|
if (Skeleton->HasAnyFlags(RF_NeedLoad))
|
|
{
|
|
Skeleton->GetLinker()->Preload(Skeleton);
|
|
}
|
|
|
|
float HighestRatio = 0.f;
|
|
#if 0 // @todoanim: not sure why we need this here
|
|
USkeletalMesh* BestSkeletalMeshMatch = NULL;
|
|
|
|
// Test preview skeletal mesh
|
|
USkeletalMesh* DefaultSkeletalMesh = LoadObject<USkeletalMesh>(NULL, *AnimSet->PreviewSkelMeshName.ToString(), NULL, LOAD_None, NULL);
|
|
float DefaultMatchRatio = 0.f;
|
|
if( DefaultSkeletalMesh )
|
|
{
|
|
DefaultMatchRatio = AnimSet->GetSkeletalMeshMatchRatio(DefaultSkeletalMesh);
|
|
}
|
|
|
|
// If our default mesh doesn't have a full match ratio, then see if we can find a better fit.
|
|
if( DefaultMatchRatio < 1.f )
|
|
{
|
|
// Find the most suitable SkeletalMesh for this AnimSet
|
|
for( TObjectIterator<USkeletalMesh> ItMesh; ItMesh; ++ItMesh )
|
|
{
|
|
USkeletalMesh* SkelMeshCandidate = *ItMesh;
|
|
if( SkelMeshCandidate != DefaultSkeletalMesh )
|
|
{
|
|
float MatchRatio = AnimSet->GetSkeletalMeshMatchRatio(SkelMeshCandidate);
|
|
if( MatchRatio > HighestRatio )
|
|
{
|
|
BestSkeletalMeshMatch = SkelMeshCandidate;
|
|
HighestRatio = MatchRatio;
|
|
|
|
// If we have found a perfect match, we can abort.
|
|
if( FMath::Abs(1.f - MatchRatio) <= KINDA_SMALL_NUMBER )
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we have found a best match
|
|
if( BestSkeletalMeshMatch )
|
|
{
|
|
// if it is different than our preview mesh and its match ratio is higher
|
|
// then replace preview mesh with this one, as it's a better match.
|
|
if( BestSkeletalMeshMatch != DefaultSkeletalMesh && HighestRatio > DefaultMatchRatio )
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Found more suitable preview mesh for %s (%s): %s (%f) instead of %s (%f)."),
|
|
*AnimSeq->GetName(), *AnimSet->GetFullName(), *BestSkeletalMeshMatch->GetFName().ToString(), HighestRatio, *AnimSet->PreviewSkelMeshName.ToString(), DefaultMatchRatio);
|
|
|
|
// We'll now use this one from now on as it's a better fit.
|
|
AnimSet->PreviewSkelMeshName = FName( *BestSkeletalMeshMatch->GetPathName() );
|
|
AnimSet->MarkPackageDirty();
|
|
|
|
DefaultSkeletalMesh = BestSkeletalMeshMatch;
|
|
bDirtyPackage = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Could not find suitable mesh for %s (%s) !!! Default was %s"),
|
|
*AnimSeq->GetName(), *AnimSet->GetFullName(), *AnimSet->PreviewSkelMeshName.ToString());
|
|
}
|
|
}
|
|
#endif
|
|
SIZE_T OldSize;
|
|
SIZE_T NewSize;
|
|
|
|
OldSize = AnimSeq->GetResourceSizeBytes(EResourceSizeMode::EstimatedTotal);
|
|
|
|
// Clear bDoNotOverrideCompression flag
|
|
if( bClearNoCompressionOverride && AnimSeq->bDoNotOverrideCompression )
|
|
{
|
|
AnimSeq->bDoNotOverrideCompression = false;
|
|
bDirtyPackage = true;
|
|
}
|
|
|
|
// Do not perform recompression on animations marked as 'bDoNotOverrideCompression'
|
|
// Unless they have no compression scheme.
|
|
if (AnimSeq->bDoNotOverrideCompression && AnimSeq->BoneCompressionSettings != nullptr)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Compressing animation '%s' [#%d / %d in package '%s']"),
|
|
*AnimSeq->GetName(),
|
|
ActiveAnimationIndex,
|
|
NumAnimationsInPackage,
|
|
*PackageFileName);
|
|
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("%s (%s) Resetting with to default compression settings."), *AnimSeq->GetName(), *AnimSeq->GetFullName());
|
|
|
|
UE::Anim::IAnimSequenceCompilingManager::FinishCompilation({ AnimSeq });
|
|
|
|
AnimSeq->ResetCompressionSettings();
|
|
AnimSeq->CacheDerivedDataForCurrentPlatform();
|
|
|
|
// Automatic compression should have picked a suitable compressor
|
|
if (!AnimSeq->IsCompressedDataValid())
|
|
{
|
|
// Update CompressCommandletVersion in that case, and create a proper DDC entry
|
|
// (with actual compressor)
|
|
AnimSeq->CompressCommandletVersion = CompressCommandletVersion;
|
|
AnimSeq->CacheDerivedDataForCurrentPlatform();
|
|
bDirtyPackage = true;
|
|
}
|
|
|
|
NewSize = AnimSeq->GetResourceSizeBytes(EResourceSizeMode::EstimatedTotal);
|
|
|
|
// Only save package if size has changed.
|
|
const int64 DeltaSize = NewSize - OldSize;
|
|
bDirtyPackage = (bDirtyPackage || bForceCompression || (DeltaSize != 0));
|
|
|
|
// if Dirty, then we need to be able to write to this package.
|
|
// If we can't, abort, don't want to waste time!!
|
|
if( bDirtyPackage )
|
|
{
|
|
// Save dirty package every 10 minutes at least, to avoid losing work in case of a crash on very large packages.
|
|
const double CurrentTime = FPlatformTime::Seconds();
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Time since last save: %f seconds"), (CurrentTime - LastSaveTime) );
|
|
if( (CurrentTime - LastSaveTime) > 10.f * 60.f )
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("It's been over 10 minutes (%f seconds), try to save package."), (CurrentTime - LastSaveTime) );
|
|
bool bCorrectlySaved = false;
|
|
|
|
SourceControlState = SourceControl.GetProvider().GetState(Package, EStateCacheUsage::ForceUpdate);
|
|
if( SourceControlState.IsValid() && SourceControlState->CanCheckout() && bAutoCheckOut )
|
|
{
|
|
SourceControl.GetProvider().Execute(ISourceControlOperation::Create<FCheckOut>(), Package);
|
|
}
|
|
|
|
SourceControlState = SourceControl.GetProvider().GetState(Package, EStateCacheUsage::ForceUpdate);
|
|
if( !SourceControlState.IsValid() || SourceControlState->CanEdit() )
|
|
{
|
|
if( SavePackageHelper( Package, PackageFileName ) == true )
|
|
{
|
|
bCorrectlySaved = true;
|
|
UE_LOG(LogPackageUtilities, Display, TEXT("Correctly saved: [%s]."), *PackageFileName );
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogPackageUtilities, Error, TEXT("Error saving [%s]"), *PackageFileName );
|
|
}
|
|
}
|
|
|
|
// Log which packages could not be saved
|
|
if( !bCorrectlySaved )
|
|
{
|
|
PackagesThatCouldNotBeSavedList.AddUnique( PackageFileName );
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("%s couldn't be saved, so abort this package, don't waste time on it."), *PackageFileName );
|
|
// Abort!
|
|
return;
|
|
}
|
|
|
|
// Correctly saved
|
|
LastSaveTime = CurrentTime;
|
|
bDirtyPackage = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// End of recompression
|
|
// Does package need to be saved?
|
|
/* bDirtyPackage = bDirtyPackage || Package->IsDirty();*/
|
|
|
|
// If we need to save package, do so.
|
|
if( bDirtyPackage )
|
|
{
|
|
bool bCorrectlySaved = false;
|
|
|
|
// see if we should skip read only packages.
|
|
bool bIsReadOnly = IFileManager::Get().IsReadOnly( *PackageFileName);
|
|
|
|
// check to see if we need to check this package out
|
|
SourceControlState = SourceControl.GetProvider().GetState(Package, EStateCacheUsage::ForceUpdate);
|
|
if( SourceControlState.IsValid() && SourceControlState->CanCheckout() && bAutoCheckOut == true )
|
|
{
|
|
SourceControl.GetProvider().Execute(ISourceControlOperation::Create<FCheckOut>(), Package);
|
|
}
|
|
|
|
SourceControlState = SourceControl.GetProvider().GetState(Package, EStateCacheUsage::ForceUpdate);
|
|
if( !SourceControlState.IsValid() || SourceControlState->CanEdit() )
|
|
{
|
|
if( SavePackageHelper( Package, PackageFileName ) == true )
|
|
{
|
|
bCorrectlySaved = true;
|
|
UE_LOG(LogPackageUtilities, Display, TEXT("Correctly saved: [%s]."), *PackageFileName );
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Error saving [%s]"), *PackageFileName );
|
|
}
|
|
}
|
|
|
|
// Log which packages could not be saved
|
|
if( !bCorrectlySaved )
|
|
{
|
|
PackagesThatCouldNotBeSavedList.AddUnique( PackageFileName );
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
UCompressAnimationsCommandlet::UCompressAnimationsCommandlet(const FObjectInitializer& ObjectInitializer)
|
|
: Super(ObjectInitializer)
|
|
{
|
|
LogToConsole = false;
|
|
}
|
|
|
|
|
|
int32 UCompressAnimationsCommandlet::Main( const FString& Params )
|
|
{
|
|
// Parse command line.
|
|
TArray<FString> Tokens;
|
|
TArray<FString> Switches;
|
|
|
|
// want everything in upper case, it's a mess otherwise
|
|
const FString ParamsUpperCase = Params.ToUpper();
|
|
const TCHAR* Parms = *ParamsUpperCase;
|
|
UCommandlet::ParseCommandLine(Parms, Tokens, Switches);
|
|
|
|
/** If we're analyzing, we're not actually going to recompress, so we can skip some significant work. */
|
|
bool bAnalyze = Switches.Contains(TEXT("ANALYZE"));
|
|
|
|
if (bAnalyze)
|
|
{
|
|
UE_LOG(LogPackageUtilities, Display, TEXT("Analyzing content for uncompressed animations..."));
|
|
DoActionToAllPackages<UAnimSequence, CompressAnimationsFunctor>(this, ParamsUpperCase);
|
|
|
|
UE_LOG(LogPackageUtilities, Display, TEXT("Done analyzing. Potential canditates: %i"), AnalyzeCompressionCandidates);
|
|
}
|
|
else
|
|
{
|
|
// Then do the animation recompression
|
|
UE_LOG(LogPackageUtilities, Display, TEXT("Recompressing all animations..."));
|
|
DoActionToAllPackages<UAnimSequence, CompressAnimationsFunctor>(this, ParamsUpperCase);
|
|
|
|
int32 NumPackagesThatCouldNotBeSaved = PackagesThatCouldNotBeSavedList.Num();
|
|
if (NumPackagesThatCouldNotBeSaved > 0)
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("\n*** Packages that could not be recompressed: %i"), PackagesThatCouldNotBeSavedList.Num());
|
|
for(int32 i=0; i<NumPackagesThatCouldNotBeSaved; i++)
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("\t%s"), *PackagesThatCouldNotBeSavedList[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
return 0;
|
|
}
|
|
|
|
//======================================================================
|
|
// UReplaceActorCommandlet
|
|
//======================================================================
|
|
UReplaceActorCommandlet::UReplaceActorCommandlet(const FObjectInitializer& ObjectInitializer)
|
|
: Super(ObjectInitializer)
|
|
{
|
|
LogToConsole = false;
|
|
}
|
|
|
|
int32 UReplaceActorCommandlet::Main(const FString& Params)
|
|
{
|
|
const TCHAR* Parms = *Params;
|
|
|
|
// // get the specified filename/wildcard
|
|
// FString PackageWildcard;
|
|
// if (!FParse::Token(Parms, PackageWildcard, 0))
|
|
// {
|
|
// UE_LOG(LogPackageUtilities, Warning, TEXT("Syntax: replaceactor <file/wildcard> <Package.Class to remove> <Package.Class to replace with>"));
|
|
// return 1;
|
|
// }
|
|
|
|
// find all the files matching the specified filename/wildcard
|
|
// TArray<FString> FilesInPath;
|
|
// IFileManager::Get().FindFiles(FilesInPath, *PackageWildcard, 1, 0);
|
|
// if (FilesInPath.Num() == 0)
|
|
// {
|
|
// UE_LOG(LogPackageUtilities, Error, TEXT("No packages found matching %s!"), *PackageWildcard);
|
|
// return 2;
|
|
// }
|
|
|
|
// Retrieve list of all packages in .ini paths.
|
|
TArray<FString> PackageList;
|
|
|
|
FString PackageWildcard;
|
|
FString PackagePrefix;
|
|
// if(FParse::Token(Parms,PackageWildcard,false))
|
|
// {
|
|
// IFileManager::Get().FindFiles(PackageList,*PackageWildcard,true,false);
|
|
// PackagePrefix = FPaths::GetPath(PackageWildcard) * TEXT("");
|
|
// }
|
|
// else
|
|
// {
|
|
FEditorFileUtils::FindAllPackageFiles(PackageList);
|
|
// }
|
|
if( !PackageList.Num() )
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT( "Found no packages to run UReplaceActorCommandlet on!" ) );
|
|
return 0;
|
|
}
|
|
|
|
// get the directory part of the filename
|
|
int32 ChopPoint = FMath::Max(PackageWildcard.Find(TEXT("/"), ESearchCase::CaseSensitive, ESearchDir::FromEnd) + 1, PackageWildcard.Find(TEXT("\\"), ESearchCase::CaseSensitive, ESearchDir::FromEnd) + 1);
|
|
if (ChopPoint < 0)
|
|
{
|
|
ChopPoint = PackageWildcard.Find( TEXT("*"), ESearchCase::CaseSensitive, ESearchDir::FromEnd );
|
|
}
|
|
|
|
FString PathPrefix = (ChopPoint < 0) ? TEXT("") : PackageWildcard.Left(ChopPoint);
|
|
|
|
// get the class to remove and the class to replace it with
|
|
FString ClassName;
|
|
if (!FParse::Token(Parms, ClassName, 0))
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Syntax: replaceactor <file/wildcard> <Package.Class to remove> <Package.Class to replace with>"));
|
|
return 1;
|
|
}
|
|
|
|
UClass* ClassToReplace = (UClass*)StaticLoadObject(UClass::StaticClass(), NULL, *ClassName, NULL, LOAD_NoWarn | LOAD_Quiet, NULL);
|
|
if (ClassToReplace == NULL)
|
|
{
|
|
UE_LOG(LogPackageUtilities, Error, TEXT("Invalid class to remove: %s"), *ClassName);
|
|
return 4;
|
|
}
|
|
else
|
|
{
|
|
ClassToReplace->AddToRoot();
|
|
}
|
|
|
|
if (!FParse::Token(Parms, ClassName, 0))
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Syntax: replaceactor <file/wildcard> <Package.Class to remove> <Package.Class to replace with>"));
|
|
return 1;
|
|
}
|
|
|
|
UClass* ReplaceWithClass = (UClass*)StaticLoadObject(UClass::StaticClass(), NULL, *ClassName, NULL, LOAD_NoWarn | LOAD_Quiet, NULL);
|
|
if (ReplaceWithClass == NULL)
|
|
{
|
|
UE_LOG(LogPackageUtilities, Error, TEXT("Invalid class to replace with: %s"), *ClassName);
|
|
return 5;
|
|
}
|
|
else
|
|
{
|
|
ReplaceWithClass->AddToRoot();
|
|
}
|
|
|
|
// find the most derived superclass common to both classes
|
|
UClass* CommonSuperclass = NULL;
|
|
for (UClass* BaseClass1 = ClassToReplace; BaseClass1 != NULL && CommonSuperclass == NULL; BaseClass1 = BaseClass1->GetSuperClass())
|
|
{
|
|
for (UClass* BaseClass2 = ReplaceWithClass; BaseClass2 != NULL && CommonSuperclass == NULL; BaseClass2 = BaseClass2->GetSuperClass())
|
|
{
|
|
if (BaseClass1 == BaseClass2)
|
|
{
|
|
CommonSuperclass = BaseClass1;
|
|
}
|
|
}
|
|
}
|
|
checkSlow(CommonSuperclass != NULL);
|
|
|
|
const bool bAutoCheckOut = FParse::Param(*Params,TEXT("AutoCheckOutPackages"));
|
|
|
|
// Ensure source control is initialized and shut down properly
|
|
FScopedSourceControl SourceControl;
|
|
|
|
for (int32 i = 0; i < PackageList.Num(); i++)
|
|
{
|
|
const FString& PackageName = PackageList[i];
|
|
// get the full path name to the file
|
|
FString FileName = PathPrefix + PackageName;
|
|
|
|
const bool bIsAutoSave = FileName.Contains( TEXT("AUTOSAVES") );
|
|
|
|
FSourceControlStatePtr SourceControlState = SourceControl.GetProvider().GetState(FileName, EStateCacheUsage::ForceUpdate);
|
|
|
|
// skip if read-only
|
|
if( !bAutoCheckOut && SourceControlState.IsValid() && SourceControlState->CanCheckout() )
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Skipping %s: the file can be checked out, but auto check out is disabled"), *FileName);
|
|
continue;
|
|
}
|
|
else if(bIsAutoSave)
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Skipping %s (non map)"), *FileName);
|
|
continue;
|
|
}
|
|
else if ( bAutoCheckOut && SourceControlState.IsValid() && !SourceControlState->IsCurrent() )
|
|
{
|
|
UE_LOG(LogPackageUtilities, Warning, TEXT("Skipping %s (Newer version exists in revision control)"), *PackageName );
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
UWorld* World = GWorld;
|
|
// clean up any previous world
|
|
if (World != NULL)
|
|
{
|
|
const bool bBroadcastWorldDestroyedEvent = false;
|
|
World->DestroyWorld(bBroadcastWorldDestroyedEvent);
|
|
}
|
|
|
|
// load the package
|
|
UE_LOG(LogPackageUtilities, Display, TEXT("Loading %s..."), *FileName);
|
|
UPackage* Package = LoadPackage(NULL, *FileName, LOAD_None);
|
|
|
|
// load the world we're interested in
|
|
World = UWorld::FindWorldInPackage(Package);
|
|
|
|
// this is the case where .uasset objects have class references (e.g. prefabs, animnodes, etc)
|
|
if( World == NULL )
|
|
{
|
|
UE_LOG(LogPackageUtilities, Display, TEXT("%s (not a map)"), *FileName);
|
|
for( FThreadSafeObjectIterator It; It; ++It )
|
|
{
|
|
UObject* OldObject = *It;
|
|
if( ( OldObject->GetOutermost() == Package )
|
|
)
|
|
{
|
|
TMap<UClass*, UClass*> ReplaceMap;
|
|
ReplaceMap.Add(ClassToReplace, ReplaceWithClass);
|
|
FArchiveReplaceObjectRef<UClass> ReplaceAr(OldObject, ReplaceMap);
|
|
if( ReplaceAr.GetCount() > 0 )
|
|
{
|
|
UE_LOG(LogPackageUtilities, Display, TEXT("Replaced %i class references in an Object: %s"), ReplaceAr.GetCount(), *OldObject->GetName() );
|
|
Package->MarkPackageDirty();
|
|
}
|
|
}
|
|
}
|
|
|
|
if( Package->IsDirty() == true )
|
|
{
|
|
if( SourceControlState.IsValid() && SourceControlState->CanCheckout() && bAutoCheckOut == true )
|
|
{
|
|
SourceControl.GetProvider().Execute(ISourceControlOperation::Create<FCheckOut>(), Package);
|
|
}
|
|
|
|
UE_LOG(LogPackageUtilities, Display, TEXT("Saving %s..."), *FileName);
|
|
FSavePackageArgs SaveArgs;
|
|
SaveArgs.TopLevelFlags = RF_Standalone;
|
|
SaveArgs.Error = GWarn;
|
|
GEditor->SavePackage(Package, nullptr, *FileName, SaveArgs);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// We shouldnt need this - but just in case
|
|
GWorld = World;
|
|
// need to have a bool so we dont' save every single map
|
|
bool bIsDirty = false;
|
|
|
|
World->WorldType = EWorldType::Editor;
|
|
|
|
// add the world to the root set so that the garbage collection to delete replaced actors doesn't garbage collect the whole world
|
|
World->AddToRoot();
|
|
// initialize the levels in the world
|
|
World->InitWorld(UWorld::InitializationValues().AllowAudioPlayback(false));
|
|
World->GetWorldSettings()->PostEditChange();
|
|
World->UpdateWorldComponents( true, false );
|
|
|
|
// iterate through all the actors in the world, looking for matches with the class to replace (must have exact match, not subclass)
|
|
for (TActorIterator<AActor> It(World, ClassToReplace); It; ++It)
|
|
{
|
|
AActor* OldActor = *It;
|
|
if (OldActor->GetClass() == ClassToReplace)
|
|
{
|
|
// replace an instance of the old actor
|
|
UE_LOG(LogPackageUtilities, Display, TEXT("Replacing actor %s"), *OldActor->GetName());
|
|
bIsDirty = true;
|
|
// make sure we spawn the new actor in the same level as the old
|
|
//@warning: this relies on the outer of an actor being the level
|
|
FVector OldLocation = OldActor->GetActorLocation();
|
|
|
|
FRotator OldRotator = OldActor->GetActorRotation();
|
|
|
|
// Cache the level this actor is in.
|
|
ULevel* Level = OldActor->GetLevel();
|
|
// destroy the old actor, which removes it from the array but doesn't destroy it until GC
|
|
OldActor->Destroy();
|
|
|
|
FActorSpawnParameters SpawnInfo;
|
|
SpawnInfo.OverrideLevel = Level;
|
|
SpawnInfo.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
|
// spawn the new actor
|
|
AActor* NewActor = World->SpawnActor<AActor>( ReplaceWithClass, OldLocation, OldRotator, SpawnInfo );
|
|
|
|
// copy non-native non-transient properties common to both that were modified in the old actor to the new actor
|
|
for (FProperty* Property = CommonSuperclass->PropertyLink; Property != NULL; Property = Property->PropertyLinkNext)
|
|
{
|
|
if ( !(Property->PropertyFlags & CPF_Transient) &&
|
|
!(Property->PropertyFlags & (CPF_InstancedReference | CPF_ContainsInstancedReference)) &&
|
|
!Property->Identical_InContainer(OldActor, OldActor->GetClass()->GetDefaultObject()) )
|
|
{
|
|
Property->CopyCompleteValue_InContainer(NewActor, OldActor);
|
|
Package->MarkPackageDirty();
|
|
}
|
|
}
|
|
|
|
if (ClassToReplace->IsChildOf(AWorldSettings::StaticClass()))
|
|
{
|
|
Level->SetWorldSettings(CastChecked<AWorldSettings>(NewActor));
|
|
}
|
|
check(OldActor->IsValidLowLevel()); // make sure DestroyActor() doesn't immediately trigger GC since that'll break the reference replacement
|
|
// check for any references to the old Actor and replace them with the new one
|
|
TMap<AActor*, AActor*> ReplaceMap;
|
|
ReplaceMap.Add(OldActor, NewActor);
|
|
FArchiveReplaceObjectRef<AActor> ReplaceAr(World, ReplaceMap);
|
|
if (ReplaceAr.GetCount() > 0)
|
|
{
|
|
UE_LOG(LogPackageUtilities, Display, TEXT("Replaced %i actor references in %s"), ReplaceAr.GetCount(), *It->GetName());
|
|
Package->MarkPackageDirty();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// check for any references to the old class and replace them with the new one
|
|
TMap<UClass*, UClass*> ReplaceMap;
|
|
ReplaceMap.Add(ClassToReplace, ReplaceWithClass);
|
|
FArchiveReplaceObjectRef<UClass> ReplaceAr(*It, ReplaceMap);
|
|
if (ReplaceAr.GetCount() > 0)
|
|
{
|
|
UE_LOG(LogPackageUtilities, Display, TEXT("Replaced %i class references in actor %s"), ReplaceAr.GetCount(), *It->GetName());
|
|
Package->MarkPackageDirty();
|
|
bIsDirty = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// collect garbage to delete replaced actors and any objects only referenced by them (components, etc)
|
|
GEngine->PerformGarbageCollectionAndCleanupActors();
|
|
|
|
// save the world
|
|
if( ( Package->IsDirty() == true ) && ( bIsDirty == true ) )
|
|
{
|
|
SourceControlState = SourceControl.GetProvider().GetState(FileName, EStateCacheUsage::ForceUpdate);
|
|
if( SourceControlState.IsValid() && SourceControlState->CanCheckout() && bAutoCheckOut == true )
|
|
{
|
|
SourceControl.GetProvider().Execute(ISourceControlOperation::Create<FCheckOut>(), Package);
|
|
}
|
|
|
|
UE_LOG(LogPackageUtilities, Display, TEXT("Saving %s..."), *FileName);
|
|
FSavePackageArgs SaveArgs;
|
|
SaveArgs.TopLevelFlags = RF_NoFlags;
|
|
SaveArgs.Error = GWarn;
|
|
GEditor->SavePackage(Package, World, *FileName, SaveArgs);
|
|
}
|
|
|
|
// clear GWorld by removing it from the root set and replacing it with a new one
|
|
const bool bBroadcastWorldDestroyedEvent = false;
|
|
World->DestroyWorld(bBroadcastWorldDestroyedEvent);
|
|
World = GWorld = NULL;
|
|
}
|
|
}
|
|
|
|
// get rid of the loaded world
|
|
UE_LOG(LogPackageUtilities, Display, TEXT("GCing..."));
|
|
CollectGarbage(RF_NoFlags);
|
|
}
|
|
|
|
// UEditorEngine::FinishDestroy() expects GWorld to exist
|
|
if( UWorld* World = GWorld )
|
|
{
|
|
World->DestroyWorld( false );
|
|
}
|
|
GWorld = UWorld::CreateWorld(EWorldType::Editor, false );
|
|
return 0;
|
|
}
|