Files
UnrealEngine/Engine/Source/Developer/CollectionManager/Private/Collection.cpp
2025-05-18 13:04:45 +08:00

1343 lines
43 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Collection.h"
#include "CollectionSettings.h"
#include "HAL/PlatformTime.h"
#include "HAL/FileManager.h"
#include "HAL/IConsoleManager.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Misc/FeedbackContext.h"
#include "Misc/ScopedSlowTask.h"
#include "CollectionManagerLog.h"
#include "ISourceControlOperation.h"
#include "SourceControlOperations.h"
#include "ISourceControlProvider.h"
#include "ISourceControlModule.h"
#include "Misc/TextFilterExpressionEvaluator.h"
#include "Misc/EngineBuildSettings.h"
#include "Misc/ScopeRWLock.h"
#include "Async/ParallelFor.h"
#include "String/ParseLines.h"
#include "ProfilingDebugging/CpuProfilerTrace.h"
#define LOCTEXT_NAMESPACE "CollectionManager"
static TAutoConsoleVariable<int32> CVarCollectionsMaxCLDescriptionPathCount(
TEXT("Collections.MaxCLDescriptionPathCount"),
1000,
TEXT("Sets the maximum number of paths reported in a changelist when checking in a collection that adds or removes entries."),
ECVF_Default);
struct FCollectionUtils
{
static void AppendCollectionToArray(const TSet<FSoftObjectPath>& InObjectSet, TArray<FSoftObjectPath>& OutObjectArray)
{
OutObjectArray.Reserve(OutObjectArray.Num() + InObjectSet.Num());
for (const FSoftObjectPath& ObjectName : InObjectSet)
{
OutObjectArray.Add(ObjectName);
}
}
};
FCollection::FCollection(const FString& InFilename, bool InUseSCC, ECollectionStorageMode::Type InStorageMode)
{
ensure(InFilename.Len() > 0);
bUseSCC = InUseSCC;
SourceFilename = InFilename;
CollectionName = FName(*FPaths::GetBaseFilename(InFilename));
StorageMode = InStorageMode;
CollectionGuid = FGuid::NewGuid();
// Initialize the file version to the most recent
FileVersion = ECollectionVersion::CurrentVersion;
}
TSharedRef<FCollection> FCollection::Clone(const FString& InFilename, bool InUseSCC, ECollectionCloneMode InCloneMode) const
{
TSharedRef<FCollection> NewCollection = MakeShareable(new FCollection(*this));
// Set the new collection name and path
NewCollection->bUseSCC = InUseSCC;
NewCollection->SourceFilename = InFilename;
NewCollection->CollectionName = FName(*FPaths::GetBaseFilename(InFilename));
NewCollection->StorageMode = StorageMode;
// Create a new GUID?
if (InCloneMode == ECollectionCloneMode::Unique)
{
NewCollection->CollectionGuid = FGuid::NewGuid();
}
return NewCollection;
}
static bool ParseUtf8Bom(FMutableMemoryView& Buffer)
{
const uint64 Size = Buffer.GetSize();
const uint8* const Bytes = static_cast<const uint8*>(Buffer.GetData());
if (!(Size & 1) && Size >= 2 && Bytes[0] == 0xff && Bytes[1] == 0xfe)
{
// Unicode Intel byte order BOM, leave it to be consumed by FFileHelper::BufferToString
return false;
}
if (!(Size & 1) && Size >= 2 && Bytes[0] == 0xfe && Bytes[1] == 0xff)
{
// Unicode non-Intel byte order BOM, leave it to be consumed by FFileHelper::BufferToString
return false;
}
if (Size >= 3 && Bytes[0] == 0xef && Bytes[1] == 0xbb && Bytes[2] == 0xbf)
{
// Utf8 BOM, consume it and treat buffer as Utf8 without BOM
Buffer += 3;
}
// Assume Utf8 without BOM
return true;
}
bool FCollection::Load(FText& OutError)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FCollection::Load)
Empty();
TArray<uint8> Buffer;
if (!FFileHelper::LoadFileToArray(Buffer, *SourceFilename))
{
OutError = FText::Format(LOCTEXT("LoadError_FailedToLoadFile", "Failed to load the collection '{0}' from disk."), FText::FromString(SourceFilename));
return false;
}
FMutableMemoryView BufferView(Buffer.GetData(), Buffer.Num());
if (ParseUtf8Bom(BufferView))
{
FUtf8StringView FullFileContentsString(
reinterpret_cast<const UTF8CHAR*>(BufferView.GetData()), IntCastChecked<int32>(BufferView.GetSize()));
return ParseCollection(FullFileContentsString, OutError);
}
else
{
FString FullFileContentsString;
FFileHelper::BufferToString(FullFileContentsString, Buffer.GetData(), Buffer.Num());
Buffer.Reset();
return ParseCollection(FStringView(FullFileContentsString), OutError);
}
}
template <typename CharType>
bool FCollection::ParseCollection(TStringView<CharType> FullFileContentsString, FText& OutError)
{
// Reserve an initial estimate and parse all lines
TArray<TStringView<CharType>> FileContents;
FileContents.Reserve(FullFileContentsString.Len() / 64);
UE::String::ParseLines(FullFileContentsString, [&FileContents](const TStringView<CharType>& Line) { FileContents.Add(Line); });
if (FileContents.Num() == 0)
{
// Empty file, assume static collection with no items
return true;
}
// Load the header from the contents array
TMap<FString, FString> HeaderPairs;
int32 LineIndex = 0;
for (int32 Num = FileContents.Num(); LineIndex < Num; ++LineIndex)
{
TStringView<CharType> Line(FileContents[LineIndex]);
Line.TrimStartAndEndInline();
if (Line.Len() == 0)
{
// Empty line. Done reading headers.
++LineIndex;
break;
}
int32 Offset;
if (Line.FindChar(CharType(':'), Offset))
{
FString Key(Line.Left(Offset));
FString Value(Line.Right(Line.Len() - Offset - 1));
HeaderPairs.Emplace(MoveTemp(Key), MoveTemp(Value));
}
}
// Now process the header pairs to prepare and validate this collection
if ( !LoadHeaderPairs(HeaderPairs) )
{
// Bad header
OutError = FText::Format(LOCTEXT("LoadError_BadHeader", "The collection file '{0}' contains a bad header and could not be loaded."), FText::FromString(SourceFilename));
return false;
}
// Now load the content if the header load was successful
if (StorageMode == ECollectionStorageMode::Static)
{
if (const int32 NamesNum = FileContents.Num() - LineIndex; NamesNum > 0)
{
struct FItem
{
FSoftObjectPath Path;
int32 Hash;
};
TArray<FItem> Paths;
Paths.SetNum(NamesNum);
typedef decltype(ObjectSet)::KeyFuncsType KeyFuncsType;
// Name hashing to register new FName takes time
// Process as much as possible in multiple threads
TStringView<CharType>* Lines = &FileContents[LineIndex];
ParallelFor(
NamesNum,
[&Lines, &Paths](int32 LocalLineIndex)
{
Paths[LocalLineIndex].Path = FSoftObjectPath::ConstructFromStringPath(Lines[LocalLineIndex].TrimStartAndEnd());
Paths[LocalLineIndex].Hash = KeyFuncsType::GetKeyHash(KeyFuncsType::GetSetKey(Paths[LocalLineIndex].Path));
},
// Do not pay for scheduling cost if number of items is too low
NamesNum < 1000 ? EParallelForFlags::ForceSingleThread : EParallelForFlags::None
);
ObjectSet.Reserve(ObjectSet.Num() + Paths.Num());
// Static collection, a flat list of asset paths
for (FItem& Item : Paths)
{
if (Item.Path.IsNull())
{
continue;
}
ObjectSet.EmplaceByHash(Item.Hash, MoveTemp(Item.Path));
}
}
}
else
{
// Dynamic collection, a single query line
DynamicQueryText = (FileContents.Num() > LineIndex) ? FString(FileContents[LineIndex].TrimStartAndEnd()) : FString();
}
DiskSnapshot.TakeSnapshot(*this);
bChangedSinceLastDiskSnapshot = false;
return true;
}
bool FCollection::Save(const TArray<FText>& AdditionalChangelistText, FText& OutError, bool bForceCommitToRevisionControl)
{
if ( !ensure(SourceFilename.Len()) )
{
OutError = LOCTEXT("Error_Internal", "There was an internal error.");
return false;
}
// Store the start time for profiling reasons
double SaveStartTime = FPlatformTime::Seconds();
// Keep track of save progress to update the slow task dialog
const int32 SaveProgressDenominator = 3;
int32 SaveProgressNumerator = 0;
GWarn->BeginSlowTask( FText::Format( LOCTEXT("SavingCollection", "Saving Collection {0}"), FText::FromName( CollectionName ) ), true);
GWarn->UpdateProgress(SaveProgressNumerator++, SaveProgressDenominator);
if ( bUseSCC )
{
// Checkout the file
if ( !CheckoutCollection(OutError) )
{
UE_LOG(LogCollectionManager, Error, TEXT("Failed to check out a collection file: %s"), *CollectionName.ToString());
GWarn->EndSlowTask();
return false;
}
}
GWarn->UpdateProgress(SaveProgressNumerator++, SaveProgressDenominator);
// Generate a string with the file contents
FString FileOutput;
// Start with the header
TMap<FString,FString> HeaderPairs;
SaveHeaderPairs(HeaderPairs);
for (const auto& HeaderPair : HeaderPairs)
{
FileOutput += HeaderPair.Key + TEXT(":") + HeaderPair.Value + LINE_TERMINATOR;
}
FileOutput += LINE_TERMINATOR;
// Now for the content
if (StorageMode == ECollectionStorageMode::Static)
{
// Write out the set as a sorted array to keep things in a known order for diffing
TArray<FSoftObjectPath> ObjectList = ObjectSet.Array();
ObjectList.Sort([](FSoftObjectPath A, FSoftObjectPath B){ return A.LexicalLess(B); });
// Static collection. Save a flat list of all objects in the collection.
for (const FSoftObjectPath& ObjectName : ObjectList)
{
FileOutput += ObjectName.ToString() + LINE_TERMINATOR;
}
}
else
{
// Dynamic collection, a single query line
FileOutput += DynamicQueryText + LINE_TERMINATOR;
}
// Attempt to save the file
bool bSaveSuccessful = false;
if ( ensure(FileOutput.Len()) )
{
// We have some output, write it to file
if ( FFileHelper::SaveStringToFile(FileOutput, *SourceFilename) )
{
bSaveSuccessful = true;
}
else
{
OutError = FText::Format(LOCTEXT("Error_WriteFailed", "Failed to write to collection file: {0}"), FText::FromString(SourceFilename));
UE_LOG(LogCollectionManager, Error, TEXT("%s"), *OutError.ToString());
}
}
else
{
OutError = LOCTEXT("Error_Internal", "There was an internal error.");
}
GWarn->UpdateProgress(SaveProgressNumerator++, SaveProgressDenominator);
if ( bSaveSuccessful )
{
if ( bUseSCC && (bForceCommitToRevisionControl || GetDefault<UCollectionSettings>()->bAutoCommitOnSave))
{
// Check in the file if the save was successful
if ( bSaveSuccessful )
{
if ( !CheckinCollection(AdditionalChangelistText, OutError) )
{
UE_LOG(LogCollectionManager, Error, TEXT("Failed to check in a collection successfully saving: %s"), *CollectionName.ToString());
bSaveSuccessful = false;
}
}
// If the save was not successful or the checkin failed, revert
if ( !bSaveSuccessful )
{
FText Unused;
if ( !RevertCollection(Unused) )
{
// The revert failed... file will be left on disk as it was saved.
// DiskAssetList will still hold the version of the file when this collection was last loaded or saved successfully so nothing will be out of sync.
// If the user closes the editor before successfully saving, this file may not be exactly what was seen at the time the editor closed.
UE_LOG(LogCollectionManager, Warning, TEXT("Failed to revert a checked out collection after failing to save or checkin: %s"), *CollectionName.ToString());
}
}
}
}
GWarn->UpdateProgress(SaveProgressNumerator++, SaveProgressDenominator);
if ( bSaveSuccessful )
{
// Files are always saved at the latest version as loading should take care of data upgrades
FileVersion = ECollectionVersion::CurrentVersion;
DiskSnapshot.TakeSnapshot(*this);
bChangedSinceLastDiskSnapshot = false;
}
GWarn->EndSlowTask();
UE_LOG(LogCollectionManager, Verbose, TEXT("Saved collection %s in %0.6f seconds"), *CollectionName.ToString(), FPlatformTime::Seconds() - SaveStartTime);
return bSaveSuccessful;
}
bool FCollection::Update(FText& OutError)
{
if ( !ensure(SourceFilename.Len()) )
{
OutError = LOCTEXT("Error_Internal", "There was an internal error.");
return false;
}
if ( !bUseSCC )
{
// Not under SCC control, so already up-to-date
return true;
}
FScopedSlowTask SlowTask(1.0f, FText::Format(LOCTEXT("UpdatingCollection", "Updating Collection {0}"), FText::FromName(CollectionName )));
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
if ( !ISourceControlModule::Get().IsEnabled() )
{
OutError = LOCTEXT("Error_SCCDisabled", "Revision control is not enabled. Enable revision control in the preferences menu.");
return false;
}
if ( !SourceControlProvider.IsAvailable() )
{
OutError = LOCTEXT("Error_SCCNotAvailable", "Revision control is currently not available. Check your connection and try again.");
return false;
}
const FString AbsoluteFilename = FPaths::ConvertRelativePathToFull(SourceFilename);
FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(AbsoluteFilename, EStateCacheUsage::ForceUpdate);
// If not at the head revision, sync up
if (SourceControlState.IsValid() && !SourceControlState->IsCurrent())
{
if ( SourceControlProvider.Execute(ISourceControlOperation::Create<FSync>(), AbsoluteFilename) == ECommandResult::Failed )
{
// Could not sync up with the head revision
OutError = FText::Format(LOCTEXT("Error_SCCSync", "Failed to sync collection '{0}' to the head revision."), FText::FromName(CollectionName));
return false;
}
// Check to see if the file exists at the head revision
if ( IFileManager::Get().FileExists(*SourceFilename) )
{
// File found! Load it and merge with our local changes
FText LoadErrorText;
FCollection NewCollection(SourceFilename, false, ECollectionStorageMode::Static);
if ( !NewCollection.Load(LoadErrorText) )
{
// Failed to load the head revision file so it isn't safe to delete it
OutError = FText::Format(LOCTEXT("Error_SCCBadHead", "Failed to load the collection '{0}' at the head revision. {1}"), FText::FromName(CollectionName), LoadErrorText);
return false;
}
// Loaded the head revision, now merge up so the files are in a consistent state
MergeWithCollection(NewCollection);
}
// Make sure we get a fresh state from the server
SourceControlState = SourceControlProvider.GetState(AbsoluteFilename, EStateCacheUsage::ForceUpdate);
// Got an updated version?
if (SourceControlState.IsValid() && !SourceControlState->IsCurrent())
{
OutError = FText::Format(LOCTEXT("Error_SCCNotCurrent", "Collection '{0}' is not at head revision after sync."), FText::FromName(CollectionName));
return false;
}
}
return true;
}
bool FCollection::Merge(const FCollection& NewCollection)
{
return MergeWithCollection(NewCollection);
}
bool FCollection::DeleteSourceFile(FText& OutError)
{
bool bSuccessfullyDeleted = false;
if ( SourceFilename.Len() )
{
if ( bUseSCC )
{
bSuccessfullyDeleted = DeleteFromSourceControl(OutError);
}
else
{
bSuccessfullyDeleted = IFileManager::Get().Delete(*SourceFilename);
if ( !bSuccessfullyDeleted )
{
OutError = FText::Format(LOCTEXT("Error_DiskDeleteFailed", "Failed to delete the collection file: {0}"), FText::FromString(SourceFilename));
}
}
}
else
{
// No source file. Since it doesn't exist we will say it is deleted.
bSuccessfullyDeleted = true;
}
if ( bSuccessfullyDeleted )
{
DiskSnapshot = FCollectionSnapshot();
bChangedSinceLastDiskSnapshot = (ObjectSet.Num() == 0);
}
return bSuccessfullyDeleted;
}
void FCollection::Empty()
{
ObjectSet.Reset();
DynamicQueryText.Reset();
DynamicQueryExpressionEvaluatorPtr.Reset();
DiskSnapshot.TakeSnapshot(*this);
bChangedSinceLastDiskSnapshot = false;
}
bool FCollection::AddObjectToCollection(const FSoftObjectPath& ObjectPath)
{
if (ObjectPath.IsNull())
{
return false;
}
if (StorageMode == ECollectionStorageMode::Static)
{
bool bAlreadyInSet = false;
ObjectSet.Add(ObjectPath, &bAlreadyInSet);
bChangedSinceLastDiskSnapshot |= !bAlreadyInSet;
return !bAlreadyInSet;
}
return false;
}
bool FCollection::RemoveObjectFromCollection(const FSoftObjectPath& ObjectPath)
{
if (ObjectPath.IsNull())
{
return false;
}
if (StorageMode == ECollectionStorageMode::Static && ObjectSet.Remove(ObjectPath) > 0)
{
bChangedSinceLastDiskSnapshot = true;
return true;
}
return false;
}
void FCollection::GetAssetsInCollection(TArray<FSoftObjectPath>& Assets) const
{
if (StorageMode == ECollectionStorageMode::Static)
{
for (const FSoftObjectPath& ObjectName : ObjectSet)
{
if (!ObjectName.GetLongPackageName().StartsWith(TEXT("/Script/")))
{
Assets.Add(ObjectName);
}
}
}
}
void FCollection::GetClassesInCollection(TArray<FTopLevelAssetPath>& Classes) const
{
if (StorageMode == ECollectionStorageMode::Static)
{
for (const FSoftObjectPath& ObjectName : ObjectSet)
{
if (ObjectName.GetLongPackageName().StartsWith(TEXT("/Script/")))
{
Classes.Add(ObjectName.GetAssetPath());
}
}
}
}
void FCollection::GetObjectsInCollection(TArray<FSoftObjectPath>& Objects) const
{
if (StorageMode == ECollectionStorageMode::Static)
{
FCollectionUtils::AppendCollectionToArray(ObjectSet, Objects);
}
}
bool FCollection::IsObjectInCollection(const FSoftObjectPath& ObjectPath) const
{
if (StorageMode == ECollectionStorageMode::Static)
{
return ObjectSet.Contains(ObjectPath);
}
return false;
}
bool FCollection::IsRedirectorInCollection(const FSoftObjectPath& ObjectPath) const
{
if (StorageMode == ECollectionStorageMode::Static)
{
// Redirectors are fixed up in-memory once the asset registry has finished loading,
// so we need to test our on-disk set of objects rather than our in-memory set of objects
return DiskSnapshot.ObjectSet.Contains(ObjectPath);
}
return false;
}
bool FCollection::SetDynamicQueryText(const FString& InQueryText)
{
if (StorageMode == ECollectionStorageMode::Dynamic)
{
DynamicQueryText = InQueryText;
return true;
}
return false;
}
FString FCollection::GetDynamicQueryText() const
{
return (StorageMode == ECollectionStorageMode::Dynamic) ? DynamicQueryText : FString();
}
void FCollection::PrepareDynamicQuery()
{
if (StorageMode == ECollectionStorageMode::Dynamic)
{
UE::TUniqueLock Lock(*DynamicQueryExpressionEvaluatorLock);
if (!DynamicQueryExpressionEvaluatorPtr.IsValid())
{
DynamicQueryExpressionEvaluatorPtr = MakeShareable(new FTextFilterExpressionEvaluator(ETextFilterExpressionEvaluatorMode::Complex));
}
if (!DynamicQueryExpressionEvaluatorPtr->GetFilterText().ToString().Equals(DynamicQueryText, ESearchCase::CaseSensitive))
{
DynamicQueryExpressionEvaluatorPtr->SetFilterText(FText::FromString(DynamicQueryText));
}
}
}
bool FCollection::TestDynamicQuery(const ITextFilterExpressionContext& InContext) const
{
if (StorageMode == ECollectionStorageMode::Dynamic)
{
return DynamicQueryExpressionEvaluatorPtr->TestTextFilter(InContext);
}
return false;
}
FCollectionStatusInfo FCollection::GetStatusInfo(bool bIsReadOnly) const
{
TRACE_CPUPROFILER_EVENT_SCOPE(FCollection::GetStatusInfo);
FCollectionStatusInfo StatusInfo;
StatusInfo.bIsDirty = !bIsReadOnly && IsDirty();
StatusInfo.bIsEmpty = IsEmpty();
StatusInfo.bUseSCC = !bIsReadOnly && bUseSCC;
StatusInfo.NumObjects = ObjectSet.Num();
if (StatusInfo.bUseSCC && ISourceControlModule::Get().IsEnabled())
{
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
if (SourceControlProvider.IsAvailable())
{
const FString AbsoluteFilename = FPaths::ConvertRelativePathToFull(SourceFilename);
StatusInfo.SCCState = SourceControlProvider.GetState(AbsoluteFilename, EStateCacheUsage::Use);
}
}
return StatusInfo;
}
bool FCollection::IsDirty() const
{
TRACE_CPUPROFILER_EVENT_SCOPE(FCollection::IsDirty);
if (ParentCollectionGuid != DiskSnapshot.ParentCollectionGuid)
{
return true;
}
if (CollectionColor != DiskSnapshot.CollectionColor)
{
return true;
}
if (StorageMode == ECollectionStorageMode::Static)
{
return bChangedSinceLastDiskSnapshot;
}
else
{
return DynamicQueryText != DiskSnapshot.DynamicQueryText;
}
}
bool FCollection::IsEmpty() const
{
if (StorageMode == ECollectionStorageMode::Static)
{
return ObjectSet.Num() == 0;
}
else
{
return DynamicQueryText.IsEmpty();
}
}
void FCollection::PrintCollection() const
{
if (StorageMode == ECollectionStorageMode::Static)
{
UE_LOG(LogCollectionManager, Log, TEXT(" Printing static elements of collection %s"), *CollectionName.ToString());
UE_LOG(LogCollectionManager, Log, TEXT(" ============================="));
// Print the set as a sorted array to keep things in a sane order
TArray<FSoftObjectPath> ObjectList = ObjectSet.Array();
ObjectList.Sort([](const FSoftObjectPath& A, const FSoftObjectPath& B){ return A.LexicalLess(B); });
for (const FSoftObjectPath& ObjectName : ObjectList)
{
UE_LOG(LogCollectionManager, Log, TEXT(" %s"), *ObjectName.ToString());
}
}
else
{
UE_LOG(LogCollectionManager, Log, TEXT(" Printing dynamic query of collection %s"), *CollectionName.ToString());
UE_LOG(LogCollectionManager, Log, TEXT(" ============================="));
UE_LOG(LogCollectionManager, Log, TEXT(" %s"), *DynamicQueryText);
}
}
void FCollection::SaveHeaderPairs(TMap<FString,FString>& OutHeaderPairs) const
{
// These pairs will appear at the top of the file followed by a newline
OutHeaderPairs.Add(TEXT("FileVersion"), FString::FromInt(ECollectionVersion::CurrentVersion)); // Files are always saved at the latest version as loading should take care of data upgrades
OutHeaderPairs.Add(TEXT("Type"), ECollectionStorageMode::ToString(StorageMode));
OutHeaderPairs.Add(TEXT("Guid"), CollectionGuid.ToString(EGuidFormats::DigitsWithHyphens));
OutHeaderPairs.Add(TEXT("ParentGuid"), ParentCollectionGuid.ToString(EGuidFormats::DigitsWithHyphens));
if (CollectionColor)
{
OutHeaderPairs.Add(TEXT("Color"), CollectionColor->ToString());
}
}
bool FCollection::LoadHeaderPairs(const TMap<FString,FString>& InHeaderPairs)
{
// These pairs will appeared at the top of the file being loaded
// First find all the known pairs
const FString* Version = InHeaderPairs.Find(TEXT("FileVersion"));
if ( !Version )
{
// FileVersion is required
return false;
}
const FString* Type = InHeaderPairs.Find(TEXT("Type"));
if ( !Type )
{
// Type is required
return false;
}
StorageMode = ECollectionStorageMode::FromString(**Type);
FileVersion = (ECollectionVersion::Type)FCString::Atoi(**Version);
if (FileVersion >= ECollectionVersion::AddedCollectionGuid)
{
const FString* GuidStr = InHeaderPairs.Find(TEXT("Guid"));
if ( !GuidStr || !FGuid::Parse(*GuidStr, CollectionGuid) )
{
// Guid is required
return false;
}
const FString* ParentGuidStr = InHeaderPairs.Find(TEXT("ParentGuid"));
if ( !ParentGuidStr || !FGuid::Parse(*ParentGuidStr, ParentCollectionGuid) )
{
ParentCollectionGuid = FGuid();
}
}
// Load the optional color
CollectionColor.Reset();
if (const FString* ColorStr = InHeaderPairs.Find(TEXT("Color")))
{
FLinearColor NewColor;
if (NewColor.InitFromString(*ColorStr))
{
CollectionColor = MoveTemp(NewColor);
}
}
return FileVersion > 0 && FileVersion <= ECollectionVersion::CurrentVersion;
}
bool FCollection::MergeWithCollection(const FCollection& Other)
{
bool bHasChanges = ParentCollectionGuid != Other.ParentCollectionGuid;
ParentCollectionGuid = Other.ParentCollectionGuid;
if (StorageMode != Other.StorageMode)
{
bHasChanges = true;
StorageMode = Other.StorageMode;
// Storage mode has changed! Empty the collection so we just copy over the new data verbatim
Empty();
}
if (StorageMode == ECollectionStorageMode::Static)
{
// Work out whether we have any changes compared to the other collection
TArray<FSoftObjectPath> ObjectsAdded;
TArray<FSoftObjectPath> ObjectsRemoved;
GetObjectDifferences(ObjectSet, Other.ObjectSet, ObjectsAdded, ObjectsRemoved);
bHasChanges = bHasChanges || ObjectsAdded.Num() > 0 || ObjectsRemoved.Num() > 0;
if (bHasChanges)
{
// Gather the differences from the file on disk
ObjectsAdded.Reset();
ObjectsRemoved.Reset();
GetObjectDifferencesFromDisk(ObjectsAdded, ObjectsRemoved);
// Copy asset list from other collection
ObjectSet = Other.ObjectSet;
// Add the objects that were added before the merge
for (const FSoftObjectPath& AddedObjectName : ObjectsAdded)
{
ObjectSet.Add(AddedObjectName);
}
// Remove the objects that were removed before the merge
for (const FSoftObjectPath& RemovedObjectName : ObjectsRemoved)
{
ObjectSet.Remove(RemovedObjectName);
}
bChangedSinceLastDiskSnapshot = true;
}
}
else
{
bHasChanges = bHasChanges || DynamicQueryText != Other.DynamicQueryText;
DynamicQueryText = Other.DynamicQueryText;
}
DiskSnapshot = Other.DiskSnapshot;
return bHasChanges;
}
void FCollection::GetObjectDifferences(const TSet<FSoftObjectPath>& BaseSet, const TSet<FSoftObjectPath>& NewSet, TArray<FSoftObjectPath>& ObjectsAdded, TArray<FSoftObjectPath>& ObjectsRemoved)
{
// Find the objects that were removed compared to the base set
for (const FSoftObjectPath& BaseObjectName : BaseSet)
{
if (!NewSet.Contains(BaseObjectName))
{
ObjectsRemoved.Add(BaseObjectName);
}
}
// If both sets have the same number of items and nothing has been removed
// we can safely infer that both collections are equals without going
// over them a second time.
if (ObjectsRemoved.Num() == 0 && BaseSet.Num() == NewSet.Num())
{
return;
}
// Find the objects that were added compare to the base set
for (const FSoftObjectPath& NewObjectName : NewSet)
{
if (!BaseSet.Contains(NewObjectName))
{
ObjectsAdded.Add(NewObjectName);
}
}
}
void FCollection::GetObjectDifferencesFromDisk(TArray<FSoftObjectPath>& ObjectsAdded, TArray<FSoftObjectPath>& ObjectsRemoved) const
{
if (StorageMode == ECollectionStorageMode::Static)
{
GetObjectDifferences(DiskSnapshot.ObjectSet, ObjectSet, ObjectsAdded, ObjectsRemoved);
}
}
bool FCollection::CheckoutCollection(FText& OutError)
{
if ( !ensure(SourceFilename.Len()) )
{
OutError = LOCTEXT("Error_Internal", "There was an internal error.");
return false;
}
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
if ( !ISourceControlModule::Get().IsEnabled() )
{
OutError = LOCTEXT("Error_SCCDisabled", "Revision control is not enabled. Enable revision control in the preferences menu.");
return false;
}
if ( !SourceControlProvider.IsAvailable() )
{
OutError = LOCTEXT("Error_SCCNotAvailable", "Revision control is currently not available. Check your connection and try again.");
return false;
}
const FString AbsoluteFilename = FPaths::ConvertRelativePathToFull(SourceFilename);
FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(AbsoluteFilename, EStateCacheUsage::ForceUpdate);
bool bSuccessfullyCheckedOut = false;
if (SourceControlState.IsValid() && SourceControlState->IsDeleted())
{
// Revert our delete
if ( !RevertCollection(OutError) )
{
return false;
}
// Make sure we get a fresh state from the server
SourceControlState = SourceControlProvider.GetState(AbsoluteFilename, EStateCacheUsage::ForceUpdate);
}
// If not at the head revision, sync up
if (SourceControlState.IsValid() && !SourceControlState->IsCurrent())
{
if ( SourceControlProvider.Execute(ISourceControlOperation::Create<FSync>(), AbsoluteFilename) == ECommandResult::Failed )
{
// Could not sync up with the head revision
OutError = FText::Format(LOCTEXT("Error_SCCSync", "Failed to sync collection '{0}' to the head revision."), FText::FromName(CollectionName));
return false;
}
// Check to see if the file exists at the head revision
if ( IFileManager::Get().FileExists(*SourceFilename) )
{
// File found! Load it and merge with our local changes
FText LoadErrorText;
FCollection NewCollection(SourceFilename, false, ECollectionStorageMode::Static);
if ( !NewCollection.Load(LoadErrorText) )
{
// Failed to load the head revision file so it isn't safe to delete it
OutError = FText::Format(LOCTEXT("Error_SCCBadHead", "Failed to load the collection '{0}' at the head revision. {1}"), FText::FromName(CollectionName), LoadErrorText);
return false;
}
// Loaded the head revision, now merge up so the files are in a consistent state
MergeWithCollection(NewCollection);
}
// Make sure we get a fresh state from the server
SourceControlState = SourceControlProvider.GetState(AbsoluteFilename, EStateCacheUsage::ForceUpdate);
}
if(SourceControlState.IsValid())
{
if(!SourceControlState->IsSourceControlled())
{
// Not yet in the depot. We'll add it when we call CheckinCollection
bSuccessfullyCheckedOut = true;
}
else if(SourceControlState->IsAdded() || SourceControlState->IsCheckedOut())
{
// Already checked out or opened for add
bSuccessfullyCheckedOut = true;
}
else if(SourceControlState->CanCheckout())
{
// In depot and needs to be checked out
bSuccessfullyCheckedOut = (SourceControlProvider.Execute(ISourceControlOperation::Create<FCheckOut>(), AbsoluteFilename) == ECommandResult::Succeeded);
if (!bSuccessfullyCheckedOut)
{
OutError = FText::Format(LOCTEXT("Error_SCCCheckout", "Failed to check out collection '{0}'"), FText::FromName(CollectionName));
}
}
else if(!SourceControlState->IsCurrent())
{
OutError = FText::Format(LOCTEXT("Error_SCCNotCurrent", "Collection '{0}' is not at head revision after sync."), FText::FromName(CollectionName));
}
else if(SourceControlState->IsCheckedOutOther())
{
OutError = FText::Format(LOCTEXT("Error_SCCCheckedOutOther", "Collection '{0}' is checked out by another user."), FText::FromName(CollectionName));
}
else
{
OutError = FText::Format(LOCTEXT("Error_SCCUnknown", "Could not determine revision control state for collection '{0}'"), FText::FromName(CollectionName));
}
}
else
{
OutError = LOCTEXT("Error_SCCInvalid", "Revision control state is invalid.");
}
return bSuccessfullyCheckedOut;
}
bool FCollection::CheckinCollection(const TArray<FText>& AdditionalChangelistText, FText& OutError)
{
if ( !ensure(SourceFilename.Len()) )
{
OutError = LOCTEXT("Error_Internal", "There was an internal error.");
return false;
}
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
if ( !ISourceControlModule::Get().IsEnabled() )
{
OutError = LOCTEXT("Error_SCCDisabled", "Revision control is not enabled. Enable revision control in the preferences menu.");
return false;
}
if ( !SourceControlProvider.IsAvailable() )
{
OutError = LOCTEXT("Error_SCCNotAvailable", "Revision control is currently not available. Check your connection and try again.");
return false;
}
const FString AbsoluteFilename = FPaths::ConvertRelativePathToFull(SourceFilename);
FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(AbsoluteFilename, EStateCacheUsage::ForceUpdate);
if (SourceControlState.IsValid() && !SourceControlState->IsSourceControlled())
{
// Not yet in the depot. Add it.
const bool bWasAdded = (SourceControlProvider.Execute(ISourceControlOperation::Create<FMarkForAdd>(), AbsoluteFilename) == ECommandResult::Succeeded);
if (!bWasAdded)
{
OutError = FText::Format(LOCTEXT("Error_SCCAdd", "Failed to add collection '{0}' to revision control."), FText::FromName(CollectionName));
return false;
}
SourceControlState = SourceControlProvider.GetState(AbsoluteFilename, EStateCacheUsage::ForceUpdate);
}
if ( SourceControlState.IsValid() && !(SourceControlState->IsCheckedOut() || SourceControlState->IsAdded()) )
{
OutError = FText::Format(LOCTEXT("Error_SCCNotCheckedOut", "Collection '{0}' not checked out or open for add."), FText::FromName(CollectionName));
return false;
}
// Form an appropriate summary for the changelist
const FText CollectionNameText = FText::FromName( CollectionName );
FTextBuilder ChangelistDescBuilder;
if (SourceControlState.IsValid() && SourceControlState->IsAdded())
{
ChangelistDescBuilder.AppendLineFormat(LOCTEXT("CollectionAddedNewDesc", "Added collection '{0}'"), CollectionNameText);
}
else
{
if (StorageMode == ECollectionStorageMode::Static)
{
auto AddFileListToDescription = [&ChangelistDescBuilder](const TArray<FSoftObjectPath>& Paths)
{
const int32 MaxPaths = CVarCollectionsMaxCLDescriptionPathCount.GetValueOnAnyThread();
const int32 ReportedPaths = FMath::Min(Paths.Num(), MaxPaths);
const int32 UnreportedPaths = FMath::Max(0, Paths.Num() - MaxPaths);
for (int32 PathIdx = 0; PathIdx < ReportedPaths; ++PathIdx)
{
const FSoftObjectPath& AddedObjectName = Paths[PathIdx];
ChangelistDescBuilder.AppendLine(FText::FromString(AddedObjectName.ToString()));
}
if (UnreportedPaths > 0)
{
ChangelistDescBuilder.AppendLineFormat(LOCTEXT("CollectionUnreportedPathsDesc", "... {0} more path(s)"), UnreportedPaths);
}
};
// Gather differences from disk
TArray<FSoftObjectPath> ObjectsAdded;
TArray<FSoftObjectPath> ObjectsRemoved;
GetObjectDifferencesFromDisk(ObjectsAdded, ObjectsRemoved);
ObjectsAdded.Sort([](FSoftObjectPath A, FSoftObjectPath B){ return A.LexicalLess(B); });
ObjectsRemoved.Sort([](FSoftObjectPath A, FSoftObjectPath B) { return A.LexicalLess(B); });
// Report added files
FFormatNamedArguments Args;
Args.Add(TEXT("FirstObjectAdded"), ObjectsAdded.Num() > 0 ? FText::FromString(ObjectsAdded[0].ToString()) : NSLOCTEXT("Core", "None", "None"));
Args.Add(TEXT("NumberAdded"), FText::AsNumber(ObjectsAdded.Num()));
Args.Add(TEXT("FirstObjectRemoved"), ObjectsRemoved.Num() > 0 ? FText::FromString(ObjectsRemoved[0].ToString()) : NSLOCTEXT("Core", "None", "None"));
Args.Add(TEXT("NumberRemoved"), FText::AsNumber(ObjectsRemoved.Num()));
Args.Add(TEXT("CollectionName"), CollectionNameText);
if (ObjectsAdded.Num() == 1)
{
ChangelistDescBuilder.AppendLineFormat(LOCTEXT("CollectionAddedSingleDesc", "Added '{FirstObjectAdded}' to collection '{CollectionName}'"), Args);
}
else if (ObjectsAdded.Num() > 1)
{
ChangelistDescBuilder.AppendLineFormat(LOCTEXT("CollectionAddedMultipleDesc", "Added {NumberAdded} objects to collection '{CollectionName}':"), Args);
ChangelistDescBuilder.Indent();
AddFileListToDescription(ObjectsAdded);
ChangelistDescBuilder.Unindent();
}
if ( ObjectsRemoved.Num() == 1 )
{
ChangelistDescBuilder.AppendLineFormat(LOCTEXT("CollectionRemovedSingleDesc", "Removed '{FirstObjectRemoved}' from collection '{CollectionName}'"), Args);
}
else if (ObjectsRemoved.Num() > 1)
{
ChangelistDescBuilder.AppendLineFormat(LOCTEXT("CollectionRemovedMultipleDesc", "Removed {NumberRemoved} objects from collection '{CollectionName}'"), Args);
ChangelistDescBuilder.Indent();
AddFileListToDescription(ObjectsRemoved);
ChangelistDescBuilder.Unindent();
}
}
else
{
if (DiskSnapshot.DynamicQueryText != DynamicQueryText)
{
ChangelistDescBuilder.AppendLineFormat(LOCTEXT("CollectionChangedDynamicQueryDesc", "Changed the dynamic query of collection '{0}' to '{1}'"), CollectionNameText, FText::FromString(DynamicQueryText));
}
}
// Parent change?
if (DiskSnapshot.ParentCollectionGuid != ParentCollectionGuid)
{
ChangelistDescBuilder.AppendLineFormat(LOCTEXT("CollectionChangedParentDesc", "Changed the parent of collection '{0}'"), CollectionNameText);
}
// Color change?
if (DiskSnapshot.CollectionColor != CollectionColor)
{
ChangelistDescBuilder.AppendLineFormat(LOCTEXT("CollectionChangedColorDesc", "Changed the color of collection '{0}'"), CollectionNameText);
}
// Version bump?
if (FileVersion < ECollectionVersion::CurrentVersion)
{
ChangelistDescBuilder.AppendLineFormat(LOCTEXT("CollectionUpgradedDesc", "Upgraded collection '{0}' (was version {1}, now version {2})"), CollectionNameText, FText::AsNumber(FileVersion), FText::AsNumber(ECollectionVersion::CurrentVersion));
}
}
if (ChangelistDescBuilder.IsEmpty())
{
// No changes could be detected
ChangelistDescBuilder.AppendLineFormat(LOCTEXT("CollectionNotModifiedDesc", "Collection '{0}' not modified"), CollectionNameText);
}
for (const FText& AdditionalText : AdditionalChangelistText)
{
ChangelistDescBuilder.AppendLine(AdditionalText);
}
FText ChangelistDesc = ChangelistDescBuilder.ToText();
// Finally check in the file
TSharedRef<FCheckIn, ESPMode::ThreadSafe> CheckInOperation = ISourceControlOperation::Create<FCheckIn>();
CheckInOperation->SetDescription( ChangelistDesc );
if ( SourceControlProvider.Execute( CheckInOperation, AbsoluteFilename ) )
{
return true;
}
else
{
OutError = FText::Format(LOCTEXT("Error_SCCCheckIn", "Failed to check in collection '{0}'."), FText::FromName(CollectionName));
return false;
}
}
bool FCollection::RevertCollection(FText& OutError)
{
if ( !ensure(SourceFilename.Len()) )
{
OutError = LOCTEXT("Error_Internal", "There was an internal error.");
return false;
}
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
if ( !ISourceControlModule::Get().IsEnabled() )
{
OutError = LOCTEXT("Error_SCCDisabled", "Revision control is not enabled. Enable revision control in the preferences menu.");
return false;
}
if ( !SourceControlProvider.IsAvailable() )
{
OutError = LOCTEXT("Error_SCCNotAvailable", "Revision control is currently not available. Check your connection and try again.");
return false;
}
FString AbsoluteFilename = FPaths::ConvertRelativePathToFull(SourceFilename);
FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(AbsoluteFilename, EStateCacheUsage::ForceUpdate);
if ( SourceControlState.IsValid() && !(SourceControlState->IsCheckedOut() || SourceControlState->IsAdded()) )
{
OutError = FText::Format(LOCTEXT("Error_SCCNotCheckedOut", "Collection '{0}' not checked out or open for add."), FText::FromName(CollectionName));
return false;
}
if ( SourceControlProvider.Execute(ISourceControlOperation::Create<FRevert>(), AbsoluteFilename) == ECommandResult::Succeeded)
{
return true;
}
else
{
OutError = FText::Format(LOCTEXT("Error_SCCRevert", "Could not revert collection '{0}'"), FText::FromName(CollectionName));
return false;
}
}
bool FCollection::DeleteFromSourceControl(FText& OutError)
{
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
if ( !ISourceControlModule::Get().IsEnabled() )
{
OutError = LOCTEXT("Error_SCCDisabled", "Revision control is not enabled. Enable revision control in the preferences menu.");
return false;
}
if ( !SourceControlProvider.IsAvailable() )
{
OutError = LOCTEXT("Error_SCCNotAvailable", "Revision control is currently not available. Check your connection and try again.");
return false;
}
bool bDeletedSuccessfully = false;
const int32 DeleteProgressDenominator = 2;
int32 DeleteProgressNumerator = 0;
const FText CollectionNameText = FText::FromName( CollectionName );
FFormatNamedArguments Args;
Args.Add( TEXT("CollectionName"), CollectionNameText );
const FText StatusUpdate = FText::Format( LOCTEXT("DeletingCollection", "Deleting Collection {CollectionName}"), Args );
GWarn->BeginSlowTask( StatusUpdate, true );
GWarn->UpdateProgress(DeleteProgressNumerator++, DeleteProgressDenominator);
FString AbsoluteFilename = FPaths::ConvertRelativePathToFull(SourceFilename);
FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(AbsoluteFilename, EStateCacheUsage::ForceUpdate);
GWarn->UpdateProgress(DeleteProgressNumerator++, DeleteProgressDenominator);
// If checked out locally for some reason, revert
if (SourceControlState.IsValid() && (SourceControlState->IsAdded() || SourceControlState->IsCheckedOut() || SourceControlState->IsDeleted()))
{
if ( !RevertCollection(OutError) )
{
// Failed to revert, just bail out
GWarn->EndSlowTask();
return false;
}
// Make sure we get a fresh state from the server
SourceControlState = SourceControlProvider.GetState(AbsoluteFilename, EStateCacheUsage::ForceUpdate);
}
// If not at the head revision, sync up
if (SourceControlState.IsValid() && !SourceControlState->IsCurrent())
{
if ( SourceControlProvider.Execute(ISourceControlOperation::Create<FSync>(), AbsoluteFilename) == ECommandResult::Failed)
{
// Could not sync up with the head revision
GWarn->EndSlowTask();
OutError = FText::Format(LOCTEXT("Error_SCCSync", "Failed to sync collection '{0}' to the head revision."), FText::FromName(CollectionName));
return false;
}
// Check to see if the file exists at the head revision
if ( !IFileManager::Get().FileExists(*SourceFilename) )
{
// File was already deleted, consider this a success
GWarn->EndSlowTask();
return true;
}
FCollection NewCollection(SourceFilename, false, ECollectionStorageMode::Static);
FText LoadErrorText;
if ( !NewCollection.Load(LoadErrorText) )
{
// Failed to load the head revision file so it isn't safe to delete it
GWarn->EndSlowTask();
OutError = FText::Format(LOCTEXT("Error_SCCBadHead", "Failed to load the collection '{0}' at the head revision. {1}"), FText::FromName(CollectionName), LoadErrorText);
return false;
}
// Loaded the head revision, now merge up so the files are in a consistent state
MergeWithCollection(NewCollection);
// Make sure we get a fresh state from the server
SourceControlState = SourceControlProvider.GetState(AbsoluteFilename, EStateCacheUsage::ForceUpdate);
}
GWarn->UpdateProgress(DeleteProgressNumerator++, DeleteProgressDenominator);
if(SourceControlState.IsValid())
{
if(SourceControlState->IsAdded() || SourceControlState->IsCheckedOut())
{
OutError = FText::Format(LOCTEXT("Error_SCCDeleteWhileCheckedOut", "Failed to delete collection '{0}' in revision control because it is checked out or open for add."), FText::FromName(CollectionName));
}
else if(SourceControlState->CanCheckout())
{
if ( SourceControlProvider.Execute(ISourceControlOperation::Create<FDelete>(), AbsoluteFilename) == ECommandResult::Succeeded )
{
// Now check in the delete
const FText ChangelistDesc = FText::Format( LOCTEXT("CollectionDeletedDesc", "Deleted collection: {CollectionName}"), Args );
TSharedRef<FCheckIn, ESPMode::ThreadSafe> CheckInOperation = ISourceControlOperation::Create<FCheckIn>();
CheckInOperation->SetDescription(ChangelistDesc);
if ( SourceControlProvider.Execute( CheckInOperation, AbsoluteFilename ) )
{
// Deleted successfully!
bDeletedSuccessfully = true;
}
else
{
FText Unused;
if ( !RevertCollection(Unused) )
{
UE_LOG(LogCollectionManager, Warning, TEXT("Failed to revert collection '%s' after failing to check in the file that was marked for delete."), *CollectionName.ToString());
}
OutError = FText::Format(LOCTEXT("Error_SCCCheckIn", "Failed to check in collection '{0}'."), FText::FromName(CollectionName));
}
}
else
{
OutError = FText::Format(LOCTEXT("Error_SCCDeleteFailed", "Failed to delete collection '{0}' in revision control."), FText::FromName(CollectionName));
}
}
else if(!SourceControlState->IsSourceControlled())
{
// Not yet in the depot or deleted. We can just delete it from disk.
bDeletedSuccessfully = IFileManager::Get().Delete(*AbsoluteFilename);
if ( !bDeletedSuccessfully )
{
OutError = FText::Format(LOCTEXT("Error_DiskDeleteFailed", "Failed to delete the collection file: {0}"), FText::FromString(AbsoluteFilename));
}
}
else if (!SourceControlState->IsCurrent())
{
OutError = FText::Format(LOCTEXT("Error_SCCNotCurrent", "Collection '{0}' is not at head revision after sync."), FText::FromName(CollectionName));
}
else if(SourceControlState->IsCheckedOutOther())
{
OutError = FText::Format(LOCTEXT("Error_SCCCheckedOutOther", "Collection '{0}' is checked out by another user."), FText::FromName(CollectionName));
}
else
{
OutError = FText::Format(LOCTEXT("Error_SCCUnknown", "Could not determine revision control state for collection '{0}'"), FText::FromName(CollectionName));
}
}
else
{
OutError = LOCTEXT("Error_SCCInvalid", "Revision control state is invalid.");
}
GWarn->UpdateProgress(DeleteProgressNumerator++, DeleteProgressDenominator);
GWarn->EndSlowTask();
return bDeletedSuccessfully;
}
#undef LOCTEXT_NAMESPACE