// Copyright Epic Games, Inc. All Rights Reserved. #include "AITestsCommon.h" #include "Engine/World.h" #include "GameplayTagsManager.h" #include "SmartObjectComponent.h" #include "SmartObjectRequestTypes.h" #include "SmartObjectTestTypes.h" #include "WorldConditions/SmartObjectWorldConditionObjectTagQuery.h" #define LOCTEXT_NAMESPACE "AITestSuite_SmartObjectsTest" namespace FSmartObjectTest { const FVector QueryExtent = FVector(5000.f); const FBox QueryBounds = FBox(EForceInit::ForceInit).ExpandBy(QueryExtent, QueryExtent); // Helper struct to define some test tags struct FNativeGameplayTags : public FGameplayTagNativeAdder { FGameplayTag TestTag1; FGameplayTag TestTag2; FGameplayTag TestTag3; virtual void AddTags() override { UGameplayTagsManager& Manager = UGameplayTagsManager::Get(); TestTag1 = Manager.AddNativeGameplayTag(TEXT("Test.SmartObject.Tag1")); TestTag2 = Manager.AddNativeGameplayTag(TEXT("Test.SmartObject.Tag2")); TestTag3 = Manager.AddNativeGameplayTag(TEXT("Test.SmartObject.Tag3")); } FORCEINLINE static const FNativeGameplayTags& Get() { return StaticInstance; } static FNativeGameplayTags StaticInstance; }; FNativeGameplayTags FNativeGameplayTags::StaticInstance; struct FSmartObjectTestBase : FAITestBase { FSmartObjectActorUserData TestContextData; FSmartObjectRequestFilter TestFilter; USmartObjectDefinition* Definition = nullptr; USmartObjectTestSubsystem* Subsystem = nullptr; TArray SOList; int32 NumCreatedSlots = 0; /** Callback that derived classes can override to tweak the SmartObjectDefinition before the runtime gets initialized */ virtual bool SetupDefinition() { return true; } virtual bool SetUp() override { UWorld* World = FAITestHelpers::GetWorld(); Subsystem = NewAutoDestroyObject(World); if (Subsystem == nullptr) { return false; } // Setup main definition Definition = NewAutoDestroyObject(); // Set activity tags Definition->SetActivityTags(FGameplayTagContainer(FNativeGameplayTags::Get().TestTag1)); FSmartObjectSlotDefinition& FirstSlot = Definition->DebugAddSlot(); FSmartObjectSlotDefinition& SecondSlot = Definition->DebugAddSlot(); FSmartObjectSlotDefinition& ThirdSlot = Definition->DebugAddSlot(); // Add some test behavior definition FirstSlot.BehaviorDefinitions.Add(NewAutoDestroyObject()); SecondSlot.BehaviorDefinitions.Add(NewAutoDestroyObject()); ThirdSlot.BehaviorDefinitions.Add(NewAutoDestroyObject()); // Add some test slot definition data FSmartObjectSlotTestDefinitionData DefinitionData; DefinitionData.SomeSharedFloat = 123.456f; FirstSlot.DefinitionData.Add(FSmartObjectDefinitionDataProxy::Make(DefinitionData)); SecondSlot.DefinitionData.Add(FSmartObjectDefinitionDataProxy::Make(DefinitionData)); ThirdSlot.DefinitionData.Add(FSmartObjectDefinitionDataProxy::Make(DefinitionData)); // Setup filter TestFilter.BehaviorDefinitionClasses = { USmartObjectTestBehaviorDefinition::StaticClass() }; // Allow derived classes to tweak the definitions before we initialize the runtime const bool DefinitionSetUp = SetupDefinition(); if (!DefinitionSetUp) { return false; } // Create some smart objects SOList = { NewAutoDestroyObject(World), NewAutoDestroyObject(World) }; // Register all to the subsystem for (USmartObjectComponent* SO : SOList) { if (SO != nullptr) { SO->SetDefinition(Definition); Subsystem->RegisterSmartObject(SO); NumCreatedSlots += Definition->GetSlots().Num(); } } Subsystem->RebuildAndInitializeForTesting(); return true; } virtual void TearDown() override { if (Subsystem != nullptr) { #if WITH_SMARTOBJECT_DEBUG // Force removal from the runtime simulation Subsystem->DebugCleanupRuntime(); #endif // Unregister all from the current test for (USmartObjectComponent* SO : SOList) { if (SO != nullptr) { Subsystem->UnregisterSmartObject(SO); } } } FAITestBase::TearDown(); } }; struct FFindSmartObject : FSmartObjectTestBase { virtual bool InstantTest() override { const FSmartObjectRequest Request(FSmartObjectTest::QueryBounds, TestFilter); // Find candidate const FSmartObjectRequestResult FindResult = Subsystem->FindSmartObject(Request); AITEST_TRUE("Result.IsValid()", FindResult.IsValid()); AITEST_TRUE("Result.SmartObjectHandle.IsValid()", FindResult.SmartObjectHandle.IsValid()); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FFindSmartObject, "System.AI.SmartObjects.Find"); struct FFindMultipleSmartObjects : FSmartObjectTestBase { virtual bool InstantTest() override { const FSmartObjectRequest Request(FSmartObjectTest::QueryBounds, TestFilter); // Find all candidates TArray Results; Subsystem->FindSmartObjects(Request, Results); AITEST_EQUAL("Results.Num()", Results.Num(), NumCreatedSlots); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FFindMultipleSmartObjects, "System.AI.SmartObjects.Find multiple"); struct FClaimAndReleaseSmartObject : FSmartObjectTestBase { virtual bool InstantTest() override { const FSmartObjectRequest Request(FSmartObjectTest::QueryBounds, TestFilter); // Find candidate const FSmartObjectRequestResult FirstFindResult = Subsystem->FindSmartObject(Request); AITEST_TRUE("Result.IsValid()", FirstFindResult.IsValid()); AITEST_TRUE("Result.SmartObjectHandle.IsValid()", FirstFindResult.SmartObjectHandle.IsValid()); // Gather all available candidates before claiming TArray ResultsBeforeClaim; Subsystem->FindSlots(FirstFindResult.SmartObjectHandle, TestFilter, ResultsBeforeClaim); // Claim candidate const FSmartObjectClaimHandle ClaimHandle = Subsystem->MarkSlotAsClaimed(FirstFindResult.SlotHandle, ESmartObjectClaimPriority::Normal); AITEST_TRUE("ClaimHandle.IsValid()", ClaimHandle.IsValid()); // Gather remaining available candidates TArray ResultsAfterClaim; Subsystem->FindSlots(FirstFindResult.SmartObjectHandle, TestFilter, ResultsAfterClaim); AITEST_NOT_EQUAL("Number of available slots before and after a claim", ResultsBeforeClaim.Num(), ResultsAfterClaim.Num()); // Release claimed candidate const bool bSuccess = Subsystem->MarkSlotAsFree(ClaimHandle); AITEST_TRUE("Release() return status", bSuccess); // Gather all available candidates after releasing TArray ResultsAfterRelease; Subsystem->FindSlots(FirstFindResult.SmartObjectHandle, TestFilter, ResultsAfterRelease); AITEST_EQUAL("Number of available slots before claiming and after releasing", ResultsBeforeClaim.Num(), ResultsAfterRelease.Num()); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FClaimAndReleaseSmartObject, "System.AI.SmartObjects.Claim & Release"); struct FFindAfterClaimSmartObject : FSmartObjectTestBase { virtual bool InstantTest() override { const FSmartObjectRequest Request(FSmartObjectTest::QueryBounds, TestFilter); // Find first candidate const FSmartObjectRequestResult FirstFindResult = Subsystem->FindSmartObject(Request); AITEST_TRUE("Result.IsValid()", FirstFindResult.IsValid()); AITEST_TRUE("Result.SmartObjectHandle.IsValid()", FirstFindResult.SmartObjectHandle.IsValid()); // Claim first candidate const FSmartObjectClaimHandle FirstClaimHandle = Subsystem->MarkSlotAsClaimed(FirstFindResult.SlotHandle, ESmartObjectClaimPriority::Normal); AITEST_TRUE("ClaimHandle.IsValid() after first find result", FirstClaimHandle.IsValid()); // Find second candidate const FSmartObjectRequestResult SecondFindResult = Subsystem->FindSmartObject(Request); AITEST_TRUE("Result.IsValid()", SecondFindResult.IsValid()); AITEST_TRUE("Result.SmartObjectHandle.IsValid()", SecondFindResult.SmartObjectHandle.IsValid()); AITEST_TRUE("Result.SlotHandle.IsValid()", SecondFindResult.SlotHandle.IsValid()); AITEST_NOT_EQUAL("Result is expected to point to a different slot since first slot was claimed", FirstFindResult.SlotHandle, SecondFindResult.SlotHandle); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FFindAfterClaimSmartObject, "System.AI.SmartObjects.Find after Claim"); struct FDoubleClaimSmartObject : FSmartObjectTestBase { virtual bool InstantTest() override { const FSmartObjectRequest Request(FSmartObjectTest::QueryBounds, TestFilter); // Find candidate const FSmartObjectRequestResult PreClaimResult = Subsystem->FindSmartObject(Request); AITEST_TRUE("Result.IsValid()", PreClaimResult.IsValid()); AITEST_TRUE("Result.SmartObjectHandle.IsValid()", PreClaimResult.SmartObjectHandle.IsValid()); // Claim first candidate const FSmartObjectClaimHandle FirstHdl = Subsystem->MarkSlotAsClaimed(PreClaimResult.SlotHandle, ESmartObjectClaimPriority::Normal); AITEST_TRUE("ClaimHandle.IsValid() after first claim", FirstHdl.IsValid()); // Claim first candidate again const FSmartObjectClaimHandle SecondHdl = Subsystem->MarkSlotAsClaimed(PreClaimResult.SlotHandle, ESmartObjectClaimPriority::Normal); AITEST_FALSE("ClaimHandle.IsValid() after second claim", SecondHdl.IsValid()); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FDoubleClaimSmartObject, "System.AI.SmartObjects.Double Claim"); struct FOverrideClaimSmartObject : FSmartObjectTestBase { virtual bool InstantTest() override { FSmartObjectRequest Request(FSmartObjectTest::QueryBounds, TestFilter); TArray PostClaimResults; // Find candidate const FSmartObjectRequestResult PreClaimResult = Subsystem->FindSmartObject(Request); AITEST_TRUE("Result.IsValid()", PreClaimResult.IsValid()); AITEST_TRUE("Result.SmartObjectHandle.IsValid()", PreClaimResult.SmartObjectHandle.IsValid()); // Claim slot the first time with Normal priority, should succeed. const FSmartObjectClaimHandle FirstHandle = Subsystem->MarkSlotAsClaimed(PreClaimResult.SlotHandle, ESmartObjectClaimPriority::Normal); AITEST_TRUE("FirstHandle.IsValid() after first claim", FirstHandle.IsValid()); // Try to claim slot again with Low priority, should fail. const FSmartObjectClaimHandle SecondHandle = Subsystem->MarkSlotAsClaimed(PreClaimResult.SlotHandle, ESmartObjectClaimPriority::Low); AITEST_FALSE("SecondHandle.IsValid() after second claim", SecondHandle.IsValid()); // Try to claim slot again with High priority, should succeed. const FSmartObjectClaimHandle ThirdHandle = Subsystem->MarkSlotAsClaimed(PreClaimResult.SlotHandle, ESmartObjectClaimPriority::High); AITEST_TRUE("ThirdHandle.IsValid() after third claim", ThirdHandle.IsValid()); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FOverrideClaimSmartObject, "System.AI.SmartObjects.Override Claim"); struct FFindClaimPrioritySmartObject : FSmartObjectTestBase { virtual bool InstantTest() override { FSmartObjectRequest Request(FSmartObjectTest::QueryBounds, TestFilter); TArray PostClaimResults; // Find candidate const FSmartObjectRequestResult PreClaimResult = Subsystem->FindSmartObject(Request); AITEST_TRUE("Result.IsValid()", PreClaimResult.IsValid()); AITEST_TRUE("Result.SmartObjectHandle.IsValid()", PreClaimResult.SmartObjectHandle.IsValid()); // Claim first candidate const FSmartObjectClaimHandle FirstHdl = Subsystem->MarkSlotAsClaimed(PreClaimResult.SlotHandle, ESmartObjectClaimPriority::Normal); AITEST_TRUE("ClaimHandle.IsValid() after first claim", FirstHdl.IsValid()); // Find candidate should not contain the first result on "normal" claim priority. Request.Filter.ClaimPriority = ESmartObjectClaimPriority::Normal; PostClaimResults.Reset(); Subsystem->FindSmartObjects(Request, PostClaimResults); const bool bContainsClaimedNormal = PostClaimResults.Contains(PreClaimResult); AITEST_FALSE("bContainsClaimed", bContainsClaimedNormal); // Find candidate should contain the first result on "high" claim priority. Request.Filter.ClaimPriority = ESmartObjectClaimPriority::High; PostClaimResults.Reset(); Subsystem->FindSmartObjects(Request, PostClaimResults); const bool bContainsClaimedHigh = PostClaimResults.Contains(PreClaimResult); AITEST_TRUE("bContainsClaimed", bContainsClaimedHigh); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FFindClaimPrioritySmartObject, "System.AI.SmartObjects.Find Claim Priority"); struct FUseAndReleaseSmartObject : FSmartObjectTestBase { virtual bool InstantTest() override { const FSmartObjectRequest Request(FSmartObjectTest::QueryBounds, TestFilter); // Find candidate const FSmartObjectRequestResult PreClaimResult = Subsystem->FindSmartObject(Request); AITEST_TRUE("Result.IsValid()", PreClaimResult.IsValid()); AITEST_TRUE("Result.SmartObjectHandle.IsValid()", PreClaimResult.SmartObjectHandle.IsValid()); // Claim & Use candidate const FSmartObjectClaimHandle Hdl = Subsystem->MarkSlotAsClaimed(PreClaimResult.SlotHandle, ESmartObjectClaimPriority::Normal); AITEST_TRUE("ClaimHandle.IsValid()", Hdl.IsValid()); // Use specific behavior const USmartObjectBehaviorDefinition* BehaviorDefinition = Subsystem->MarkSlotAsOccupied(Hdl); AITEST_NOT_NULL("Behavior definition pointer", BehaviorDefinition); // Release candidate const bool bSuccess = Subsystem->MarkSlotAsFree(Hdl); AITEST_TRUE("Release() return status", bSuccess); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FUseAndReleaseSmartObject, "System.AI.SmartObjects.Use & Release"); struct FFindAfterUseSmartObject : FSmartObjectTestBase { virtual bool InstantTest() override { const FSmartObjectRequest Request(FSmartObjectTest::QueryBounds, TestFilter); constexpr uint32 ExpectedNumRegisteredObjects = 2; AITEST_EQUAL("Number of registered smart objects", SOList.Num(), ExpectedNumRegisteredObjects); // Find first candidate const FSmartObjectRequestResult FirstFindResult = Subsystem->FindSmartObject(Request); AITEST_TRUE("Result.IsValid()", FirstFindResult.IsValid()); AITEST_TRUE("Result.SmartObjectHandle.IsValid()", FirstFindResult.SmartObjectHandle.IsValid()); // Claim & Use first candidate const FSmartObjectClaimHandle FirstClaimHandle = Subsystem->MarkSlotAsClaimed(FirstFindResult.SlotHandle, ESmartObjectClaimPriority::Normal); AITEST_TRUE("ClaimHandle.IsValid() after first claim", FirstClaimHandle.IsValid()); const USmartObjectBehaviorDefinition* FirstDefinition = Subsystem->MarkSlotAsOccupied(FirstClaimHandle); AITEST_NOT_NULL("Behavior definition pointer", FirstDefinition); // Find second candidate const FSmartObjectRequestResult SecondFindResult = Subsystem->FindSmartObject(Request); AITEST_TRUE("Result.IsValid()", SecondFindResult.IsValid()); AITEST_TRUE("Result.SmartObjectHandle.IsValid()", SecondFindResult.SmartObjectHandle.IsValid()); AITEST_TRUE("Result.SlotHandle.IsValid()", SecondFindResult.SlotHandle.IsValid()); AITEST_NOT_EQUAL("Result is expected to point to a different slot since first slot was claimed", FirstFindResult.SlotHandle, SecondFindResult.SlotHandle); // Claim & use second candidate const FSmartObjectClaimHandle SecondClaimHandle = Subsystem->MarkSlotAsClaimed(SecondFindResult.SlotHandle, ESmartObjectClaimPriority::Normal); AITEST_TRUE("ClaimHandle.IsValid() after second claim", SecondClaimHandle.IsValid()); const USmartObjectBehaviorDefinition* SecondDefinition = Subsystem->MarkSlotAsOccupied(SecondClaimHandle); AITEST_NOT_NULL("Behavior definition pointer", SecondDefinition); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FFindAfterUseSmartObject, "System.AI.SmartObjects.Find after Use"); struct FSlotCustomData : FSmartObjectTestBase { virtual bool InstantTest() override { const FSmartObjectRequest Request(FSmartObjectTest::QueryBounds, TestFilter); // Find an object const FSmartObjectRequestResult FindResult = Subsystem->FindSmartObject(Request); AITEST_TRUE("Result.IsValid()", FindResult.IsValid()); AITEST_TRUE("Result.SmartObjectHandle.IsValid()", FindResult.SmartObjectHandle.IsValid()); AITEST_TRUE("Result.SlotHandle.IsValid()", FindResult.SlotHandle.IsValid()); FSmartObjectSlotHandle SlotHandleInView; const FSmartObjectSlotTestDefinitionData* DefinitionData = nullptr; const FSmartObjectSlotTestRuntimeData* RuntimeData = nullptr; const bool bSlotViewValid = Subsystem->ReadSlotData(FindResult.SlotHandle , [&SlotHandleInView, &DefinitionData, &RuntimeData](FConstSmartObjectSlotView SlotView) { SlotHandleInView = SlotView.GetSlotHandle(); DefinitionData = SlotView.GetDefinitionDataPtr(); RuntimeData = SlotView.GetStateDataPtr(); }); AITEST_TRUE("SlotView.IsValid()", bSlotViewValid); AITEST_TRUE("SlotView.SlotHandle.IsValid()", SlotHandleInView.IsValid()); AITEST_NOT_NULL("Data definition pointer (for cooldown)", DefinitionData); AITEST_NULL("Runtime data pointer", RuntimeData); // Claim const FSmartObjectClaimHandle ClaimHandle = Subsystem->MarkSlotAsClaimed(FindResult.SlotHandle, ESmartObjectClaimPriority::Normal); AITEST_TRUE("ClaimHandle.IsValid() after first claim", ClaimHandle.IsValid()); // Add new data, note that this will invalidate the view... FSmartObjectSlotTestRuntimeData NewRuntimeData; constexpr float SomeFloatConstant = 654.321f; NewRuntimeData.SomePerInstanceSharedFloat = SomeFloatConstant; Subsystem->AddSlotData(ClaimHandle, FConstStructView::Make(NewRuntimeData)); // Fetch a fresh slot view bool bDataFound = false; FSmartObjectSlotTestRuntimeData OutRuntimeDataAfter; Subsystem->ReadSlotData(ClaimHandle.SlotHandle , [&OutRuntimeDataAfter, &bDataFound](const FConstSmartObjectSlotView SlotView) { if (const FSmartObjectSlotTestRuntimeData* Data = SlotView.GetStateDataPtr()) { OutRuntimeDataAfter = *Data; bDataFound = true; } }); AITEST_TRUE("Runtime data found", bDataFound); AITEST_EQUAL("Runtime data float from SlotView", OutRuntimeDataAfter.SomePerInstanceSharedFloat, SomeFloatConstant); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FSlotCustomData, "System.AI.SmartObjects.Slot custom data"); struct FDebugUnregister : FSmartObjectTestBase { virtual bool InstantTest() override { for (const USmartObjectComponent* SmartObjectComponent : SOList) { AITEST_TRUE("SmartObjectComponent is initially bound to simulation", SmartObjectComponent->IsBoundToSimulation()); } #if WITH_SMARTOBJECT_DEBUG Subsystem->DebugUnregisterAllSmartObjects(); for (const USmartObjectComponent* SmartObjectComponent : SOList) { AITEST_FALSE("SmartObjectComponent is not bound to simulation after calling RemoveComponentFromSimulation", SmartObjectComponent->IsBoundToSimulation()); } #endif // WITH_SMARTOBJECT_DEBUG // Simulate a call to EndPlay by calling UnregisterSmartObject after using DebugUnregisterAllSmartObjects for (USmartObjectComponent* SmartObjectComponent : SOList) { Subsystem->UnregisterSmartObject(SmartObjectComponent); AITEST_FALSE("SmartObjectComponent is not bound to simulation after calling UnregisterSmartObject", SmartObjectComponent->IsBoundToSimulation()); } return true; } }; IMPLEMENT_AI_INSTANT_TEST(FDebugUnregister, "System.AI.SmartObjects.Debug Unregister"); struct FBoundToSimulation : FSmartObjectTestBase { virtual bool InstantTest() override { for (const USmartObjectComponent* SmartObjectComponent : SOList) { AITEST_TRUE("SmartObjectComponent is initially bound to simulation", SmartObjectComponent->IsBoundToSimulation()); } #if WITH_SMARTOBJECT_DEBUG Subsystem->DebugUnregisterAllSmartObjects(); for (const USmartObjectComponent* SmartObjectComponent : SOList) { AITEST_FALSE("SmartObjectComponent is not bound to simulation after calling RemoveComponentFromSimulation", SmartObjectComponent->IsBoundToSimulation()); } Subsystem->DebugRegisterAllSmartObjects(); for (const USmartObjectComponent* SmartObjectComponent : SOList) { AITEST_TRUE("SmartObjectComponent is bound to simulation after calling AddComponentToSimulation", SmartObjectComponent->IsBoundToSimulation()); } #endif // WITH_SMARTOBJECT_DEBUG return true; } }; IMPLEMENT_AI_INSTANT_TEST(FBoundToSimulation, "System.AI.SmartObjects.Add to/Remove from simulation"); struct FEnabledReasons : FSmartObjectTestBase { virtual bool InstantTest() override { const USmartObjectComponent* SmartObjectComponent = SOList[0]; const FSmartObjectHandle Handle = SmartObjectComponent->GetRegisteredHandle(); AITEST_TRUE("SmartObjectComponent is enabled", Subsystem->IsEnabled(Handle)); Subsystem->SetEnabled(Handle, false); AITEST_FALSE("SmartObjectComponent is enabled", Subsystem->IsEnabled(Handle)); AITEST_FALSE("SmartObjectComponent is enabled", Subsystem->IsEnabledForReason(Handle, UE::SmartObject::EnabledReason::Gameplay)); AITEST_TRUE("SmartObjectComponent is enabled", Subsystem->IsEnabledForReason(Handle, FNativeGameplayTags::Get().TestTag1)); AITEST_TRUE("SmartObjectComponent is enabled", Subsystem->IsEnabledForReason(Handle, FNativeGameplayTags::Get().TestTag2)); AITEST_TRUE("SmartObjectComponent is enabled", Subsystem->IsEnabledForReason(Handle, FNativeGameplayTags::Get().TestTag3)); Subsystem->SetEnabledForReason(Handle, FNativeGameplayTags::Get().TestTag1, false); AITEST_FALSE("SmartObjectComponent is enabled", Subsystem->IsEnabledForReason(Handle, FNativeGameplayTags::Get().TestTag1)); AITEST_FALSE("SmartObjectComponent is enabled", Subsystem->IsEnabled(Handle)); Subsystem->SetEnabledForReason(Handle, FNativeGameplayTags::Get().TestTag2, false); AITEST_FALSE("SmartObjectComponent is enabled", Subsystem->IsEnabledForReason(Handle, FNativeGameplayTags::Get().TestTag2)); AITEST_FALSE("SmartObjectComponent is enabled", Subsystem->IsEnabled(Handle)); Subsystem->SetEnabledForReason(Handle, FNativeGameplayTags::Get().TestTag3, false); AITEST_FALSE("SmartObjectComponent is enabled", Subsystem->IsEnabledForReason(Handle, FNativeGameplayTags::Get().TestTag3)); AITEST_FALSE("SmartObjectComponent is enabled", Subsystem->IsEnabled(Handle)); Subsystem->SetEnabled(Handle, true); AITEST_FALSE("SmartObjectComponent is enabled", Subsystem->IsEnabled(Handle)); Subsystem->SetEnabledForReason(Handle, FNativeGameplayTags::Get().TestTag1, true); AITEST_TRUE("SmartObjectComponent is enabled", Subsystem->IsEnabledForReason(Handle, FNativeGameplayTags::Get().TestTag1)); AITEST_FALSE("SmartObjectComponent is enabled", Subsystem->IsEnabled(Handle)); Subsystem->SetEnabledForReason(Handle, FNativeGameplayTags::Get().TestTag2, true); AITEST_TRUE("SmartObjectComponent is enabled", Subsystem->IsEnabledForReason(Handle, FNativeGameplayTags::Get().TestTag2)); AITEST_FALSE("SmartObjectComponent is enabled", Subsystem->IsEnabled(Handle)); Subsystem->SetEnabledForReason(Handle, FNativeGameplayTags::Get().TestTag3, true); AITEST_TRUE("SmartObjectComponent is enabled", Subsystem->IsEnabledForReason(Handle, FNativeGameplayTags::Get().TestTag3)); AITEST_TRUE("SmartObjectComponent is enabled", Subsystem->IsEnabled(Handle)); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FEnabledReasons, "System.AI.SmartObjects.Enabled Reasons"); struct FActivityTagsMergingPolicy : FSmartObjectTestBase { virtual bool SetupDefinition() override { const TArrayView Slots = Definition->GetMutableSlots(); constexpr int32 NumRequiredSlots = 2; if (!ensureMsgf(Slots.Num() >= NumRequiredSlots, TEXT("Expecting at least %d slots"), NumRequiredSlots)) { return false; } // Tags setup looks like: // Object: ActivityTags: TestTag1 // Slot1: ActivityTags: TestTag2 // Slot2: ActivityTags: TestTag1, TestTag2 // Slot3: ActivityTags: --- FSmartObjectSlotDefinition& FirstSlot = Slots[0]; FSmartObjectSlotDefinition& SecondSlot = Slots[1]; FirstSlot.ActivityTags.AddTag(FNativeGameplayTags::Get().TestTag2); SecondSlot.ActivityTags.AddTag(FNativeGameplayTags::Get().TestTag1); SecondSlot.ActivityTags.AddTag(FNativeGameplayTags::Get().TestTag2); return true; } }; struct FActivityTagsMergingPolicyCombine : FActivityTagsMergingPolicy { virtual bool SetupDefinition() override { if (!FActivityTagsMergingPolicy::SetupDefinition()) { return false; } Definition->SetActivityTagsMergingPolicy(ESmartObjectTagMergingPolicy::Combine); return true; } virtual bool InstantTest() override { FSmartObjectRequest DefaultRequest(FSmartObjectTest::QueryBounds, TestFilter); { // No activity requirements, should return registered slots TArray Results; Subsystem->FindSmartObjects(DefaultRequest, Results); AITEST_EQUAL("Results.Num() using 'Combine' policy with an empty query", Results.Num(), NumCreatedSlots); } { // Adding activity requirements to the query FSmartObjectRequest ModifiedRequest = DefaultRequest; ModifiedRequest.Filter.ActivityRequirements = FGameplayTagQuery::BuildQuery( FGameplayTagQueryExpression() .NoTagsMatch() .AddTag(FNativeGameplayTags::Get().TestTag1) ); TArray Results; Subsystem->FindSmartObjects(ModifiedRequest, Results); // All slots inherit TestTag1 from parent object, so all invalid AITEST_EQUAL("Results.Num() using 'Combine' policy with NoMatch(TestTag1)", Results.Num(), 0); } { // Adding activity requirements to the query FSmartObjectRequest ModifiedRequest = DefaultRequest; ModifiedRequest.Filter.ActivityRequirements = FGameplayTagQuery::BuildQuery( FGameplayTagQueryExpression() .AnyTagsMatch() .AddTag(FNativeGameplayTags::Get().TestTag1) .AddTag(FNativeGameplayTags::Get().TestTag2) ); TArray Results; Subsystem->FindSmartObjects(ModifiedRequest, Results); // (Slot 1 & 2 & 3) = 3 matching slots / object AITEST_EQUAL("Results.Num() using 'Combine' policy with AnyMatch(TestTag1, TestTag2)", Results.Num(), SOList.Num() * 3); } { // Adding activity requirements to the query FSmartObjectRequest ModifiedRequest = DefaultRequest; ModifiedRequest.Filter.ActivityRequirements = FGameplayTagQuery::BuildQuery( FGameplayTagQueryExpression() .AllTagsMatch() .AddTag(FNativeGameplayTags::Get().TestTag1) .AddTag(FNativeGameplayTags::Get().TestTag2) ); TArray Results; Subsystem->FindSmartObjects(ModifiedRequest, Results); // (Slot 1 & 2) = 2 matching slots / object AITEST_EQUAL("Results.Num() using 'Combine' policy with AllMatch(TestTag1, TestTag2)", Results.Num(), SOList.Num() * 2); } return true; } }; IMPLEMENT_AI_INSTANT_TEST(FActivityTagsMergingPolicyCombine, "System.AI.SmartObjects.Merging policy 'Combine' on ActivityTags"); struct FActivityTagsMergingPolicyOverride : FActivityTagsMergingPolicy { virtual bool SetupDefinition() override { if (!FActivityTagsMergingPolicy::SetupDefinition()) { return false; } Definition->SetActivityTagsMergingPolicy(ESmartObjectTagMergingPolicy::Override); return true; } virtual bool InstantTest() override { FSmartObjectRequest DefaultRequest(FSmartObjectTest::QueryBounds, TestFilter); { // No activity requirements, should return registered slots TArray Results; Subsystem->FindSmartObjects(DefaultRequest, Results); AITEST_EQUAL("Results.Num() using 'Override' policy with an empty query", Results.Num(), NumCreatedSlots); } { // Adding activity requirements to the query FSmartObjectRequest ModifiedRequest = DefaultRequest; ModifiedRequest.Filter.ActivityRequirements = FGameplayTagQuery::BuildQuery( FGameplayTagQueryExpression() .NoTagsMatch() .AddTag(FNativeGameplayTags::Get().TestTag1) ); TArray Results; Subsystem->FindSmartObjects(ModifiedRequest, Results); // Slot 1 only has TestTag2 (Slot 2 has TestTag1 in override and Slot 3 inherits from parent) = 1 matching slots / object AITEST_EQUAL("Results.Num() using 'Override' policy with NoMatch(TestTag1)", Results.Num(), SOList.Num() * 1); } { // Adding activity requirements to the query FSmartObjectRequest ModifiedRequest = DefaultRequest; ModifiedRequest.Filter.ActivityRequirements = FGameplayTagQuery::BuildQuery( FGameplayTagQueryExpression() .AllTagsMatch() .AddTag(FNativeGameplayTags::Get().TestTag1) .AddTag(FNativeGameplayTags::Get().TestTag2) ); TArray Results; Subsystem->FindSmartObjects(ModifiedRequest, Results); // (Slot 2) = 1 matching slot / object AITEST_EQUAL("Results.Num() using 'Override' policy with AllMatch(TestTag1, TestTag2)", Results.Num(), SOList.Num() * 1); } return true; } }; IMPLEMENT_AI_INSTANT_TEST(FActivityTagsMergingPolicyOverride, "System.AI.SmartObjects.Merging policy 'Override' on ActivityTags"); struct FUserTagsFilterPolicy : FSmartObjectTestBase { virtual bool SetupDefinition() override { const TArrayView Slots = Definition->GetMutableSlots(); constexpr int32 NumRequiredSlots = 2; if (!ensureMsgf(Slots.Num() >= NumRequiredSlots, TEXT("Expecting at least %d slots"), NumRequiredSlots)) { return false; } // Tags setup looks like: // Object: UserTagFilter: Match(TestTag1) // Slot1: UserTagFilter: NoMatch(TestTag2) // Slot2: UserTagFilter: AnyMatch(TestTag1, TestTag2, TestTag3) FSmartObjectSlotDefinition& FirstSlot = Slots[0]; FSmartObjectSlotDefinition& SecondSlot = Slots[1]; // Set first slot user tag filter FirstSlot.UserTagFilter = FGameplayTagQuery::BuildQuery( FGameplayTagQueryExpression() .NoTagsMatch() .AddTag(FNativeGameplayTags::Get().TestTag2) ); // Set second slot user tag filter SecondSlot.UserTagFilter = FGameplayTagQuery::BuildQuery( FGameplayTagQueryExpression() .AnyTagsMatch() .AddTag(FNativeGameplayTags::Get().TestTag1) .AddTag(FNativeGameplayTags::Get().TestTag3) ); // Set user tag filter Definition->SetUserTagFilter(FGameplayTagQuery::BuildQuery( FGameplayTagQueryExpression() .AllTagsMatch() .AddTag(FNativeGameplayTags::Get().TestTag1) )); return true; } }; struct FUserTagsFilterPolicyNoFilter : FUserTagsFilterPolicy { virtual bool SetupDefinition() override { if (!FUserTagsFilterPolicy::SetupDefinition()) { return false; } Definition->SetUserTagsFilteringPolicy(ESmartObjectTagFilteringPolicy::NoFilter); return true; } virtual bool InstantTest() override { FSmartObjectRequest DefaultRequest(FSmartObjectTest::QueryBounds, TestFilter); { // Providing user tags to the query FSmartObjectRequest ModifiedRequest = DefaultRequest; ModifiedRequest.Filter.UserTags.AddTag(FNativeGameplayTags::Get().TestTag2); TArray Results; Subsystem->FindSmartObjects(ModifiedRequest, Results); AITEST_EQUAL("Results.Num() using 'NoFilter' policy with user tags = TestTag2", Results.Num(), NumCreatedSlots); } return true; } }; IMPLEMENT_AI_INSTANT_TEST(FUserTagsFilterPolicyNoFilter, "System.AI.SmartObjects.Filter policy 'NoFilter' on UserTags"); struct FUserTagsFilterPolicyCombine : FUserTagsFilterPolicy { virtual bool SetupDefinition() override { if (!FUserTagsFilterPolicy::SetupDefinition()) { return false; } Definition->SetUserTagsFilteringPolicy(ESmartObjectTagFilteringPolicy::Combine); return true; } virtual bool InstantTest() override { FSmartObjectRequest DefaultRequest(FSmartObjectTest::QueryBounds, TestFilter); { // User tags are empty so none should be found TArray Results; Subsystem->FindSmartObjects(DefaultRequest, Results); AITEST_EQUAL("Results.Num() using 'Combine' policy with an empty query", Results.Num(), 0); } { // Add TestTag1 to user tags FSmartObjectRequest ModifiedRequest = DefaultRequest; ModifiedRequest.Filter.UserTags.AddTag(FNativeGameplayTags::Get().TestTag1); TArray Results; Subsystem->FindSmartObjects(ModifiedRequest, Results); // (Slot 1 & 2 & 3) = 3 matching slots / object AITEST_EQUAL("Results.Num() using 'Combine' policy with user tags = TestTag1", Results.Num(), SOList.Num() * 3); // Add TestTag2 to User tags so first slot should match ModifiedRequest.Filter.UserTags.AddTag(FNativeGameplayTags::Get().TestTag2); Results.Reset(); Subsystem->FindSmartObjects(ModifiedRequest, Results); // (Slot 2 & 3) = 2 matching slots / object AITEST_EQUAL("Results.Num() using 'Combine' policy with user tags = TestTag1, TestTag2", Results.Num(), SOList.Num() * 2); } { // Add TestTag3 to User tags so first slot should match FSmartObjectRequest ModifiedRequest = DefaultRequest; ModifiedRequest.Filter.UserTags.AddTag(FNativeGameplayTags::Get().TestTag3); TArray Results; Subsystem->FindSmartObjects(ModifiedRequest, Results); AITEST_EQUAL("Results.Num() using 'Combine' policy with user tags = TestTag3", Results.Num(), 0); } return true; } }; IMPLEMENT_AI_INSTANT_TEST(FUserTagsFilterPolicyCombine, "System.AI.SmartObjects.Filter policy 'Combine' on UserTags"); struct FUserTagsFilterPolicyOverride : FUserTagsFilterPolicy { virtual bool SetupDefinition() override { if (!FUserTagsFilterPolicy::SetupDefinition()) { return false; } Definition->SetUserTagsFilteringPolicy(ESmartObjectTagFilteringPolicy::Override); return true; } virtual bool InstantTest() override { FSmartObjectRequest DefaultRequest(FSmartObjectTest::QueryBounds, TestFilter); { // User tags are empty TArray Results; Subsystem->FindSmartObjects(DefaultRequest, Results); // (Slot 1) = 1 matching slots / object AITEST_EQUAL("Results.Num() using 'Override' policy with an empty query", Results.Num(), SOList.Num() * 1); } { // Add TestTag1 to user tags FSmartObjectRequest ModifiedRequest = DefaultRequest; ModifiedRequest.Filter.UserTags.AddTag(FNativeGameplayTags::Get().TestTag1); TArray Results; Subsystem->FindSmartObjects(ModifiedRequest, Results); // (Slot 1 & 2 & 3) = 3 matching slots / object AITEST_EQUAL("Results.Num() using 'Override' policy with user tags = TestTag1", Results.Num(), SOList.Num() * 3); // Add TestTag2 to User tags so first slot should match ModifiedRequest.Filter.UserTags.AddTag(FNativeGameplayTags::Get().TestTag2); Results.Reset(); Subsystem->FindSmartObjects(ModifiedRequest, Results); // (Slot 2 & 3) = 2 matching slots / object AITEST_EQUAL("Results.Num() using 'Override' policy with user tags = TestTag1, TestTag2", Results.Num(), SOList.Num() * 2); } { // Add TestTag3 to User tags so first slot should match FSmartObjectRequest ModifiedRequest = DefaultRequest; ModifiedRequest.Filter.UserTags.AddTag(FNativeGameplayTags::Get().TestTag3); TArray Results; Subsystem->FindSmartObjects(ModifiedRequest, Results); // (Slot 1 & 2) = 2 matching slots / object AITEST_EQUAL("Results.Num() using 'Override' policy with user tags = TestTag3", Results.Num(), SOList.Num() * 2); } return true; } }; IMPLEMENT_AI_INSTANT_TEST(FUserTagsFilterPolicyOverride, "System.AI.SmartObjects.Filter policy 'Override' on UserTags"); struct FInstanceTagsFilter : FSmartObjectTestBase { virtual bool SetupDefinition() override { if (!FSmartObjectTestBase::SetupDefinition()) { return false; } // Tags setup looks like: // Object: FSmartObjectWorldConditionObjectTagQuery: NoMatch(TestTag1) FWorldConditionQueryDefinition& ConditionQueryDefinition = Definition->GetMutablePreconditions(); FSmartObjectWorldConditionObjectTagQuery NewCondition; NewCondition.TagQuery = FGameplayTagQuery::BuildQuery( FGameplayTagQueryExpression() .NoTagsMatch() .AddTag(FNativeGameplayTags::Get().TestTag1)); ConditionQueryDefinition.Initialize(Definition, Definition->GetWorldConditionSchemaClass(), { FWorldConditionEditable(0, EWorldConditionOperator::And, FConstStructView::Make(NewCondition)) }); return true; } virtual bool InstantTest() override { const FConstStructView ConditionsUserData = FConstStructView::Make(TestContextData); const FSmartObjectRequest DefaultRequest(FSmartObjectTest::QueryBounds, TestFilter); FSmartObjectRequestResult SingleResult = Subsystem->FindSmartObject(DefaultRequest); AITEST_TRUE("SingleResult.IsValid()", SingleResult.IsValid()); TArray Results; Subsystem->FindSmartObjects(DefaultRequest, Results); AITEST_EQUAL("Results.Num() for objects without instance tags", Results.Num(), NumCreatedSlots); AITEST_NOT_EQUAL("Num results", Results.Num(), 0); const FSmartObjectHandle ObjectToDisableByTag = Results.Top().SmartObjectHandle; // Find candidate slots TArray SlotHandles; Subsystem->FindSlots(ObjectToDisableByTag, TestFilter, SlotHandles, ConditionsUserData); AITEST_TRUE("Num slot handles", SlotHandles.Num() >= 3); // Claim first slot const FSmartObjectClaimHandle FirstClaimHandle = Subsystem->MarkSlotAsClaimed(SlotHandles[0], ESmartObjectClaimPriority::Normal); AITEST_TRUE("FirstClaimHandle.IsValid()", FirstClaimHandle.IsValid()); // Use First slot const USmartObjectBehaviorDefinition* BehaviorDefinition = Subsystem->MarkSlotAsOccupied(FirstClaimHandle); AITEST_NOT_NULL("Behavior definition pointer for first slot before activation", BehaviorDefinition); // Apply tag that will cause preconditions to fail for some results AITEST_TRUE("Result should pass selection conditions", Subsystem->EvaluateSelectionConditions(SingleResult.SlotHandle, ConditionsUserData)); Subsystem->AddTagToInstance(ObjectToDisableByTag, FNativeGameplayTags::Get().TestTag1); AITEST_FALSE("Result should fail selection conditions", Subsystem->EvaluateSelectionConditions(SingleResult.SlotHandle, ConditionsUserData)); // Find new list of candidates that should exclude all slots from 'ObjectToDisableByTag' TArray ResultsAfter; Subsystem->FindSmartObjects(DefaultRequest, ResultsAfter); // (Slot 1 & 2 & 3) = 3 matching slots / object AITEST_EQUAL("Results.Num() for 1 object with instance tags (InstanceTags=TestTag1)", ResultsAfter.Num(), (SOList.Num()-1) * 3); // Find candidate slots from deactivated object TArray SlotHandlesAfter; Subsystem->FindSlots(ObjectToDisableByTag, TestFilter, SlotHandlesAfter, ConditionsUserData); AITEST_EQUAL("Num slot handles from deactivated object", SlotHandlesAfter.Num(), 0); // Validate that 3rd slot with previously valid stored results can not be claimed or used const bool bThirdClaimPossible = Subsystem->EvaluateSelectionConditions(SlotHandles[2], ConditionsUserData); AITEST_FALSE("bThirdClaimPossible", bThirdClaimPossible); // Release all valid claim handles const bool bFirstSlotReleaseSuccess = Subsystem->MarkSlotAsFree(FirstClaimHandle); AITEST_TRUE("bFirstSlotReleaseSuccess", bFirstSlotReleaseSuccess); // Remove tag Subsystem->RemoveTagFromInstance(ObjectToDisableByTag, FNativeGameplayTags::Get().TestTag1); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FInstanceTagsFilter, "System.AI.SmartObjects.Filter policy on InstanceTags"); } // namespace FSmartObjectTest #undef LOCTEXT_NAMESPACE