// Headless tests of the carrier (DESIGN_UE.md section 10, M2): the layout tables, the dry volume and the cabin temperature (U5, U6), // the blockout geometry as pure data (floors where the layout puts them, doors you can walk through), and the route graph. #include "Tests/DescentTestCommon.h" #include "Core/DescentEnvQuery.h" #include "Core/DescentMath.h" #include "Core/Tuning/DescentTune_Carrier.h" #include "Vehicles/CarrierBuild.h" #include "Vehicles/CarrierLayout.h" #if WITH_DEV_AUTOMATION_TESTS namespace { using namespace Descent::Carrier; namespace TC = Descent::Tune::Carrier; /** The axis-aligned box of a convex hull given as points, when the hull IS an axis-aligned box (8 points, volume equals the box). */ bool CarrierTestAsAabb(const TArray& Pts, FBox& Out) { if (Pts.Num() != 8) { return false; } Out = FBox(Pts); return Out.IsValid != 0; } void CarrierTestCollectBoxes(TArray& Out) { FMeshBuilder All; for (const FRoomDef& R : FLayout::Get().Rooms) { FMeshBuilder B; BuildRoomMesh(R, B); All.Append(B); } FMeshBuilder Doors, Solid, Water; BuildDoorsMesh(Doors); BuildPoolMesh(Solid, Water); All.Append(Doors); All.Append(Solid); for (const FAnchorDef& A : FLayout::Get().Anchors) { FMeshBuilder B; BuildFixtureMesh(A, B); All.Append(B); } for (const TArray& Pts : All.Convex) { FBox Box; if (CarrierTestAsAabb(Pts, Box)) { Out.Add(Box); } } } } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDescentCarrierLayoutTest, "Descent.Carrier.Layout", DESCENT_TEST_FLAGS) bool FDescentCarrierLayoutTest::RunTest(const FString& Parameters) { const FLayout& L = FLayout::Get(); TestEqual(TEXT("16 rooms"), L.Rooms.Num(), 16); TestEqual(TEXT("15 doors (design 3.3)"), L.Doors.Num(), 15); // hull radius at the design's anchor points TestEqual(TEXT("HullRadius in the cylinder"), HullRadius(0.0), 12.0, 1e-9); TestEqual(TEXT("HullRadius at the nose tip"), HullRadius(60.0), 0.0, 1e-9); TestEqual(TEXT("HullRadius at x = 50"), HullRadius(50.0), 12.0 * FMath::Sqrt(1.0 - 0.25), 1e-9); TestEqual(TEXT("HullRadius at the stern cap"), HullRadius(-60.0), 7.0, 1e-9); // every room box lies inside HullRadius(x): all eight corners, both ends for (const FRoomDef& R : L.Rooms) { for (double X : { R.X0, R.X1 }) { for (double Y : { R.Y0, R.Y1 }) { for (double Z : { R.FloorZ - TC::FloorSlabM, R.CeilZ + TC::CeilingSlabM }) { const double Rad = FMath::Sqrt(Y * Y + Z * Z); if (Rad > HullRadius(X) + 1e-9) { AddError(FString::Printf(TEXT("room %s corner (%.1f, %.1f, %.1f) is at radius %.3f, outside the hull %.3f"), *R.Id.ToString(), X, Y, Z, Rad, HullRadius(X))); } } } } } // every door joins two different rooms for (const FDoorDef& D : L.Doors) { const FRoomDef* A = nullptr; const FRoomDef* B = nullptr; if (!L.DoorRooms(D, A, B)) { AddError(FString::Printf(TEXT("door at deck %d (%.1f..%.1f, %.1f..%.1f) does not join two rooms"), D.Deck, D.Y0, D.Y1, D.X0, D.X1)); } } // every room has a door, a lamp and a stop point inside it on its own floor for (const FRoomDef& R : L.Rooms) { TestTrue(*(R.Id.ToString() + TEXT(" has lamps")), R.Lamps.Num() > 0); TestTrue(*(R.Id.ToString() + TEXT(" stop point is inside the room")), R.ContainsXY(R.Stop.X, R.Stop.Y)); const FRoomDef* Found = L.RoomAt(FVector(R.Stop.X, R.Stop.Y, R.FloorZ + 0.3)); TestTrue(*(R.Id.ToString() + TEXT(" RoomAt finds itself at its stop point")), Found == &R); int32 Doors = 0; for (const FDoorDef& D : L.Doors) { const FRoomDef* A = nullptr; const FRoomDef* B = nullptr; if (L.DoorRooms(D, A, B) && (A == &R || B == &R)) { ++Doors; } } TestTrue(*(R.Id.ToString() + TEXT(" has a door")), Doors > 0); } TestEqual(TEXT("deck heights D1 D2 D3"), FVector(TC::DeckFloorZ1, TC::DeckFloorZ2, TC::DeckFloorZ3), FVector(-7.5, -3.0, 1.5)); return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDescentCarrierDryVolumeTest, "Descent.Carrier.DryVolume", DESCENT_TEST_FLAGS) bool FDescentCarrierDryVolumeTest::RunTest(const FString& Parameters) { const FLayout& L = FLayout::Get(); for (const FRoomDef& R : L.Rooms) { TestTrue(*(R.Id.ToString() + TEXT(": dry at the room centre")), IsDryLocal(R.CentreM())); } TestFalse(TEXT("1 m outside the cylinder (starboard)"), IsDryLocal(FVector(0.0, 13.0, 0.0))); TestFalse(TEXT("1 m below the keel line"), IsDryLocal(FVector(0.0, 0.0, -13.0))); TestFalse(TEXT("1 m above the hull"), IsDryLocal(FVector(0.0, 0.0, 13.0))); TestFalse(TEXT("beyond the nose"), IsDryLocal(FVector(61.0, 0.0, 0.0))); TestFalse(TEXT("1 m outside the stern taper"), IsDryLocal(FVector(-55.0, 0.0, HullRadius(-55.0) + 1.0))); TestFalse(TEXT("1 m below the pool surface (-8.3)"), IsDryLocal(FVector(-29.0, 6.0, TC::PoolWaterZ - 1.0))); TestTrue(TEXT("1 m above the pool surface"), IsDryLocal(FVector(-29.0, 6.0, TC::PoolWaterZ + 1.0))); TestTrue(TEXT("the pool column below the surface is only wet inside the pool rectangle"), IsDryLocal(FVector(-29.0, 2.0, -9.3))); return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDescentCarrierTemperatureTest, "Descent.Carrier.Temperature", DESCENT_TEST_FLAGS) bool FDescentCarrierTemperatureTest::RunTest(const FString& Parameters) { // The carrier registers exactly this provider (Vehicles/DescentCarrier.cpp BeginPlay) with the carrier at its start pose. FDescentTestWorld TW; UDescentEnvQuerySubsystem* Q = TW.Sub(); if (!TestNotNull(TEXT("env query"), Q)) { return false; } const FVector Origin(0.0, 0.0, TC::StartZM); Q->RegisterDryVolume(TEXT("Carrier"), [Origin](const FVector& WorldUU) { return IsDryLocal(Descent::UUToMetres(WorldUU) - Origin); }); const FVector InBridge = Descent::MetresToUU(Origin + FVector(42.0, 0.0, 3.0)); const FVector Outside = Descent::MetresToUU(Origin + FVector(0.0, 40.0 + 12.0, 0.0)); TestEqual(TEXT("U5: 26.0 C on the bridge"), Descent::AmbientTemperatureAt(TW.Get(), InBridge), 26.0, 0.0); TestEqual(TEXT("U5: 26.0 C in the engine room"), Descent::AmbientTemperatureAt(TW.Get(), Descent::MetresToUU(Origin + FLayout::Get().FindRoom(TEXT("engine"))->CentreM())), 26.0, 0.0); const double Water = UDescentEnvQuerySubsystem::WaterTemperatureC(Descent::DepthOfZ(TC::StartZM)); TestEqual(TEXT("outside: the water formula at depth 1,810"), Descent::AmbientTemperatureAt(TW.Get(), Outside), Water, 1e-9); TestTrue(TEXT("outside is colder than the cabin"), Water < 26.0); TestTrue(TEXT("U6: dry inside, wet outside"), Descent::IsDry(TW.Get(), InBridge) && !Descent::IsDry(TW.Get(), Outside)); return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDescentCarrierGeometryTest, "Descent.Carrier.Geometry", DESCENT_TEST_FLAGS) bool FDescentCarrierGeometryTest::RunTest(const FString& Parameters) { const FLayout& L = FLayout::Get(); TArray Boxes; CarrierTestCollectBoxes(Boxes); TestTrue(TEXT("collision boxes were built"), Boxes.Num() > 200); // a floor under every room's stop point: the highest box top below the head is the deck height for (const FRoomDef& R : L.Rooms) { double Top = -1e9; for (const FBox& B : Boxes) { if (R.Stop.X >= B.Min.X && R.Stop.X <= B.Max.X && R.Stop.Y >= B.Min.Y && R.Stop.Y <= B.Max.Y && B.Max.Z <= R.FloorZ + 1.0) { Top = FMath::Max(Top, B.Max.Z); } } TestTrue(*FString::Printf(TEXT("%s: floor top %.3f is the deck height %.2f"), *R.Id.ToString(), Top, R.FloorZ), FMath::Abs(Top - R.FloorZ) < 0.01); // nothing stands on the stop point (a 0.68 m wide, 1.8 m tall column above the floor) const FBox Column(FVector(R.Stop.X - 0.34, R.Stop.Y - 0.34, R.FloorZ + 0.05), FVector(R.Stop.X + 0.34, R.Stop.Y + 0.34, R.FloorZ + 1.8)); for (const FBox& B : Boxes) { if (B.Intersect(Column)) { AddError(FString::Printf(TEXT("%s: a prop stands on the stop point"), *R.Id.ToString())); break; } } } // every door can be walked through: a 0.6 m column, 0.35 m (step height) to 1.8 m above the floor, sweeps through the wall pair for (const FDoorDef& D : L.Doors) { const FVector2D Mid = D.Mid(); const FVector2D N = D.WallAtConstX() ? FVector2D(1, 0) : FVector2D(0, 1); for (double T = -1.2; T <= 1.2; T += 0.1) { const FVector C(Mid.X + N.X * T, Mid.Y + N.Y * T, D.FloorZ()); const FBox Column(C + FVector(-0.3, -0.3, 0.35), C + FVector(0.3, 0.3, 1.8)); for (const FBox& B : Boxes) { if (B.Intersect(Column)) { AddError(FString::Printf(TEXT("door deck %d at (%.1f, %.1f): blocked at %.1f m along the wall normal"), D.Deck, Mid.X, Mid.Y, T)); T = 9.0; break; } } } // ... and the floor is continuous under it: a box top at the deck height within every 0.5 m for (double T = -1.25; T <= 1.25; T += 0.25) { const FVector2D P = Mid + N * T; bool bFloor = false; for (const FBox& B : Boxes) { if (P.X >= B.Min.X && P.X <= B.Max.X && P.Y >= B.Min.Y && P.Y <= B.Max.Y && FMath::Abs(B.Max.Z - D.FloorZ()) < 0.01) { bFloor = true; break; } } if (!bFloor) { AddError(FString::Printf(TEXT("door deck %d at (%.1f, %.1f): no floor %.2f m along the normal"), D.Deck, Mid.X, Mid.Y, T)); break; } } } return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDescentCarrierRouteTest, "Descent.Carrier.Route", DESCENT_TEST_FLAGS) bool FDescentCarrierRouteTest::RunTest(const FString& Parameters) { const FLayout& L = FLayout::Get(); const FRoomDef* Bridge = L.FindRoom(TEXT("bridge")); for (const FRoomDef& R : L.Rooms) { TArray Way; const bool bOk = L.FindRoute(FVector(Bridge->Stop.X, Bridge->Stop.Y, Bridge->FloorZ), R.Id, nullptr, Way); TestTrue(*(TEXT("a route from the bridge to ") + R.Id.ToString()), bOk && Way.Num() > 0); if (bOk) { TestTrue(*(R.Id.ToString() + TEXT(": the route ends at its stop point")), Way.Last().Equals(R.Stop, 1e-6)); } } TArray Way; TestTrue(TEXT("the landing and the gallery are routable targets"), L.FindRoute(FVector(41.8, 0, 1.5), TEXT("stair_up"), nullptr, Way) && L.FindRoute(FVector(41.8, 0, 1.5), TEXT("bay_up"), nullptr, Way)); return true; } #endif // WITH_DEV_AUTOMATION_TESTS