// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License. #include "Tasks/DirectiveUtilTask_AsyncTrace.h" #include "Tasks/DirectiveUtilTask_MoveToLocation.h" #include "Tests/DirectiveUtilTestObject.h" #include "Engine/World.h" #include "Engine/Engine.h" #include "Engine/EngineBaseTypes.h" #include "Engine/EngineTypes.h" #include "Engine/StaticMesh.h" #include "Engine/StaticMeshActor.h" #include "Components/StaticMeshComponent.h" #include "GameFramework/DefaultPawn.h" #include "GameFramework/PlayerController.h" #include "TimerManager.h" #include "Misc/AutomationTest.h" namespace DirectiveUtilAsyncTraceTestHelpers { UWorld* CreateTraceWorld() { UWorld* World = UWorld::CreateWorld(EWorldType::Game, false); if (!World) { return nullptr; } FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Game); WorldContext.SetCurrentWorld(World); World->InitializeActorsForPlay(FURL()); World->BeginPlay(); return World; } AStaticMeshActor* SpawnBlockingCube(UWorld* World, UStaticMesh* CubeMesh) { AStaticMeshActor* Cube = World->SpawnActor(FVector::ZeroVector, FRotator::ZeroRotator); UStaticMeshComponent* Component = Cube->GetStaticMeshComponent(); Component->SetMobility(EComponentMobility::Movable); Component->SetStaticMesh(CubeMesh); Component->SetCollisionProfileName(TEXT("BlockAll")); Component->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); return Cube; } } bool UDirectiveUtilTestMoveToLocationTask::HasRegisteredTimers() const { const UWorld* World = TimerWorld.Get(); return World && (World->GetTimerManager().TimerExists(TimerHandle) || World->GetTimerManager().TimerExists(StuckTimerHandle)); } void UDirectiveUtilTestMoveToLocationTask::RegisterTimersForTest(UWorld* World) { TimerWorld = World; World->GetTimerManager().SetTimer(TimerHandle, FTimerDelegate::CreateLambda([] {}), 60.0f, true); World->GetTimerManager().SetTimer(StuckTimerHandle, FTimerDelegate::CreateLambda([] {}), 60.0f, true); } bool UDirectiveUtilTestMoveToActorTask::HasRegisteredTimers() const { const UWorld* World = TimerWorld.Get(); return World && (World->GetTimerManager().TimerExists(TimerHandle) || World->GetTimerManager().TimerExists(StuckTimerHandle)); } void UDirectiveUtilTestMoveToActorTask::RegisterTimersForTest(UWorld* World) { TimerWorld = World; World->GetTimerManager().SetTimer(TimerHandle, FTimerDelegate::CreateLambda([] {}), 60.0f, true); World->GetTimerManager().SetTimer(StuckTimerHandle, FTimerDelegate::CreateLambda([] {}), 60.0f, true); } class FDirectiveUtilTickTraceWorld : public IAutomationLatentCommand { public: FDirectiveUtilTickTraceWorld(FAutomationTestBase* InTest, UWorld* InWorld, const TArray& InListeners, int32 InFrames) : Test(InTest) , World(InWorld) , Listeners(InListeners) , FramesRemaining(InFrames) { } virtual bool Update() override { if (UWorld* TickWorld = World.Get()) { TickWorld->Tick(LEVELTICK_All, 0.05f); } bool bAllComplete = true; for (const UDirectiveUtilDelegateListener* Listener : Listeners) { if (Listener && !Listener->bCompleted) { bAllComplete = false; break; } } if (bAllComplete || --FramesRemaining <= 0) { for (UDirectiveUtilDelegateListener* Listener : Listeners) { if (!Listener) { continue; } Test->TestTrue(TEXT("Async trace broadcasts Completed"), Listener->bCompleted); Test->TestTrue(TEXT("Async trace through a blocking cube reports a hit"), Listener->HitCount > 0); Listener->Keepalive = nullptr; Listener->RemoveFromRoot(); } if (UWorld* TearDownWorld = World.Get()) { GEngine->DestroyWorldContext(TearDownWorld); TearDownWorld->DestroyWorld(false); } return true; } return false; } private: FAutomationTestBase* Test; TWeakObjectPtr World; TArray Listeners; int32 FramesRemaining; }; IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilAsyncTraceTest, "DirectiveUtilities.AsyncTaskTraceTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter) bool FDirectiveUtilAsyncTraceTest::RunTest(const FString& Parameters) { AddExpectedMessagePlain(TEXT("Async Trace failed to activate. World is null."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1); { UDirectiveUtilDelegateListener* Listener = NewObject(); Listener->AddToRoot(); UDirectiveUtilTask_AsyncTrace* Task = UDirectiveUtilTask_AsyncTrace::AsyncLineTraceByChannel(nullptr, FVector::ZeroVector, FVector(0, 0, 100), ETraceTypeQuery::TraceTypeQuery1, false); Listener->Keepalive = Task; Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnTraceCompleted); Task->Activate(); TestTrue("Null world trace still broadcasts Completed", Listener->bCompleted); TestEqual("Null world trace reports no hits", Listener->HitCount, 0); Listener->Keepalive = nullptr; Listener->RemoveFromRoot(); } UStaticMesh* CubeMesh = LoadObject(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube")); if (!CubeMesh) { AddError(TEXT("Engine cube mesh unavailable for async trace hit scenarios.")); return false; } UWorld* World = DirectiveUtilAsyncTraceTestHelpers::CreateTraceWorld(); if (!World) { AddError(TEXT("Failed to create a transient game world for the async trace test.")); return false; } DirectiveUtilAsyncTraceTestHelpers::SpawnBlockingCube(World, CubeMesh); const FVector Start(0.0f, 0.0f, 500.0f); const FVector End(0.0f, 0.0f, -500.0f); const ETraceTypeQuery VisibilityChannel = ETraceTypeQuery::TraceTypeQuery1; auto MakeListener = [](UDirectiveUtilTask_AsyncTrace* Task) -> UDirectiveUtilDelegateListener* { UDirectiveUtilDelegateListener* Listener = NewObject(); Listener->AddToRoot(); Listener->Keepalive = Task; Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnTraceCompleted); Task->Activate(); return Listener; }; TArray Listeners; Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncLineTraceByChannel(World, Start, End, VisibilityChannel, false))); Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncSphereTraceByChannel(World, Start, End, 25.0f, VisibilityChannel, false))); Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncBoxTraceByChannel(World, Start, End, FVector(25.0f), FRotator::ZeroRotator, VisibilityChannel, false))); Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncCapsuleTraceByChannel(World, Start, End, 25.0f, 50.0f, VisibilityChannel, false))); ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickTraceWorld(this, World, Listeners, 120)); return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMoveToLocationTest, "DirectiveUtilities.AsyncTaskMoveToLocationTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter) bool FDirectiveUtilMoveToLocationTest::RunTest(const FString& Parameters) { AddExpectedMessagePlain(TEXT("Controller, pawn, or world is unavailable while moving to location. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1); AddExpectedMessagePlain(TEXT("Controller or pawn has been destroyed while moving to location. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1); { UDirectiveUtilDelegateListener* Listener = NewObject(); Listener->AddToRoot(); UDirectiveUtilTask_MoveToLocation* Task = UDirectiveUtilTask_MoveToLocation::MoveToLocation(nullptr, nullptr, FVector(100.0f, 0.0f, 0.0f)); Listener->Keepalive = Task; Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted); Task->Activate(); TestTrue("Null controller broadcasts Completed", Listener->bCompleted); TestFalse("Null controller reports failure", Listener->bLastSuccess); Listener->Keepalive = nullptr; Listener->RemoveFromRoot(); } UWorld* World = UWorld::CreateWorld(EWorldType::Game, false); if (!World) { AddError(TEXT("Failed to create a transient world for the move-to-location test.")); return false; } FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Game); WorldContext.SetCurrentWorld(World); { APlayerController* Controller = World->SpawnActor(); UDirectiveUtilDelegateListener* Listener = NewObject(); Listener->AddToRoot(); UDirectiveUtilTask_MoveToLocation* Task = UDirectiveUtilTask_MoveToLocation::MoveToLocation(World, Controller, FVector(100.0f, 0.0f, 0.0f)); Listener->Keepalive = Task; Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted); Task->Activate(); TestTrue("Controller without a pawn broadcasts Completed", Listener->bCompleted); TestFalse("Controller without a pawn reports failure", Listener->bLastSuccess); Listener->Keepalive = nullptr; Listener->RemoveFromRoot(); } { APlayerController* Controller = World->SpawnActor(); UDirectiveUtilDelegateListener* Listener = NewObject(); Listener->AddToRoot(); UDirectiveUtilTask_MoveToLocation* Task = UDirectiveUtilTask_MoveToLocation::MoveToLocation(World, Controller, FVector(100.0f, 0.0f, 0.0f)); Listener->Keepalive = Task; Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted); Task->EndTask(); TestTrue("EndTask broadcasts Completed", Listener->bCompleted); TestFalse("EndTask reports failure", Listener->bLastSuccess); Listener->Keepalive = nullptr; Listener->RemoveFromRoot(); } { APlayerController* Controller = World->SpawnActor(); UDirectiveUtilDelegateListener* Listener = NewObject(); Listener->AddToRoot(); UDirectiveUtilTask_MoveToLocation* Task = UDirectiveUtilTask_MoveToLocation::MoveToLocation(World, Controller, FVector(100.0f, 0.0f, 0.0f)); Listener->Keepalive = Task; Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted); Task->EndTask(); Task->EndTask(); TestEqual("Double EndTask broadcasts Completed exactly once", Listener->CompletedCount, 1); Listener->Keepalive = nullptr; Listener->RemoveFromRoot(); } { APlayerController* Controller = World->SpawnActor(); ADefaultPawn* Pawn = World->SpawnActor(); if (Controller && Pawn) { Controller->SetPawn(Pawn); UDirectiveUtilTestMoveToLocationTask* Task = NewObject(); Task->Configure(Controller, FVector(1000.0f, 0.0f, 0.0f), true); Task->RegisterTimersForTest(World); TestTrue("Move to location registers both lifecycle timers", Task->HasRegisteredTimers()); Task->ClearController(); Task->Complete(); TestFalse("Move to location clears timers without a controller", Task->HasRegisteredTimers()); Task->Configure(Controller, FVector(1000.0f, 0.0f, 0.0f), true); Task->Activate(); TestFalse("A completed move to location should not restart", Task->HasRegisteredTimers()); } } GEngine->DestroyWorldContext(World); World->DestroyWorld(false); { UWorld* MoveWorld = DirectiveUtilAsyncTraceTestHelpers::CreateTraceWorld(); if (!MoveWorld) { AddError(TEXT("Failed to create a transient game world for the no-navigation move scenario.")); return false; } APlayerController* Controller = MoveWorld->SpawnActor(); ADefaultPawn* Pawn = MoveWorld->SpawnActor(FVector::ZeroVector, FRotator::ZeroRotator); if (!Controller || !Pawn) { AddError(TEXT("Failed to spawn a controller or pawn for the no-navigation move scenario.")); GEngine->DestroyWorldContext(MoveWorld); MoveWorld->DestroyWorld(false); return false; } Controller->SetPawn(Pawn); UDirectiveUtilDelegateListener* Listener = NewObject(); Listener->AddToRoot(); UDirectiveUtilTask_MoveToLocation* Task = UDirectiveUtilTask_MoveToLocation::MoveToLocation(MoveWorld, Controller, FVector(10000.0f, 0.0f, 0.0f), 100.0f, false); Listener->Keepalive = Task; Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted); Task->Activate(); TestTrue("A location move without navigation broadcasts Completed", Listener->bCompleted); TestFalse("A location move without navigation reports failure", Listener->bLastSuccess); TestEqual("A location move without navigation completes exactly once", Listener->CompletedCount, 1); Listener->Keepalive = nullptr; Listener->RemoveFromRoot(); GEngine->DestroyWorldContext(MoveWorld); MoveWorld->DestroyWorld(false); } return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMoveToActorTest, "DirectiveUtilities.AsyncTaskMoveToActorTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter) bool FDirectiveUtilMoveToActorTest::RunTest(const FString& Parameters) { AddExpectedMessagePlain(TEXT("Controller, pawn, goal, or world is unavailable while moving to actor. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1); AddExpectedMessagePlain(TEXT("Controller, pawn, or world is unavailable while moving to actor. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1); AddExpectedMessagePlain(TEXT("Controller, pawn, or goal has been destroyed while moving to actor. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1); { UDirectiveUtilDelegateListener* Listener = NewObject(); Listener->AddToRoot(); UDirectiveUtilTask_MoveToActor* Task = UDirectiveUtilTask_MoveToActor::MoveToActor(nullptr, nullptr, nullptr); Listener->Keepalive = Task; Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted); Task->Activate(); TestTrue("Null controller broadcasts Completed", Listener->bCompleted); TestFalse("Null controller reports failure", Listener->bLastSuccess); Listener->Keepalive = nullptr; Listener->RemoveFromRoot(); } UWorld* World = UWorld::CreateWorld(EWorldType::Game, false); if (!World) { AddError(TEXT("Failed to create a transient world for the move-to-actor test.")); return false; } FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Game); WorldContext.SetCurrentWorld(World); { APlayerController* Controller = World->SpawnActor(); ADefaultPawn* Pawn = World->SpawnActor(FVector::ZeroVector, FRotator::ZeroRotator); if (Controller && Pawn) { Controller->SetPawn(Pawn); UDirectiveUtilDelegateListener* Listener = NewObject(); Listener->AddToRoot(); UDirectiveUtilTask_MoveToActor* Task = UDirectiveUtilTask_MoveToActor::MoveToActor(World, Controller, nullptr); Listener->Keepalive = Task; Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted); Task->Activate(); TestTrue("Null goal broadcasts Completed", Listener->bCompleted); TestFalse("Null goal reports failure", Listener->bLastSuccess); Listener->Keepalive = nullptr; Listener->RemoveFromRoot(); } else { AddError(TEXT("Failed to spawn a controller or pawn for the null-goal scenario.")); } } { APlayerController* Controller = World->SpawnActor(); UDirectiveUtilDelegateListener* Listener = NewObject(); Listener->AddToRoot(); UDirectiveUtilTask_MoveToActor* Task = UDirectiveUtilTask_MoveToActor::MoveToActor(World, Controller, nullptr); Listener->Keepalive = Task; Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted); Task->EndTask(); Task->EndTask(); TestEqual("Double EndTask broadcasts Completed exactly once", Listener->CompletedCount, 1); Listener->Keepalive = nullptr; Listener->RemoveFromRoot(); } { APlayerController* Controller = World->SpawnActor(); ADefaultPawn* Pawn = World->SpawnActor(); AStaticMeshActor* Goal = World->SpawnActor(); if (Controller && Pawn && Goal) { Controller->SetPawn(Pawn); UDirectiveUtilTestMoveToActorTask* Task = NewObject(); Task->Configure(Controller, Goal, true); Task->RegisterTimersForTest(World); TestTrue("Move to actor registers both lifecycle timers", Task->HasRegisteredTimers()); Task->ClearController(); Task->Complete(); TestFalse("Move to actor clears timers without a controller", Task->HasRegisteredTimers()); Task->Configure(Controller, Goal, true); Task->Activate(); TestFalse("A completed move to actor should not restart", Task->HasRegisteredTimers()); } } GEngine->DestroyWorldContext(World); World->DestroyWorld(false); { UWorld* MoveWorld = DirectiveUtilAsyncTraceTestHelpers::CreateTraceWorld(); if (!MoveWorld) { AddError(TEXT("Failed to create a transient game world for the no-navigation move scenario.")); return false; } APlayerController* Controller = MoveWorld->SpawnActor(); ADefaultPawn* Pawn = MoveWorld->SpawnActor(FVector::ZeroVector, FRotator::ZeroRotator); AStaticMeshActor* GoalActor = MoveWorld->SpawnActor(FVector(10000.0f, 0.0f, 0.0f), FRotator::ZeroRotator); if (!Controller || !Pawn || !GoalActor) { AddError(TEXT("Failed to spawn a controller, pawn, or goal for the no-navigation move scenario.")); GEngine->DestroyWorldContext(MoveWorld); MoveWorld->DestroyWorld(false); return false; } Controller->SetPawn(Pawn); UDirectiveUtilDelegateListener* Listener = NewObject(); Listener->AddToRoot(); UDirectiveUtilTask_MoveToActor* Task = UDirectiveUtilTask_MoveToActor::MoveToActor(MoveWorld, Controller, GoalActor, 100.0f, false); Listener->Keepalive = Task; Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted); Task->Activate(); TestTrue("An actor move without navigation broadcasts Completed", Listener->bCompleted); TestFalse("An actor move without navigation reports failure", Listener->bLastSuccess); TestEqual("An actor move without navigation completes exactly once", Listener->CompletedCount, 1); Listener->Keepalive = nullptr; Listener->RemoveFromRoot(); GEngine->DestroyWorldContext(MoveWorld); MoveWorld->DestroyWorld(false); } return true; }