Files
UnrealEngine/Engine/Source/Editor/UnrealEd/Private/Commandlets/ConvertLevelsToExternalActorsCommandlet.cpp
2025-05-18 13:04:45 +08:00

426 lines
14 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
/*=============================================================================
ConvertLevelsToExternalActorsCommandlet: Commandlet used to convert levels uses external actors in batch
=============================================================================*/
#include "Commandlets/ConvertLevelsToExternalActorsCommandlet.h"
#include "HAL/FileManager.h"
#include "Misc/Paths.h"
#include "Editor.h"
#include "Engine/Level.h"
#include "Engine/World.h"
#include "Engine/LevelStreaming.h"
#include "UObject/SavePackage.h"
#include "UObject/UObjectHash.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "PackageHelperFunctions.h"
#include "ISourceControlOperation.h"
#include "SourceControlOperations.h"
#include "SourceControlHelpers.h"
#include "ISourceControlModule.h"
#include "ProfilingDebugging/ScopedTimers.h"
#include "Algo/Sort.h"
#include "Algo/Unique.h"
DEFINE_LOG_CATEGORY_STATIC(LogConvertLevelsToExternalActorsCommandlet, All, All);
UConvertLevelsToExternalActorsCommandlet::UConvertLevelsToExternalActorsCommandlet(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
ULevel* UConvertLevelsToExternalActorsCommandlet::LoadLevel(const FString& LevelToLoad) const
{
SET_WARN_COLOR(COLOR_WHITE);
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Log, TEXT("Loading level %s."), *LevelToLoad);
CLEAR_WARN_COLOR();
FString MapLoadCommand = FString::Printf(TEXT("MAP LOAD FILE=%s TEMPLATE=0 SHOWPROGRESS=0 FEATURELEVEL=3"), *LevelToLoad);
GEditor->Exec(nullptr, *MapLoadCommand, *GError);
FlushAsyncLoading();
UPackage* MapPackage = FindPackage(nullptr, *LevelToLoad);
UWorld* World = MapPackage ? UWorld::FindWorldInPackage(MapPackage) : nullptr;
return World ? World->PersistentLevel : nullptr;
}
void UConvertLevelsToExternalActorsCommandlet::GetSubLevelsToConvert(ULevel* MainLevel, TSet<ULevel*>& SubLevels, bool bRecursive)
{
UWorld* World = MainLevel->GetTypedOuter<UWorld>();
for(ULevelStreaming* StreamingLevel: World->GetStreamingLevels())
{
if (ULevel* SubLevel = StreamingLevel->GetLoadedLevel())
{
SubLevels.Add(SubLevel);
if (bRecursive)
{
// Recursively obtain sub levels to convert
GetSubLevelsToConvert(SubLevel, SubLevels, bRecursive);
}
}
}
}
bool UConvertLevelsToExternalActorsCommandlet::CheckExternalActors(const FString& Level, bool bRepair)
{
// Gather duplicated actor files.
TMultiMap<FSoftObjectPath, FName> DuplicatedActorFiles;
{
TMap<FSoftObjectPath, FName> ActorFiles;
IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")).Get();
// Let the asset registry process any pending operations before continuing
AssetRegistry.Tick(-1);
FDelegateHandle AddedCheckHandle = AssetRegistry.OnAssetAdded().AddLambda([&ActorFiles](const FAssetData& AssetData)
{
check(!ActorFiles.Contains(AssetData.GetSoftObjectPath()));
ActorFiles.Add(AssetData.GetSoftObjectPath(), AssetData.PackageName);
});
FDelegateHandle UpdatedCheckHandle = AssetRegistry.OnAssetUpdated().AddLambda([&ActorFiles, &DuplicatedActorFiles](const FAssetData& AssetData)
{
FName ExistingPackageName;
if (ActorFiles.RemoveAndCopyValue(AssetData.GetSoftObjectPath(), ExistingPackageName))
{
DuplicatedActorFiles.Add(AssetData.GetSoftObjectPath(), ExistingPackageName);
}
DuplicatedActorFiles.Add(AssetData.GetSoftObjectPath(), AssetData.PackageName);
});
AssetRegistry.ScanPathsSynchronous(ULevel::GetExternalObjectsPaths(Level), /*bForceRescan*/true, /*bIgnoreDenyListScanFilters*/true);
AssetRegistry.OnAssetAdded().Remove(AddedCheckHandle);
AssetRegistry.OnAssetUpdated().Remove(UpdatedCheckHandle);
}
if (DuplicatedActorFiles.Num())
{
// Gather unique keys from the duplicated map.
// Note: TMultiMap::GenerateKeyArray will return duplicated keys, clean that.
TArray<FSoftObjectPath> DuplicatedActorFilesKeys;
DuplicatedActorFiles.GenerateKeyArray(DuplicatedActorFilesKeys);
DuplicatedActorFilesKeys.Sort([](const FSoftObjectPath& A, const FSoftObjectPath& B) { return A.FastLess(B); });
int32 EndIndex = Algo::Unique(DuplicatedActorFilesKeys);
DuplicatedActorFilesKeys.RemoveAt(EndIndex, DuplicatedActorFilesKeys.Num() - EndIndex);
// Report or delete duplicated entries, keeping the latest one
for (const FSoftObjectPath& DuplicatedActorFileKey : DuplicatedActorFilesKeys)
{
TArray<FName> DuplicatedActorFilesPaths;
DuplicatedActorFiles.MultiFind(DuplicatedActorFileKey, DuplicatedActorFilesPaths);
check(DuplicatedActorFilesPaths.Num() > 1);
FDateTime MostRecentStamp;
int32 MostRecentIndex = INDEX_NONE;
for (int32 i=0; i<DuplicatedActorFilesPaths.Num(); i++)
{
FString Filename = FPackageName::LongPackageNameToFilename(DuplicatedActorFilesPaths[i].ToString(), FPackageName::GetAssetPackageExtension());
FDateTime FileTimeStamp = IFileManager::Get().GetTimeStamp(*Filename);
if ((MostRecentIndex == INDEX_NONE) || (FileTimeStamp > MostRecentStamp))
{
MostRecentIndex = i;
MostRecentStamp = FileTimeStamp;
}
}
check(MostRecentIndex != INDEX_NONE);
for (int32 i=0; i<DuplicatedActorFilesPaths.Num(); i++)
{
if (i != MostRecentIndex)
{
FString Filename = FPackageName::LongPackageNameToFilename(DuplicatedActorFilesPaths[i].ToString(), FPackageName::GetAssetPackageExtension());
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Warning, TEXT("Found duplicated actor file %s"), *Filename);
if (bRepair)
{
DeleteFile(Filename);
}
}
}
}
return bRepair;
}
return true;
}
bool UConvertLevelsToExternalActorsCommandlet::AddPackageToSourceControl(UPackage* Package)
{
if (UseSourceControl())
{
FString PackageFilename = SourceControlHelpers::PackageFilename(Package);
FSourceControlStatePtr SourceControlState = GetSourceControlProvider().GetState(PackageFilename, EStateCacheUsage::ForceUpdate);
if (SourceControlState.IsValid() && !SourceControlState->IsSourceControlled())
{
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Log, TEXT("Adding package %s to revision control"), *PackageFilename);
if (GetSourceControlProvider().Execute(ISourceControlOperation::Create<FMarkForAdd>(), Package) != ECommandResult::Succeeded)
{
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Error, TEXT("Error adding %s to revision control."), *PackageFilename);
return false;
}
}
}
return true;
}
bool UConvertLevelsToExternalActorsCommandlet::SavePackage(UPackage* Package)
{
// Use GEditor save as it does some UWorld specific shenanigans such as handle level offsets
FString PackageFileName = SourceControlHelpers::PackageFilename(Package);
FSavePackageArgs SaveArgs;
SaveArgs.TopLevelFlags = RF_Standalone;
FSavePackageResultStruct SaveResult = GEditor->Save(Package, nullptr, *PackageFileName, SaveArgs);
if (SaveResult.Result != ESavePackageResult::Success)
{
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Error, TEXT("Error saving %s"), *PackageFileName);
return false;
}
return true;
}
bool UConvertLevelsToExternalActorsCommandlet::CheckoutPackage(UPackage* Package)
{
if (UseSourceControl())
{
FString PackageFilename = SourceControlHelpers::PackageFilename(Package);
FSourceControlStatePtr SourceControlState = GetSourceControlProvider().GetState(PackageFilename, EStateCacheUsage::ForceUpdate);
if (SourceControlState.IsValid())
{
FString OtherCheckedOutUser;
if (SourceControlState->IsCheckedOutOther(&OtherCheckedOutUser))
{
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Error, TEXT("Overwriting package %s already checked out by %s, will not submit"), *PackageFilename, *OtherCheckedOutUser);
return false;
}
else if (!SourceControlState->IsCurrent())
{
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Error, TEXT("Overwriting package %s (not at head revision), will not submit"), *PackageFilename);
return false;
}
else if (SourceControlState->IsCheckedOut() || SourceControlState->IsAdded())
{
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Log, TEXT("Skipping package %s (already checked out)"), *PackageFilename);
return true;
}
else if (SourceControlState->IsSourceControlled())
{
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Log, TEXT("Checking out package %s from revision control"), *PackageFilename);
return GetSourceControlProvider().Execute(ISourceControlOperation::Create<FCheckOut>(), Package) == ECommandResult::Succeeded;
}
}
}
else
{
FString PackageFilename = SourceControlHelpers::PackageFilename(Package);
if (IPlatformFile::GetPlatformPhysical().FileExists(*PackageFilename))
{
if (!IPlatformFile::GetPlatformPhysical().SetReadOnly(*PackageFilename, false))
{
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Error, TEXT("Error setting %s writable"), *PackageFilename);
return false;
}
}
}
return true;
}
bool UConvertLevelsToExternalActorsCommandlet::DeleteFile(const FString& Filename)
{
if (!UseSourceControl())
{
if (!IPlatformFile::GetPlatformPhysical().SetReadOnly(*Filename, false) ||
!IPlatformFile::GetPlatformPhysical().DeleteFile(*Filename))
{
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Error, TEXT("Error deleting %s"), *Filename);
return false;
}
}
else
{
FSourceControlStatePtr SourceControlState = GetSourceControlProvider().GetState(Filename, EStateCacheUsage::ForceUpdate);
if (SourceControlState.IsValid() && SourceControlState->IsSourceControlled())
{
FString OtherCheckedOutUser;
if (SourceControlState->IsCheckedOutOther(&OtherCheckedOutUser))
{
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Error, TEXT("Overwriting package %s already checked out by %s, will not submit"), *Filename, *OtherCheckedOutUser);
return false;
}
else if (!SourceControlState->IsCurrent())
{
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Error, TEXT("Overwriting package %s (not at head revision), will not submit"), *Filename);
return false;
}
else if (SourceControlState->IsAdded())
{
if (GetSourceControlProvider().Execute(ISourceControlOperation::Create<FRevert>(), Filename) != ECommandResult::Succeeded)
{
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Error, TEXT("Error reverting package %s from revision control"), *Filename);
return false;
}
}
else
{
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Log, TEXT("Deleting package %s from revision control"), *Filename);
if (SourceControlState->IsCheckedOut())
{
if (GetSourceControlProvider().Execute(ISourceControlOperation::Create<FRevert>(), Filename) != ECommandResult::Succeeded)
{
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Error, TEXT("Error reverting package %s from revision control"), *Filename);
return false;
}
}
if (GetSourceControlProvider().Execute(ISourceControlOperation::Create<FDelete>(), Filename) != ECommandResult::Succeeded)
{
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Error, TEXT("Error deleting package %s from revision control"), *Filename);
return false;
}
}
}
else
{
if (!IFileManager::Get().Delete(*Filename, false, true))
{
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Error, TEXT("Error deleting package %s locally"), *Filename);
return false;
}
}
}
return true;
}
int32 UConvertLevelsToExternalActorsCommandlet::Main(const FString& Params)
{
FAutoScopedDurationTimer ConversionTimer;
TArray<FString> Tokens, Switches;
ParseCommandLine(*Params, Tokens, Switches);
// Need at least the level to convert
if (Tokens.Num() < 1)
{
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Error, TEXT("ConvertLevelToExternalActors bad parameters"));
return 1;
}
bool bNoSourceControl = Switches.Contains(TEXT("nosourcecontrol"));
bool bConvertSubLevel = Switches.Contains(TEXT("convertsublevels"));
bool bRecursiveSubLevel = Switches.Contains(TEXT("recursive"));
bool bConvertToExternal = !Switches.Contains(TEXT("internal"));
bool bRepairActorFiles = Switches.Contains(TEXT("repair"));
FScopedSourceControl SourceControl;
SourceControlProvider = bNoSourceControl ? nullptr : &ISourceControlModule::Get().GetProvider();
// This will convert incomplete package name to a fully qualifed path
if (!FPackageName::SearchForPackageOnDisk(Tokens[0], &Tokens[0]))
{
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Error, TEXT("Unknown level '%s'"), *Tokens[0]);
return 1;
}
// Check external actors consistency for this level
if (!CheckExternalActors(Tokens[0], bRepairActorFiles))
{
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Error, TEXT("External actor files inconsistency"));
return 1;
}
// Load persistent level
ULevel* MainLevel = LoadLevel(Tokens[0]);
if (!MainLevel)
{
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Error, TEXT("Unable to load level '%s'"), *Tokens[0]);
return 1;
}
UWorld* MainWorld = MainLevel->GetWorld();
UPackage* MainPackage = MainLevel->GetPackage();
TSet<ULevel*> LevelsToConvert;
LevelsToConvert.Add(MainLevel);
if (bConvertSubLevel)
{
GetSubLevelsToConvert(MainLevel, LevelsToConvert, bRecursiveSubLevel);
}
bool bNeedsResaveSubLevels = false;
for(ULevel* Level : LevelsToConvert)
{
if (!Level->bContainsStableActorGUIDs)
{
bNeedsResaveSubLevels |= true;
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Error, TEXT("Unable to convert level '%s' with non-stable actor GUIDs. Resave the level before converting."), *Level->GetPackage()->GetName());
}
}
if (bNeedsResaveSubLevels)
{
return 1;
}
TArray<UPackage*> PackagesToSave;
for(ULevel* Level : LevelsToConvert)
{
Level->ConvertAllActorsToPackaging(bConvertToExternal);
UPackage* LevelPackage = Level->GetPackage();
PackagesToSave.Add(LevelPackage);
for (UPackage* ExternalPackage : Level->GetLoadedExternalObjectPackages())
{
if (!UPackage::IsEmptyPackage(ExternalPackage))
{
PackagesToSave.Add(ExternalPackage);
}
}
}
for (UPackage* PackageToSave : PackagesToSave)
{
if(!CheckoutPackage(PackageToSave))
{
return 1;
}
}
// Save packages
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Log, TEXT("Saving %d packages."), PackagesToSave.Num());
for (UPackage* PackageToSave : PackagesToSave)
{
if (!SavePackage(PackageToSave))
{
return 1;
}
}
// Add new packages to source control
for (UPackage* PackageToSave : PackagesToSave)
{
if(!AddPackageToSourceControl(PackageToSave))
{
return 1;
}
}
UE_LOG(LogConvertLevelsToExternalActorsCommandlet, Log, TEXT("Conversion took %.2f seconds"), ConversionTimer.GetTime());
return 0;
}