// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License. #if WITH_EDITOR #include "Subsystems/DirectiveUtilEditorActorSubsystem.h" #include "Types/DirectiveUtilEditorTypes.h" #include "Misc/AutomationTest.h" #include "Engine/StaticMesh.h" #include "Engine/StaticMeshActor.h" #include "Engine/Texture2D.h" #include "Components/StaticMeshComponent.h" #include "Components/BoxComponent.h" #include "Components/CapsuleComponent.h" #include "Materials/Material.h" #include "Materials/MaterialExpressionTextureSample.h" #include "Materials/MaterialInterface.h" #include "Engine/World.h" #include "Engine/Engine.h" #include "UObject/Package.h" IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemTest, "DirectiveUtilities.EditorActorSubsystemTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) bool FDirectiveUtilEditorActorSubsystemTest::RunTest(const FString& Parameters) { UDirectiveUtilEditorActorSubsystem::FocusActorsInViewport({nullptr}); // IsActorWithinBoxBounds should not crash and should return false with null Actor TestFalse("IsActorWithinBoxBounds should return false with null Actor", UDirectiveUtilEditorActorSubsystem::IsActorWithinBoxBounds(nullptr, nullptr)); // IsActorWithinSphereBounds should not crash and should return false with null Actor TestFalse("IsActorWithinSphereBounds should return false with null Actor", UDirectiveUtilEditorActorSubsystem::IsActorWithinSphereBounds(nullptr, nullptr)); // IsActorWithinCapsuleBounds should not crash and should return false with null Actor TestFalse("IsActorWithinCapsuleBounds should return false with null CapsuleComponent", UDirectiveUtilEditorActorSubsystem::IsActorWithinCapsuleBounds(nullptr, nullptr)); // FilterEmptyActors should not crash with an empty array TArray EmptyActors; TArray FilteredActors; UDirectiveUtilEditorActorSubsystem::FilterEmptyActors(EmptyActors, FilteredActors, Include); TestEqual("FilterEmptyActors with empty input should produce empty output", FilteredActors.Num(), 0); // FilterActorsByMaterialName should not crash with an empty array TArray MaterialFiltered; UDirectiveUtilEditorActorSubsystem::FilterActorsByMaterialName(EmptyActors, MaterialFiltered, TEXT("TestMaterial"), OverrideOnly, Include); TestEqual("FilterActorsByMaterialName with empty input should produce empty output", MaterialFiltered.Num(), 0); // FilterActorsByVertCount should not crash with an empty array TArray VertFiltered; UDirectiveUtilEditorActorSubsystem::FilterActorsByVertCount(EmptyActors, VertFiltered, 0, 1000, Include); TestEqual("FilterActorsByVertCount with empty input should produce empty output", VertFiltered.Num(), 0); // FilterActorsByBounds should not crash with an empty array TArray BoundsFiltered; UDirectiveUtilEditorActorSubsystem::FilterActorsByBounds(EmptyActors, BoundsFiltered, FVector::ZeroVector, FVector::OneVector, Include); TestEqual("FilterActorsByBounds with empty input should produce empty output", BoundsFiltered.Num(), 0); return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemFilterTest, "DirectiveUtilities.EditorActorSubsystemFilterTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) bool FDirectiveUtilEditorActorSubsystemFilterTest::RunTest(const FString& Parameters) { UStaticMesh* Cube = LoadObject(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube")); UStaticMesh* Sphere = LoadObject(nullptr, TEXT("/Engine/BasicShapes/Sphere.Sphere")); if (!Cube || !Sphere) { AddError(TEXT("Engine basic shapes unavailable for the include/exclude behaviour test.")); return false; } UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false); if (!World) { AddError(TEXT("Failed to create a transient world for the filter test.")); return false; } FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor); WorldContext.SetCurrentWorld(World); auto SpawnWithMeshes = [World](const TArray& Meshes) -> AActor* { AActor* Actor = World->SpawnActor(); USceneComponent* Root = NewObject(Actor); Actor->SetRootComponent(Root); Root->RegisterComponent(); for (UStaticMesh* Mesh : Meshes) { UStaticMeshComponent* MeshComponent = NewObject(Actor); MeshComponent->SetupAttachment(Root); MeshComponent->RegisterComponent(); MeshComponent->SetStaticMesh(Mesh); Actor->AddInstanceComponent(MeshComponent); } return Actor; }; AActor* ActorCubeAndSphere = SpawnWithMeshes({ Cube, Sphere }); AActor* ActorCubeOnly = SpawnWithMeshes({ Cube }); AActor* ActorNoMesh = SpawnWithMeshes({}); const TArray Source = { ActorCubeAndSphere, ActorCubeOnly, ActorNoMesh }; // Include: actors that contain the cube mesh. TArray Included; UDirectiveUtilEditorActorSubsystem::FilterActorsByStaticMesh(Source, Included, Cube, Include); TestTrue("Include: multi-mesh actor containing the cube is included", Included.Contains(ActorCubeAndSphere)); TestTrue("Include: cube-only actor is included", Included.Contains(ActorCubeOnly)); TestFalse("Include: actor with no mesh is excluded", Included.Contains(ActorNoMesh)); // Exclude: actors that do NOT contain the cube. A multi-mesh actor that uses the cube in one slot // must still be excluded even though another slot uses a different mesh (the aggregation fix). TArray Excluded; UDirectiveUtilEditorActorSubsystem::FilterActorsByStaticMesh(Source, Excluded, Cube, Exclude); TestFalse("Exclude: multi-mesh actor containing the cube is not mistakenly included", Excluded.Contains(ActorCubeAndSphere)); TestFalse("Exclude: cube-only actor is excluded", Excluded.Contains(ActorCubeOnly)); TestTrue("Exclude: actor with no mesh is included", Excluded.Contains(ActorNoMesh)); GEngine->DestroyWorldContext(World); World->DestroyWorld(false); return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemFilterCoverageTest, "DirectiveUtilities.EditorActorSubsystemFilterCoverageTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) bool FDirectiveUtilEditorActorSubsystemFilterCoverageTest::RunTest(const FString& Parameters) { UStaticMesh* Cube = LoadObject(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube")); UStaticMesh* Sphere = LoadObject(nullptr, TEXT("/Engine/BasicShapes/Sphere.Sphere")); UMaterialInterface* MatA = LoadObject(nullptr, TEXT("/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial")); UMaterialInterface* MatB = LoadObject(nullptr, TEXT("/Engine/EngineMaterials/WorldGridMaterial.WorldGridMaterial")); if (!Cube || !Sphere || !MatA || !MatB) { AddError(TEXT("Engine basic shapes/materials unavailable for the filter coverage test.")); return false; } UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false); if (!World) { AddError(TEXT("Failed to create a transient world for the filter coverage test.")); return false; } FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor); WorldContext.SetCurrentWorld(World); auto SpawnMesh = [World](UStaticMesh* Mesh, EComponentMobility::Type Mobility) -> UStaticMeshComponent* { AActor* Actor = World->SpawnActor(); USceneComponent* Root = NewObject(Actor); Root->SetMobility(Mobility); Actor->SetRootComponent(Root); Root->RegisterComponent(); UStaticMeshComponent* MeshComponent = NewObject(Actor); MeshComponent->SetMobility(Mobility); MeshComponent->SetupAttachment(Root); if (Mesh) { MeshComponent->SetStaticMesh(Mesh); } MeshComponent->RegisterComponent(); Actor->AddInstanceComponent(MeshComponent); return MeshComponent; }; // Actor A: cube, MatA override, Static mobility, BlockAll collision, tag Alpha, at origin. UStaticMeshComponent* CompA = SpawnMesh(Cube, EComponentMobility::Static); AActor* ActorA = CompA->GetOwner(); CompA->SetMaterial(0, MatA); CompA->SetCollisionProfileName(TEXT("BlockAll")); ActorA->Tags.Add(FName("Alpha")); ActorA->SetActorLocation(FVector::ZeroVector); // Actor B: sphere, MatB override, Movable mobility, far away, no tag. UStaticMeshComponent* CompB = SpawnMesh(Sphere, EComponentMobility::Movable); AActor* ActorB = CompB->GetOwner(); CompB->SetMaterial(0, MatB); ActorB->SetActorLocation(FVector(100000.0f, 0.0f, 0.0f)); // Actor C: no static mesh component at all. AActor* ActorC = World->SpawnActor(); USceneComponent* RootC = NewObject(ActorC); ActorC->SetRootComponent(RootC); RootC->RegisterComponent(); const TArray Src = { ActorA, ActorB, ActorC }; auto RunFilter = [&Src](TFunctionRef&, TArray&)> Fn) -> TArray { TArray Out; Fn(Src, Out); return Out; }; // Class: everything is an AActor. TestEqual("ByClass(AActor) includes all", RunFilter([](const TArray& S, TArray& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByClass(S, O, AActor::StaticClass(), Include); }).Num(), 3); // Tag (Include + Exclude complement). { const TArray Inc = RunFilter([](const TArray& S, TArray& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByTag(S, O, FName("Alpha"), Include); }); const TArray Exc = RunFilter([](const TArray& S, TArray& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByTag(S, O, FName("Alpha"), Exclude); }); TestTrue("ByTag Include => A only", Inc.Contains(ActorA) && !Inc.Contains(ActorB) && !Inc.Contains(ActorC)); TestTrue("ByTag Exclude => B and C", !Exc.Contains(ActorA) && Exc.Contains(ActorB) && Exc.Contains(ActorC)); } // Static mesh by reference + by name. { const TArray Inc = RunFilter([&](const TArray& S, TArray& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByStaticMesh(S, O, Cube, Include); }); TestTrue("ByStaticMesh(Cube) => A only", Inc.Contains(ActorA) && !Inc.Contains(ActorB) && !Inc.Contains(ActorC)); const TArray ByName = RunFilter([](const TArray& S, TArray& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByStaticMeshName(S, O, TEXT("Cube"), Include); }); TestTrue("ByStaticMeshName(Cube) => A", ByName.Contains(ActorA) && !ByName.Contains(ActorB)); } // Material by reference + by name (override slot). { const TArray Inc = RunFilter([&](const TArray& S, TArray& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByMaterial(S, O, MatA, OverrideOnly, Include); }); const TArray Exc = RunFilter([&](const TArray& S, TArray& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByMaterial(S, O, MatA, OverrideOnly, Exclude); }); TestTrue("ByMaterial(MatA) Include => A only", Inc.Contains(ActorA) && !Inc.Contains(ActorB)); TestTrue("ByMaterial(MatA) Exclude => B and C, not A", !Exc.Contains(ActorA) && Exc.Contains(ActorB) && Exc.Contains(ActorC)); const TArray ByName = RunFilter([&](const TArray& S, TArray& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByMaterialName(S, O, MatA->GetName(), OverrideOnly, Include); }); TestTrue("ByMaterialName(MatA) => A", ByName.Contains(ActorA) && !ByName.Contains(ActorB)); } // Vert / tri count: query the cube's actual counts, then assert in-range includes and out-of-range excludes. { const int32 Verts = Cube->GetNumVertices(0); TestTrue("ByVertCount [V,V] => A", RunFilter([Verts](const TArray& S, TArray& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByVertCount(S, O, Verts, Verts, Include); }).Contains(ActorA)); TestFalse("ByVertCount out-of-range => not A", RunFilter([Verts](const TArray& S, TArray& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByVertCount(S, O, Verts + 100000, Verts + 200000, Include); }).Contains(ActorA)); const int32 Tris = Cube->GetNumTriangles(0); TestTrue("ByTriCount [T,T] => A", RunFilter([Tris](const TArray& S, TArray& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByTriCount(S, O, Tris, Tris, Include); }).Contains(ActorA)); } // Mobility. { TestTrue("ByMobility(Static) => A, not B", RunFilter([](const TArray& S, TArray& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByMobility(S, O, EComponentMobility::Static, Include); }).Contains(ActorA)); TestTrue("ByMobility(Movable) => B, not A", RunFilter([&](const TArray& S, TArray& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByMobility(S, O, EComponentMobility::Movable, Include); }).Contains(ActorB)); } // Collision (BlockAll on A implies WorldStatic object type, QueryAndPhysics, blocking responses). { TestTrue("ByCollisionProfile(BlockAll) => A", RunFilter([](const TArray& S, TArray& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByCollisionProfile(S, O, FName("BlockAll"), Include); }).Contains(ActorA)); TestTrue("ByCollisionChannel(WorldStatic) => A", RunFilter([](const TArray& S, TArray& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByCollisionChannel(S, O, ECC_WorldStatic, Include); }).Contains(ActorA)); TestTrue("ByCollisionEnabled(QueryAndPhysics) => A", RunFilter([](const TArray& S, TArray& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByCollisionEnabled(S, O, ECollisionEnabled::QueryAndPhysics, Include); }).Contains(ActorA)); TestTrue("ByCollisionResponse(WorldDynamic, Block) => A", RunFilter([](const TArray& S, TArray& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByCollisionResponse(S, O, ECC_WorldDynamic, ECR_Block, Include); }).Contains(ActorA)); } // Nanite: assert the filter partitions correctly (A matched by exactly one of true/false) without // reading the deprecated member directly. { const bool bInFalse = RunFilter([](const TArray& S, TArray& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByNaniteState(S, O, false, Include); }).Contains(ActorA); const bool bInTrue = RunFilter([](const TArray& S, TArray& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByNaniteState(S, O, true, Include); }).Contains(ActorA); TestTrue("ByNaniteState partitions A into exactly one of true/false", bInFalse != bInTrue); } // LOD count: query the cube's LOD count and assert in-range includes A. { const int32 LODs = Cube->GetNumLODs(); TestTrue("ByLODCount [n,n] => A", RunFilter([LODs](const TArray& S, TArray& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByLODCount(S, O, LODs, LODs, Include); }).Contains(ActorA)); } // Actor bounds: query A's own size and assert a range around it includes A. { FVector Origin, Extent; ActorA->GetActorBounds(false, Origin, Extent); const FVector Size = Extent * 2.0f; TestTrue("ByBounds around A's size => A", RunFilter([&](const TArray& S, TArray& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByBounds(S, O, Size - FVector(1.0f), Size + FVector(1.0f), Include); }).Contains(ActorA)); } // Static-mesh bounds use the source mesh dimensions, independently of actor transform. { const FVector MeshSize = Cube->GetBounds().BoxExtent * 2.0f; const TArray Match = RunFilter([&](const TArray& S, TArray& O) { UDirectiveUtilEditorActorSubsystem::FilterActorsByStaticMeshBounds( S, O, MeshSize - FVector(1.0f), MeshSize + FVector(1.0f), Include); }); TestTrue("ByStaticMeshBounds includes the cube actor", Match.Contains(ActorA)); TestFalse("ByStaticMeshBounds excludes the actor without a mesh", Match.Contains(ActorC)); } // Override lightmap resolution is a distinct search lane from the mesh default. { CompA->bOverrideLightMapRes = true; CompA->OverriddenLightMapRes = 128; const TArray Match = RunFilter([](const TArray& S, TArray& O) { UDirectiveUtilEditorActorSubsystem::FilterActorsByLightmapResolution( S, O, 128, 128, OverrideOnly, Include); }); TestTrue("ByLightmapResolution finds the exact override", Match.Contains(ActorA)); TestFalse("ByLightmapResolution rejects a different override", Match.Contains(ActorB)); } // World location: A at origin, B far away. { const TArray Near = RunFilter([](const TArray& S, TArray& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByWorldLocation(S, O, FVector::ZeroVector, 50.0f, Include); }); TestTrue("ByWorldLocation near origin => A, not the far B", Near.Contains(ActorA) && !Near.Contains(ActorB)); } // Texture by name: a name no material uses -> Include matches none, Exclude matches all (exercises traversal). { TestEqual("ByTextureName(absent) Include => none", RunFilter([](const TArray& S, TArray& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByTextureName(S, O, TEXT("__udcore_absent_texture__"), BaseAndOverride, Include); }).Num(), 0); TestEqual("ByTextureName(absent) Exclude => all", RunFilter([](const TArray& S, TArray& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByTextureName(S, O, TEXT("__udcore_absent_texture__"), BaseAndOverride, Exclude); }).Num(), 3); } // Texture-reference filtering must follow the same include/exclude contract. { UTexture2D* AbsentTexture = NewObject(GetTransientPackage()); const TArray Inc = RunFilter([&](const TArray& S, TArray& O) { UDirectiveUtilEditorActorSubsystem::FilterActorsByTexture(S, O, AbsentTexture, BaseAndOverride, Include); }); const TArray Exc = RunFilter([&](const TArray& S, TArray& O) { UDirectiveUtilEditorActorSubsystem::FilterActorsByTexture(S, O, AbsentTexture, BaseAndOverride, Exclude); }); TestEqual("ByTexture(absent) Include returns none", Inc.Num(), 0); TestEqual("ByTexture(absent) Exclude returns every valid source actor", Exc.Num(), Src.Num()); } // Missing-resource convenience filters are semantic aliases, not merely spawn-tested nodes. { UStaticMesh* MissingMaterialMesh = DuplicateObject(Cube, GetTransientPackage()); MissingMaterialMesh->SetMaterial(0, nullptr); UStaticMeshComponent* MissingMaterialComp = SpawnMesh(MissingMaterialMesh, EComponentMobility::Static); UStaticMeshComponent* MissingMeshComp = SpawnMesh(nullptr, EComponentMobility::Static); const TArray MissingSource = { MissingMaterialComp->GetOwner(), MissingMeshComp->GetOwner(), ActorA }; TArray MissingMaterials; UDirectiveUtilEditorActorSubsystem::FilterActorsByMissingMaterials( MissingSource, MissingMaterials, BaseOnly, Include); TestTrue("MissingMaterials finds the null source material", MissingMaterials.Contains(MissingMaterialComp->GetOwner())); TestFalse("MissingMaterials rejects the valid cube material", MissingMaterials.Contains(ActorA)); TArray MissingMeshes; UDirectiveUtilEditorActorSubsystem::FilterActorsByMissingStaticMeshes(MissingSource, MissingMeshes, Include); TestTrue("MissingStaticMeshes finds a mesh component with no mesh", MissingMeshes.Contains(MissingMeshComp->GetOwner())); TestFalse("MissingStaticMeshes rejects a valid mesh", MissingMeshes.Contains(ActorA)); } { UMaterial* MissingTextureMaterial = NewObject(GetTransientPackage()); UMaterialExpressionTextureSample* MissingTextureExpression = NewObject(MissingTextureMaterial); MissingTextureMaterial->GetExpressionCollection().AddExpression(MissingTextureExpression); CompA->SetMaterial(0, MissingTextureMaterial); const TArray MissingTextures = RunFilter([](const TArray& S, TArray& O) { UDirectiveUtilEditorActorSubsystem::FilterActorsByMissingTextures(S, O, OverrideOnly, Include); }); TestTrue("Missing texture filter should include an actor with an unset texture expression", MissingTextures.Contains(ActorA)); TestFalse("Missing texture filter should exclude actors without unset texture expressions", MissingTextures.Contains(ActorB)); CompA->SetMaterial(0, MatA); } { AActor* DestroyedActor = World->SpawnActor(); World->DestroyActor(DestroyedActor); TestFalse("Destroyed actor should be invalid", IsValid(DestroyedActor)); TArray InvalidActors = {DestroyedActor}; TArray FilteredInvalidActors; UDirectiveUtilEditorActorSubsystem::FilterActorsByTag( InvalidActors, FilteredInvalidActors, TEXT("Absent"), Exclude); TestEqual("Actor filters should skip invalid actors", FilteredInvalidActors.Num(), 0); } GEngine->DestroyWorldContext(World); World->DestroyWorld(false); return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemEmptyActorsTest, "DirectiveUtilities.EditorActorSubsystemEmptyActorsTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) bool FDirectiveUtilEditorActorSubsystemEmptyActorsTest::RunTest(const FString& Parameters) { UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false); if (!World) { AddError(TEXT("Failed to create a transient world for the empty actors test.")); return false; } FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor); WorldContext.SetCurrentWorld(World); // A bare actor whose only component is a childless scene root: the "Empty Actor" shape. AActor* BareActor = World->SpawnActor(); USceneComponent* BareRoot = NewObject(BareActor); BareActor->SetRootComponent(BareRoot); BareRoot->RegisterComponent(); AStaticMeshActor* MeshActor = World->SpawnActor(); const TArray Source = { BareActor, MeshActor }; TArray Included; UDirectiveUtilEditorActorSubsystem::FilterEmptyActors(Source, Included, Include); TestTrue("Include: bare actor with only a scene root is empty", Included.Contains(BareActor)); TestFalse("Include: static mesh actor is not empty", Included.Contains(MeshActor)); TestEqual("Include: only the bare actor is returned", Included.Num(), 1); TArray Excluded; UDirectiveUtilEditorActorSubsystem::FilterEmptyActors(Source, Excluded, Exclude); TestFalse("Exclude: bare actor is not returned", Excluded.Contains(BareActor)); TestTrue("Exclude: static mesh actor is returned", Excluded.Contains(MeshActor)); TestEqual("Exclude: only the static mesh actor is returned", Excluded.Num(), 1); // Aliasing: filtering an array into itself rebuilds it in place. TArray Aliased = { BareActor, MeshActor }; UDirectiveUtilEditorActorSubsystem::FilterEmptyActors(Aliased, Aliased, Include); TestEqual("Aliased: array is rebuilt in place with one entry", Aliased.Num(), 1); TestTrue("Aliased: only the bare actor remains", Aliased.Contains(BareActor)); GEngine->DestroyWorldContext(World); World->DestroyWorld(false); return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemQueryAlignmentTest, "DirectiveUtilities.EditorActorSubsystemQueryAlignmentTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) bool FDirectiveUtilEditorActorSubsystemQueryAlignmentTest::RunTest(const FString& Parameters) { UStaticMesh* Cube = LoadObject(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube")); UMaterialInterface* BasicMaterial = LoadObject( nullptr, TEXT("/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial")); if (!Cube || !BasicMaterial) { AddError(TEXT("Engine basic shapes or materials unavailable for the query alignment test.")); return false; } // The GetActorsBy* queries read from the editor world rather than a passed array. UWorld* EditorWorld = nullptr; for (const FWorldContext& Context : GEngine->GetWorldContexts()) { if (Context.WorldType == EWorldType::Editor && Context.World()) { EditorWorld = Context.World(); break; } } if (!EditorWorld) { AddError(TEXT("No editor world available for the query alignment test.")); return false; } AActor* MeshActor = EditorWorld->SpawnActor(); USceneComponent* Root = NewObject(MeshActor); Root->SetMobility(EComponentMobility::Movable); MeshActor->SetRootComponent(Root); Root->RegisterComponent(); UStaticMeshComponent* MeshComponent = NewObject(MeshActor); MeshComponent->SetMobility(EComponentMobility::Movable); MeshComponent->SetupAttachment(Root); MeshComponent->SetStaticMesh(Cube); MeshComponent->SetMaterial(0, BasicMaterial); MeshComponent->RegisterComponent(); MeshActor->AddInstanceComponent(MeshComponent); MeshActor->SetActorLocation(FVector::ZeroVector); // The query methods keep no instance state, so a transient instance is enough headless. UDirectiveUtilEditorActorSubsystem* Subsystem = NewObject(); TArray ByClass; Subsystem->GetActorsByClass(ByClass, AActor::StaticClass(), World, Include); TestTrue("Class: AActor query finds the fixture actor", ByClass.Contains(MeshActor)); // Bounding box: an enclosing box finds the actor, a disjoint one does not. TArray InBox; Subsystem->GetActorsByBoundingBox(InBox, FVector(-100000.0f), FVector(100000.0f), World, Include); TestTrue("BoundingBox: enclosing box finds the actor", InBox.Contains(MeshActor)); TArray OutOfBox; Subsystem->GetActorsByBoundingBox(OutOfBox, FVector(900000.0f), FVector(900100.0f), World, Include); TestFalse("BoundingBox: disjoint box does not find the actor", OutOfBox.Contains(MeshActor)); // Mesh name: a lowercase substring matches case-insensitively, as in the Filter variant. TArray ByName; Subsystem->GetActorsByStaticMeshName(ByName, TEXT("cub"), World, Include); TestTrue("StaticMeshName: lowercase substring finds the actor", ByName.Contains(MeshActor)); TArray ByMaterial; Subsystem->GetActorsByMaterial(ByMaterial, BasicMaterial, OverrideOnly, World, Include); TestTrue("Material: generic actor with a static mesh component is included", ByMaterial.Contains(MeshActor)); TArray ByMaterialSoftReference; Subsystem->GetActorsByMaterialSoftReference( ByMaterialSoftReference, BasicMaterial, OverrideOnly, World, Include); TestTrue("Material soft reference: generic actor with a static mesh component is included", ByMaterialSoftReference.Contains(MeshActor)); TArray ByMaterialName; Subsystem->GetActorsByMaterialName( ByMaterialName, BasicMaterial->GetName(), OverrideOnly, World, Include); TestTrue("Material name: generic actor with a static mesh component is included", ByMaterialName.Contains(MeshActor)); TArray WithoutMaterial; Subsystem->GetActorsByMaterial(WithoutMaterial, BasicMaterial, OverrideOnly, World, Exclude); TestFalse("Material exclude: matching generic actor is excluded", WithoutMaterial.Contains(MeshActor)); const int32 VertexCount = Cube->GetNumVertices(0); TArray ByVertexCount; Subsystem->GetActorsByVertexCount(ByVertexCount, VertexCount, VertexCount, World, Include); TestTrue("VertexCount: exact range finds the fixture actor", ByVertexCount.Contains(MeshActor)); const int32 TriangleCount = Cube->GetNumTriangles(0); TArray ByTriangleCount; Subsystem->GetActorsByTriCount(ByTriangleCount, TriangleCount, TriangleCount, World, Include); TestTrue("TriangleCount: exact range finds the fixture actor", ByTriangleCount.Contains(MeshActor)); const float MeshSize = Cube->GetBoundingBox().GetSize().Size(); TArray ByMeshSize; Subsystem->GetActorsByMeshSize(ByMeshSize, MeshSize - 1.0f, MeshSize + 1.0f, World, Include); TestTrue("MeshSize: enclosing range finds the fixture actor", ByMeshSize.Contains(MeshActor)); TArray ByWorldLocation; Subsystem->GetActorsByWorldLocation(ByWorldLocation, FVector::ZeroVector, 100.0f, World, Include); TestTrue("WorldLocation: nearby query finds the fixture actor", ByWorldLocation.Contains(MeshActor)); const int32 LODCount = Cube->GetNumLODs(); TArray ByLODCount; Subsystem->GetActorsByLODCount(ByLODCount, LODCount, LODCount, World, Include); TestTrue("LODCount: exact range finds the fixture actor", ByLODCount.Contains(MeshActor)); TArray NaniteOff; TArray NaniteOn; Subsystem->GetActorsByNaniteEnabled(NaniteOff, false, World, Include); Subsystem->GetActorsByNaniteEnabled(NaniteOn, true, World, Include); TestTrue("Nanite: fixture belongs to exactly one state", NaniteOff.Contains(MeshActor) != NaniteOn.Contains(MeshActor)); const int32 SourceLightmapResolution = Cube->GetLightMapResolution(); TArray ByLightmapResolution; Subsystem->GetActorsByLightmapResolution( ByLightmapResolution, SourceLightmapResolution, SourceLightmapResolution, World, Include); TestTrue("LightmapResolution: exact source resolution finds the fixture actor", ByLightmapResolution.Contains(MeshActor)); TArray ByMobility; Subsystem->GetActorsByMobility(ByMobility, EComponentMobility::Movable, World, Include); TestTrue("Mobility query finds the movable fixture actor", ByMobility.Contains(MeshActor)); TArray ByStaticMesh; Subsystem->GetActorsByStaticMesh(ByStaticMesh, Cube, World, Include); TestTrue("StaticMesh reference query finds the fixture actor", ByStaticMesh.Contains(MeshActor)); TArray ByStaticMeshSoft; Subsystem->GetActorsByStaticMeshSoftReference(ByStaticMeshSoft, Cube, World, Include); TestTrue("StaticMesh soft-reference query finds the fixture actor", ByStaticMeshSoft.Contains(MeshActor)); UTexture2D* AbsentTexture = NewObject(GetTransientPackage()); TArray ByTexture; Subsystem->GetActorsByTexture(ByTexture, AbsentTexture, World, Include); TestFalse("Texture reference query rejects an absent texture", ByTexture.Contains(MeshActor)); TArray ByTextureSoft; Subsystem->GetActorsByTextureSoftReference(ByTextureSoft, AbsentTexture, World, Include); TestFalse("Texture soft-reference query rejects an absent texture", ByTextureSoft.Contains(MeshActor)); TArray ByTextureName; Subsystem->GetActorsByTextureName(ByTextureName, TEXT("__udcore_absent_texture__"), World, Include); TestFalse("Texture-name query rejects an absent texture", ByTextureName.Contains(MeshActor)); TArray InvalidActors = { MeshActor }; Subsystem->GetInvalidActors(InvalidActors); TestEqual("GetInvalidActors resets its deprecated output", InvalidActors.Num(), 0); // Mobility: the root component's mobility is what counts. const TArray Source = { MeshActor }; TArray MovableActors; UDirectiveUtilEditorActorSubsystem::FilterActorsByMobility(Source, MovableActors, EComponentMobility::Movable, Include); TestTrue("Mobility: movable root is matched", MovableActors.Contains(MeshActor)); TArray StaticActors; UDirectiveUtilEditorActorSubsystem::FilterActorsByMobility(Source, StaticActors, EComponentMobility::Static, Include); TestFalse("Mobility: static does not match a movable root", StaticActors.Contains(MeshActor)); EditorWorld->DestroyActor(MeshActor); return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilPushOverrideMaterialsTest, "DirectiveUtilities.PushOverrideMaterialsTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) bool FDirectiveUtilPushOverrideMaterialsTest::RunTest(const FString& Parameters) { UMaterialInterface* BaseMaterial = LoadObject( nullptr, TEXT("/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial")); UMaterialInterface* OverrideMaterial = LoadObject( nullptr, TEXT("/Engine/EngineMaterials/WorldGridMaterial.WorldGridMaterial")); UStaticMesh* Cube = LoadObject(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube")); if (!BaseMaterial || !OverrideMaterial || !Cube) { AddError(TEXT("Engine materials unavailable for the override material test.")); return false; } UStaticMesh* StaticMesh = DuplicateObject(Cube, GetTransientPackage()); StaticMesh->GetStaticMaterials()[0].MaterialInterface = BaseMaterial; UStaticMeshComponent* StaticMeshComponent = NewObject(GetTransientPackage()); StaticMeshComponent->SetStaticMesh(StaticMesh); StaticMeshComponent->SetMaterial(0, OverrideMaterial); UDirectiveUtilEditorActorSubsystem::PushOverrideMaterialsToSource(StaticMeshComponent); TestEqual("Override material is copied to its source slot", StaticMesh->GetMaterial(0), OverrideMaterial); UStaticMesh* MeshWithoutOverride = DuplicateObject(Cube, GetTransientPackage()); MeshWithoutOverride->GetStaticMaterials()[0].MaterialInterface = BaseMaterial; UStaticMeshComponent* ComponentWithoutOverride = NewObject(GetTransientPackage()); ComponentWithoutOverride->SetStaticMesh(MeshWithoutOverride); UDirectiveUtilEditorActorSubsystem::PushOverrideMaterialsToSource(ComponentWithoutOverride); TestEqual("A source slot is unchanged when the component has no override", MeshWithoutOverride->GetMaterial(0), BaseMaterial); AddExpectedError(TEXT("Static Mesh Component is invalid."), EAutomationExpectedErrorFlags::Exact, 1); UDirectiveUtilEditorActorSubsystem::PushOverrideMaterialsToSource(nullptr); UStaticMeshComponent* ComponentWithoutMesh = NewObject(GetTransientPackage()); AddExpectedError(TEXT("Static Mesh Component has no valid static mesh."), EAutomationExpectedErrorFlags::Exact, 1); UDirectiveUtilEditorActorSubsystem::PushOverrideMaterialsToSource(ComponentWithoutMesh); return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemBoundsTest, "DirectiveUtilities.EditorActorSubsystemBoundsTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) bool FDirectiveUtilEditorActorSubsystemBoundsTest::RunTest(const FString& Parameters) { UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false); if (!World) { AddError(TEXT("Failed to create a transient world for the bounds test.")); return false; } FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor); WorldContext.SetCurrentWorld(World); auto SpawnPoint = [World](const FVector& Location, const FVector& Scale) -> AActor* { AActor* Actor = World->SpawnActor(); USceneComponent* Root = NewObject(Actor); Actor->SetRootComponent(Root); Root->RegisterComponent(); Actor->SetActorScale3D(Scale); Actor->SetActorLocation(Location); return Actor; }; // Axis-aligned box of extent 100 at the origin. AActor* BoxActor = World->SpawnActor(); USceneComponent* BoxRoot = NewObject(BoxActor); BoxActor->SetRootComponent(BoxRoot); BoxRoot->RegisterComponent(); UBoxComponent* Box = NewObject(BoxActor); Box->SetupAttachment(BoxRoot); Box->RegisterComponent(); Box->SetBoxExtent(FVector(100.0f, 100.0f, 100.0f)); // Point well inside the box, on an actor scaled to 0.1. The previous code multiplied the box // extent by the queried actor's scale and would wrongly report this as outside. TestTrue("Box: point inside is detected regardless of the queried actor's scale", UDirectiveUtilEditorActorSubsystem::IsActorWithinBoxBounds(SpawnPoint(FVector(50.0f, 50.0f, 50.0f), FVector(0.1f)), Box)); TestFalse("Box: point beyond the extent is rejected", UDirectiveUtilEditorActorSubsystem::IsActorWithinBoxBounds(SpawnPoint(FVector(250.0f, 0.0f, 0.0f), FVector::OneVector), Box)); // Capsule of radius 50 and half-height 100 at the origin. AActor* CapsuleActor = World->SpawnActor(); USceneComponent* CapsuleRoot = NewObject(CapsuleActor); CapsuleActor->SetRootComponent(CapsuleRoot); CapsuleRoot->RegisterComponent(); UCapsuleComponent* Capsule = NewObject(CapsuleActor); Capsule->SetupAttachment(CapsuleRoot); Capsule->RegisterComponent(); Capsule->SetCapsuleSize(50.0f, 100.0f); TestTrue("Capsule: point within the upper cap is detected", UDirectiveUtilEditorActorSubsystem::IsActorWithinCapsuleBounds(SpawnPoint(FVector(0.0f, 0.0f, 90.0f), FVector::OneVector), Capsule)); TestTrue("Capsule: point within the radius is detected", UDirectiveUtilEditorActorSubsystem::IsActorWithinCapsuleBounds(SpawnPoint(FVector(40.0f, 0.0f, 0.0f), FVector::OneVector), Capsule)); TestFalse("Capsule: point beyond the radius is rejected (not a broad sphere)", UDirectiveUtilEditorActorSubsystem::IsActorWithinCapsuleBounds(SpawnPoint(FVector(60.0f, 0.0f, 0.0f), FVector::OneVector), Capsule)); TestFalse("Capsule: point beyond the end cap is rejected", UDirectiveUtilEditorActorSubsystem::IsActorWithinCapsuleBounds(SpawnPoint(FVector(0.0f, 0.0f, 200.0f), FVector::OneVector), Capsule)); GEngine->DestroyWorldContext(World); World->DestroyWorld(false); return true; } #endif