// Copyright Epic Games, Inc. All Rights Reserved. #include "StateTreeEditingSubsystem.h" #include "SStateTreeView.h" #include "StateTreeCompiler.h" #include "StateTreeCompilerLog.h" #include "StateTreeCompilerManager.h" #include "StateTreeDelegates.h" #include "StateTreeEditorData.h" #include "StateTreeEditorModule.h" #include "StateTreeEditorSchema.h" #include "StateTreeObjectHash.h" #include "StateTreeTaskBase.h" #include "UObject/UObjectGlobals.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(StateTreeEditingSubsystem) UStateTreeEditingSubsystem::UStateTreeEditingSubsystem() { PostGarbageCollectHandle = FCoreUObjectDelegates::GetPostGarbageCollect().AddUObject(this, &UStateTreeEditingSubsystem::HandlePostGarbageCollect); PostCompileHandle = UE::StateTree::Delegates::OnPostCompile.AddUObject(this, &UStateTreeEditingSubsystem::HandlePostCompile); } void UStateTreeEditingSubsystem::BeginDestroy() { UE::StateTree::Delegates::OnPostCompile.Remove(PostCompileHandle); FCoreUObjectDelegates::GetPostGarbageCollect().Remove(PostGarbageCollectHandle); Super::BeginDestroy(); } bool UStateTreeEditingSubsystem::CompileStateTree(TNotNull InStateTree, FStateTreeCompilerLog& InOutLog) { return UE::StateTree::Compiler::FCompilerManager::CompileSynchronously(InStateTree, InOutLog); } TSharedPtr UStateTreeEditingSubsystem::FindViewModel(TNotNull InStateTree) const { const FObjectKey StateTreeKey = InStateTree; TSharedPtr ViewModelPtr = StateTreeViewModels.FindRef(StateTreeKey); if (ViewModelPtr) { // The StateTree could be re-instantiated. Can occur when the object is destroyed and recreated in a pool or when reloaded in editor. //The object might have the same pointer value or the same path but it's a new object and all weakptr are now invalid. if (ViewModelPtr->GetStateTree() == InStateTree) { return ViewModelPtr.ToSharedRef(); } } return nullptr; } TSharedRef UStateTreeEditingSubsystem::FindOrAddViewModel(TNotNull InStateTree) { const FObjectKey StateTreeKey = InStateTree; TSharedPtr ViewModelPtr = StateTreeViewModels.FindRef(StateTreeKey); if (ViewModelPtr) { // The StateTree could be re-instantiated. Can occur when the object is destroyed and recreated in a pool or when reloaded in editor. //The object might have the same pointer value or the same path but it's a new object and all weakptr are now invalid. if (ViewModelPtr->GetStateTree() == InStateTree) { return ViewModelPtr.ToSharedRef(); } else { StateTreeViewModels.Remove(StateTreeKey); ViewModelPtr = nullptr; } } ValidateStateTree(InStateTree); TSharedRef SharedModel = StateTreeViewModels.Add(StateTreeKey, MakeShared()).ToSharedRef(); UStateTreeEditorData* EditorData = Cast(InStateTree->EditorData); SharedModel->Init(EditorData); return SharedModel; } TSharedRef UStateTreeEditingSubsystem::GetStateTreeView(TSharedRef InViewModel, const TSharedRef& TreeViewCommandList) { return SNew(SStateTreeView, InViewModel, TreeViewCommandList); } void UStateTreeEditingSubsystem::ValidateStateTree(TNotNull InStateTree) { auto FixChangedStateLinkName = [](FStateTreeStateLink& StateLink, const TMap& IDToName) -> bool { if (StateLink.ID.IsValid()) { const FName* Name = IDToName.Find(StateLink.ID); if (Name == nullptr) { // Missing link, we'll show these in the UI return false; } if (StateLink.Name != *Name) { // Name changed, fix! StateLink.Name = *Name; return true; } } return false; }; auto ValidateLinkedStates = [&FixChangedStateLinkName](TNotNull TreeData) { // Make sure all state links are valid and update the names if needed. // Create ID to state name map. TMap IDToName; TreeData->VisitHierarchy([&IDToName](const UStateTreeState& State, UStateTreeState* /*ParentState*/) { IDToName.Add(State.ID, State.Name); return EStateTreeVisitor::Continue; }); // Fix changed names. TreeData->VisitHierarchy([&IDToName, FixChangedStateLinkName](UStateTreeState& State, UStateTreeState* /*ParentState*/) { constexpr bool bMarkDirty = false; State.Modify(bMarkDirty); if (State.Type == EStateTreeStateType::Linked) { FixChangedStateLinkName(State.LinkedSubtree, IDToName); } for (FStateTreeTransition& Transition : State.Transitions) { FixChangedStateLinkName(Transition.State, IDToName); } return EStateTreeVisitor::Continue; }); }; auto FixEditorData = [](TNotNull StateTree) { UStateTreeEditorData* EditorData = Cast(StateTree->EditorData); // The schema is defined in the EditorData. If we can't find the editor data (probably because the class doesn't exist anymore), then try the compiled schema in the state tree asset. TSubclassOf SchemaClass; if (EditorData && EditorData->Schema) { SchemaClass = EditorData->Schema->GetClass(); } else if (StateTree->GetSchema()) { SchemaClass = StateTree->GetSchema()->GetClass(); } if (SchemaClass.Get() == nullptr) { UE_LOG(LogStateTreeEditor, Error, TEXT("The state tree '%s' does not have a schema."), *StateTree->GetPathName()); return; } TNonNullSubclassOf EditorDataClass = FStateTreeEditorModule::GetModule().GetEditorDataClass(SchemaClass.Get()); if (EditorData == nullptr) { EditorData = NewObject(StateTree, EditorDataClass.Get(), FName(), RF_Transactional); EditorData->AddRootState(); EditorData->Schema = NewObject(EditorData, SchemaClass.Get()); EditorData->EditorBindings.SetBindingsOwner(EditorData); constexpr bool bMarkDirty = false; StateTree->Modify(bMarkDirty); StateTree->EditorData = EditorData; } else if (!EditorData->IsA(EditorDataClass.Get())) { // The current EditorData is not of the correct type. The data needs to be patched by the schema desired editor data subclass. UStateTreeEditorData* PreviousEditorData = EditorData; EditorData = CastChecked(StaticDuplicateObject(EditorData, StateTree, FName(), RF_Transactional, EditorDataClass.Get())); if (EditorData->SubTrees.Num() == 0) { EditorData->AddRootState(); } if (EditorData->Schema == nullptr || !EditorData->Schema->IsA(SchemaClass.Get())) { EditorData->Schema = NewObject(EditorData, SchemaClass.Get()); } EditorData->EditorBindings.SetBindingsOwner(EditorData); constexpr bool bMarkDirty = false; StateTree->Modify(bMarkDirty); StateTree->EditorData = EditorData; // Trash the previous EditorData const FName TrashStateTreeName = MakeUniqueObjectName(GetTransientPackage(), UStateTree::StaticClass(), *FString::Printf(TEXT("TRASH_%s"), *UStateTree::StaticClass()->GetName())); UStateTree* TransientOuter = NewObject(GetTransientPackage(), TrashStateTreeName, RF_Transient); const FName TrashSchemaName = *FString::Printf(TEXT("TRASH_%s"), *PreviousEditorData->GetName()); constexpr ERenameFlags RenameFlags = REN_DoNotDirty | REN_DontCreateRedirectors; PreviousEditorData->Rename(*TrashSchemaName.ToString(), TransientOuter, RenameFlags); PreviousEditorData->SetFlags(RF_Transient); } }; auto FixEditorSchema = [](TNotNull EditorData) { TSubclassOf SchemaClass = EditorData->Schema ? EditorData->Schema->GetClass() : nullptr; if (SchemaClass.Get() == nullptr) { return; } TNonNullSubclassOf EditorSchemaClass = FStateTreeEditorModule::GetModule().GetEditorSchemaClass(SchemaClass.Get()); if (EditorData->EditorSchema == nullptr) { EditorData->EditorSchema = NewObject(EditorData, EditorSchemaClass.Get(), FName(), RF_Transactional); } else if (!EditorData->EditorSchema->IsA(EditorSchemaClass.Get())) { // The current EditorSchema is not of the correct type. The data needs to be patched by the schema desired editor data subclass. UStateTreeEditorSchema* PreviousEditorSchema = EditorData->EditorSchema; EditorData->EditorSchema = CastChecked(StaticDuplicateObject(PreviousEditorSchema, EditorData, FName(), RF_Transactional, EditorSchemaClass.Get())); // Trash the previous EditorData const FName TrashName = MakeUniqueObjectName(GetTransientPackage(), UStateTreeEditorSchema::StaticClass(), *FString::Printf(TEXT("TRASH_%s"), *PreviousEditorSchema->GetName())); constexpr ERenameFlags RenameFlags = REN_DoNotDirty | REN_DontCreateRedirectors; PreviousEditorSchema->Rename(*TrashName.ToString(), PreviousEditorSchema, RenameFlags); PreviousEditorSchema->SetFlags(RF_Transient); } }; auto RemoveUnusedBindings = [](TNotNull EditorData) { TMap AllStructValues; EditorData->GetAllStructValues(AllStructValues); EditorData->GetPropertyEditorBindings()->RemoveInvalidBindings(AllStructValues); }; auto UpdateLinkedStateParameters = [](TNotNull TreeData) { const EStateTreeVisitor Result = TreeData->VisitHierarchy([](UStateTreeState& State, UStateTreeState* /*ParentState*/) { if (State.Type == EStateTreeStateType::Linked || State.Type == EStateTreeStateType::LinkedAsset) { constexpr bool bMarkDirty = false; State.Modify(bMarkDirty); State.UpdateParametersFromLinkedSubtree(); } return EStateTreeVisitor::Continue; }); }; auto UpdateTransactionalFlags = [](TNotNull EditorData) { for (UStateTreeState* SubTree : EditorData->SubTrees) { TArray Stack; Stack.Add(SubTree); while (!Stack.IsEmpty()) { if (UStateTreeState* State = Stack.Pop()) { State->SetFlags(RF_Transactional); for (UStateTreeState* ChildState : State->Children) { Stack.Add(ChildState); } } } } }; FixEditorData(InStateTree); if (InStateTree->EditorData) { constexpr bool bMarkDirty = false; InStateTree->EditorData->Modify(bMarkDirty); UStateTreeEditorData* EditorData = CastChecked(InStateTree->EditorData); FixEditorSchema(EditorData); EditorData->ReparentStates(); if (EditorData->EditorSchema) { EditorData->EditorSchema->Validate(InStateTree); } RemoveUnusedBindings(EditorData); ValidateLinkedStates(EditorData); UpdateLinkedStateParameters(EditorData); UpdateTransactionalFlags(EditorData); } } uint32 UStateTreeEditingSubsystem::CalculateStateTreeHash(TNotNull InStateTree) { uint32 EditorDataHash = 0; if (InStateTree->EditorData != nullptr) { FStateTreeObjectCRC32 Archive; EditorDataHash = Archive.Crc32(InStateTree->EditorData, 0); } return EditorDataHash; } void UStateTreeEditingSubsystem::HandlePostGarbageCollect() { // Remove the stale viewmodels for (TMap>::TIterator It(StateTreeViewModels); It; ++It) { if (!It.Key().ResolveObjectPtr()) { It.RemoveCurrent(); } else if (!It.Value() || !It.Value()->GetStateTree()) { It.RemoveCurrent(); } } } void UStateTreeEditingSubsystem::HandlePostCompile(const UStateTree& InStateTree) { // Notify the UI that something changed. Make sure to not request a new viewmodel. That way, we are not creating new viewmodel when cooking/PIE. if (TSharedPtr ViewModel = FindViewModel(&InStateTree)) { ViewModel->NotifyAssetChangedExternally(); } }