// Copyright Epic Games, Inc. All Rights Reserved. #include "AsyncStateTreeDiff.h" #include "DiffUtils.h" #include "SStateTreeView.h" #include "StateTreeDiffHelper.h" #include "StateTreeEditorData.h" #include "StateTreeState.h" #include "StateTreeViewModel.h" namespace UE::StateTree::Diff { static bool AreObjectsEqual(const UObject* ObjectA, const UObject* ObjectB) { if (!ObjectA || !ObjectB) { return ObjectA == ObjectB; } if (ObjectA->GetClass() != ObjectB->GetClass()) { return false; } if (ObjectA != ObjectB) { const FProperty* ClassProperty = ObjectA->GetClass()->PropertyLink; while (ClassProperty) { if (!ClassProperty->Identical_InContainer(ObjectA, ObjectB, /*ArrayIndex*/ 0, PPF_DeepComparison | PPF_ForDiff)) { return false; } ClassProperty = ClassProperty->PropertyLinkNext; } } return true; } static bool AreNodesEqual(const FStateTreeEditorNode& NodeA, const FStateTreeEditorNode& NodeB) { return AreObjectsEqual(NodeA.InstanceObject.Get(), NodeB.InstanceObject.Get()) && NodeA.Node.Identical(&NodeB.Node, PPF_DeepComparison | PPF_ForDiff) && NodeA.Instance.Identical(&NodeB.Instance, PPF_DeepComparison | PPF_ForDiff) && NodeA.ExpressionIndent == NodeB.ExpressionIndent && NodeA.ExpressionOperand == NodeB.ExpressionOperand; } static bool AreNodeArraysEqual(const TArray& ArrayA, const TArray& ArrayB) { const int32 Count = ArrayA.Num(); if (Count != ArrayB.Num()) { return false; } for (int32 Index = 0; Index < Count; Index++) { if (!AreNodesEqual(ArrayA[Index], ArrayB[Index])) { return false; } } return true; } static bool AreStateTreeStatePropertyBagsEqual(const FInstancedPropertyBag& ParametersA, const FInstancedPropertyBag& ParametersB) { if (ParametersA.GetNumPropertiesInBag() != ParametersB.GetNumPropertiesInBag()) { return false; } const UPropertyBag* BagA = ParametersA.GetPropertyBagStruct(); const UPropertyBag* BagB = ParametersB.GetPropertyBagStruct(); if (!BagA || !BagB) { return BagA == BagB; } const TConstArrayView DescsA = BagA->GetPropertyDescs(); const TConstArrayView DescsB = BagB->GetPropertyDescs(); const int32 Count = DescsA.Num(); for (int32 Index = 0; Index < Count; Index++) { if (DescsA[Index].Name != DescsB[Index].Name || !DescsA[Index].CompatibleType(DescsB[Index])) { return false; } const FName Name = DescsA[Index].Name; TValueOrError SerializedA = ParametersA.GetValueSerializedString(Name); TValueOrError SerializedB = ParametersB.GetValueSerializedString(Name); if (SerializedA.HasError() || SerializedB.HasError()) { return false; } if (SerializedA.HasValue() != SerializedB.HasValue()) { return false; } if (SerializedA.GetValue() != SerializedB.GetValue()) { return false; } } return true; } static bool AreStateTreeStateParametersEqual(const FStateTreeStateParameters& ParametersA, const FStateTreeStateParameters& ParametersB) { if (!AreStateTreeStatePropertyBagsEqual(ParametersA.Parameters, ParametersB.Parameters)) { return false; } if (ParametersA.PropertyOverrides != ParametersB.PropertyOverrides) { return false; } return true; } static bool ArePropertiesEqual(const UStateTreeState* StateA, const UStateTreeState* StateB) { if (!StateA || !StateB) { return StateA == StateB; } return StateA->Name == StateB->Name && StateA->Tag == StateB->Tag && StateA->ColorRef == StateB->ColorRef && StateA->Type == StateB->Type && StateA->SelectionBehavior == StateB->SelectionBehavior; } static bool AreParametersEqual(const UStateTreeState* StateA, const UStateTreeState* StateB) { return AreStateTreeStateParametersEqual(StateA->Parameters, StateB->Parameters); } static bool AreConditionsEqual(const UStateTreeState* StateA, const UStateTreeState* StateB) { return AreNodeArraysEqual(StateA->EnterConditions, StateB->EnterConditions); } static bool AreConsiderationsEqual(const UStateTreeState* StateA, const UStateTreeState* StateB) { return AreNodeArraysEqual(StateA->Considerations, StateB->Considerations); } static bool AreTasksEqual(const UStateTreeState* StateA, const UStateTreeState* StateB) { return AreNodeArraysEqual(StateA->Tasks, StateB->Tasks); } static bool AreTransitionsEqual(const UStateTreeState* StateA, const UStateTreeState* StateB) { if (StateA->Transitions.Num() != StateB->Transitions.Num()) { return false; } for (int32 Index = 0; Index < StateA->Transitions.Num(); Index++) { const FStateTreeTransition* TransitionA = &StateA->Transitions[Index]; const FStateTreeTransition* TransitionB = &StateB->Transitions[Index]; if (!TransitionA || !TransitionB) { return TransitionA == TransitionB; } // Not checking transitions on IDs const bool bEqual = FStateTreeTransition::StaticStruct()->CompareScriptStruct(TransitionB, TransitionB, 0); if (!bEqual) { return false; } } return true; } static bool AreStateTreePropertiesEqual(const UStateTreeEditorData* StateTreeDataA, const UStateTreeEditorData* StateTreeDataB) { // Check the differences in Bindings bool bBindingsEqual = StateTreeDataA->EditorBindings.GetBindings().Num() == StateTreeDataB->EditorBindings.GetBindings().Num(); if (bBindingsEqual) { for (const FPropertyBindingBinding& PropertyPathBinding : StateTreeDataA->EditorBindings.GetBindings()) { const FPropertyBindingPath& PropertyPathTarget = PropertyPathBinding.GetTargetPath(); if (StateTreeDataB->EditorBindings.HasBinding(PropertyPathBinding.GetTargetPath())) { if (*StateTreeDataA->EditorBindings.GetBindingSource(PropertyPathTarget) != *StateTreeDataB->EditorBindings.GetBindingSource(PropertyPathTarget)) { bBindingsEqual = false; break; } } else { bBindingsEqual = false; break; } } } if (!bBindingsEqual) { return false; } // Check the differences in Evaluators and Tasks if (!AreNodeArraysEqual(StateTreeDataA->Evaluators, StateTreeDataB->Evaluators)) { return false; } if (!AreNodeArraysEqual(StateTreeDataA->GlobalTasks, StateTreeDataB->GlobalTasks)) { return false; } // Check the differences in StateTree root level parameters if (!AreStateTreeStatePropertyBagsEqual(StateTreeDataA->GetRootParametersPropertyBag(), StateTreeDataB->GetRootParametersPropertyBag())) { return false; } return true; } static FPropertySoftPath GetPropertyPath(const FPropertyBindingPath& StateTreePropertyPath, const UStateTreeState* StateTreeState) { TArray Path; auto CheckNodes = [&StateTreePropertyPath, &Path](const TArray& List, FName PathSegmentName) { for (int i = 0; i < List.Num(); i++) { if (List[i].ID == StateTreePropertyPath.GetStructID()) { Path.Add(PathSegmentName); Path.Add(FName(FString::FromInt(i))); if (List[i].InstanceObject) { Path.Add(FName("InstanceObject")); } else { Path.Add(FName("Instance")); } return true; } } return false; }; auto CheckTransitions = [StateTreePropertyPath, &Path](const TArray& List, FName PathSegmentName) { for (int i = 0; i < List.Num(); i++) { if (List[i].ID == StateTreePropertyPath.GetStructID()) { Path.Add(PathSegmentName); Path.Add(FName(FString::FromInt(i))); return true; } } return false; }; if (CheckNodes(StateTreeState->EnterConditions, "EnterConditions") || CheckNodes(StateTreeState->Tasks, "Tasks") || CheckTransitions(StateTreeState->Transitions, "Transitions")) { for (const FPropertyBindingPathSegment& PropertySegment : StateTreePropertyPath.GetSegments()) { Path.Add(PropertySegment.GetName()); } } return FPropertySoftPath(Path); } static void GetBindingsDifferences(UStateTreeEditorData* StateTreeDataA, UStateTreeEditorData* StateTreeDataB, TArray& OutDiffEntries) { struct FBindingDiff { FPropertyBindingPath TargetPath; FPropertyBindingPath SourcePathA; FPropertyBindingPath SourcePathB; }; TArray BindingDiffs; // Check the differences in Bindings for (const FPropertyBindingBinding& PropertyPathBinding : StateTreeDataA->EditorBindings.GetBindings()) { FPropertyBindingPath PropertyPathTarget = PropertyPathBinding.GetTargetPath(); FPropertyBindingPath PropertyPathSource = PropertyPathBinding.GetSourcePath(); FBindingDiff Entry; Entry.TargetPath = PropertyPathTarget; Entry.SourcePathA = PropertyPathSource; BindingDiffs.Add(Entry); } for (const FPropertyBindingBinding& PropertyPathBinding : StateTreeDataB->EditorBindings.GetBindings()) { FPropertyBindingPath PropertyPathTarget = PropertyPathBinding.GetTargetPath(); FPropertyBindingPath PropertyPathSource = PropertyPathBinding.GetSourcePath(); bool bFound = false; for (FBindingDiff& Diff : BindingDiffs) { if (Diff.TargetPath == PropertyPathTarget) { Diff.SourcePathB = PropertyPathSource; bFound = true; break; } } if (!bFound) { FBindingDiff Entry; Entry.TargetPath = PropertyPathTarget; Entry.SourcePathB = PropertyPathSource; BindingDiffs.Add(Entry); } } for (const FBindingDiff& DiffEntry : BindingDiffs) { if (DiffEntry.SourcePathA != DiffEntry.SourcePathB) { const UStateTreeState* TargetStateA = StateTreeDataA->GetStateByStructID(DiffEntry.TargetPath.GetStructID()); const UStateTreeState* TargetStateB = StateTreeDataB->GetStateByStructID(DiffEntry.TargetPath.GetStructID()); if (TargetStateA && TargetStateB) { FStateSoftPath StatePathA(TargetStateA); FStateSoftPath StatePathB(TargetStateB); FPropertySoftPath PropertyPath = GetPropertyPath(DiffEntry.TargetPath, TargetStateA); EStateDiffType StateTreeDiffType = EStateDiffType::BindingChanged; if (DiffEntry.SourcePathA.IsPathEmpty()) { StateTreeDiffType = EStateDiffType::BindingAddedToB; } else if (DiffEntry.SourcePathB.IsPathEmpty()) { StateTreeDiffType = EStateDiffType::BindingAddedToA; } OutDiffEntries.Add(FSingleDiffEntry(StatePathA, StatePathB, StateTreeDiffType, PropertyPath)); } } } } FAsyncDiff::FAsyncDiff(const TSharedRef& LeftTree, const TSharedRef& RightTree) : TAsyncTreeDifferences(RootNodesAttribute(LeftTree), RootNodesAttribute(RightTree)) , LeftView(LeftTree) , RightView(RightTree) {} TAttribute>> FAsyncDiff::RootNodesAttribute(TWeakPtr StateTreeView) { return TAttribute>>::CreateLambda([StateTreeView]() { if (const TSharedPtr TreeView = StaticCastSharedPtr(StateTreeView.Pin())) { TArray> SubTrees; TreeView->GetViewModel()->GetSubTrees(SubTrees); return SubTrees; } return TArray>(); }); } void FAsyncDiff::GetStatesDifferences(TArray& OutDiffEntries) const { TArray RemovedStates; TArray AddedStates; ForEach(ETreeTraverseOrder::PreOrder, [&](const TUniquePtr& Node) -> ETreeTraverseControl { FStateSoftPath StatePath; UStateTreeState* LeftState = Node->ValueA.Get(); UStateTreeState* RightState = Node->ValueB.Get(); if (LeftState) { StatePath = FStateSoftPath(LeftState); } else if (RightState) { StatePath = FStateSoftPath(RightState); } EStateDiffType StateTreeDiffType = EStateDiffType::Invalid; bool bSkipChildren = false; switch (Node->DiffResult) { case ETreeDiffResult::MissingFromTree1: StateTreeDiffType = EStateDiffType::StateAddedToB; AddedStates.Add(StatePath.ToDisplayName(true)); if (RemovedStates.Contains(StatePath.ToDisplayName(true))) { StateTreeDiffType = EStateDiffType::StateMoved; } bSkipChildren = true; break; case ETreeDiffResult::MissingFromTree2: StateTreeDiffType = EStateDiffType::StateAddedToA; RemovedStates.Add(StatePath.ToDisplayName(true)); if (AddedStates.Contains(StatePath.ToDisplayName(true))) { StateTreeDiffType = EStateDiffType::StateMoved; } bSkipChildren = true; break; case ETreeDiffResult::DifferentValues: StateTreeDiffType = EStateDiffType::StateChanged; break; case ETreeDiffResult::Identical: StateTreeDiffType = EStateDiffType::Identical; if (LeftState && RightState) { if (LeftState->bEnabled != RightState->bEnabled) { StateTreeDiffType = RightState->bEnabled ? EStateDiffType::StateEnabled : EStateDiffType::StateDisabled; } } break; default: return ETreeTraverseControl::Continue; } if (StateTreeDiffType == EStateDiffType::Identical) { return ETreeTraverseControl::Continue; } if (StateTreeDiffType == EStateDiffType::StateMoved) { for (FSingleDiffEntry& DiffEntry : OutDiffEntries) { if (DiffEntry.Identifier.ToDisplayName(true) == StatePath.ToDisplayName(true)) { if (DiffEntry.DiffType == EStateDiffType::StateAddedToA) { DiffEntry.SecondaryIdentifier = StatePath; } else { DiffEntry.SecondaryIdentifier = DiffEntry.Identifier; DiffEntry.Identifier = StatePath; } DiffEntry.DiffType = EStateDiffType::StateMoved; // For now, we are skipping children, we may need to revisit that return ETreeTraverseControl::SkipChildren; } } } OutDiffEntries.Add(FSingleDiffEntry(StatePath, StateTreeDiffType)); return bSkipChildren ? ETreeTraverseControl::SkipChildren : ETreeTraverseControl::Continue; }); } void FAsyncDiff::GetStateTreeDifferences(TArray& OutDiffEntries) const { if (LeftView && RightView) { const FStateTreeViewModel* LeftViewModel = LeftView->GetViewModel().Get(); const FStateTreeViewModel* RightViewModel = RightView->GetViewModel().Get(); if (LeftViewModel && RightViewModel) { UStateTreeEditorData* LeftEditorData = Cast(LeftViewModel->GetStateTree()->EditorData); UStateTreeEditorData* RightEditorData = Cast(RightViewModel->GetStateTree()->EditorData); if (!AreStateTreePropertiesEqual(LeftEditorData, RightEditorData)) { OutDiffEntries.Add(FSingleDiffEntry( /*Identifier*/FStateSoftPath(), EStateDiffType::StateTreePropertiesChanged)); } GetStatesDifferences(OutDiffEntries); GetBindingsDifferences(LeftEditorData, RightEditorData, OutDiffEntries); } } } } // UE::AsyncStateTreeDiff bool TTreeDiffSpecification>::AreValuesEqual(const TWeakObjectPtr& StateTreeNodeA, const TWeakObjectPtr& StateTreeNodeB, TArray*) const { const TStrongObjectPtr StrongStateA = StateTreeNodeA.Pin(); const TStrongObjectPtr StrongStateB = StateTreeNodeB.Pin(); const UStateTreeState* StateA = StrongStateA.Get(); const UStateTreeState* StateB = StrongStateB.Get(); if (!StateA || !StateB) { return StateA == StateB; } using namespace UE::StateTree::Diff; return ArePropertiesEqual(StateA, StateB) && AreParametersEqual(StateA, StateB) && AreConditionsEqual(StateA, StateB) && AreTasksEqual(StateA, StateB) && AreTransitionsEqual(StateA, StateB) && AreConsiderationsEqual(StateA, StateB); } bool TTreeDiffSpecification>::AreMatching(const TWeakObjectPtr& StateTreeNodeA, const TWeakObjectPtr& StateTreeNodeB, TArray*) const { const TStrongObjectPtr StrongStateA = StateTreeNodeA.Pin(); const TStrongObjectPtr StrongStateB = StateTreeNodeB.Pin(); const UStateTreeState* StateA = StrongStateA.Get(); const UStateTreeState* StateB = StrongStateB.Get(); if (!StateA || !StateB) { return StateA == StateB; } return StateA->ID == StateB->ID; } void TTreeDiffSpecification>::GetChildren(const TWeakObjectPtr& InParent, TArray>& OutChildren) const { const TStrongObjectPtr StrongParent = InParent.Pin(); if (UStateTreeState* InParentPtr = StrongParent.Get()) { OutChildren.Reserve(InParentPtr->Children.Num()); for (const TObjectPtr& Child : InParentPtr->Children) { OutChildren.Add(Child); } } }