// Copyright Epic Games, Inc. All Rights Reserved. #include "StateTreeViewModel.h" #include "Customizations/StateTreeEditorNodeUtils.h" #include "StateTree.h" #include "StateTreeEditorData.h" #include "StateTreeDelegates.h" #include "Debugger/StateTreeDebugger.h" #include "Editor.h" #include "ScopedTransaction.h" #include "HAL/PlatformApplicationMisc.h" #include "UnrealExporter.h" #include "Exporters/Exporter.h" #include "Factories.h" #include "StateTreeTaskBase.h" #include "StateTreeConditionBase.h" #include "StateTreeConsiderationBase.h" #include "StateTreePropertyHelpers.h" #include "StateTreeScopedEditorDataFixer.h" #include "Misc/NotNull.h" #define LOCTEXT_NAMESPACE "StateTreeEditor" namespace UE::StateTree::Editor { class FStateTreeStateTextFactory : public FCustomizableTextObjectFactory { public: FStateTreeStateTextFactory() : FCustomizableTextObjectFactory(GWarn) {} virtual bool CanCreateClass(UClass* InObjectClass, bool& bOmitSubObjs) const override { return InObjectClass->IsChildOf(UStateTreeState::StaticClass()) || InObjectClass->IsChildOf(UStateTreeClipboardBindings::StaticClass()); } virtual void ProcessConstructedObject(UObject* NewObject) override { if (UStateTreeState* State = Cast(NewObject)) { States.Add(State); } else if (UStateTreeClipboardBindings* Bindings = Cast(NewObject)) { ClipboardBindings = Bindings; } } public: TArray States; UStateTreeClipboardBindings* ClipboardBindings = nullptr; }; void CollectBindingsCopiesRecursive(UStateTreeEditorData* TreeData, UStateTreeState* State, TArray& AllBindings) { if (!State) { return; } TreeData->VisitStateNodes(*State, [TreeData, &AllBindings](const UStateTreeState* State, const FStateTreeBindableStructDesc& Desc, const FStateTreeDataView Value) { TArray NodeBindings; TreeData->GetPropertyEditorBindings()->FPropertyBindingBindingCollection::GetBindingsFor(Desc.ID, NodeBindings); Algo::Transform(NodeBindings, AllBindings, [](const FPropertyBindingBinding* BindingPtr) { return *static_cast(BindingPtr); }); return EStateTreeVisitor::Continue; }); for (UStateTreeState* ChildState : State->Children) { CollectBindingsCopiesRecursive(TreeData, ChildState, AllBindings); } } FString ExportStatesToText(UStateTreeEditorData* TreeData, const TArrayView States) { if (States.IsEmpty()) { return FString(); } // Clear the mark state for saving. UnMarkAllObjects(EObjectMark(OBJECTMARK_TagExp | OBJECTMARK_TagImp)); FStringOutputDevice Archive; const FExportObjectInnerContext Context; UStateTreeClipboardBindings* ClipboardBindings = NewObject(); check(ClipboardBindings); for (UStateTreeState* State : States) { UObject* ThisOuter = State->GetOuter(); UExporter::ExportToOutputDevice(&Context, State, nullptr, Archive, TEXT("copy"), 0, PPF_ExportsNotFullyQualified | PPF_Copy | PPF_Delimited, false, ThisOuter); CollectBindingsCopiesRecursive(TreeData, State, ClipboardBindings->Bindings); } UExporter::ExportToOutputDevice(&Context, ClipboardBindings, nullptr, Archive, TEXT("copy"), 0, PPF_ExportsNotFullyQualified | PPF_Copy | PPF_Delimited, false); return *Archive; } void CollectStateLinks(const UStruct* Struct, void* Memory, TArray& Links) { for (TPropertyValueIterator It(Struct, Memory); It; ++It) { if (It->Key->Struct == TBaseStructure::Get()) { FStateTreeStateLink* StateLink = static_cast(const_cast(It->Value)); Links.Add(StateLink); } } } // todo: Should refactor it into FStateTreeScopedEditorDataFixer void FixNodesAfterDuplication(TArrayView Nodes, TMap& IDsMap, TArray& Links) { for (FStateTreeEditorNode& Node : Nodes) { const FGuid NewNodeID = FGuid::NewGuid(); IDsMap.Emplace(Node.ID, NewNodeID); Node.ID = NewNodeID; if (Node.Node.IsValid()) { CollectStateLinks(Node.Node.GetScriptStruct(), Node.Node.GetMutableMemory(), Links); } if (Node.Instance.IsValid()) { CollectStateLinks(Node.Instance.GetScriptStruct(), Node.Instance.GetMutableMemory(), Links); } if (Node.InstanceObject) { CollectStateLinks(Node.InstanceObject->GetClass(), Node.InstanceObject, Links); } } } // todo: Should refactor it into FStateTreeScopedEditorDataFixer void FixStateAfterDuplication(UStateTreeState* State, UStateTreeState* NewParentState, TMap& IDsMap, TArray& Links, TArray& NewStates) { State->Modify(); const FGuid NewStateID = FGuid::NewGuid(); IDsMap.Emplace(State->ID, NewStateID); State->ID = NewStateID; const FGuid NewParametersID = FGuid::NewGuid(); IDsMap.Emplace(State->Parameters.ID, NewParametersID); State->Parameters.ID = NewParametersID; State->Parent = NewParentState; NewStates.Add(State); if (State->Type == EStateTreeStateType::Linked) { Links.Emplace(&State->LinkedSubtree); } FixNodesAfterDuplication(TArrayView(&State->SingleTask, 1), IDsMap, Links); FixNodesAfterDuplication(State->Tasks, IDsMap, Links); FixNodesAfterDuplication(State->EnterConditions, IDsMap, Links); FixNodesAfterDuplication(State->Considerations, IDsMap, Links); for (FStateTreeTransition& Transition : State->Transitions) { // Transition Ids are not used by nodes so no need to add to 'IDsMap' Transition.ID = FGuid::NewGuid(); FixNodesAfterDuplication(Transition.Conditions, IDsMap, Links); Links.Emplace(&Transition.State); } for (UStateTreeState* Child : State->Children) { FixStateAfterDuplication(Child, State, IDsMap, Links, NewStates); } } // Removes states from the array which are children of any other state. void RemoveContainedChildren(TArray& States) { TSet UniqueStates; for (UStateTreeState* State : States) { UniqueStates.Add(State); } for (int32 i = 0; i < States.Num(); ) { UStateTreeState* State = States[i]; // Walk up the parent state sand if the current state // exists in any of them, remove it. UStateTreeState* StateParent = State->Parent; bool bShouldRemove = false; while (StateParent) { if (UniqueStates.Contains(StateParent)) { bShouldRemove = true; break; } StateParent = StateParent->Parent; } if (bShouldRemove) { States.RemoveAt(i); } else { i++; } } } // Returns true if the state is child of parent state. bool IsChildOf(const UStateTreeState* ParentState, const UStateTreeState* State) { for (const UStateTreeState* Child : ParentState->Children) { if (Child == State) { return true; } if (IsChildOf(Child, State)) { return true; } } return false; } namespace Private { /* Short-lived helper struct for node manipulation in the editor */ struct FStateTreeStateNodeEditorHandle { const TNotNull EditorData; const TNotNull OwnerState; private: FStringView NodePath; int32 ArrayIndex = INDEX_NONE; void* TargetArray = nullptr; void* TargetNode = nullptr; bool bIsTransition = false; public: FStateTreeStateNodeEditorHandle(const TNotNull InEditorData, const TNotNull InOwnerState, const FGuid& NodeID) : EditorData(InEditorData), OwnerState(InOwnerState) { auto FindNode = [Self = this, &NodeID](T& Nodes, FStringView Path) { for (int32 Index = 0; Index < Nodes.Num(); ++Index) { if (NodeID == Nodes[Index].ID) { Self->ArrayIndex = Index; Self->TargetArray = &Nodes; Self->TargetNode = &Nodes[Self->ArrayIndex]; Self->NodePath = Path; return true; } } return false; }; if (!NodeID.IsValid()) { return; } if (FindNode(OwnerState->EnterConditions, TEXT("EnterConditions"))) { return; } if (FindNode(OwnerState->Tasks, TEXT("Tasks"))) { return; } if (NodeID == OwnerState->SingleTask.ID) { NodePath = TEXT("SingleTask"); TargetNode = &OwnerState->SingleTask; return; } if (FindNode(OwnerState->Considerations, TEXT("Considerations"))) { return; } if (FindNode(OwnerState->Transitions, TEXT("Transitions"))) { bIsTransition = true; return; } } bool IsValid() const { return TargetNode != nullptr; } bool IsTransition() const { return bIsTransition; } FStateTreeEditorNode& GetEditorNode() const { check(IsValid() && !IsTransition()); return *static_cast(TargetNode); } TArray& GetEditorNodeArray() const { check(IsValid() && !IsTransition() && GetNodeIndex() != INDEX_NONE); return *static_cast*>(TargetArray); } FStateTreeTransition& GetTransition() const { check(IsValid() && IsTransition()); return *static_cast(TargetNode); } TArray& GetTransitionArray() const { check(IsValid() && IsTransition() && GetNodeIndex() != INDEX_NONE); return *static_cast*>(TargetArray); } FStringView GetNodePath() const { return NodePath; } int32 GetNodeIndex() const { return ArrayIndex; } }; } }; FStateTreeViewModel::FStateTreeViewModel() : TreeDataWeak(nullptr) #if WITH_STATETREE_TRACE_DEBUGGER , Debugger(MakeShareable(new FStateTreeDebugger)) #endif // WITH_STATETREE_TRACE_DEBUGGER { } FStateTreeViewModel::~FStateTreeViewModel() { if (GEditor) { GEditor->UnregisterForUndo(this); } UE::StateTree::Delegates::OnIdentifierChanged.RemoveAll(this); } void FStateTreeViewModel::Init(UStateTreeEditorData* InTreeData) { TreeDataWeak = InTreeData; GEditor->RegisterForUndo(this); UE::StateTree::Delegates::OnIdentifierChanged.AddSP(this, &FStateTreeViewModel::HandleIdentifierChanged); #if WITH_STATETREE_TRACE_DEBUGGER UE::StateTree::Delegates::OnBreakpointsChanged.AddSP(this, &FStateTreeViewModel::HandleBreakpointsChanged); UE::StateTree::Delegates::OnPostCompile.AddSP(this, &FStateTreeViewModel::HandlePostCompile); Debugger->SetAsset(GetStateTree()); BindToDebuggerDelegates(); RefreshDebuggerBreakpoints(); #endif // WITH_STATETREE_TRACE_DEBUGGER } const UStateTree* FStateTreeViewModel::GetStateTree() const { if (const UStateTreeEditorData* TreeData = TreeDataWeak.Get()) { return TreeData->GetTypedOuter(); } return nullptr; } const UStateTreeEditorData* FStateTreeViewModel::GetStateTreeEditorData() const { return TreeDataWeak.Get(); } const UStateTreeState* FStateTreeViewModel::GetStateByID(const FGuid StateID) const { if (const UStateTreeEditorData* TreeData = TreeDataWeak.Get()) { return const_cast(TreeData->GetStateByID(StateID)); } return nullptr; } UStateTreeState* FStateTreeViewModel::GetMutableStateByID(const FGuid StateID) const { if (UStateTreeEditorData* TreeData = TreeDataWeak.Get()) { return TreeData->GetMutableStateByID(StateID); } return nullptr; } void FStateTreeViewModel::HandleIdentifierChanged(const UStateTree& StateTree) const { if (GetStateTree() == &StateTree) { OnAssetChanged.Broadcast(); } } #if WITH_STATETREE_TRACE_DEBUGGER bool FStateTreeViewModel::CanAddStateBreakpoint(const EStateTreeBreakpointType Type) const { const UStateTreeEditorData* EditorData = TreeDataWeak.Get(); if (!ensure(EditorData != nullptr)) { return false; } for (const TWeakObjectPtr& WeakState : SelectedStates) { if (const UStateTreeState* State = WeakState.Get()) { if (EditorData->HasBreakpoint(State->ID, Type) == false) { return true; } } } return false; } bool FStateTreeViewModel::CanRemoveStateBreakpoint(const EStateTreeBreakpointType Type) const { const UStateTreeEditorData* EditorData = TreeDataWeak.Get(); if (!ensure(EditorData != nullptr)) { return false; } for (const TWeakObjectPtr& WeakState : SelectedStates) { if (const UStateTreeState* State = WeakState.Get()) { if (EditorData->HasBreakpoint(State->ID, Type)) { return true; } } } return false; } ECheckBoxState FStateTreeViewModel::GetStateBreakpointCheckState(const EStateTreeBreakpointType Type) const { const bool bCanAdd = CanAddStateBreakpoint(Type); const bool bCanRemove = CanRemoveStateBreakpoint(Type); if (bCanAdd && bCanRemove) { return ECheckBoxState::Undetermined; } if (bCanRemove) { return ECheckBoxState::Checked; } if (bCanAdd) { return ECheckBoxState::Unchecked; } // Should not happen since action is not visible in this case return ECheckBoxState::Undetermined; } void FStateTreeViewModel::HandleEnableStateBreakpoint(EStateTreeBreakpointType Type) { TArray ValidatedSelectedStates; GetSelectedStates(ValidatedSelectedStates); if (ValidatedSelectedStates.IsEmpty()) { return; } UStateTreeEditorData* EditorData = TreeDataWeak.Get(); if (!ensure(EditorData != nullptr)) { return; } TBitArray<> HasBreakpoint; HasBreakpoint.Reserve(ValidatedSelectedStates.Num()); for (const UStateTreeState* SelectedState : ValidatedSelectedStates) { HasBreakpoint.Add(SelectedState != nullptr && EditorData->HasBreakpoint(SelectedState->ID, Type)); } check(HasBreakpoint.Num() == ValidatedSelectedStates.Num()); // Process CanAdd first so in case of undetermined state (mixed selection) we add by default. if (CanAddStateBreakpoint(Type)) { const FScopedTransaction Transaction(LOCTEXT("AddStateBreakpoint", "Add State Breakpoint(s)")); EditorData->Modify(); for (int Index = 0; Index < ValidatedSelectedStates.Num(); ++Index) { const UStateTreeState* SelectedState = ValidatedSelectedStates[Index]; if (HasBreakpoint[Index] == false && SelectedState != nullptr) { EditorData->AddBreakpoint(SelectedState->ID, Type); } } } else if (CanRemoveStateBreakpoint(Type)) { const FScopedTransaction Transaction(LOCTEXT("RemoveStateBreakpoint", "Remove State Breakpoint(s)")); EditorData->Modify(); for (int Index = 0; Index < ValidatedSelectedStates.Num(); ++Index) { const UStateTreeState* SelectedState = ValidatedSelectedStates[Index]; if (HasBreakpoint[Index] && SelectedState != nullptr) { EditorData->RemoveBreakpoint(SelectedState->ID, Type); } } } } UStateTreeState* FStateTreeViewModel::FindStateAssociatedToBreakpoint(FStateTreeDebuggerBreakpoint Breakpoint) const { UStateTreeEditorData* EditorData = TreeDataWeak.Get(); if (EditorData == nullptr) { return nullptr; } const UStateTree* StateTree = GetStateTree(); if (StateTree == nullptr) { return nullptr; } UStateTreeState* StateTreeState = nullptr; if (const FStateTreeStateHandle* StateHandle = Breakpoint.ElementIdentifier.TryGet()) { const FGuid StateId = StateTree->GetStateIdFromHandle(*StateHandle); StateTreeState = EditorData->GetMutableStateByID(StateId); } else if (const FStateTreeDebuggerBreakpoint::FStateTreeTaskIndex* TaskIndex = Breakpoint.ElementIdentifier.TryGet()) { const FGuid TaskId = StateTree->GetNodeIdFromIndex(TaskIndex->Index); EditorData->VisitHierarchy([&TaskId, &StateTreeState](UStateTreeState& State, UStateTreeState* /*ParentState*/) { for (const FStateTreeEditorNode& EditorNode : State.Tasks) { if (EditorNode.ID == TaskId) { StateTreeState = &State; return EStateTreeVisitor::Break; } } return EStateTreeVisitor::Continue; }); } else if (const FStateTreeDebuggerBreakpoint::FStateTreeTransitionIndex* TransitionIndex = Breakpoint.ElementIdentifier.TryGet()) { const FGuid TransitionId = StateTree->GetTransitionIdFromIndex(TransitionIndex->Index); EditorData->VisitHierarchy([&TransitionId, &StateTreeState](UStateTreeState& State, UStateTreeState* /*ParentState*/) { for (const FStateTreeTransition& StateTransition : State.Transitions) { if (StateTransition.ID == TransitionId) { StateTreeState = &State; return EStateTreeVisitor::Break; } } return EStateTreeVisitor::Continue; }); } return StateTreeState; } void FStateTreeViewModel::HandleBreakpointsChanged(const UStateTree& StateTree) { if (GetStateTree() == &StateTree) { RefreshDebuggerBreakpoints(); } } void FStateTreeViewModel::HandlePostCompile(const UStateTree& StateTree) { if (GetStateTree() == &StateTree) { RefreshDebuggerBreakpoints(); } } void FStateTreeViewModel::RefreshDebuggerBreakpoints() { const UStateTree* StateTree = GetStateTree(); const UStateTreeEditorData* TreeData = TreeDataWeak.Get(); if (StateTree != nullptr && TreeData != nullptr) { Debugger->ClearAllBreakpoints(); for (const FStateTreeEditorBreakpoint& Breakpoint : TreeData->Breakpoints) { // Test if the ID is associated to a task const FStateTreeIndex16 Index = StateTree->GetNodeIndexFromId(Breakpoint.ID); if (Index.IsValid()) { Debugger->SetTaskBreakpoint(Index, Breakpoint.BreakpointType); } else { // Then test if the ID is associated to a State FStateTreeStateHandle StateHandle = StateTree->GetStateHandleFromId(Breakpoint.ID); if (StateHandle.IsValid()) { Debugger->SetStateBreakpoint(StateHandle, Breakpoint.BreakpointType); } else { // Then test if the ID is associated to a transition const FStateTreeIndex16 TransitionIndex = StateTree->GetTransitionIndexFromId(Breakpoint.ID); if (TransitionIndex.IsValid()) { Debugger->SetTransitionBreakpoint(TransitionIndex, Breakpoint.BreakpointType); } } } } } } #endif // WITH_STATETREE_TRACE_DEBUGGER void FStateTreeViewModel::NotifyAssetChangedExternally() const { OnAssetChanged.Broadcast(); } void FStateTreeViewModel::NotifyStatesChangedExternally(const TSet& ChangedStates, const FPropertyChangedEvent& PropertyChangedEvent) const { OnStatesChanged.Broadcast(ChangedStates, PropertyChangedEvent); } TArray>* FStateTreeViewModel::GetSubTrees() const { UStateTreeEditorData* TreeData = TreeDataWeak.Get(); return TreeData != nullptr ? &TreeData->SubTrees : nullptr; } int32 FStateTreeViewModel::GetSubTreeCount() const { UStateTreeEditorData* TreeData = TreeDataWeak.Get(); return TreeData != nullptr ? TreeData->SubTrees.Num() : 0; } void FStateTreeViewModel::GetSubTrees(TArray>& OutSubtrees) const { OutSubtrees.Reset(); if (UStateTreeEditorData* TreeData = TreeDataWeak.Get()) { for (UStateTreeState* Subtree : TreeData->SubTrees) { OutSubtrees.Add(Subtree); } } } void FStateTreeViewModel::GetLinkStates(FGuid StateID, TArray& LinkingIn, TArray& LinkedOut) const { const UStateTreeState* State = GetStateByID(StateID); if (State == nullptr) { return; } if (UStateTreeEditorData* TreeData = TreeDataWeak.Get()) { TreeData->VisitHierarchy([&LinkingIn, &LinkedOut, StateID = State->ID](UStateTreeState& State, UStateTreeState* ParentState) { if (State.ID == StateID) { return EStateTreeVisitor::Continue; } if (State.Type == EStateTreeStateType::Linked && StateID == State.LinkedSubtree.ID) { LinkingIn.AddUnique(State.ID); } else { for (const FStateTreeTransition& Transition : State.Transitions) { if (Transition.State.ID == StateID) { LinkingIn.AddUnique(State.ID); } } } return EStateTreeVisitor::Continue; }); if (State->Type == EStateTreeStateType::Linked) { LinkedOut.AddUnique(State->LinkedSubtree.ID); } for (const FStateTreeTransition& Transition : State->Transitions) { LinkedOut.AddUnique(Transition.State.ID); } } } void FStateTreeViewModel::PostUndo(bool bSuccess) { // TODO: see if we can narrow this down. OnAssetChanged.Broadcast(); } void FStateTreeViewModel::PostRedo(bool bSuccess) { OnAssetChanged.Broadcast(); } void FStateTreeViewModel::ClearSelection() { if (SelectedStates.IsEmpty()) { return; } SelectedStates.Reset(); const TArray> SelectedStatesArr; OnSelectionChanged.Broadcast(SelectedStatesArr); } void FStateTreeViewModel::SetSelection(UStateTreeState* Selected) { if (SelectedStates.Num() == 1 && SelectedStates.Contains(Selected)) { return; } SelectedStates.Reset(); if (Selected != nullptr) { SelectedStates.Add(Selected); } TArray> SelectedStatesArr; SelectedStatesArr.Add(Selected); OnSelectionChanged.Broadcast(SelectedStatesArr); } void FStateTreeViewModel::DeleteNode(TWeakObjectPtr State, const FGuid& ID) { if (UStateTreeEditorData* EditorData = TreeDataWeak.Get()) { if (UStateTreeState* OwnerState = State.Get()) { const UE::StateTree::Editor::Private::FStateTreeStateNodeEditorHandle StateNodeHandle(EditorData, OwnerState, ID); // If the op can not be executed, don't even start the transaction if (!StateNodeHandle.IsValid()) { return; } auto DeleteFunc = [&StateNodeHandle](TNotNull InOwnerState, TNotNull InEditorData, const FStateTreeEditPropertyPath& InPropertyPath) { StateNodeHandle.EditorData->Modify(); UE::StateTreeEditor::FScopedEditorDataFixer DataFixer(InEditorData); DataFixer.bRemoveInvalidBindings = true; if (StateNodeHandle.IsTransition()) { StateNodeHandle.GetTransitionArray().RemoveAt(StateNodeHandle.GetNodeIndex()); } else { StateNodeHandle.GetEditorNodeArray().RemoveAt(StateNodeHandle.GetNodeIndex()); } }; UE::StateTree::PropertyHelpers::ModifyStateInPreAndPostEdit( LOCTEXT("DeleteNodeTransaction", "Delete Node"), StateNodeHandle.OwnerState, StateNodeHandle.EditorData, StateNodeHandle.GetNodePath(), DeleteFunc, StateNodeHandle.GetNodeIndex(), EPropertyChangeType::ArrayRemove); OnStateNodesChanged.Broadcast(OwnerState); } } } void FStateTreeViewModel::DeleteAllNodes(TWeakObjectPtr State, const FGuid& ID) { if (UStateTreeEditorData* EditorData = TreeDataWeak.Get()) { if (UStateTreeState* OwnerState = State.Get()) { UE::StateTree::Editor::Private::FStateTreeStateNodeEditorHandle StateNodeHandle(EditorData, OwnerState, ID); // If the op can not be executed, don't even start the transaction if (!StateNodeHandle.IsValid()) { return; } auto DeleteAllFunc = [&StateNodeHandle](const TNotNull InOwnerState, const TNotNull InEditorData, const FStateTreeEditPropertyPath& InPropertyPath) { StateNodeHandle.EditorData->Modify(); UE::StateTreeEditor::FScopedEditorDataFixer DataFixer(InEditorData); DataFixer.bRemoveInvalidBindings = true; if (StateNodeHandle.IsTransition()) { StateNodeHandle.GetTransitionArray().Empty(); } else { StateNodeHandle.GetEditorNodeArray().Empty(); } }; UE::StateTree::PropertyHelpers::ModifyStateInPreAndPostEdit( LOCTEXT("DeleteAllNodesTransaction", "Delete All Nodes"), StateNodeHandle.OwnerState, StateNodeHandle.EditorData, StateNodeHandle.GetNodePath(), DeleteAllFunc, INDEX_NONE, // Pass Invalid Index to Array Clear Op EPropertyChangeType::ArrayClear); OnStateNodesChanged.Broadcast(OwnerState); } } } void FStateTreeViewModel::CopyNode(TWeakObjectPtr State, const FGuid& ID) { if (UStateTreeEditorData* EditorData = TreeDataWeak.Get()) { if (UStateTreeState* OwnerState = State.Get()) { UE::StateTree::Editor::Private::FStateTreeStateNodeEditorHandle StateNodeHandle(EditorData, OwnerState, ID); // If the op can not be executed, don't even start the transaction if (!StateNodeHandle.IsValid()) { return; } const UScriptStruct* BaseStruct = nullptr; if (StateNodeHandle.IsTransition()) { BaseStruct = FStateTreeTransition::StaticStruct(); } else { BaseStruct = FStateTreeEditorNode::StaticStruct(); } // Use PPF_Copy so that all properties get copied. FString Value; constexpr EPropertyPortFlags PortFlags = PPF_Copy; BaseStruct->ExportText(Value, StateNodeHandle.IsTransition() ? (void*)&StateNodeHandle.GetTransition() : (void*)&StateNodeHandle.GetEditorNode(), nullptr, nullptr, PortFlags, nullptr); FPlatformApplicationMisc::ClipboardCopy(*Value); } } } void FStateTreeViewModel::PasteNode(TWeakObjectPtr State, const FGuid& ID) { if (UStateTreeEditorData* EditorData = TreeDataWeak.Get()) { if (UStateTreeState* OwnerState = State.Get()) { UE::StateTree::Editor::Private::FStateTreeStateNodeEditorHandle StateNodeHandle(EditorData, OwnerState, ID); // If the op can not be executed, don't even start the transaction if (!StateNodeHandle.IsValid()) { return; } // For Editor Node, we have a validation process which might early out if failed. Dry run with a Temp. FStateTreeEditorNode TempEditorNode; if (!StateNodeHandle.IsTransition()) { static const UScriptStruct* TaskBaseStruct = FStateTreeTaskBase::StaticStruct(); static const UScriptStruct* ConditionBaseStruct = FStateTreeConditionBase::StaticStruct(); static const UScriptStruct* ConsiderationBaseStruct = FStateTreeConsiderationBase::StaticStruct(); const UScriptStruct* BaseNodeScriptStruct = nullptr; const UScriptStruct* NodeScriptStruct = StateNodeHandle.GetEditorNode().Node.GetScriptStruct(); if (NodeScriptStruct->IsChildOf(TaskBaseStruct)) { BaseNodeScriptStruct = TaskBaseStruct; } else if (NodeScriptStruct->IsChildOf(ConditionBaseStruct)) { BaseNodeScriptStruct = ConditionBaseStruct; } else if (NodeScriptStruct->IsChildOf(ConsiderationBaseStruct)) { BaseNodeScriptStruct = ConsiderationBaseStruct; } const bool bSuccess = UE::StateTreeEditor::EditorNodeUtils::ImportTextAsNode(BaseNodeScriptStruct, StateNodeHandle.EditorData, TempEditorNode); // If the op can not be executed, don't even start the transaction if (!bSuccess) { return; } } auto PasteFunc = [&StateNodeHandle, &TempEditorNode](const TNotNull InOwnerState, const TNotNull InEditorData, const FStateTreeEditPropertyPath& InPropertyPath) { InEditorData->Modify(); UE::StateTreeEditor::FScopedEditorDataFixer DataFixer(StateNodeHandle.EditorData); constexpr bool bShouldCopyBindings = true; constexpr bool bShouldReinstantiateInstanceData = true; constexpr bool bShouldRegenerateGUID = true; if (StateNodeHandle.IsTransition()) { FString PastedText; FPlatformApplicationMisc::ClipboardPaste(PastedText); const UScriptStruct* TransitionStruct = FStateTreeTransition::StaticStruct(); TransitionStruct->ImportText(*PastedText, &StateNodeHandle.GetTransition(), nullptr, PPF_None, nullptr, TransitionStruct->GetName()); DataFixer.TransitionsToFix.Emplace(StateNodeHandle.OwnerState, StateNodeHandle.GetTransition(), bShouldCopyBindings, bShouldReinstantiateInstanceData, bShouldRegenerateGUID); } else { StateNodeHandle.GetEditorNode() = TempEditorNode; DataFixer.EditorNodesToFix.Emplace(StateNodeHandle.OwnerState, StateNodeHandle.GetEditorNode(), bShouldCopyBindings, bShouldReinstantiateInstanceData, bShouldRegenerateGUID); } }; UE::StateTree::PropertyHelpers::ModifyStateInPreAndPostEdit( LOCTEXT("PasteNodeTransaction", "Paste Node"), StateNodeHandle.OwnerState, StateNodeHandle.EditorData, StateNodeHandle.GetNodePath(), PasteFunc, INDEX_NONE, // Value Set Op, skip the index EPropertyChangeType::ValueSet); OnStateNodesChanged.Broadcast(OwnerState); } } } void FStateTreeViewModel::DuplicateNode(TWeakObjectPtr State, const FGuid& ID) { if (UStateTreeEditorData* EditorData = TreeDataWeak.Get()) { if (UStateTreeState* OwnerState = State.Get()) { UE::StateTree::Editor::Private::FStateTreeStateNodeEditorHandle StateNodeHandle(EditorData, OwnerState, ID); // If the op can not be executed, don't even start the transaction if (!StateNodeHandle.IsValid()) { return; } auto DuplicateFunc = [&StateNodeHandle](const TNotNull InOwnerState, const TNotNull InEditorData, const FStateTreeEditPropertyPath& InPropertyPath) { InEditorData->Modify(); UE::StateTreeEditor::FScopedEditorDataFixer DataFixer(StateNodeHandle.EditorData); constexpr bool bShouldCopyBindings = true; constexpr bool bShouldReinstantiateInstanceData = true; constexpr bool bShouldRegenerateGUID = true; if (StateNodeHandle.IsTransition()) { // Make a temp copy const FStateTreeTransition TempTransition = StateNodeHandle.GetTransition(); FStateTreeTransition& NewTransition = StateNodeHandle.GetTransitionArray().Insert_GetRef(TempTransition, StateNodeHandle.GetNodeIndex()); DataFixer.TransitionsToFix.Emplace(StateNodeHandle.OwnerState, NewTransition, bShouldCopyBindings, bShouldReinstantiateInstanceData, bShouldRegenerateGUID); } else { // Make a temp copy const FStateTreeEditorNode TempEditorNode = StateNodeHandle.GetEditorNode(); FStateTreeEditorNode& NewEditorNode = StateNodeHandle.GetEditorNodeArray().Insert_GetRef(TempEditorNode, StateNodeHandle.GetNodeIndex()); DataFixer.EditorNodesToFix.Emplace(StateNodeHandle.OwnerState, NewEditorNode, bShouldCopyBindings, bShouldReinstantiateInstanceData, bShouldRegenerateGUID); } }; UE::StateTree::PropertyHelpers::ModifyStateInPreAndPostEdit( LOCTEXT("DuplicateNodeTransaction", "Duplicate Node"), StateNodeHandle.OwnerState, StateNodeHandle.EditorData, StateNodeHandle.GetNodePath(), DuplicateFunc, StateNodeHandle.GetNodeIndex(), EPropertyChangeType::Duplicate); OnStateNodesChanged.Broadcast(OwnerState); } } } void FStateTreeViewModel::SetSelection(const TArray>& InSelectedStates) { if (SelectedStates.Num() == InSelectedStates.Num() && SelectedStates.Array() == InSelectedStates) { return; } SelectedStates.Reset(); for (const TWeakObjectPtr& State : InSelectedStates) { if (State.Get()) { SelectedStates.Add(State); } } OnSelectionChanged.Broadcast(InSelectedStates); } bool FStateTreeViewModel::IsSelected(const UStateTreeState* State) const { const TWeakObjectPtr WeakState = const_cast(State); return SelectedStates.Contains(WeakState); } bool FStateTreeViewModel::IsChildOfSelection(const UStateTreeState* State) const { for (const TWeakObjectPtr& WeakSelectedState : SelectedStates) { if (const UStateTreeState* SelectedState = Cast(WeakSelectedState.Get())) { if (SelectedState == State) { return true; } if (UE::StateTree::Editor::IsChildOf(SelectedState, State)) { return true; } } } return false; } void FStateTreeViewModel::GetSelectedStates(TArray& OutSelectedStates) const { OutSelectedStates.Reset(); for (const TWeakObjectPtr& WeakState : SelectedStates) { if (UStateTreeState* State = WeakState.Get()) { OutSelectedStates.Add(State); } } } void FStateTreeViewModel::GetSelectedStates(TArray>& OutSelectedStates) const { OutSelectedStates.Reset(); for (const TWeakObjectPtr& WeakState : SelectedStates) { if (WeakState.Get()) { OutSelectedStates.Add(WeakState); } } } bool FStateTreeViewModel::HasSelection() const { return SelectedStates.Num() > 0; } void FStateTreeViewModel::BringNodeToFocus(UStateTreeState* State, const FGuid NodeID) { SetSelection(State); OnBringNodeToFocus.Broadcast(State, NodeID); } void FStateTreeViewModel::GetPersistentExpandedStates(TSet>& OutExpandedStates) { OutExpandedStates.Reset(); if (UStateTreeEditorData* TreeData = TreeDataWeak.Get()) { for (UStateTreeState* SubTree : TreeData->SubTrees) { GetExpandedStatesRecursive(SubTree, OutExpandedStates); } } } void FStateTreeViewModel::GetExpandedStatesRecursive(UStateTreeState* State, TSet>& OutExpandedStates) { if (State->bExpanded) { OutExpandedStates.Add(State); } for (UStateTreeState* Child : State->Children) { GetExpandedStatesRecursive(Child, OutExpandedStates); } } void FStateTreeViewModel::SetPersistentExpandedStates(TSet>& InExpandedStates) { UStateTreeEditorData* TreeData = TreeDataWeak.Get(); if (TreeData == nullptr) { return; } TreeData->Modify(); for (TWeakObjectPtr& WeakState : InExpandedStates) { if (UStateTreeState* State = WeakState.Get()) { State->bExpanded = true; } } } void FStateTreeViewModel::AddState(UStateTreeState* AfterState) { UStateTreeEditorData* TreeData = TreeDataWeak.Get(); if (TreeData == nullptr) { return; } const FScopedTransaction Transaction(LOCTEXT("AddStateTransaction", "Add State")); UStateTreeState* NewState = NewObject(TreeData, FName(), RF_Transactional); UStateTreeState* ParentState = nullptr; if (AfterState == nullptr) { // If no subtrees, add a subtree, or add to the root state. if (TreeData->SubTrees.IsEmpty()) { TreeData->Modify(); TreeData->SubTrees.Add(NewState); } else { UStateTreeState* RootState = TreeData->SubTrees[0]; if (ensureMsgf(RootState, TEXT("%s: Root state is empty."), *GetNameSafe(TreeData->GetOuter()))) { RootState->Modify(); RootState->Children.Add(NewState); NewState->Parent = RootState; ParentState = RootState; } } } else { ParentState = AfterState->Parent; if (ParentState != nullptr) { ParentState->Modify(); } else { TreeData->Modify(); } TArray>& ParentArray = ParentState ? ParentState->Children : TreeData->SubTrees; const int32 TargetIndex = ParentArray.Find(AfterState); if (TargetIndex != INDEX_NONE) { // Insert After ParentArray.Insert(NewState, TargetIndex + 1); NewState->Parent = ParentState; } else { // Fallback, should never happen. ensureMsgf(false, TEXT("%s: Failed to find specified target state %s on state %s while adding new state."), *GetNameSafe(TreeData->GetOuter()), *GetNameSafe(AfterState), *GetNameSafe(ParentState)); ParentArray.Add(NewState); NewState->Parent = ParentState; } } OnStateAdded.Broadcast(ParentState, NewState); } void FStateTreeViewModel::AddChildState(UStateTreeState* ParentState) { UStateTreeEditorData* TreeData = TreeDataWeak.Get(); if (TreeData == nullptr || ParentState == nullptr) { return; } const FScopedTransaction Transaction(LOCTEXT("AddChildStateTransaction", "Add Child State")); UStateTreeState* NewState = NewObject(ParentState, FName(), RF_Transactional); ParentState->Modify(); ParentState->Children.Add(NewState); NewState->Parent = ParentState; OnStateAdded.Broadcast(ParentState, NewState); } void FStateTreeViewModel::RenameState(UStateTreeState* State, FName NewName) { if (State == nullptr) { return; } const FScopedTransaction Transaction(LOCTEXT("RenameTransaction", "Rename")); State->Modify(); State->Name = NewName; TSet AffectedStates; AffectedStates.Add(State); FProperty* NameProperty = FindFProperty(UStateTreeState::StaticClass(), GET_MEMBER_NAME_CHECKED(UStateTreeState, Name)); FPropertyChangedEvent PropertyChangedEvent(NameProperty, EPropertyChangeType::ValueSet); OnStatesChanged.Broadcast(AffectedStates, PropertyChangedEvent); } void FStateTreeViewModel::RemoveSelectedStates() { UStateTreeEditorData* TreeData = TreeDataWeak.Get(); if (TreeData == nullptr) { return; } TArray States; GetSelectedStates(States); // Remove items whose parent also exists in the selection. UE::StateTree::Editor::RemoveContainedChildren(States); if (States.Num() > 0) { const FScopedTransaction Transaction(LOCTEXT("DeleteStateTransaction", "Delete State")); TSet AffectedParents; for (UStateTreeState* StateToRemove : States) { if (StateToRemove) { StateToRemove->Modify(); UStateTreeState* ParentState = StateToRemove->Parent; if (ParentState != nullptr) { AffectedParents.Add(ParentState); ParentState->Modify(); } else { AffectedParents.Add(nullptr); TreeData->Modify(); } TArray>& ArrayToRemoveFrom = ParentState ? ParentState->Children : TreeData->SubTrees; const int32 ItemIndex = ArrayToRemoveFrom.Find(StateToRemove); if (ItemIndex != INDEX_NONE) { ArrayToRemoveFrom.RemoveAt(ItemIndex); StateToRemove->Parent = nullptr; } } } OnStatesRemoved.Broadcast(AffectedParents); ClearSelection(); } } void FStateTreeViewModel::CopySelectedStates() { UStateTreeEditorData* TreeData = TreeDataWeak.Get(); if (TreeData == nullptr) { return; } TArray States; GetSelectedStates(States); UE::StateTree::Editor::RemoveContainedChildren(States); FString ExportedText = UE::StateTree::Editor::ExportStatesToText(TreeData, States); FPlatformApplicationMisc::ClipboardCopy(*ExportedText); } bool FStateTreeViewModel::CanPasteStatesFromClipboard() const { FString TextToImport; FPlatformApplicationMisc::ClipboardPaste(TextToImport); UE::StateTree::Editor::FStateTreeStateTextFactory Factory; return Factory.CanCreateObjectsFromText(TextToImport); } void FStateTreeViewModel::PasteStatesFromClipboard(UStateTreeState* AfterState) { UStateTreeEditorData* TreeData = TreeDataWeak.Get(); if (TreeData == nullptr) { return; } if (AfterState) { const int32 Index = AfterState->Parent ? AfterState->Parent->Children.Find(AfterState) : TreeData->SubTrees.Find(AfterState); if (Index != INDEX_NONE) { FString TextToImport; FPlatformApplicationMisc::ClipboardPaste(TextToImport); const FScopedTransaction Transaction(LOCTEXT("PasteStatesTransaction", "Paste State(s)")); PasteStatesAsChildrenFromText(TextToImport, AfterState->Parent, Index + 1); } } } void FStateTreeViewModel::PasteStatesAsChildrenFromClipboard(UStateTreeState* ParentState) { UStateTreeEditorData* TreeData = TreeDataWeak.Get(); if (TreeData == nullptr) { return; } FString TextToImport; FPlatformApplicationMisc::ClipboardPaste(TextToImport); const FScopedTransaction Transaction(LOCTEXT("PasteStatesTransaction", "Paste State(s)")); PasteStatesAsChildrenFromText(TextToImport, ParentState, INDEX_NONE); } void FStateTreeViewModel::PasteStatesAsChildrenFromText(const FString& TextToImport, UStateTreeState* ParentState, const int32 IndexToInsertAt) { UStateTreeEditorData* TreeData = TreeDataWeak.Get(); if (TreeData == nullptr) { return; } UObject* Outer = ParentState ? static_cast(ParentState) : static_cast(TreeData); Outer->Modify(); UE::StateTree::Editor::FStateTreeStateTextFactory Factory; Factory.ProcessBuffer(Outer, RF_Transactional, TextToImport); TArray>& ParentArray = ParentState ? ParentState->Children : TreeData->SubTrees; const int32 TargetIndex = (IndexToInsertAt == INDEX_NONE) ? ParentArray.Num() : IndexToInsertAt; ParentArray.Insert(Factory.States, TargetIndex); TArray Links; TMap IDsMap; TArray NewStates; for (UStateTreeState* State : Factory.States) { UE::StateTree::Editor::FixStateAfterDuplication(State, ParentState, IDsMap, Links, NewStates); } // Copy property bindings for the duplicated states. if (Factory.ClipboardBindings) { for (FStateTreePropertyPathBinding& Binding : Factory.ClipboardBindings->Bindings) { if (Binding.GetPropertyFunctionNode().IsValid()) { UE::StateTree::Editor::FixNodesAfterDuplication(TArrayView(Binding.GetMutablePropertyFunctionNode().GetPtr(), 1), IDsMap, Links); } } for (const TPair& Entry : IDsMap) { const FGuid OldTargetID = Entry.Key; const FGuid NewTargetID = Entry.Value; for (FStateTreePropertyPathBinding& Binding : Factory.ClipboardBindings->Bindings) { if (Binding.GetTargetPath().GetStructID() == OldTargetID) { Binding.GetMutableTargetPath().SetStructID(NewTargetID); if (const FGuid* NewSourceID = IDsMap.Find(Binding.GetSourcePath().GetStructID())) { Binding.GetMutableSourcePath().SetStructID(*NewSourceID); } TreeData->GetPropertyEditorBindings()->AddStateTreeBinding(MoveTemp(Binding)); } } } } // Patch IDs in state links. for (FStateTreeStateLink* Link : Links) { if (FGuid* NewID = IDsMap.Find(Link->ID)) { Link->ID = *NewID; } } for (UStateTreeState* State : NewStates) { OnStateAdded.Broadcast(State->Parent, State); } } void FStateTreeViewModel::DuplicateSelectedStates() { UStateTreeEditorData* TreeData = TreeDataWeak.Get(); if (TreeData == nullptr) { return; } TArray States; GetSelectedStates(States); UE::StateTree::Editor::RemoveContainedChildren(States); if (States.IsEmpty()) { return; } FString ExportedText = UE::StateTree::Editor::ExportStatesToText(TreeData, States); // Place duplicates after first selected state. UStateTreeState* AfterState = States[0]; const int32 Index = AfterState->Parent ? AfterState->Parent->Children.Find(AfterState) : TreeData->SubTrees.Find(AfterState); if (Index != INDEX_NONE) { const FScopedTransaction Transaction(LOCTEXT("DuplicateStatesTransaction", "Duplicate State(s)")); PasteStatesAsChildrenFromText(ExportedText, AfterState->Parent, Index + 1); } } void FStateTreeViewModel::MoveSelectedStatesBefore(UStateTreeState* TargetState) { MoveSelectedStates(TargetState, EStateTreeViewModelInsert::Before); } void FStateTreeViewModel::MoveSelectedStatesAfter(UStateTreeState* TargetState) { MoveSelectedStates(TargetState, EStateTreeViewModelInsert::After); } void FStateTreeViewModel::MoveSelectedStatesInto(UStateTreeState* TargetState) { MoveSelectedStates(TargetState, EStateTreeViewModelInsert::Into); } bool FStateTreeViewModel::CanEnableStates() const { TArray States; GetSelectedStates(States); for (const UStateTreeState* State : States) { // Stop if at least one state can be enabled if (State->bEnabled == false) { return true; } } return false; } bool FStateTreeViewModel::CanDisableStates() const { TArray States; GetSelectedStates(States); for (const UStateTreeState* State : States) { // Stop if at least one state can be disabled if (State->bEnabled) { return true; } } return false; } void FStateTreeViewModel::SetSelectedStatesEnabled(const bool bEnable) { TArray States; GetSelectedStates(States); if (States.Num() > 0) { const FScopedTransaction Transaction(LOCTEXT("SetStatesEnabledTransaction", "Set State Enabled")); for (UStateTreeState* State : States) { State->Modify(); State->bEnabled = bEnable; } OnAssetChanged.Broadcast(); } } void FStateTreeViewModel::MoveSelectedStates(UStateTreeState* TargetState, const EStateTreeViewModelInsert RelativeLocation) { UStateTreeEditorData* TreeData = TreeDataWeak.Get(); if (TreeData == nullptr || TargetState == nullptr) { return; } TArray States; GetSelectedStates(States); // Remove child items whose parent also exists in the selection. UE::StateTree::Editor::RemoveContainedChildren(States); // Remove states which contain target state as child. States.RemoveAll([TargetState](const UStateTreeState* State) { return UE::StateTree::Editor::IsChildOf(State, TargetState); }); if (States.Num() > 0 && TargetState != nullptr) { const FScopedTransaction Transaction(LOCTEXT("MoveTransaction", "Move")); TSet AffectedParents; TSet AffectedStates; UStateTreeState* TargetParent = TargetState->Parent; if (RelativeLocation == EStateTreeViewModelInsert::Into) { AffectedParents.Add(TargetState); } else { AffectedParents.Add(TargetParent); } for (int32 i = States.Num() - 1; i >= 0; i--) { if (UStateTreeState* State = States[i]) { State->Modify(); AffectedParents.Add(State->Parent); } } if (RelativeLocation == EStateTreeViewModelInsert::Into) { // Move into TargetState->Modify(); } for (UStateTreeState* Parent : AffectedParents) { if (Parent) { Parent->Modify(); } else { TreeData->Modify(); } } // Add in reverse order to keep the original order. for (int32 i = States.Num() - 1; i >= 0; i--) { if (UStateTreeState* SelectedState = States[i]) { AffectedStates.Add(SelectedState); UStateTreeState* SelectedParent = SelectedState->Parent; // Remove from current parent TArray>& ArrayToRemoveFrom = SelectedParent ? SelectedParent->Children : TreeData->SubTrees; const int32 ItemIndex = ArrayToRemoveFrom.Find(SelectedState); if (ItemIndex != INDEX_NONE) { ArrayToRemoveFrom.RemoveAt(ItemIndex); SelectedState->Parent = nullptr; } // Insert to new parent if (RelativeLocation == EStateTreeViewModelInsert::Into) { // Into TargetState->Children.Insert(SelectedState, /*Index*/0); SelectedState->Parent = TargetState; } else { TArray>& ArrayToMoveTo = TargetParent ? TargetParent->Children : TreeData->SubTrees; const int32 TargetIndex = ArrayToMoveTo.Find(TargetState); if (TargetIndex != INDEX_NONE) { if (RelativeLocation == EStateTreeViewModelInsert::Before) { // Before ArrayToMoveTo.Insert(SelectedState, TargetIndex); SelectedState->Parent = TargetParent; } else if (RelativeLocation == EStateTreeViewModelInsert::After) { // After ArrayToMoveTo.Insert(SelectedState, TargetIndex + 1); SelectedState->Parent = TargetParent; } } else { // Fallback, should never happen. ensureMsgf(false, TEXT("%s: Failed to find specified target state %s on state %s while moving a state."), *GetNameSafe(TreeData->GetOuter()), *GetNameSafe(TargetState), *GetNameSafe(SelectedParent)); ArrayToMoveTo.Add(SelectedState); SelectedState->Parent = TargetParent; } } } } OnStatesMoved.Broadcast(AffectedParents, AffectedStates); TArray> WeakStates; for (UStateTreeState* State : States) { WeakStates.Add(State); } SetSelection(WeakStates); } } void FStateTreeViewModel::BindToDebuggerDelegates() { #if WITH_STATETREE_TRACE_DEBUGGER Debugger->OnActiveStatesChanged.BindSPLambda(this, [this](const FStateTreeTraceActiveStates& NewActiveStates) { if (NewActiveStates.PerAssetStates.Num() == 0) { ActiveStates.Empty(); } else if (const UStateTree* OuterStateTree = GetStateTree()) { for (const FStateTreeTraceActiveStates::FAssetActiveStates& AssetActiveStates : NewActiveStates.PerAssetStates) { // Only track states owned by the StateTree associated to the view model (skip linked assets) if (AssetActiveStates.WeakStateTree == OuterStateTree) { ActiveStates.Reset(AssetActiveStates.ActiveStates.Num()); for (const FStateTreeStateHandle Handle : AssetActiveStates.ActiveStates) { ActiveStates.Add(OuterStateTree->GetStateIdFromHandle(Handle)); } } } } }); #endif // WITH_STATETREE_TRACE_DEBUGGER } bool FStateTreeViewModel::IsStateActiveInDebugger(const UStateTreeState& State) const { #if WITH_STATETREE_TRACE_DEBUGGER return ActiveStates.Contains(State.ID); #else return false; #endif // WITH_STATETREE_TRACE_DEBUGGER } #undef LOCTEXT_NAMESPACE