// Acceptance scenarios of milestone M1, rock part (DESIGN_UE.md section 10): m1_world_slices, m1_terrain_tour, m1_carve. // Every scenario possesses a fly pawn first (the real player replaces it as the default pawn in M2). #include "Debug/DescentScenario.h" #include "World/DescentOreSubsystem.h" #include "World/DescentWorldDef.h" #include "World/DescentWorldScenarioSteps.h" using namespace DescentWorldScn; using namespace Descent::World; namespace { /** * The lamp lights 90 m, and many places (the middle of the Great Void) have no wall that near. The tour view starts at the * place and, so the picture shows lit rock, moves up to 200 m along the line of sight to 11 m before the first wall; when the * line of sight meets no wall within 260 m it looks down at the floor instead. Open water all the way: it uses the density ray. */ void TourFrame(UWorld* World, const FPlace& Place, FVector& OutCam, FVector& OutLook) { UDescentTerrainSubsystem* T = UDescentTerrainSubsystem::Get(World); FVector P = Place.PosM; FVector LookAt = Place.LookAtM; if (Place.Name == TEXT("start")) { // Since the carrier merge the start pose is the carrier's centre: a camera there is inside the hull. The tour looks at the // rock from 150 m down the canyon, with the carrier behind it, so the picture is of the terrain whether or not the carrier is there. P = FVector(150.0, 30.0, -30.0); LookAt = FVector(260.0, 30.0, -60.0); } FVector Dir = (LookAt - P).GetSafeNormal(); OutCam = P; OutLook = LookAt; if (!T) { return; } double H = T->Raycast(P, Dir, 260.0); if (H < 0.0) { Dir = (Dir * 0.2 + FVector(0, 0, -1) * 0.98).GetSafeNormal(); H = T->Raycast(P, Dir, 260.0); } if (H > 0.0) { OutCam = P + Dir * FMath::Clamp(H - 11.0, 0.0, 200.0); OutLook = P + Dir * H; } } } DESCENT_SCENARIO(m1_world_slices, "Writes the eight world maps of DESIGN 10 M1 (plan views and sections over the claim box) and checks the definition has 600 deposits.") { S.TimeLimit(240.f); StartWithFlyPawn(S); struct FSlice { const TCHAR* Cmd; }; static const TCHAR* Cmds[] = { TEXT("Descent.World.Slice Z -30 m1_world_slices_plan_z-30 2"), TEXT("Descent.World.Slice Z -200 m1_world_slices_plan_z-200 2"), TEXT("Descent.World.Slice Z -700 m1_world_slices_plan_z-700 2"), TEXT("Descent.World.Slice Z -1020 m1_world_slices_plan_z-1020 2"), TEXT("Descent.World.Slice Z -1330 m1_world_slices_plan_z-1330 2"), TEXT("Descent.World.Slice Y -25 m1_world_slices_section_y-25 2"), TEXT("Descent.World.Slice Y -330 m1_world_slices_section_y-330 2"), TEXT("Descent.World.Slice X 760 m1_world_slices_section_x760 2"), }; for (const TCHAR* Cmd : Cmds) { S.Exec(Cmd); } S.ExpectNear(TEXT("world_def.deposits"), [](FDescentScenarioContext& C) { return SnapNumber(C, TEXT("world_def"), TEXT("deposits")); }, 600.0, 0.5); S.DumpState(TEXT("end")); S.Quit(); } DESCENT_SCENARIO(m1_terrain_tour, "Flies the view to nine places (start, wellA, shelfA, h1, void, hollow, grotto, undercroft, goal), waits for the stream, photographs each and checks the budget.") { S.TimeLimit(900.f); StartWithFlyPawn(S); static const TCHAR* Places[] = { TEXT("start"), TEXT("wellA"), TEXT("shelfA"), TEXT("h1"), TEXT("void"), TEXT("hollow"), TEXT("grotto"), TEXT("undercroft"), TEXT("goal") }; for (const TCHAR* Name : Places) { const FString Place = Name; S.Do(FString::Printf(TEXT("go to %s"), Name), [Place](FDescentScenarioContext& C) { const FPlace* P = FDescentWorldDef::Get().FindPlace(FName(*Place)); if (!P) { C.Fail(TEXT("no such place ") + Place); return; } FVector Cam, Look; TourFrame(C.World, *P, Cam, Look); if (!PlaceView(C.World, Cam, Look, 75.f)) { C.Fail(TEXT("no pawn to place")); } }); WaitStreamIdle(S); S.Shot(FString::Printf(TEXT("tour_%s"), Name)); MeasureFrames(S, Name); S.Expect(FString::Printf(TEXT("%s: the stream is idle (terrain.pending_jobs == 0)"), Name), [](FDescentScenarioContext& C) { return SnapNumber(C, TEXT("terrain"), TEXT("pending_jobs")) == 0.0; }); S.Expect(FString::Printf(TEXT("%s: terrain.triangles <= 900,000"), Name), [Place](FDescentScenarioContext& C) { const double Tris = SnapNumber(C, TEXT("terrain"), TEXT("triangles")); UE_LOG(LogDescent, Display, TEXT("TOUR %s: chunks meshed %.0f, triangles %.0f, fill %.3f ms, mesh %.3f ms avg"), *Place, SnapNumber(C, TEXT("terrain"), TEXT("chunks_meshed")), Tris, SnapNumber(C, TEXT("terrain"), TEXT("fill_ms_avg")), SnapNumber(C, TEXT("terrain"), TEXT("mesh_ms_avg"))); return Tris <= 900000.0; }); S.Expect(FString::Printf(TEXT("%s: shot is not black (non_black > 0.2)"), Name), [](FDescentScenarioContext& C) { return C.LastShotStats.NonBlack > 0.2; }); } S.DumpState(TEXT("end")); S.Quit(); } DESCENT_SCENARIO(m1_carve, "Finds the nearest ore vein, photographs it, carves five spheres of radius 3 into it, cleans up, photographs the hole and a chunk border in wireframe.") { S.TimeLimit(400.f); StartWithFlyPawn(S); S.Do(TEXT("go to the start and find the nearest ferrite vein"), [](FDescentScenarioContext& C) { PlaceView(C.World, FVector(0, 0, -30), FVector(100, 0, -30)); UDescentOreSubsystem* Ore = UDescentOreSubsystem::Get(C.World); if (!Ore || !Ore->QueryNearest(FVector(0, 0, -30), 0)) { C.Fail(TEXT("no ferrite deposit")); return; } const UDescentOreSubsystem::FQuery& Q = Ore->LastQuery; const FVector P = Q.PosM, N = Q.NormalM; // Off the wall along its outward normal and a little along the vein, so the lamp rakes the surface and a hole shows its shape. const FDeposit& D = FDescentWorldDef::Get().Deposits[Q.Index]; const FVector Mid = (D.A + D.B) * 0.5; PlaceView(C.World, Mid + N * 7.0 + (D.B - D.A).GetSafeNormal() * 4.0, Mid, 70.f); UE_LOG(LogDescent, Display, TEXT("m1_carve: vein #%d at %.1f %.1f %.1f, normal %.2f %.2f %.2f, yield %d"), Q.Index, P.X, P.Y, P.Z, N.X, N.Y, N.Z, Q.Yield); }); WaitStreamIdle(S); S.Shot(TEXT("vein_before")); for (int32 K = 0; K < 5; ++K) { S.Do(FString::Printf(TEXT("carve %d"), K + 1), [K](FDescentScenarioContext& C) { UDescentOreSubsystem* Ore = UDescentOreSubsystem::Get(C.World); UDescentTerrainSubsystem* T = UDescentTerrainSubsystem::Get(C.World); const FDeposit& D = FDescentWorldDef::Get().Deposits[Ore->LastQuery.Index]; const FVector Dir = (D.B - D.A).GetSafeNormal(); const FVector Mid = (D.A + D.B) * 0.5; const FVector At = Mid + Dir * (static_cast(K) - 2.0) * 2.0; const int32 Removed = T->Carve(At, 3.0, 999.0, 1.0, true); UE_LOG(LogDescent, Display, TEXT("m1_carve: carve %d at %.1f %.1f %.1f removed %d samples; mined total %lld"), K + 1, At.X, At.Y, At.Z, Removed, Ore->GetMinedTotal()); if (K == 0 && Removed <= 0) { C.Fail(TEXT("the first carve removed nothing")); } }); S.WaitFrames(4); } S.Expect(TEXT("ore.mined_total rose"), [](FDescentScenarioContext& C) { return SnapNumber(C, TEXT("ore"), TEXT("mined_total")) > 0.0; }); S.Do(TEXT("clean up floating rock"), [](FDescentScenarioContext& C) { UDescentOreSubsystem* Ore = UDescentOreSubsystem::Get(C.World); UDescentTerrainSubsystem* T = UDescentTerrainSubsystem::Get(C.World); const FDeposit& D = FDescentWorldDef::Get().Deposits[Ore->LastQuery.Index]; const FVector Mid = (D.A + D.B) * 0.5; const int32 Freed = T->CleanupFloating(Mid, 6.0 + 4.0); UE_LOG(LogDescent, Display, TEXT("m1_carve: cleanup freed %d samples, %d unconnected left"), Freed, T->FloatingAfterCleanup); }); S.Expect(TEXT("terrain.floating_after_cleanup == 0"), [](FDescentScenarioContext& C) { return SnapNumber(C, TEXT("terrain"), TEXT("floating_after_cleanup")) == 0.0; }); WaitStreamIdle(S); S.Shot(TEXT("carved_after")); // A wireframe at a chunk border: the camera sits just beside the plane X = k * 20 m nearest the hole, looking along it. S.Do(TEXT("camera on a chunk border"), [](FDescentScenarioContext& C) { UDescentOreSubsystem* Ore = UDescentOreSubsystem::Get(C.World); const FDeposit& D = FDescentWorldDef::Get().Deposits[Ore->LastQuery.Index]; const FVector Mid = (D.A + D.B) * 0.5; const double Border = FMath::RoundToDouble(Mid.X / 20.0) * 20.0; PlaceView(C.World, FVector(Border - 7.0, Mid.Y, Mid.Z) + D.N * 6.0, FVector(Border, Mid.Y, Mid.Z), 80.f); }); S.Exec(TEXT("ShowFlag.PostProcessing 0")); S.Exec(TEXT("ShowFlag.Wireframe 1")); WaitStreamIdle(S); S.Shot(TEXT("seam_wire")); S.Exec(TEXT("ShowFlag.Wireframe 0")); S.Exec(TEXT("ShowFlag.PostProcessing 1")); S.DumpState(TEXT("end")); S.Quit(); } DESCENT_SCENARIO(m1_commands, "Runs every Descent.World.* console command once in a running game (Goto, Density, Carve, Cleanup, NearestDeposit, Slice, ClearRoute) and checks the carved route.") { S.TimeLimit(300.f); StartWithFlyPawn(S); S.Exec(TEXT("Descent.World.Goto canyon_mid")); S.Expect(TEXT("Goto moved the pawn to canyon_mid"), [](FDescentScenarioContext& C) { const APawn* P = C.World->GetFirstPlayerController()->GetPawn(); return P && FVector::Dist(Descent::UUToMetres(P->GetActorLocation()), FVector(425, 25, -120)) < 0.5; }); S.Exec(TEXT("Descent.World.Density 425 25 -120")); S.Expect(TEXT("canyon_mid is open water"), [](FDescentScenarioContext& C) { return SnapNumber(C, TEXT("terrain"), TEXT("last_density")) < 0.0; }); S.Exec(TEXT("Descent.World.NearestDeposit")); S.Expect(TEXT("ore.query is filled"), [](FDescentScenarioContext& C) { const TSharedRef Snap = C.Snapshot(); const TSharedPtr* Ore = nullptr; const TSharedPtr* Q = nullptr; bool bValid = false; return Snap->TryGetObjectField(TEXT("ore"), Ore) && (*Ore)->TryGetObjectField(TEXT("query"), Q) && (*Q)->TryGetBoolField(TEXT("valid"), bValid) && bValid; }); S.Exec(TEXT("Descent.World.Carve 4000 4000 -600 5")); S.Expect(TEXT("Carve removed rock"), [](FDescentScenarioContext& C) { return SnapNumber(C, TEXT("terrain"), TEXT("last_removed")) > 0.0; }); S.Exec(TEXT("Descent.World.Cleanup 4000 4000 -600 8")); S.Expect(TEXT("Cleanup leaves nothing floating"), [](FDescentScenarioContext& C) { return SnapNumber(C, TEXT("terrain"), TEXT("floating_after_cleanup")) == 0.0; }); S.Exec(TEXT("Descent.World.Slice Z -30 m1_commands_slice 4")); S.Exec(TEXT("Descent.World.ClearRoute 1")); S.Expect(TEXT("ClearRoute 1 modified chunks"), [](FDescentScenarioContext& C) { return SnapNumber(C, TEXT("terrain"), TEXT("chunks_modified")) > 100.0; }); S.Expect(TEXT("the carrier's route is clear at its first choke (SA, hull axis through the slot)"), [](FDescentScenarioContext& C) { UDescentTerrainSubsystem* T = UDescentTerrainSubsystem::Get(C.World); return T && T->Density(FVector(760, -25, -350)) < 0.0; }); S.DumpState(TEXT("end")); S.Quit(); }