Files
UnrealEngine/Engine/Source/Editor/UnrealEd/Private/Commandlets/WorldPartitionBuilderCommandlet.cpp
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

366 lines
12 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Commandlets/WorldPartitionBuilderCommandlet.h"
#include "CoreMinimal.h"
#include "EngineUtils.h"
#include "EditorWorldUtils.h"
#include "FileHelpers.h"
#include "Logging/LogMacros.h"
#include "Misc/CommandLine.h"
#include "Misc/EngineVersion.h"
#include "HAL/FileManager.h"
#include "HAL/PlatformFileManager.h"
#include "ProfilingDebugging/ScopedTimers.h"
#include "WorldPartition/WorldPartition.h"
#include "WorldPartition/WorldPartitionBuilder.h"
#include "UObject/GCObjectScopeGuard.h"
#include "Trace/Trace.h"
#include "CollectionManagerModule.h"
#include "ICollectionContainer.h"
#include "ICollectionManager.h"
#include "ISourceControlModule.h"
#include "ISourceControlProvider.h"
#include "SourceControlOperations.h"
#include "AssetRegistry/AssetRegistryHelpers.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(WorldPartitionBuilderCommandlet)
DEFINE_LOG_CATEGORY_STATIC(LogWorldPartitionBuilderCommandlet, All, All);
UWorldPartitionBuilderCommandlet::UWorldPartitionBuilderCommandlet(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{}
int32 UWorldPartitionBuilderCommandlet::Main(const FString& Params)
{
FPackageSourceControlHelper PackageHelper;
// Use the commandlet parameters as it may differ from FCommandline::Get()
// Provided through this scope as most WP builders are retrieving their arguments from their
// constructors which can't receive parameters.
FWorldPartitionBuilderArgsScope BuilderArgsScope(Params);
TRACE_CPUPROFILER_EVENT_SCOPE(UWorldPartitionBuilderCommandlet::Main);
UE_SCOPED_TIMER(TEXT("Execution"), LogWorldPartitionBuilderCommandlet, Display);
TArray<FString> Tokens, Switches;
ParseCommandLine(*Params, Tokens, Switches);
if (Tokens.Num() != 1)
{
UE_LOG(LogWorldPartitionBuilderCommandlet, Error, TEXT("Missing world name"));
return 1;
}
bAutoSubmit = Switches.Contains(TEXT("AutoSubmit"));
if (bAutoSubmit)
{
if (!ISourceControlModule::Get().GetProvider().IsEnabled())
{
UE_LOG(LogWorldPartitionBuilderCommandlet, Error, TEXT("-AutoSubmit requires that a valid revision control provider is enabled, exiting..."));
return 0;
}
FParse::Value(*Params, TEXT("AutoSubmitTags="), AutoSubmitTags);
}
if (Switches.Contains(TEXT("Verbose")))
{
LogWorldPartitionBuilderCommandlet.SetVerbosity(ELogVerbosity::Verbose);
}
if (Switches.Contains(TEXT("RunningFromUnrealEd")))
{
UseCommandletResultAsExitCode = true; // The process return code will match the return code of the commandlet
FastExit = true; // Faster exit which avoids crash during shutdown. The engine isn't shutdown cleanly.
}
ICollectionManager& CollectionManager = FModuleManager::LoadModuleChecked<FCollectionManagerModule>("CollectionManager").Get();
TArray<FString> MapPackagesNames;
// Parse map name or maps collection
TSharedPtr<ICollectionContainer> CollectionContainer;
FName CollectionName;
ECollectionShareType::Type ShareType = ECollectionShareType::CST_All;
if (CollectionManager.TryParseCollectionPath(Tokens[0], &CollectionContainer, &CollectionName, &ShareType) &&
CollectionContainer->CollectionExists(CollectionName, ShareType))
{
MapPackagesNames = GatherMapsFromCollection(*CollectionContainer, CollectionName, ShareType);
if (MapPackagesNames.IsEmpty())
{
UE_LOG(LogWorldPartitionBuilderCommandlet, Warning, TEXT("Found no maps to process in collection %s, exiting"), *Tokens[0]);
return 0;
}
}
else if (Tokens[0].StartsWith(TEXT("*")))
{
IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")).Get();
AssetRegistry.WaitForCompletion();
FARFilter Filter;
Filter.bIncludeOnlyOnDiskAssets = true;
Filter.ClassPaths.Add(UWorld::StaticClass()->GetClassPathName());
TArray<FAssetData> WorldAssets;
AssetRegistry.GetAssets(Filter, WorldAssets);
Algo::Transform(WorldAssets, MapPackagesNames, [](const FAssetData& AssetData) { return AssetData.GetSoftObjectPath().ToString(); });
// Sort maps gathered from the Asset Registry in case the commandlet crashes, so we can specify *,100 to start at a specific index
MapPackagesNames.Sort();
TArray<FString> SubTokens;
Tokens[0].ParseIntoArray(SubTokens, TEXT(","));
if (SubTokens.IsValidIndex(1))
{
MapPackagesNames.RemoveAt(0, FCString::Atoi(*SubTokens[1]));
}
}
else
{
TArray<FString> MapList;
Tokens[0].ParseIntoArray(MapList, TEXT(","));
for (const FString& Map : MapList)
{
FString MapLongPackageName;
if (FPackageName::SearchForPackageOnDisk(Map, &MapLongPackageName))
{
MapPackagesNames.Add(MapLongPackageName);
}
else
{
FString PackageFilename;
if (FPackageName::TryConvertLongPackageNameToFilename(Map, PackageFilename, TEXT("")))
{
IFileManager::Get().IterateDirectoryRecursively(*PackageFilename, [&MapLongPackageName, &MapPackagesNames](const TCHAR* FilenameOrDirectory, bool bIsDirectory)
{
if (!bIsDirectory && FString(FilenameOrDirectory).EndsWith(FPackageName::GetMapPackageExtension()))
{
if (FPackageName::TryConvertFilenameToLongPackageName(FilenameOrDirectory, MapLongPackageName))
{
MapPackagesNames.Add(MapLongPackageName);
}
}
return true;
});
}
}
}
}
if (MapPackagesNames.IsEmpty())
{
UE_LOG(LogWorldPartitionBuilderCommandlet, Error, TEXT("Missing world(s) as the first argument to the commandlet. Either supply the world name directly (WorldName or /Path/To/WorldName), or provide a collection name to have the builder operate on a set of maps."));
return 1;
}
// Parse builder class name
FString BuilderClassName;
if (!FParse::Value(*Params, TEXT("Builder="), BuilderClassName, false))
{
UE_LOG(LogWorldPartitionBuilderCommandlet, Error, TEXT("Invalid builder name."));
return 1;
}
// Find builder class
TSubclassOf<UWorldPartitionBuilder> BuilderClass = FindFirstObject<UClass>(*BuilderClassName, EFindFirstObjectOptions::EnsureIfAmbiguous);
if (!BuilderClass)
{
UE_LOG(LogWorldPartitionBuilderCommandlet, Error, TEXT("Unknown/invalid world partition builder class: %s."), *BuilderClassName);
return 1;
}
// Run the builder on the provided map(s)
int32 Result = 0;
uint32 PackageIndex = 0;
const uint32 PackageCount = MapPackagesNames.Num();
for (const FString& MapPackageName : MapPackagesNames)
{
if (PackageCount > 1)
{
UE_LOG(LogWorldPartitionBuilderCommandlet, Display, TEXT("##################################################"));
UE_LOG(LogWorldPartitionBuilderCommandlet, Display, TEXT("[%d / %d] Executing %s on map %s..."), ++PackageIndex, PackageCount, *BuilderClassName, *MapPackageName);
}
if (!RunBuilder(BuilderClass, MapPackageName))
{
UE_LOG(LogWorldPartitionBuilderCommandlet, Warning, TEXT("Failed to execute %s on map %s."), *BuilderClassName, *MapPackageName);
Result = 1;
}
for (int32 AdditionalPackageIndex = 0; AdditionalPackageIndex < AdditionalWorldPackagesToProcess.Num(); AdditionalPackageIndex++)
{
const FString& AdditionalPackageName = AdditionalWorldPackagesToProcess[AdditionalPackageIndex];
UE_LOG(LogWorldPartitionBuilderCommandlet, Display, TEXT("[%d.%d / %d] Executing %s on additional map %s..."), PackageIndex, AdditionalPackageIndex + 1, PackageCount, *BuilderClassName, *AdditionalPackageName);
if (!RunBuilder(BuilderClass, AdditionalPackageName))
{
UE_LOG(LogWorldPartitionBuilderCommandlet, Warning, TEXT("Failed to execute %s on additional map %s."), *BuilderClassName, *AdditionalPackageName);
Result = 1;
}
}
AdditionalWorldPackagesToProcess.Empty();
}
// Autosubmit
if (!Result && !AutoSubmitModifiedFiles())
{
return 1;
}
return Result;
}
TArray<FString> UWorldPartitionBuilderCommandlet::GatherMapsFromCollection(ICollectionContainer& CollectionContainer, FName CollectionName, ECollectionShareType::Type ShareType) const
{
TSet<FString> MapPackagesNames;
TArray<FSoftObjectPath> AssetsPaths;
CollectionContainer.GetAssetsInCollection(CollectionName, ShareType, AssetsPaths, ECollectionRecursionFlags::SelfAndChildren);
UE_LOG(LogWorldPartitionBuilderCommandlet, Display, TEXT("Processing collection %s (%d items)"), *CollectionName.ToString(), AssetsPaths.Num());
for (FSoftObjectPath& AssetPath : AssetsPaths)
{
UAssetRegistryHelpers::FixupRedirectedAssetPath(AssetPath);
FString PackageName = AssetPath.GetLongPackageName();
if (FEditorFileUtils::IsMapPackageAsset(PackageName))
{
UE_LOG(LogWorldPartitionBuilderCommandlet, Display, TEXT("* %s"), *PackageName);
MapPackagesNames.Add(PackageName);
}
else
{
UE_LOG(LogWorldPartitionBuilderCommandlet, Log, TEXT("%s was not found or is not a map package"), *PackageName);
}
}
return MapPackagesNames.Array();
}
bool UWorldPartitionBuilderCommandlet::RunBuilder(TSubclassOf<UWorldPartitionBuilder> InBuilderClass, const FString& InWorldPackageName)
{
// This will convert incomplete package name to a fully qualified path
FString WorldLongPackageName;
FString WorldFilename;
if (!FPackageName::SearchForPackageOnDisk(InWorldPackageName, &WorldLongPackageName, &WorldFilename))
{
UE_LOG(LogWorldPartitionBuilderCommandlet, Error, TEXT("Package '%s' not found"), *InWorldPackageName);
return false;
}
// Load the world package
UPackage* WorldPackage = LoadWorldPackageForEditor(WorldLongPackageName);
if (!WorldPackage)
{
UE_LOG(LogWorldPartitionBuilderCommandlet, Error, TEXT("Couldn't load package %s."), *WorldLongPackageName);
return false;
}
// Find the world in the given package
UWorld* World = UWorld::FindWorldInPackage(WorldPackage);
if (!World)
{
UE_LOG(LogWorldPartitionBuilderCommandlet, Error, TEXT("No world in specified package %s."), *WorldLongPackageName);
return false;
}
// Load configuration file
FString WorldConfigFilename = FPackageName::LongPackageNameToFilename(World->GetPackage()->GetName(), TEXT(".ini"));
if (FPlatformFileManager::Get().GetPlatformFile().FileExists(*WorldConfigFilename))
{
LoadConfig(GetClass(), *WorldConfigFilename);
}
// Create builder instance
UWorldPartitionBuilder* Builder = NewObject<UWorldPartitionBuilder>(GetTransientPackage(), InBuilderClass);
if (!Builder)
{
UE_LOG(LogWorldPartitionBuilderCommandlet, Error, TEXT("Failed to create builder."));
return false;
}
Builder->SetModifiedFilesHandler(UWorldPartitionBuilder::FModifiedFilesHandler::CreateUObject(this, &UWorldPartitionBuilderCommandlet::OnFilesModified));
bool bResult;
{
FGCObjectScopeGuard BuilderGuard(Builder);
bResult = Builder->RunBuilder(World);
}
// Save configuration file
if (!FPlatformFileManager::Get().GetPlatformFile().FileExists(*WorldConfigFilename) ||
!FPlatformFileManager::Get().GetPlatformFile().IsReadOnly(*WorldConfigFilename))
{
SaveConfig(CPF_Config, *WorldConfigFilename);
}
TArray<FString> WorldPackages;
if (Builder->ShouldProcessAdditionalWorlds(World, WorldPackages))
{
AdditionalWorldPackagesToProcess.Append(WorldPackages);
}
return bResult;
}
bool UWorldPartitionBuilderCommandlet::OnFilesModified(const TArray<FString>& InModifiedFiles, const FString& InChangeDescription)
{
if (!InModifiedFiles.IsEmpty())
{
AutoSubmitFiles.Emplace(InChangeDescription, InModifiedFiles);
}
return true;
}
bool UWorldPartitionBuilderCommandlet::AutoSubmitModifiedFiles() const
{
bool bSucceeded = true;
if (bAutoSubmit)
{
UE_LOG(LogWorldPartitionBuilderCommandlet, Display, TEXT("Submitting changes to revision control..."));
if (!AutoSubmitFiles.IsEmpty())
{
FString AllChanges;
TArray<FString> AllModifiedFiles;
for (const auto& [Description, Files] : AutoSubmitFiles)
{
AllChanges += Description + TEXT("\n");
AllModifiedFiles.Append(Files);
}
FText ChangelistDescription = FText::FromString(FString::Printf(TEXT("%s\nBased on CL %d\n%s"), *AllChanges, FEngineVersion::Current().GetChangelist(), *AutoSubmitTags));
TSharedRef<FCheckIn, ESPMode::ThreadSafe> CheckInOperation = ISourceControlOperation::Create<FCheckIn>();
CheckInOperation->SetDescription(ChangelistDescription);
if (ISourceControlModule::Get().GetProvider().Execute(CheckInOperation, AllModifiedFiles) != ECommandResult::Succeeded)
{
UE_LOG(LogWorldPartitionBuilderCommandlet, Error, TEXT("Failed to submit changes to revision control."));
bSucceeded = false;
}
else
{
UE_LOG(LogWorldPartitionBuilderCommandlet, Display, TEXT("Submitted changes to revision control"));
}
}
else
{
UE_LOG(LogWorldPartitionBuilderCommandlet, Display, TEXT("No files to submit!"));
}
}
return bSucceeded;
}