// Copyright Epic Games, Inc. All Rights Reserved. #include "Debugger/StateTreeRuntimeValidation.h" #include "Debugger/StateTreeRuntimeValidationInstanceData.h" #if WITH_STATETREE_DEBUG #include "HAL/IConsoleManager.h" #include "StateTree.h" #endif namespace UE::StateTree::Debug { #if WITH_STATETREE_DEBUG namespace Private { bool bRuntimeValidationContext = true; static FAutoConsoleVariableRef CVarRuntimeValidationContext( TEXT("StateTree.RuntimeValidation.Context"), bRuntimeValidationContext, TEXT("Test if the context creation parameters are the same between each creation of StateTreeExecutionContext.") ); bool bRuntimeValidationDoesNewerVersionExists = true; static FAutoConsoleVariableRef CVarRuntimeValidationDoesNewerVersionExists( TEXT("StateTree.RuntimeValidation.DoesNewerVersionExists"), bRuntimeValidationDoesNewerVersionExists, TEXT("Test if a StateTreeExecutionContext started with an old version of a blueprint type.") ); bool bRuntimeValidationEnterExitState = true; static FAutoConsoleVariableRef CVarRuntimeValidationEnterExitState( TEXT("StateTree.RuntimeValidation.EnterExitState"), bRuntimeValidationEnterExitState, TEXT("Test that if a node get a EnterState, it will receive an ExitState.\n" "Test that if a node get a ExitState, it did receive an EnterState before.") ); FString NodeToString(const UObject* Obj, FGuid Id) { TStringBuilderWithBuffer Buffer; if (Obj) { Obj->GetPathName(nullptr, Buffer); } Buffer << TEXT(':'); Buffer << Id; return Buffer.ToString(); } } // namespace Private FRuntimeValidationInstanceData::~FRuntimeValidationInstanceData() { if (Private::bRuntimeValidationEnterExitState && UObjectInitialized() && !IsEngineExitRequested()) { for (const FNodeStatePair& Pair : NodeStates) { if (EnumHasAllFlags(Pair.State, EState::BetweenEnterExitState)) { ensureAlwaysMsgf(false, TEXT("Tree exited. Missing ExitState on %s."), *Private::NodeToString(StateTree.Get(), Pair.NodeID)); Private::bRuntimeValidationEnterExitState = false; } } } } void FRuntimeValidationInstanceData::SetContext(const UObject* InNewOwner, const UStateTree* InNewStateTree) { TWeakObjectPtr NewStateTree = InNewStateTree; TWeakObjectPtr NewOwner = InNewOwner; if (Private::bRuntimeValidationContext) { if (StateTree.IsValid() && StateTree != NewStateTree) { ensureAlwaysMsgf(false, TEXT("StateTree runtime check failed: The StateTree '%s' is different from the previously set '%s'.\n" "Make sure you initialize FStateTreeExecutionContext with the same value every time.\n" "Auto deactivate Runtime check StateTree.RuntimeValidation.Context to prevent reporting the same error multiple times.") , InNewStateTree ? *InNewStateTree->GetFullName() : TEXT("StateTree"), *StateTree.Get()->GetFullName()); Private::bRuntimeValidationContext = false; } if (Owner.IsValid() && Owner != NewOwner) { ensureAlwaysMsgf(false, TEXT("StateTree runtime check failed: The owner '%s' is different from the previously set '%s'.\n" "Make sure you initialize FStateTreeExecutionContext with the same values every time.\n" "Auto deactivate Runtime check StateTree.RuntimeValidation.Context to prevent reporting the same error multiple times.") , InNewOwner ? *InNewOwner->GetFullName() : TEXT("owner"), *Owner.Get()->GetFullName()); Private::bRuntimeValidationContext = false; } } ValidatesTreeNodes(InNewStateTree); StateTree = NewStateTree; Owner = NewOwner; } void FRuntimeValidationInstanceData::NodeEnterState(FGuid NodeID, FActiveFrameID FrameID) { FNodeStatePair* Found = NodeStates.FindByPredicate([&NodeID, &FrameID](const FNodeStatePair& Other) { return Other.NodeID == NodeID && Other.FrameID == FrameID; }); if (Found) { if (Private::bRuntimeValidationEnterExitState && EnumHasAllFlags(Found->State, EState::BetweenEnterExitState)) { ensureAlwaysMsgf(false, TEXT("StateTree runtime check failed: EnterState executed on node %s without an ExitState.\n" "Auto deactivate Runtime check StateTree.RuntimeValidation.EnterExitState to prevent reporting the same error multiple times.") , *Private::NodeToString(Owner.Get(), NodeID)); Private::bRuntimeValidationEnterExitState = false; } EnumAddFlags(Found->State, EState::BetweenEnterExitState); } else { NodeStates.Add(FNodeStatePair{.NodeID = NodeID, .FrameID = FrameID, .State = EState::BetweenEnterExitState }); } } void FRuntimeValidationInstanceData::NodeExitState(FGuid NodeID, FActiveFrameID FrameID) { FNodeStatePair* Found = NodeStates.FindByPredicate([&NodeID, &FrameID](const FNodeStatePair& Other) { return Other.NodeID == NodeID && Other.FrameID == FrameID; }); if (Found) { if (Private::bRuntimeValidationEnterExitState && !EnumHasAllFlags(Found->State, EState::BetweenEnterExitState)) { ensureAlwaysMsgf(false, TEXT("StateTree runtime check failed: ExitState executed on node %s without an EnterState.\n" "Auto deactivate Runtime check StateTree.RuntimeValidation.EnterExitState to prevent reporting the same error multiple times.") , *Private::NodeToString(Owner.Get(), NodeID)); Private::bRuntimeValidationEnterExitState = false; } EnumRemoveFlags(Found->State, EState::BetweenEnterExitState); } else if (Private::bRuntimeValidationEnterExitState) { ensureAlwaysMsgf(false, TEXT("StateTree runtime check failed: ExitState executed on node %s without an EnterState.\n" "Auto deactivate Runtime check StateTree.RuntimeValidation.EnterExitState to prevent reporting the same error multiple times.") , *Private::NodeToString(Owner.Get(), NodeID)); Private::bRuntimeValidationEnterExitState = false; } } void FRuntimeValidationInstanceData::ValidatesTreeNodes(const UStateTree* InNewStateTree) const { if (Private::bRuntimeValidationDoesNewerVersionExists) { if (InNewStateTree && InNewStateTree->IsReadyToRun()) { auto DoesNewerVersionExists = [](const UObject* InstanceDataType) { // Is the class/scriptstruct a blueprint that got replaced by another class. bool bHasNewerVersionExistsFlag = InstanceDataType->HasAnyFlags(RF_NewerVersionExists); if (!bHasNewerVersionExistsFlag) { if (const UClass* InstanceDataClass = Cast(InstanceDataType)) { bHasNewerVersionExistsFlag = InstanceDataClass->HasAnyClassFlags(CLASS_NewerVersionExists); } else if (const UScriptStruct* InstanceDataStruct = Cast(InstanceDataType)) { bHasNewerVersionExistsFlag = (InstanceDataStruct->StructFlags & STRUCT_NewerVersionExists) != 0; } } return bHasNewerVersionExistsFlag; }; { const FStateTreeInstanceData& InstanceData = InNewStateTree->GetDefaultInstanceData(); const int32 InstanceDataNum = InstanceData.Num(); for (int32 Index = 0; Index < InstanceDataNum; ++Index) { bool bFailed = false; const UObject* InstanceObject = nullptr; if (InstanceData.IsObject(Index)) { InstanceObject = InstanceData.GetObject(Index); bFailed = DoesNewerVersionExists(InstanceObject) || (InstanceObject && DoesNewerVersionExists(InstanceObject->GetClass())); } else { InstanceObject = InstanceData.GetStruct(Index).GetScriptStruct(); bFailed = DoesNewerVersionExists(InstanceObject); } if (bFailed) { ensureAlwaysMsgf(false, TEXT("StateTree runtime check failed: The data '%s' has a newest version.\n" "It should be detected in StateTree::Link.\n" "Auto deactivate Runtime check StateTree.RuntimeValidation.DoesNewerVersionExists to prevent reporting the same error multiple times.") , *InstanceObject->GetFullName()); Private::bRuntimeValidationDoesNewerVersionExists = false; } } } for (FConstStructView NodeView : InNewStateTree->GetNodes()) { const FStateTreeNodeBase* Node = NodeView.GetPtr(); if (Node) { const UStruct* DesiredInstanceDataType = Node->GetInstanceDataType(); if (DoesNewerVersionExists(DesiredInstanceDataType)) { ensureAlwaysMsgf(false, TEXT("StateTree runtime check failed: The node '%s' has a newest version.\n" "It should be detected in StateTree::Link.\n" "Auto deactivate Runtime check StateTree.RuntimeValidation.DoesNewerVersionExists to prevent reporting the same error multiple times.") , *DesiredInstanceDataType->GetFullName()); Private::bRuntimeValidationDoesNewerVersionExists = false; } } } } } } #endif //WITH_STATETREE_DEBUG /** * */ #if WITH_STATETREE_DEBUG FRuntimeValidation::FRuntimeValidation(TNotNull Instance) : RuntimeValidationData(Instance) { } FRuntimeValidationInstanceData* FRuntimeValidation::GetInstanceData() const { return RuntimeValidationData; } void FRuntimeValidation::SetContext(const UObject* Owner, const UStateTree* StateTree) const { if (RuntimeValidationData) { RuntimeValidationData->SetContext(Owner, StateTree); } } #else void FRuntimeValidation::SetContext(const UObject*, const UStateTree*) const { } #endif //WITH_STATETREE_DEBUG } // UE::StateTree::Debug