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

1644 lines
56 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "DiffUtils.h"
#include "AssetDefinitionRegistry.h"
#include "Components/ActorComponent.h"
#include "Containers/BitArray.h"
#include "EditorCategoryUtils.h"
#include "IAssetTools.h"
#include "Engine/Blueprint.h"
#include "Engine/Level.h"
#include "IAssetTypeActions.h"
#include "ISourceControlModule.h"
#include "ISourceControlProvider.h"
#include "ISourceControlRevision.h"
#include "Internationalization/Internationalization.h"
#include "Math/UnrealMathSSE.h"
#include "Misc/Attribute.h"
#include "Misc/CString.h"
#include "Misc/DateTime.h"
#include "Misc/Optional.h"
#include "ObjectEditorUtils.h"
#include "SlotBase.h"
#include "Styling/AppStyle.h"
#include "Styling/SlateColor.h"
#include "Templates/SubclassOf.h"
#include "Templates/UnrealTemplate.h"
#include "Types/SlateConstants.h"
#include "Types/SlateEnums.h"
#include "UObject/Class.h"
#include "UObject/Field.h"
#include "UObject/Object.h"
#include "UObject/ObjectMacros.h"
#include "UObject/PropertyPortFlags.h"
#include "UObject/SavePackage.h"
#include "Widgets/DeclarativeSyntaxSupport.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Views/STableRow.h"
#include "Widgets/Views/STableViewBase.h"
#include "Widgets/Views/STreeView.h"
#include "HAL/PlatformFileManager.h"
#include "UnrealEngine.h"
#include "UObject/Linker.h"
class ITableRow;
class SWidget;
namespace UEDiffUtils_Private
{
static FProperty* Resolve( const UStruct* Class, FName PropertyName )
{
if(Class == nullptr )
{
return nullptr;
}
for (FProperty* Prop : TFieldRange<FProperty>(Class))
{
if( Prop->GetFName() == PropertyName )
{
return Prop;
}
}
return nullptr;
}
static FPropertySoftPathSet GetPropertyNameSet(const UStruct* ForStruct)
{
return FPropertySoftPathSet(DiffUtils::GetVisiblePropertiesInOrderDeclared(ForStruct));
}
static const FString DiffSyntaxHelp = TEXT("format: 'diff <lhs> <rhs>");
static const FString MergeSyntaxHelp = TEXT("format: 'merge <remote> <local> <base> [-o out_path]' or 'merge <local> [-o out_path]'");
static void RunDiffCommand(const TArray<FString>& Args);
static void RunMergeCommand(const TArray<FString>& Args);
FAutoConsoleCommand MergeConsoleCommand(
TEXT("merge"),
*FString::Format(TEXT("Either merge three assets or a single conflicted asset.\n{0}"), {MergeSyntaxHelp}),
FConsoleCommandWithArgsDelegate::CreateStatic(&RunMergeCommand),
ECVF_Default
);
FAutoConsoleCommand DiffConsoleCommand(
TEXT("diff"),
*FString::Format(TEXT("diff two assets against one another.\n{0}"), {DiffSyntaxHelp}),
FConsoleCommandWithArgsDelegate::CreateStatic(&RunDiffCommand),
ECVF_Default
);
}
static UObject* LoadAssetFromExternalPath(FString Path)
{
FPackagePath PackagePath;
if (!FPackagePath::TryFromPackageName(Path, PackagePath))
{
// copy to the temp directory so it can be loaded properly
FString File = FPaths::GetBaseFilename(Path) + TEXT("-");
for (const ANSICHAR Char : "#(){}[].")
{
File.ReplaceCharInline(Char, '-');
}
const FString Extension = TEXT(".") + FPaths::GetExtension(Path);
const FString SourcePath = Path;
FPlatformFileManager::Get().GetPlatformFile().CreateDirectory(*FPaths::DiffDir());
Path = FPaths::CreateTempFilename(*FPaths::DiffDir(), *File, *Extension);
Path = FPaths::ConvertRelativePathToFull(Path);
if (!FPlatformFileManager::Get().GetPlatformFile().CopyFile(*Path, *SourcePath))
{
UE_LOG(LogEngine, Display, TEXT("Failed to Copy %s"), *SourcePath);
return nullptr;
}
// load the temp package
PackagePath = FPackagePath::FromLocalPath(Path);
}
if (PackagePath.IsEmpty())
{
UE_LOG(LogEngine, Display, TEXT("Invalid Path: %s"), *Path);
return nullptr;
}
if (const UPackage* TempPackage = DiffUtils::LoadPackageForDiff(PackagePath, {}))
{
if (UObject* Object = TempPackage->FindAssetInPackage())
{
return Object;
}
}
UE_LOG(LogEngine, Display, TEXT("Failed to load: %s"), *Path);
return nullptr;
}
static void UEDiffUtils_Private::RunDiffCommand(const TArray<FString>& Args)
{
if (Args.Num() != 2)
{
UE_LOG(LogEngine, Display, TEXT("%s"), *DiffSyntaxHelp);
return;
}
UObject* LHS = LoadAssetFromExternalPath(Args[0]);
UObject* RHS = LoadAssetFromExternalPath(Args[1]);
if (LHS && RHS)
{
IAssetTools::Get().DiffAssets(LHS, RHS, {}, {});
}
}
namespace UE::CmdLink
{
// CmdLinkServerModule will set these methods if loaded.
// they're used by the merge command because we need to keep the CmdLink client running until the user closes the merge window and saves the output
UNREALED_API void(*GBeginAsyncCommand)(const FString&, const TArray<FString>&) = [](const FString&, const TArray<FString>&){};
UNREALED_API void(*GEndAsyncCommand)(const FString&, const TArray<FString>&) = [](const FString&, const TArray<FString>&){};
}
static void UEDiffUtils_Private::RunMergeCommand(const TArray<FString>& Args)
{
UObject* Local = nullptr;
UObject* Base = nullptr;
UObject* Remote = nullptr;
FString OutDirectory;
bool bThreeWayMerge = false;
bool bInvalidSyntax = false;
switch (Args.Num())
{
case 1: // merge <local>
Local = LoadAssetFromExternalPath(Args[0]);
bThreeWayMerge = false;
break;
case 3:
if (Args[1] == TEXT("-o")) // merge <local> -o <output_file>
{
Local = LoadAssetFromExternalPath(Args[0]);
OutDirectory = Args[2];
bThreeWayMerge = false;
}
else // merge <local> <base> <remote>
{
Remote = LoadAssetFromExternalPath(Args[0]);
Local = LoadAssetFromExternalPath(Args[1]);
Base = LoadAssetFromExternalPath(Args[2]);
bThreeWayMerge = true;
}
break;
case 5: // merge <local> <base> <remote> -o <output_file>
if (Args[3] == TEXT("-o"))
{
Remote = LoadAssetFromExternalPath(Args[0]);
Local = LoadAssetFromExternalPath(Args[1]);
Base = LoadAssetFromExternalPath(Args[2]);
OutDirectory = Args[4];
bThreeWayMerge = true;
break;
}
// 5 parameters requires output file at the end
bInvalidSyntax = true;
break;
default:
// unsupported parameter count
bInvalidSyntax = true;
break;
}
if (bInvalidSyntax)
{
// invalid syntax. display help.
UE_LOG(LogEngine, Display, TEXT("%s"), *MergeSyntaxHelp);
return;
}
const FOnAssetMergeResolved ResolutionCallback = FOnAssetMergeResolved::CreateLambda([Args, Local, OutDirectory](const FAssetMergeResults& Results)
{
if (!OutDirectory.IsEmpty() && Results.Result == EAssetMergeResult::Completed)
{
// save a copy of the asset to the output directory
FSavePackageArgs SaveArgs;
SaveArgs.TopLevelFlags = RF_Standalone;
SaveArgs.Error = GLog;
UPackage::SavePackage(Results.MergedPackage, Local, *OutDirectory, SaveArgs);
ResetLoaders(Results.MergedPackage);
}
UE::CmdLink::GEndAsyncCommand(TEXT("merge"), Args);
});
if (bThreeWayMerge)
{
FAssetManualMergeArgs MergeArgs;
MergeArgs.LocalAsset = Local;
MergeArgs.BaseAsset = Base;
MergeArgs.RemoteAsset = Remote;
MergeArgs.ResolutionCallback = ResolutionCallback;
MergeArgs.Flags = MF_NONE;
if (MergeArgs.LocalAsset && MergeArgs.BaseAsset && MergeArgs.RemoteAsset)
{
const UAssetDefinition* AssetDefinition = UAssetDefinitionRegistry::Get()->GetAssetDefinitionForClass(Local->GetClass());
if (!AssetDefinition->CanMerge())
{
UE_LOG(LogEngine, Error, TEXT("%s of class type %s does not support merging"), *Local->GetName(), *Local->GetClass()->GetName());
return;
}
UE::CmdLink::GBeginAsyncCommand(TEXT("merge"), Args);
AssetDefinition->Merge(MergeArgs);
}
}
else
{
FAssetAutomaticMergeArgs MergeArgs;
MergeArgs.LocalAsset = Local;
MergeArgs.ResolutionCallback = ResolutionCallback;
MergeArgs.Flags = MF_NONE;
if (MergeArgs.LocalAsset)
{
const UAssetDefinition* AssetDefinition = UAssetDefinitionRegistry::Get()->GetAssetDefinitionForClass(Local->GetClass());
if (!AssetDefinition->CanMerge())
{
UE_LOG(LogEngine, Error, TEXT("%s does not support merging"), *Local->GetName());
return;
}
UE::CmdLink::GBeginAsyncCommand(TEXT("merge"), Args);
AssetDefinition->Merge(MergeArgs);
}
}
}
FPropertySoftPath::FPropertySoftPath()
: RootTypeHint(nullptr)
{
}
FPropertySoftPath::FPropertySoftPath(const FProperty* Property)
: FPropertySoftPath()
{
PropertyChain.Push(FChainElement(Property));
}
FPropertySoftPath::FPropertySoftPath(TArray<FName> InPropertyChain)
: FPropertySoftPath()
{
for (FName PropertyName : InPropertyChain)
{
PropertyChain.Push(FChainElement(PropertyName));
}
}
FPropertySoftPath::FPropertySoftPath(FPropertyPath InPropertyPath)
: FPropertySoftPath()
{
for (int32 PropertyIndex = 0, end = InPropertyPath.GetNumProperties(); PropertyIndex != end; ++PropertyIndex)
{
const FPropertyInfo& Info = InPropertyPath.GetPropertyInfo(PropertyIndex);
if (Info.ArrayIndex != INDEX_NONE)
{
PropertyChain.Push(FName(*FString::FromInt(Info.ArrayIndex)));
}
else
{
PropertyChain.Push(FChainElement(Info.Property.Get()));
}
}
if (InPropertyPath.GetNumProperties() > 0)
{
const FProperty* RootProperty = InPropertyPath.GetPropertyInfo(0).Property.Get();
if(RootProperty)
{
RootTypeHint = RootProperty->GetOwnerStruct();
}
}
}
FPropertySoftPath::FPropertySoftPath(const FPropertySoftPath& MainPropertyPath, const FPropertySoftPath& SubPropertyPath)
: PropertyChain(MainPropertyPath.PropertyChain)
, RootTypeHint(MainPropertyPath.RootTypeHint)
{
PropertyChain.Append(SubPropertyPath.PropertyChain);
}
FPropertySoftPath::FPropertySoftPath(const FPropertySoftPath& SubPropertyPath, const FProperty* LeafProperty)
: PropertyChain(SubPropertyPath.PropertyChain)
, RootTypeHint(SubPropertyPath.RootTypeHint)
{
PropertyChain.Push(FChainElement(LeafProperty));
}
FPropertySoftPath::FPropertySoftPath(const FPropertySoftPath& SubPropertyPath, int32 ContainerIndex)
: PropertyChain(SubPropertyPath.PropertyChain)
, RootTypeHint(SubPropertyPath.RootTypeHint)
{
PropertyChain.Push(FName(*FString::FromInt(ContainerIndex)));
}
FResolvedProperty FPropertySoftPath::Resolve(const UObject* Object) const
{
FResolvedProperty ResolvedProperty = Resolve(Object->GetClass(), Object);
return ResolvedProperty;
}
int32 FPropertySoftPath::TryReadIndex(const TArray<FChainElement>& LocalPropertyChain, int32& OutIndex)
{
if(OutIndex + 1 < LocalPropertyChain.Num())
{
FString AsString = LocalPropertyChain[OutIndex + 1].DisplayString;
if(AsString.IsNumeric())
{
++OutIndex;
return FCString::Atoi(*AsString);
}
}
return INDEX_NONE;
};
FResolvedProperty FPropertySoftPath::Resolve(const UStruct* Struct, const void* StructData) const
{
if (RootTypeHint && RootTypeHint != Struct)
{
if (const UClass* AsClass = Cast<UClass>(Struct))
{
const UScriptStruct* SparseClassDataStruct = AsClass->GetSparseClassDataStruct();
if (SparseClassDataStruct && SparseClassDataStruct->IsChildOf(RootTypeHint))
{
if (const void* SparseClassData = const_cast<UClass*>(AsClass)->GetSparseClassData(EGetSparseClassDataMethod::ReturnIfNull))
{
return Resolve(RootTypeHint, SparseClassData);
}
}
}
}
// dig into the object, finding nested objects, etc:
const void* CurrentBlock = StructData;
const UStruct* NextClass = Struct;
const void* NextBlock = CurrentBlock;
const FProperty* Property = nullptr;
for (int32 i = 0; i < PropertyChain.Num(); ++i)
{
CurrentBlock = NextBlock;
const FProperty* NextProperty = UEDiffUtils_Private::Resolve(NextClass, PropertyChain[i].PropertyName);
if (!NextProperty)
{
if (const FStructProperty* StructProperty = CastField<FStructProperty>(Property))
{
StructProperty->FindInnerPropertyInstance(PropertyChain[i].PropertyName, CurrentBlock, NextProperty, NextBlock);
}
}
CurrentBlock = NextBlock;
// if an index was provided, resolve it
const int32 PropertyIndex = TryReadIndex(PropertyChain, i);
if (NextProperty && PropertyIndex != INDEX_NONE)
{
Property = NextProperty;
if (const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Property))
{
FScriptArrayHelper ArrayHelper(ArrayProperty, Property->ContainerPtrToValuePtr<UObject*>(CurrentBlock));
if (ArrayHelper.IsValidIndex(PropertyIndex))
{
NextProperty = ArrayProperty->Inner;
NextBlock = ArrayHelper.GetRawPtr(PropertyIndex);
}
}
else if( const FSetProperty* SetProperty = CastField<FSetProperty>(Property) )
{
FScriptSetHelper SetHelper(SetProperty, Property->ContainerPtrToValuePtr<UObject*>(CurrentBlock));
NextProperty = SetHelper.GetElementProperty();
NextBlock = SetHelper.FindNthElementPtr(PropertyIndex);
}
else if( const FMapProperty* MapProperty = CastField<FMapProperty>(Property) )
{
FScriptMapHelper MapHelper(MapProperty, Property->ContainerPtrToValuePtr<UObject*>(CurrentBlock));
NextProperty = MapHelper.GetValueProperty();
NextBlock = MapHelper.FindNthPairPtr(PropertyIndex);
}
}
CurrentBlock = NextBlock;
if (NextProperty)
{
Property = NextProperty;
if (const FObjectProperty* ObjectProperty = CastField<FObjectProperty>(Property))
{
const UObject* NextObject = ObjectProperty->GetObjectPropertyValue(Property->ContainerPtrToValuePtr<UObject*>(CurrentBlock));
NextBlock = NextObject;
NextClass = NextObject ? NextObject->GetClass() : nullptr;
}
else if (const FStructProperty* StructProperty = CastField<FStructProperty>(Property))
{
NextBlock = StructProperty->ContainerPtrToValuePtr<void>(CurrentBlock);
NextClass = StructProperty->Struct;
}
else
{
break;
}
}
else
{
break;
}
}
return FResolvedProperty(CurrentBlock, Property);
}
FPropertyPath FPropertySoftPath::ResolvePath(const UObject* Object) const
{
auto UpdateContainerAddress = [](const FProperty* Property, const void* Instance, const void*& OutContainerAddress, const UStruct*& OutContainerStruct)
{
if( ensure(Instance) )
{
if(const FObjectProperty* ObjectProperty = CastField<FObjectProperty>(Property))
{
const UObject* const* InstanceObject = reinterpret_cast<const UObject* const*>(Instance);
if( *InstanceObject)
{
OutContainerAddress = *InstanceObject;
OutContainerStruct = (*InstanceObject)->GetClass();
}
}
else if(const FStructProperty* StructProperty = CastField<FStructProperty>(Property))
{
OutContainerAddress = Instance;
OutContainerStruct = StructProperty->Struct;
}
}
};
const void* ContainerAddress = Object;
const UStruct* ContainerStruct = (Object ? Object->GetClass() : nullptr);
FPropertyPath Ret;
for( int32 I = 0; I < PropertyChain.Num(); ++I )
{
FName PropertyIdentifier = PropertyChain[I].PropertyName;
FProperty* ResolvedProperty = UEDiffUtils_Private::Resolve(ContainerStruct, PropertyIdentifier);
// if property wasn't found in ContainerStruct, check for it in SparseClassData
if (!ResolvedProperty && RootTypeHint && RootTypeHint != ContainerStruct)
{
if (const UClass* AsClass = Cast<UClass>(ContainerStruct))
{
const UScriptStruct* SparseClassDataStruct = AsClass->GetSparseClassDataStruct();
if (SparseClassDataStruct && SparseClassDataStruct->IsChildOf(RootTypeHint))
{
ResolvedProperty = UEDiffUtils_Private::Resolve(RootTypeHint, PropertyIdentifier);
// return if null won't mutate...
ContainerAddress = const_cast<UClass*>(AsClass)->GetSparseClassData(EGetSparseClassDataMethod::ReturnIfNull);
}
}
}
// If the property didn't exist inside the container, return an invalid property
if (!ResolvedProperty)
{
return FPropertyPath();
}
FPropertyInfo Info(ResolvedProperty, INDEX_NONE);
Ret.AddProperty(Info);
int32 PropertyIndex = TryReadIndex(PropertyChain, I);
// calculate offset so we can continue resolving object properties/structproperties:
if( const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(ResolvedProperty) )
{
FScriptArrayHelper ArrayHelper(ArrayProperty, ArrayProperty->ContainerPtrToValuePtr<const void*>( ContainerAddress ));
if (ArrayHelper.IsValidIndex(PropertyIndex))
{
UpdateContainerAddress( ArrayProperty->Inner, ArrayHelper.GetRawPtr(PropertyIndex), ContainerAddress, ContainerStruct );
FPropertyInfo ArrayInfo(ArrayProperty->Inner, PropertyIndex);
Ret.AddProperty(ArrayInfo);
}
}
else if( const FSetProperty* SetProperty = CastField<FSetProperty>(ResolvedProperty) )
{
FScriptSetHelper SetHelper(SetProperty, SetProperty->ContainerPtrToValuePtr<const void*>( ContainerAddress ));
if (SetHelper.IsValidIndex(PropertyIndex))
{
const int32 InternalIndex = SetHelper.FindInternalIndex(PropertyIndex);
UpdateContainerAddress( SetProperty->ElementProp, SetHelper.GetElementPtr(InternalIndex), ContainerAddress, ContainerStruct );
FPropertyInfo SetInfo(SetProperty->ElementProp, PropertyIndex);
Ret.AddProperty(SetInfo);
}
}
else if( const FMapProperty* MapProperty = CastField<FMapProperty>(ResolvedProperty) )
{
FScriptMapHelper MapHelper(MapProperty, MapProperty->ContainerPtrToValuePtr<const void*>( ContainerAddress ));
if (MapHelper.IsValidIndex(PropertyIndex))
{
const int32 InternalIndex = MapHelper.FindInternalIndex(PropertyIndex);
// we have an index, but are we looking into a key or value? Peek ahead to find out:
if(ensure((I + 1 < PropertyChain.Num())))
{
if(PropertyChain[I+1].PropertyName == MapProperty->KeyProp->GetFName())
{
++I;
UpdateContainerAddress( MapProperty->KeyProp, MapHelper.GetKeyPtr(InternalIndex), ContainerAddress, ContainerStruct );
FPropertyInfo MakKeyInfo(MapProperty->KeyProp, PropertyIndex);
Ret.AddProperty(MakKeyInfo);
}
else if(ensure( PropertyChain[I+1].PropertyName == MapProperty->ValueProp->GetFName() ))
{
++I;
UpdateContainerAddress( MapProperty->ValueProp, MapHelper.GetValuePtr(InternalIndex), ContainerAddress, ContainerStruct );
FPropertyInfo MapValueInfo(MapProperty->ValueProp, PropertyIndex);
Ret.AddProperty(MapValueInfo);
}
}
}
}
else if (const FObjectProperty* ObjectProperty = CastField<FObjectProperty>(ResolvedProperty))
{
UpdateContainerAddress( ObjectProperty, ObjectProperty->ContainerPtrToValuePtr<const void*>( ContainerAddress, FMath::Max(PropertyIndex, 0) ), ContainerAddress, ContainerStruct );
// handle static arrays:
if(PropertyIndex != INDEX_NONE )
{
FPropertyInfo ObjectInfo(ResolvedProperty, PropertyIndex);
Ret.AddProperty(ObjectInfo);
}
}
else if( const FStructProperty* StructProperty = CastField<FStructProperty>(ResolvedProperty) )
{
UpdateContainerAddress( StructProperty, StructProperty->ContainerPtrToValuePtr<const void*>( ContainerAddress, FMath::Max(PropertyIndex, 0) ), ContainerAddress, ContainerStruct );
// handle static arrays:
if(PropertyIndex != INDEX_NONE )
{
FPropertyInfo StructInfo(ResolvedProperty, PropertyIndex);
Ret.AddProperty(StructInfo);
}
}
else
{
// handle static arrays:
if(PropertyIndex != INDEX_NONE )
{
FPropertyInfo StaticArrayInfo(ResolvedProperty, PropertyIndex);
Ret.AddProperty(StaticArrayInfo);
}
}
}
return Ret;
}
FString FPropertySoftPath::ToDisplayName(const int32 NumberOfElements /*= INDEX_NONE*/) const
{
FString Ret;
const int32 Count = NumberOfElements != INDEX_NONE
? FMath::Clamp(NumberOfElements, 1, PropertyChain.Num())
: PropertyChain.Num();
for (int32 i = PropertyChain.Num() - Count; i < PropertyChain.Num(); i++)
{
FString PropertyAsString = PropertyChain[i].DisplayString;
if (Ret.IsEmpty())
{
Ret.Append(PropertyAsString);
}
else if (PropertyAsString.IsNumeric())
{
Ret.AppendChar('[');
Ret.Append(PropertyAsString);
Ret.AppendChar(']');
}
else
{
Ret.AppendChar(' ');
Ret.Append(PropertyAsString);
}
}
return Ret;
}
FPropertySoftPath FPropertySoftPath::GetRootProperty(int32 Depth) const
{
FPropertySoftPath RootPath;
Depth = FMath::Clamp(Depth, 1, PropertyChain.Num());
for (int32 ElementIndex = 0; ElementIndex < Depth; ElementIndex++)
{
RootPath.PropertyChain.Push(PropertyChain[ElementIndex]);
}
return RootPath;
}
int32 FPropertySoftPath::TryReadIndex(int32 Index) const
{
return TryReadIndex(PropertyChain, Index);
};
const UObject* DiffUtils::GetCDO(const UBlueprint* ForBlueprint)
{
if (!ForBlueprint
|| !ForBlueprint->GeneratedClass)
{
return NULL;
}
return ForBlueprint->GeneratedClass->GetDefaultObject(false);
}
void DiffUtils::CompareUnrelatedStructs(const UStruct* StructA, const void* A, const UStruct* StructB, const void* B,
TArray<FSingleObjectDiffEntry>& OutDifferingProperties)
{
CompareUnrelatedStructs(StructA, A, nullptr, StructB, B, nullptr, OutDifferingProperties);
}
void DiffUtils::CompareUnrelatedStructs(const UStruct* StructA, const void* A, const UObject* OwningOuterA, const UStruct* StructB, const void* B,
const UObject* OwningOuterB, TArray<FSingleObjectDiffEntry>& OutDifferingProperties)
{
FPropertySoftPathSet PropertiesInA = UEDiffUtils_Private::GetPropertyNameSet(StructA);
FPropertySoftPathSet PropertiesInB = UEDiffUtils_Private::GetPropertyNameSet(StructB);
// any properties in A that aren't in B are differing:
auto AddedToA = PropertiesInA.Difference(PropertiesInB).Array();
for (const auto& Entry : AddedToA)
{
OutDifferingProperties.Push(FSingleObjectDiffEntry(Entry, EPropertyDiffType::PropertyAddedToA));
}
// and the converse:
auto AddedToB = PropertiesInB.Difference(PropertiesInA).Array();
for (const auto& Entry : AddedToB)
{
OutDifferingProperties.Push(FSingleObjectDiffEntry(Entry, EPropertyDiffType::PropertyAddedToB));
}
// for properties in common, dig out the uproperties and determine if they're identical:
if (A && B)
{
FPropertySoftPathSet Common = PropertiesInA.Intersect(PropertiesInB);
for (const auto& PropertyName : Common)
{
FResolvedProperty AProp = PropertyName.Resolve(StructA, A);
FResolvedProperty BProp = PropertyName.Resolve(StructB, B);
check(AProp != FResolvedProperty() && BProp != FResolvedProperty());
TArray<FPropertySoftPath> DifferingSubProperties;
if (!DiffUtils::Identical(AProp, BProp, OwningOuterA, OwningOuterB, FDiffParameters(PropertyName), DifferingSubProperties))
{
for (int DifferingIndex = 0; DifferingIndex < DifferingSubProperties.Num(); DifferingIndex++)
{
OutDifferingProperties.Push(FSingleObjectDiffEntry(DifferingSubProperties[DifferingIndex], EPropertyDiffType::PropertyValueChanged));
}
}
}
}
}
void DiffUtils::CompareUnrelatedObjects(const UObject* A, const UObject* B, TArray<FSingleObjectDiffEntry>& OutDifferingProperties)
{
if (A && B)
{
return CompareUnrelatedStructs(A->GetClass(), A, A->GetPackage(), B->GetClass(), B, B->GetPackage(), OutDifferingProperties);
}
}
void DiffUtils::CompareUnrelatedSCS(const UBlueprint* Old, const TArray< FSCSResolvedIdentifier >& OldHierarchy, const UBlueprint* New, const TArray< FSCSResolvedIdentifier >& NewHierarchy, FSCSDiffRoot& OutDifferingEntries )
{
const auto FindEntry = [](TArray< FSCSResolvedIdentifier > const& InArray, const FSCSIdentifier* Value) -> const FSCSResolvedIdentifier*
{
const FSCSResolvedIdentifier* BestMatch = nullptr;
for (const auto& Node : InArray)
{
if (Node.Identifier.Name == Value->Name)
{
if (Node.Identifier.TreeLocation == Value->TreeLocation)
{
return &Node;
}
else if (BestMatch == nullptr)
{
BestMatch = &Node;
}
}
}
return BestMatch;
};
for (const auto& OldNode : OldHierarchy)
{
const FSCSResolvedIdentifier* NewEntry = FindEntry(NewHierarchy, &OldNode.Identifier);
if (NewEntry != nullptr)
{
bool bShouldDiffProperties = true;
// did it change class?
const bool bObjectTypesDiffer = OldNode.Object != nullptr && NewEntry->Object != nullptr && OldNode.Object->GetClass() != NewEntry->Object->GetClass();
if (bObjectTypesDiffer)
{
FSCSDiffEntry Diff = { OldNode.Identifier, ETreeDiffType::NODE_TYPE_CHANGED, FSingleObjectDiffEntry() };
OutDifferingEntries.Entries.Push(Diff);
// Only diff properties if we're still within the same class inheritance hierarchy.
bShouldDiffProperties = OldNode.Object->GetClass()->IsChildOf(NewEntry->Object->GetClass()) || NewEntry->Object->GetClass()->IsChildOf(OldNode.Object->GetClass());
}
// did a property change?
if(bShouldDiffProperties)
{
TArray<FSingleObjectDiffEntry> DifferingProperties;
DiffUtils::CompareUnrelatedObjects(OldNode.Object, NewEntry->Object, DifferingProperties);
for (const auto& Property : DifferingProperties)
{
// Only include property value change entries if the object types differ.
if (!bObjectTypesDiffer || Property.DiffType == EPropertyDiffType::PropertyValueChanged)
{
FSCSDiffEntry Diff = { OldNode.Identifier, ETreeDiffType::NODE_PROPERTY_CHANGED, Property };
OutDifferingEntries.Entries.Push(Diff);
}
}
}
// did it move?
if( NewEntry->Identifier.TreeLocation != OldNode.Identifier.TreeLocation )
{
FSCSDiffEntry Diff = { OldNode.Identifier, ETreeDiffType::NODE_MOVED, FSingleObjectDiffEntry() };
OutDifferingEntries.Entries.Push(Diff);
}
// did it become corrupted? or stop being corrupted?
const bool bNewIsCorrupt = New->GeneratedClass && NewEntry->Object && !NewEntry->Object->IsIn(New->GeneratedClass);
const bool bOldIsCorrupt = Old->GeneratedClass && OldNode.Object && !OldNode.Object->IsIn(Old->GeneratedClass);
if (bNewIsCorrupt != bOldIsCorrupt)
{
if (bNewIsCorrupt)
{
FSCSDiffEntry Diff = { NewEntry->Identifier, ETreeDiffType::NODE_CORRUPTED, FSingleObjectDiffEntry() };
OutDifferingEntries.Entries.Push(Diff);
}
else
{
FSCSDiffEntry Diff = { OldNode.Identifier, ETreeDiffType::NODE_FIXED, FSingleObjectDiffEntry() };
OutDifferingEntries.Entries.Push(Diff);
}
}
// no change! Do nothing.
}
else
{
// not found in the new data, must have been deleted:
FSCSDiffEntry Entry = { OldNode.Identifier, ETreeDiffType::NODE_REMOVED, FSingleObjectDiffEntry() };
OutDifferingEntries.Entries.Push( Entry );
}
}
for (const auto& NewNode : NewHierarchy)
{
const FSCSResolvedIdentifier* OldEntry = FindEntry(OldHierarchy, &NewNode.Identifier);
if (OldEntry == nullptr)
{
FSCSDiffEntry Entry = { NewNode.Identifier, ETreeDiffType::NODE_ADDED, FSingleObjectDiffEntry() };
OutDifferingEntries.Entries.Push( Entry );
}
}
}
static void IdenticalHelper(const FProperty* AProperty, const FProperty* BProperty, const void* AValue, const void* BValue,
const UObject* OwningOuterA, const UObject* OwningOuterB, DiffUtils::FDiffParameters DiffParameters,
TArray<FPropertySoftPath>& DifferingSubProperties)
{
if (AProperty == nullptr || BProperty == nullptr || AProperty->ArrayDim != BProperty->ArrayDim || AProperty->GetClass() != BProperty->GetClass())
{
DifferingSubProperties.Push(DiffParameters.RootPath);
return;
}
if (AValue == nullptr || BValue == nullptr)
{
if (AValue != BValue)
{
DifferingSubProperties.Push(DiffParameters.RootPath);
}
return;
}
if (DiffParameters.ShouldIgnorePropertyPredicate
&& DiffParameters.ShouldIgnorePropertyPredicate(*AProperty))
{
return;
}
// Keep copy of the initial RootPath since we reuse DiffParameters with
// an updated path to nested calls to 'IdenticalHelper'
const FPropertySoftPath RootPath = DiffParameters.RootPath;
if (DiffParameters.bShouldDiffArrayElements && AProperty->ArrayDim != 1)
{
// Identical does not handle static array case automatically and we have to do the offset calculation ourself because
// our container (e.g. the struct or class or dynamic array) has already done the initial offset calculation:
for (int32 I = 0; I < AProperty->ArrayDim; ++I)
{
int32 Offset = AProperty->GetElementSize() * I;
DiffParameters.RootPath = FPropertySoftPath(RootPath, I);
DiffParameters.bShouldDiffArrayElements = false;
const void* CurAValue = reinterpret_cast<const void*>(reinterpret_cast<const uint8*>(AValue) + Offset);
const void* CurBValue = reinterpret_cast<const void*>(reinterpret_cast<const uint8*>(BValue) + Offset);
IdenticalHelper(AProperty, BProperty, CurAValue, CurBValue, OwningOuterA, OwningOuterB, DiffParameters, DifferingSubProperties);
}
return;
}
const FStructProperty* APropAsStruct = CastField<FStructProperty>(AProperty);
const FArrayProperty* APropAsArray = CastField<FArrayProperty>(AProperty);
const FSetProperty* APropAsSet = CastField<FSetProperty>(AProperty);
const FMapProperty* APropAsMap = CastField<FMapProperty>(AProperty);
const FObjectProperty* APropAsObject = CastField<FObjectProperty>(AProperty);
if (APropAsStruct != nullptr)
{
const FStructProperty* BPropAsStruct = CastFieldChecked<FStructProperty>(const_cast<FProperty*>(BProperty));
if (BPropAsStruct->Struct == APropAsStruct->Struct)
{
if (APropAsStruct->Struct->StructFlags & STRUCT_IdenticalNative)
{
// If the struct uses CPP identical tests then we need to honor that
if (!AProperty->Identical(AValue, BValue, PPF_DeepComparison))
{
DifferingSubProperties.Push(RootPath);
}
}
else
{
// Compare sub-properties to detect more granular changes
for (TFieldIterator<FProperty> PropertyIt(APropAsStruct->Struct); PropertyIt; ++PropertyIt)
{
const FProperty* StructProp = *PropertyIt;
DiffParameters.RootPath = FPropertySoftPath(RootPath, StructProp);
const void* SubValueA = StructProp->ContainerPtrToValuePtr<void>(AValue, 0);
const void* SubValueB = StructProp->ContainerPtrToValuePtr<void>(BValue, 0);
IdenticalHelper(StructProp, StructProp, SubValueA, SubValueB,
OwningOuterA, OwningOuterB, DiffParameters, DifferingSubProperties);
}
}
}
else
{
DifferingSubProperties.Push(RootPath);
}
}
else if (APropAsArray != nullptr)
{
const FArrayProperty* BPropAsArray = CastFieldChecked<const FArrayProperty>(BProperty);
if(BPropAsArray->Inner->GetClass() == APropAsArray->Inner->GetClass())
{
FScriptArrayHelper ArrayHelperA(APropAsArray, AValue);
FScriptArrayHelper ArrayHelperB(BPropAsArray, BValue);
// note any differences in contained types:
for (int32 ArrayIndex = 0; ArrayIndex < ArrayHelperA.Num() && ArrayIndex < ArrayHelperB.Num(); ArrayIndex++)
{
DiffParameters.RootPath = FPropertySoftPath(RootPath, ArrayIndex);
const void* SubValueA = ArrayHelperA.GetRawPtr(ArrayIndex);
const void* SubValueB = ArrayHelperB.GetRawPtr(ArrayIndex);
IdenticalHelper(APropAsArray->Inner, BPropAsArray->Inner, SubValueA, SubValueB,
OwningOuterA, OwningOuterB, DiffParameters, DifferingSubProperties);
}
// note any size difference:
if (ArrayHelperA.Num() != ArrayHelperB.Num())
{
DifferingSubProperties.Push(RootPath);
}
}
else
{
DifferingSubProperties.Push(RootPath);
}
}
else if(APropAsSet != nullptr)
{
const FSetProperty* BPropAsSet = CastFieldChecked<const FSetProperty>(BProperty);
if(BPropAsSet->ElementProp->GetClass() == APropAsSet->ElementProp->GetClass())
{
FScriptSetHelper SetHelperA(APropAsSet, AValue);
FScriptSetHelper SetHelperB(BPropAsSet, BValue);
if (SetHelperA.Num() != SetHelperB.Num())
{
// API not robust enough to indicate changes made to # of set elements, would
// need to return something more detailed than DifferingSubProperties array:
DifferingSubProperties.Push(RootPath);
}
// note any differences in contained elements:
FScriptSetHelper::FIterator IteratorA(SetHelperA);
FScriptSetHelper::FIterator IteratorB(SetHelperB);
for (; IteratorA && IteratorB; ++IteratorA, ++IteratorB)
{
DiffParameters.RootPath = FPropertySoftPath(RootPath, IteratorA.GetLogicalIndex());
const void* SubValueA = SetHelperA.GetElementPtr(IteratorA);
const void* SubValueB = SetHelperB.GetElementPtr(IteratorB);
IdenticalHelper(APropAsSet->ElementProp, BPropAsSet->ElementProp, SubValueA, SubValueB,
OwningOuterA, OwningOuterB, DiffParameters, DifferingSubProperties);
}
}
else
{
DifferingSubProperties.Push(RootPath);
}
}
else if(APropAsMap != nullptr)
{
const FMapProperty* BPropAsMap = CastFieldChecked<const FMapProperty>(BProperty);
if(APropAsMap->KeyProp->GetClass() == BPropAsMap->KeyProp->GetClass() && APropAsMap->ValueProp->GetClass() == BPropAsMap->ValueProp->GetClass())
{
FScriptMapHelper MapHelperA(APropAsMap, AValue);
FScriptMapHelper MapHelperB(BPropAsMap, BValue);
if (MapHelperA.Num() != MapHelperB.Num())
{
// API not robust enough to indicate changes made to # of set elements, would
// need to return something more detailed than DifferingSubProperties array:
DifferingSubProperties.Push(RootPath);
}
FScriptMapHelper::FIterator IteratorA(MapHelperA);
FScriptMapHelper::FIterator IteratorB(MapHelperB);
for (; IteratorA && IteratorB; ++IteratorA, ++IteratorB)
{
const FPropertySoftPath EntryIndexPath = FPropertySoftPath(RootPath, IteratorA.GetLogicalIndex());
DiffParameters.RootPath = FPropertySoftPath(EntryIndexPath, BPropAsMap->KeyProp);
IdenticalHelper(APropAsMap->KeyProp, BPropAsMap->KeyProp, MapHelperA.GetKeyPtr(IteratorA), MapHelperB.GetKeyPtr(IteratorB),
OwningOuterA, OwningOuterB, DiffParameters, DifferingSubProperties);
DiffParameters.RootPath = FPropertySoftPath(EntryIndexPath, BPropAsMap->ValueProp);
IdenticalHelper(APropAsMap->ValueProp, BPropAsMap->ValueProp, MapHelperA.GetValuePtr(IteratorA), MapHelperB.GetValuePtr(IteratorB),
OwningOuterA, OwningOuterB, DiffParameters, DifferingSubProperties);
}
}
else
{
DifferingSubProperties.Push(RootPath);
}
}
else if(APropAsObject != nullptr)
{
// Past container check, do a normal identical check now before going into components
if (AProperty->Identical(AValue, BValue, PPF_DeepComparison))
{
return;
}
const FObjectProperty* BPropAsObject = CastFieldChecked<const FObjectProperty>(BProperty);
const UObject* A = *((const UObject* const*)AValue);
const UObject* B = *((const UObject* const*)BValue);
// dig into the objects if they are in the same package as our initial object
if (A && B && A->GetClass() == B->GetClass() && OwningOuterA && OwningOuterB && A->IsIn(OwningOuterA) && B->IsIn(OwningOuterB))
{
const UClass* AClass = A->GetClass(); // BClass and AClass are identical!
// We only want to recurse if this is EditInlineNew and not a component
// Other instanced refs are likely to form a type-specific web so recursion doesn't make sense and won't be displayed properly in the details pane
if (AClass->HasAnyClassFlags(CLASS_EditInlineNew) && !AClass->IsChildOf(UActorComponent::StaticClass()))
{
for (TFieldIterator<FProperty> PropertyIt(AClass); PropertyIt; ++PropertyIt)
{
const FProperty* ClassProp = *PropertyIt;
DiffParameters.RootPath = FPropertySoftPath(RootPath, ClassProp);
const void* SubValueA = ClassProp->ContainerPtrToValuePtr<void>(A, 0);
const void* SubValueB = ClassProp->ContainerPtrToValuePtr<void>(B, 0);
IdenticalHelper(ClassProp, ClassProp, SubValueA, SubValueB,
OwningOuterA, OwningOuterB, DiffParameters, DifferingSubProperties);
}
}
else
{
const FString PathA = A->GetPathName(OwningOuterA);
const FString PathB = B->GetPathName(OwningOuterB);
if (PathA != PathB)
{
DifferingSubProperties.Push(RootPath);
}
}
}
else
{
DifferingSubProperties.Push(RootPath);
}
}
else
{
// Passed all container tests that would check for nested properties being wrong
if (AProperty->Identical(AValue, BValue, PPF_DeepComparison))
{
return;
}
DifferingSubProperties.Push(RootPath);
}
}
bool DiffUtils::Identical(const FResolvedProperty& AProp, const FResolvedProperty& BProp, const FPropertySoftPath& RootPath, TArray<FPropertySoftPath>& DifferingProperties)
{
if( AProp.Property == nullptr && BProp.Property == nullptr )
{
return true;
}
else if( AProp.Property == nullptr || BProp.Property == nullptr )
{
return false;
}
const void* AValue = AProp.Property->ContainerPtrToValuePtr<void>(AProp.Object);
const void* BValue = BProp.Property->ContainerPtrToValuePtr<void>(BProp.Object);
// We _could_ just ask the property for comparison but that would make the "identical" functions significantly more complex.
// Instead let's write a new function, specific to DiffUtils, that handles the sub properties
// NOTE: For Static Arrays, AValue and BValue were, and are, only references to the value at index 0. So changes to values past index 0 didn't show up before and
// won't show up now. Changes to index 0 will show up as a change to the entire array.
IdenticalHelper(AProp.Property, BProp.Property, AValue, BValue, nullptr, nullptr, FDiffParameters(RootPath), DifferingProperties);
return DifferingProperties.Num() == 0;
}
bool DiffUtils::Identical(const FResolvedProperty& AProp, const FResolvedProperty& BProp, const UObject* OwningOuterA,
const UObject* OwningOuterB)
{
TArray<FPropertySoftPath> DifferingProperties;
return Identical(AProp, BProp, OwningOuterA, OwningOuterB, FDiffParameters{}, DifferingProperties);
}
bool DiffUtils::Identical(const FResolvedProperty& AProp, const FResolvedProperty& BProp, const UObject* OwningOuterA,
const UObject* OwningOuterB, const FPropertySoftPath& RootPath, TArray<FPropertySoftPath>& DifferingProperties)
{
return Identical(AProp, BProp, OwningOuterA, OwningOuterB, FDiffParameters(RootPath), DifferingProperties);
}
bool DiffUtils::Identical(const FResolvedProperty& AProp, const FResolvedProperty& BProp, const UObject* OwningOuterA,
const UObject* OwningOuterB, FDiffParameters DiffParameters, TArray<FPropertySoftPath>& DifferingProperties)
{
if( AProp.Property == nullptr && BProp.Property == nullptr )
{
return true;
}
else if( AProp.Property == nullptr || BProp.Property == nullptr )
{
return false;
}
const void* AValue = AProp.Property->ContainerPtrToValuePtr<void>(AProp.Object);
const void* BValue = BProp.Property->ContainerPtrToValuePtr<void>(BProp.Object);
// note that we're not directly calling FProperty::Identical because sub-object properties should be weakly compared based on
// their paths instead of their pointers or data
IdenticalHelper(AProp.Property, BProp.Property, AValue, BValue, OwningOuterA, OwningOuterB, DiffParameters, DifferingProperties);
return DifferingProperties.Num() == 0;
}
bool DiffUtils::Identical(const TSharedPtr<IPropertyHandle>& PropertyHandleA, const TSharedPtr<IPropertyHandle>& PropertyHandleB,
const TArray<TWeakObjectPtr<UObject>>& OwningOutersA, const TArray<TWeakObjectPtr<UObject>>& OwningOutersB)
{
TArray<FPropertySoftPath> DifferingProperties;
return Identical(DifferingProperties, PropertyHandleA, PropertyHandleB, OwningOutersA, OwningOutersB);
}
bool DiffUtils::Identical(
TArray<FPropertySoftPath>& OutDifferingProperties,
const TSharedPtr<IPropertyHandle>& PropertyHandleA,
const TSharedPtr<IPropertyHandle>& PropertyHandleB,
const TArray<TWeakObjectPtr<UObject>>& OwningOutersA,
const TArray<TWeakObjectPtr<UObject>>& OwningOutersB
)
{
TArray<void*> ValuesA;
TArray<void*> ValuesB;
PropertyHandleA->AccessRawData(ValuesA);
PropertyHandleB->AccessRawData(ValuesB);
// if OwningOuters weren't provided, fallback to using the property handles to find them
TArray<UObject*> HandleOutersA;
TArray<UObject*> HandleOutersB;
if (OwningOutersA.IsEmpty())
{
PropertyHandleA->GetOuterObjects(HandleOutersA);
}
if (OwningOutersB.IsEmpty())
{
PropertyHandleB->GetOuterObjects(HandleOutersB);
}
if (!ensure(ValuesA.Num() == OwningOutersA.Num()))
{
// Outer count mismatch
return false;
}
if (!ensure(ValuesB.Num() == OwningOutersB.Num()))
{
// Outer count mismatch
return false;
}
auto IsIdenticalAtIndex = [&](int32 IndexA, int32 IndexB)
{
const void* ValueA = ValuesA[IndexA];
const void* ValueB = ValuesB[IndexB];
const UObject* OwningOuterA = OwningOutersA.IsEmpty() ? HandleOutersA[IndexA] : OwningOutersA[IndexA].Get();
const UObject* OwningOuterB = OwningOutersB.IsEmpty() ? HandleOutersB[IndexB] : OwningOutersB[IndexB].Get();
if (!OwningOuterA || !OwningOuterB)
{
// objects were Garbage Collected!
return !OwningOuterA && !OwningOuterB;
}
// note that we're not directly calling FProperty::Identical because sub-object properties should be weakly compared based on
// their paths instead of their pointers or data
FDiffParameters DiffParameters;
DiffParameters.bShouldDiffArrayElements = false;
IdenticalHelper(PropertyHandleA->GetProperty(), PropertyHandleB->GetProperty(), ValueA, ValueB,
OwningOuterA, OwningOuterB, DiffParameters, OutDifferingProperties);
return OutDifferingProperties.IsEmpty();
};
if (ValuesA.Num() == ValuesB.Num())
{
// compare AValues[I] with BValues[I]
for (int32 I = 0; I < ValuesA.Num(); ++I)
{
if (!IsIdenticalAtIndex(I,I))
{
return false;
}
}
}
else if (ValuesA.Num() == 1)
{
// compare AValues[0] with BValues[0...N]
for (int32 I = 0; I < ValuesB.Num(); ++I)
{
if (!IsIdenticalAtIndex(0,I))
{
return false;
}
}
}
else if (ValuesB.Num() == 1)
{
// compare BValues[0] with AValues[0...N]
for (int32 I = 0; I < ValuesA.Num(); ++I)
{
if (!IsIdenticalAtIndex(I,0))
{
return false;
}
}
}
else
{
// number of values doesn't match... this cannot be compared
return ensure(false);
}
return true;
}
TArray<FPropertySoftPath> DiffUtils::GetVisiblePropertiesInOrderDeclared(const UStruct* ForStruct, const FPropertySoftPath& Scope /*= TArray<FName>()*/)
{
TArray<FPropertySoftPath> Ret;
if (ForStruct)
{
TSet<FString> HiddenCategories = FEditorCategoryUtils::GetHiddenCategories(ForStruct);
for (TFieldIterator<FProperty> PropertyIt(ForStruct); PropertyIt; ++PropertyIt)
{
FName CategoryName = FObjectEditorUtils::GetCategoryFName(*PropertyIt);
if (!HiddenCategories.Contains(CategoryName.ToString()))
{
if (PropertyIt->PropertyFlags&CPF_Edit)
{
// We don't need to recurse into objects/structs as those will be picked up in the Identical check later
FPropertySoftPath NewPath(Scope, *PropertyIt);
Ret.Push(NewPath);
}
}
}
}
return Ret;
}
TArray<FPropertyPath> DiffUtils::ResolveAll(const UObject* Object, const TArray<FPropertySoftPath>& InSoftProperties)
{
TArray< FPropertyPath > Ret;
for (const auto& Path : InSoftProperties)
{
Ret.Push(Path.ResolvePath(Object));
}
return Ret;
}
TArray<FPropertyPath> DiffUtils::ResolveAll(const UObject* Object, const TArray<FSingleObjectDiffEntry>& InDifferences)
{
TArray< FPropertyPath > Ret;
for (const auto& Difference : InDifferences)
{
Ret.Push(Difference.Identifier.ResolvePath(Object));
}
return Ret;
}
UPackage* DiffUtils::LoadPackageForDiff(const FPackagePath& InTempPackagePath, const FPackagePath& InOriginalPackagePath)
{
// if this is a local asset, load it normally
if (!FPackageName::IsTempPackage(InTempPackagePath.GetPackageName()))
{
return LoadPackage(nullptr, *InTempPackagePath.GetPackageName(), LOAD_None);
}
// set up instancing context
FLinkerInstancingContext Context;
if (!InOriginalPackagePath.GetLocalFullPath().IsEmpty())
{
Context.AddTag(ULevel::DontLoadExternalObjectsTag);
Context.AddPackageMapping(InOriginalPackagePath.GetPackageFName(), InTempPackagePath.GetPackageFName());
}
return LoadPackage(nullptr, *InTempPackagePath.GetPackageName(),
LOAD_ForDiff | LOAD_DisableCompileOnLoad | LOAD_DisableEngineVersionChecks, nullptr, &Context);
}
UPackage* DiffUtils::LoadPackageForDiff(TSharedPtr<ISourceControlRevision> Revision)
{
FString TempFileName;
if(Revision->Get(TempFileName))
{
// Try and load that package
const FPackagePath TempPackagePath = FPackagePath::FromLocalPath(TempFileName);
const FPackagePath OriginalPackagePath = FPackagePath::FromLocalPath(Revision->GetFilename());
return LoadPackageForDiff(TempPackagePath, OriginalPackagePath);
}
return nullptr;
}
TSharedPtr<FBlueprintDifferenceTreeEntry> FBlueprintDifferenceTreeEntry::NoDifferencesEntry()
{
// This just generates a widget that tells the user that no differences were detected. Without this
// the treeview displaying differences is confusing when no differences are present because it is not obvious
// that the control is a treeview (a treeview with no children looks like a listview).
const auto GenerateWidget = []() -> TSharedRef<SWidget>
{
return SNew(STextBlock)
.ColorAndOpacity(FLinearColor(.7f, .7f, .7f))
.TextStyle(FAppStyle::Get(), TEXT("BlueprintDif.ItalicText"))
.Text(NSLOCTEXT("FBlueprintDifferenceTreeEntry", "NoDifferencesLabel", "No differences detected..."));
};
return TSharedPtr<FBlueprintDifferenceTreeEntry>(new FBlueprintDifferenceTreeEntry(
FOnDiffEntryFocused()
, FGenerateDiffEntryWidget::CreateStatic(GenerateWidget)
, TArray< TSharedPtr<FBlueprintDifferenceTreeEntry> >()
) );
}
TSharedPtr<FBlueprintDifferenceTreeEntry> FBlueprintDifferenceTreeEntry::UnknownDifferencesEntry()
{
// Warn about there being unknown differences
const auto GenerateWidget = []() -> TSharedRef<SWidget>
{
return SNew(STextBlock)
.ColorAndOpacity(FLinearColor(.7f, .7f, .7f))
.TextStyle(FAppStyle::Get(), TEXT("BlueprintDif.ItalicText"))
.Text(NSLOCTEXT("FBlueprintDifferenceTreeEntry", "BlueprintTypeNotSupported", "Warning: Detecting differences in this Blueprint type specific data is not yet supported..."));
};
return TSharedPtr<FBlueprintDifferenceTreeEntry>(new FBlueprintDifferenceTreeEntry(
FOnDiffEntryFocused()
, FGenerateDiffEntryWidget::CreateStatic(GenerateWidget)
, TArray< TSharedPtr<FBlueprintDifferenceTreeEntry> >()
));
}
TSharedPtr<FBlueprintDifferenceTreeEntry> FBlueprintDifferenceTreeEntry::CreateCategoryEntry(const FText& LabelText, const FText& ToolTipText, FOnDiffEntryFocused FocusCallback, const TArray< TSharedPtr<FBlueprintDifferenceTreeEntry> >& Children, bool bHasDifferences)
{
const auto CreateDefaultsRootEntry = [](FText LabelText, FText ToolTipText, FLinearColor Color) -> TSharedRef<SWidget>
{
return SNew(STextBlock)
.ToolTipText(ToolTipText)
.ColorAndOpacity(Color)
.Text(LabelText);
};
return TSharedPtr<FBlueprintDifferenceTreeEntry>(new FBlueprintDifferenceTreeEntry(
FocusCallback
, FGenerateDiffEntryWidget::CreateStatic(CreateDefaultsRootEntry, LabelText, ToolTipText, DiffViewUtils::LookupColor(bHasDifferences))
, Children
));
}
TSharedPtr<FBlueprintDifferenceTreeEntry> FBlueprintDifferenceTreeEntry::CreateCategoryEntryForMerge(const FText& LabelText, const FText& ToolTipText, FOnDiffEntryFocused FocusCallback, const TArray< TSharedPtr<FBlueprintDifferenceTreeEntry> >& Children, bool bHasRemoteDifferences, bool bHasLocalDifferences, bool bHasConflicts)
{
const auto CreateDefaultsRootEntry = [](FText LabelText, FText ToolTipText, bool bInHasRemoteDifferences, bool bInHasLocalDifferences, bool bInHasConflicts) -> TSharedRef<SWidget>
{
const FLinearColor BaseColor = DiffViewUtils::LookupColor(bInHasRemoteDifferences || bInHasLocalDifferences, bInHasConflicts);
return SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
[
SNew(STextBlock)
.ToolTipText(ToolTipText)
.ColorAndOpacity(BaseColor)
.Text(LabelText)
]
+ DiffViewUtils::Box(true, DiffViewUtils::LookupColor(bInHasRemoteDifferences, bInHasConflicts))
+ DiffViewUtils::Box(true, BaseColor)
+ DiffViewUtils::Box(true, DiffViewUtils::LookupColor(bInHasLocalDifferences, bInHasConflicts));
};
return TSharedPtr<FBlueprintDifferenceTreeEntry>(new FBlueprintDifferenceTreeEntry(
FocusCallback
, FGenerateDiffEntryWidget::CreateStatic(CreateDefaultsRootEntry, LabelText, ToolTipText, bHasRemoteDifferences, bHasLocalDifferences, bHasConflicts)
, Children
));
}
TSharedRef< STreeView<TSharedPtr< FBlueprintDifferenceTreeEntry > > > DiffTreeView::CreateTreeView(TArray< TSharedPtr<FBlueprintDifferenceTreeEntry> >* DifferencesList)
{
const auto RowGenerator = [](TSharedPtr< FBlueprintDifferenceTreeEntry > Entry, const TSharedRef<STableViewBase>& Owner) -> TSharedRef< ITableRow >
{
return SNew(STableRow<TSharedPtr<FBlueprintDifferenceTreeEntry> >, Owner)
[
Entry->GenerateWidget.Execute()
];
};
const auto ChildrenAccessor = [](TSharedPtr<FBlueprintDifferenceTreeEntry> InTreeItem, TArray< TSharedPtr< FBlueprintDifferenceTreeEntry > >& OutChildren)
{
OutChildren = InTreeItem->Children;
};
const auto Selector = [](TSharedPtr<FBlueprintDifferenceTreeEntry> InTreeItem, ESelectInfo::Type Type)
{
if (InTreeItem.IsValid())
{
InTreeItem->OnFocus.ExecuteIfBound();
}
};
return SNew(STreeView< TSharedPtr< FBlueprintDifferenceTreeEntry > >)
.OnGenerateRow(STreeView< TSharedPtr< FBlueprintDifferenceTreeEntry > >::FOnGenerateRow::CreateStatic(RowGenerator))
.OnGetChildren(STreeView< TSharedPtr< FBlueprintDifferenceTreeEntry > >::FOnGetChildren::CreateStatic(ChildrenAccessor))
.OnSelectionChanged(STreeView< TSharedPtr< FBlueprintDifferenceTreeEntry > >::FOnSelectionChanged::CreateStatic(Selector))
.TreeItemsSource(DifferencesList);
}
int32 DiffTreeView::CurrentDifference(TSharedRef< STreeView<TSharedPtr< FBlueprintDifferenceTreeEntry > > > TreeView, const TArray< TSharedPtr<class FBlueprintDifferenceTreeEntry> >& Differences)
{
auto SelectedItems = TreeView->GetSelectedItems();
if (SelectedItems.Num() == 0)
{
return INDEX_NONE;
}
for (int32 Iter = 0; Iter < SelectedItems.Num(); ++Iter)
{
int32 Index = Differences.Find(SelectedItems[Iter]);
if (Index != INDEX_NONE)
{
return Index;
}
}
return INDEX_NONE;
}
void DiffTreeView::HighlightNextDifference(TSharedRef< STreeView<TSharedPtr< FBlueprintDifferenceTreeEntry > > > TreeView, const TArray< TSharedPtr<class FBlueprintDifferenceTreeEntry> >& Differences, const TArray< TSharedPtr<class FBlueprintDifferenceTreeEntry> >& RootDifferences)
{
int32 CurrentIndex = CurrentDifference(TreeView, Differences);
auto Next = Differences[CurrentIndex + 1];
// we have to manually expand our parent:
for (auto& Test : RootDifferences)
{
if (Test->Children.Contains(Next))
{
TreeView->SetItemExpansion(Test, true);
break;
}
}
TreeView->SetSelection(Next);
TreeView->RequestScrollIntoView(Next);
}
void DiffTreeView::HighlightPrevDifference(TSharedRef< STreeView<TSharedPtr< FBlueprintDifferenceTreeEntry > > > TreeView, const TArray< TSharedPtr<class FBlueprintDifferenceTreeEntry> >& Differences, const TArray< TSharedPtr<class FBlueprintDifferenceTreeEntry> >& RootDifferences)
{
int32 CurrentIndex = CurrentDifference(TreeView, Differences);
auto Prev = Differences[CurrentIndex - 1];
// we have to manually expand our parent:
for (auto& Test : RootDifferences)
{
if (Test->Children.Contains(Prev))
{
TreeView->SetItemExpansion(Test, true);
break;
}
}
TreeView->SetSelection(Prev);
TreeView->RequestScrollIntoView(Prev);
}
bool DiffTreeView::HasNextDifference(TSharedRef< STreeView<TSharedPtr< FBlueprintDifferenceTreeEntry > > > TreeView, const TArray< TSharedPtr<class FBlueprintDifferenceTreeEntry> >& Differences)
{
int32 CurrentIndex = CurrentDifference(TreeView, Differences);
return Differences.IsValidIndex(CurrentIndex + 1);
}
bool DiffTreeView::HasPrevDifference(TSharedRef< STreeView<TSharedPtr< FBlueprintDifferenceTreeEntry > > > TreeView, const TArray< TSharedPtr<class FBlueprintDifferenceTreeEntry> >& Differences)
{
int32 CurrentIndex = CurrentDifference(TreeView, Differences);
return Differences.IsValidIndex(CurrentIndex - 1);
}
FLinearColor DiffViewUtils::LookupColor(bool bDiffers, bool bConflicts)
{
if( bConflicts )
{
return DiffViewUtils::Conflicting();
}
else if( bDiffers )
{
return DiffViewUtils::Differs();
}
else
{
return DiffViewUtils::Identical();
}
}
FLinearColor DiffViewUtils::Differs()
{
// yellow color
return FLinearColor(0.85f,0.71f,0.25f);
}
FLinearColor DiffViewUtils::Identical()
{
const static FLinearColor ForegroundColor = FAppStyle::GetColor("Graph.ForegroundColor");
return ForegroundColor;
}
FLinearColor DiffViewUtils::Missing()
{
// blue color
return FLinearColor(0.3f,0.3f,1.f);
}
FLinearColor DiffViewUtils::Conflicting()
{
// red color
return FLinearColor(1.0f,0.2f,0.3f);
}
FText DiffViewUtils::PropertyDiffMessage(FSingleObjectDiffEntry Difference, FText ObjectName)
{
FText Message;
FString PropertyName = Difference.Identifier.ToDisplayName();
switch (Difference.DiffType)
{
case EPropertyDiffType::PropertyAddedToA:
Message = FText::Format(NSLOCTEXT("DiffViewUtils", "PropertyValueChange_Removed", "{0} removed from {1}"), FText::FromString(PropertyName), ObjectName);
break;
case EPropertyDiffType::PropertyAddedToB:
Message = FText::Format(NSLOCTEXT("DiffViewUtils", "PropertyValueChange_Added", "{0} added to {1}"), FText::FromString(PropertyName), ObjectName);
break;
case EPropertyDiffType::PropertyValueChanged:
Message = FText::Format(NSLOCTEXT("DiffViewUtils", "PropertyValueChange", "{0} changed value in {1}"), FText::FromString(PropertyName), ObjectName);
break;
}
return Message;
}
FText DiffViewUtils::SCSDiffMessage(const FSCSDiffEntry& Difference, FText ObjectName)
{
const FText NodeName = FText::FromName(Difference.TreeIdentifier.Name);
FText Text;
switch (Difference.DiffType)
{
case ETreeDiffType::NODE_ADDED:
Text = FText::Format(NSLOCTEXT("DiffViewUtils", "NodeAdded", "Added Node {0} to {1}"), NodeName, ObjectName);
break;
case ETreeDiffType::NODE_REMOVED:
Text = FText::Format(NSLOCTEXT("DiffViewUtils", "NodeRemoved", "Removed Node {0} from {1}"), NodeName, ObjectName);
break;
case ETreeDiffType::NODE_TYPE_CHANGED:
Text = FText::Format(NSLOCTEXT("DiffViewUtils", "NodeTypeChanged", "Node {0} changed type in {1}"), NodeName, ObjectName);
break;
case ETreeDiffType::NODE_PROPERTY_CHANGED:
Text = FText::Format(NSLOCTEXT("DiffViewUtils", "NodePropertyChanged", "{0} on {1}"), DiffViewUtils::PropertyDiffMessage(Difference.PropertyDiff, NodeName), ObjectName);
break;
case ETreeDiffType::NODE_MOVED:
Text = FText::Format(NSLOCTEXT("DiffViewUtils", "NodeMoved", "Moved Node {0} in {1}"), NodeName, ObjectName);
break;
case ETreeDiffType::NODE_CORRUPTED:
Text = FText::Format(NSLOCTEXT("DiffViewUtils", "NodeCorrupted", "Node {0} in {1} has corrupt outer - must be recreated"), NodeName, ObjectName);
break;
case ETreeDiffType::NODE_FIXED:
Text = FText::Format(NSLOCTEXT("DiffViewUtils", "NodeFixed", "Node {0} in {1} has been recreated to correct outer"), NodeName, ObjectName);
break;
}
return Text;
}
FText DiffViewUtils::GetPanelLabel(const UObject* Asset, const FRevisionInfo& Revision, FText Label)
{
if( !Asset )
{
return NSLOCTEXT("DiffViewUtils", "NoBlueprint", "None" );
}
if( !Revision.Revision.IsEmpty() )
{
FText RevisionData;
if(ISourceControlModule::Get().GetProvider().UsesChangelists())
{
RevisionData = FText::Format(NSLOCTEXT("DiffViewUtils", "RevisionData", "Revision {0} - CL {1} - {2}")
, FText::FromString(Revision.Revision)
, FText::AsNumber(Revision.Changelist, &FNumberFormattingOptions::DefaultNoGrouping())
, FText::AsDateTime(Revision.Date));
}
else
{
RevisionData = FText::Format(NSLOCTEXT("DiffViewUtils", "RevisionDataNoChangelist", "Revision {0} - {1}")
, FText::FromString(Revision.Revision)
, FText::AsDateTime(Revision.Date));
}
if (Label.IsEmpty())
{
return FText::Format(NSLOCTEXT("DiffViewUtils", "RevisionLabelTwoLines", "{0}\n{1}")
, FText::FromString(Asset->GetName())
, RevisionData);
}
else
{
return FText::Format(NSLOCTEXT("DiffViewUtils", "RevisionLabel", "{0}\n{1}\n{2}")
, Label
, FText::FromString(Asset->GetName())
, RevisionData);
}
}
else
{
if (Label.IsEmpty())
{
return FText::Format(NSLOCTEXT("DiffViewUtils", "RevisionLabelTwoLines", "{0}\n{1}")
, FText::FromString(Asset->GetName())
, NSLOCTEXT("DiffViewUtils", "LocalRevisionLabel", "Local Revision"));
}
else
{
return FText::Format(NSLOCTEXT("DiffViewUtils", "RevisionLabel", "{0}\n{1}\n{2}")
, Label
, FText::FromString(Asset->GetName())
, NSLOCTEXT("DiffViewUtils", "LocalRevisionLabel", "Local Revision"));
}
}
}
SHorizontalBox::FSlot::FSlotArguments DiffViewUtils::Box(bool bIsPresent, FLinearColor Color)
{
return MoveTemp(SHorizontalBox::Slot()
.AutoWidth()
.HAlign(HAlign_Right)
.VAlign(VAlign_Center)
.Padding(0.5f, 0.f)
[
SNew(SImage)
.ColorAndOpacity(Color)
.Image(bIsPresent ? FAppStyle::GetBrush("BlueprintDif.HasGraph") : FAppStyle::GetBrush("BlueprintDif.MissingGraph"))
]);
};