// Headless tests of the terrain (DESIGN_UE.md section 10, M1): Descent.Terrain.SeamMatch, .CarveOre, .Cleanup, .NoBounds, .Queries #include "Tests/DescentTestCommon.h" #include "Core/DescentMath.h" #include "World/DescentOreSubsystem.h" #include "World/DescentTerrainSubsystem.h" #include "World/DescentWorldDef.h" #include "World/SurfaceNets.h" #if WITH_DEV_AUTOMATION_TESTS using namespace Descent::World; namespace TerrainTestTune = Descent::Tune::Terrain; namespace { bool TerrainTestMesh(UDescentTerrainSubsystem& T, const FIntVector& Key, FProcMeshSection& Out, int32& Tris) { FTerrainChunk* C = T.FindOrCreateChunk(Key); if (C->State == EChunkState::Uniform) { return false; } T.EnsureData(*C); FSurfaceNetsInput In; In.Data = C->Data.GetData(); In.Cut = C->Cut.GetData(); In.Key = Key; In.Def = &FDescentWorldDef::Get(); return FSurfaceNets::Mesh(In, Out, Tris); } /** Vertices of a section as world metres plus normal. */ void TerrainTestVerts(const FProcMeshSection& S, const FIntVector& Key, TArray>& Out) { const FVector Origin = FVector(Key) * TerrainTestTune::ChunkM; for (const FProcMeshVertex& V : S.ProcVertexBuffer) { Out.Add({ Origin + V.Position / Descent::UUPerMetre, V.Normal }); } } } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDescentTerrainSeamMatchTest, "Descent.Terrain.SeamMatch", DESCENT_TEST_FLAGS) bool FDescentTerrainSeamMatchTest::RunTest(const FString& Parameters) { FDescentTestWorld TW; UDescentTerrainSubsystem* T = TW.Sub(); if (!TestNotNull(TEXT("terrain subsystem"), T)) { return false; } // Find two chunks that touch each other along an axis and both hold a surface, near the canyon floor and walls. int32 PairsChecked = 0; for (int32 Axis = 0; Axis < 3 && PairsChecked < 3; ++Axis) { for (int32 Z = -9; Z <= -4 && PairsChecked < (Axis + 1); ++Z) { for (int32 Y = -5; Y <= 5 && PairsChecked < (Axis + 1); ++Y) { const FIntVector A(2, Y, Z); FIntVector B = A; B[Axis] += 1; FProcMeshSection SA, SB; int32 TA = 0, TB = 0; if (!TerrainTestMesh(*T, A, SA, TA) || !TerrainTestMesh(*T, B, SB, TB)) { continue; } TArray> VA, VB; TerrainTestVerts(SA, A, VA); TerrainTestVerts(SB, B, VB); // The slab both chunks compute: the last cell column of A is the first of B (world 18.75 - 20 m into A). const double Plane = B[Axis] * TerrainTestTune::ChunkM; int32 Shared = 0, Mismatched = 0; for (const TPair& Va : VA) { if (Va.Key[Axis] < Plane - TerrainTestTune::VoxelM - 1e-6 || Va.Key[Axis] > Plane + 1e-6) { continue; } double Best = 1e9; FVector BestN = FVector::ZeroVector; for (const TPair& Vb : VB) { const double D = FVector::Dist(Va.Key, Vb.Key); if (D < Best) { Best = D; BestN = Vb.Value; } } if (Best < 1e-3) { ++Shared; if (!Va.Value.Equals(BestN, 1e-4)) { ++Mismatched; } } else if (Best < 0.5) { ++Mismatched; } } if (Shared == 0) { continue; } ++PairsChecked; TestEqual(FString::Printf(TEXT("axis %d chunks (%d,%d,%d)/(%d,%d,%d): shared border vertices identical (%d shared)"), Axis, A.X, A.Y, A.Z, B.X, B.Y, B.Z, Shared), Mismatched, 0); } } } TestTrue(TEXT("at least 3 pairs of surface-bearing neighbours were compared"), PairsChecked >= 3); return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDescentTerrainCarveOreTest, "Descent.Terrain.CarveOre", DESCENT_TEST_FLAGS) bool FDescentTerrainCarveOreTest::RunTest(const FString& Parameters) { FDescentTestWorld TW; UDescentTerrainSubsystem* T = TW.Sub(); UDescentOreSubsystem* Ore = TW.Sub(); if (!TestNotNull(TEXT("terrain and ore subsystems"), T) || !TestNotNull(TEXT("ore subsystem"), Ore)) { return false; } // The counter itself: 6 removed vein samples give 1 chunk, exactly (section 2.5). int32 Freed = 0; Ore->OnOreFreed.AddLambda([&Freed](int32, const FVector&) { ++Freed; }); for (int32 I = 0; I < 5; ++I) { Ore->NoteSampleRemoved(0, FVector::ZeroVector); } TestEqual(TEXT("5 removed samples give no chunk"), Freed, 0); Ore->NoteSampleRemoved(0, FVector::ZeroVector); TestEqual(TEXT("the 6th gives one chunk"), Freed, 1); TestEqual(TEXT("and the deposit's mined counter is 1"), Ore->GetMined(0), 1); Ore->ResetState(); Freed = 0; // Through the terrain: carving into a vein frees chunks of ore in proportion to the rock removed from it. const FDescentWorldDef& Def = FDescentWorldDef::Get(); const int32 Dep = Def.NearestDeposit(FVector(0, 0, -30), 0); if (!TestTrue(TEXT("a ferrite deposit exists"), Dep >= 0)) { return false; } const FDeposit& D = Def.Deposits[Dep]; const FVector Mid = (D.A + D.B) * 0.5; const int32 Removed = T->Carve(Mid, 3.0, 999.0, 1.0, true); TestTrue(FString::Printf(TEXT("carving a vein removes rock (%d samples)"), Removed), Removed > 0); TestTrue(FString::Printf(TEXT("and frees ore (mined total %lld, freed events %d)"), Ore->GetMinedTotal(), Freed), Ore->GetMinedTotal() > 0 && Freed > 0); TestEqual(TEXT("every mined chunk fired the event"), static_cast(Freed), Ore->GetMinedTotal()); TestTrue(TEXT("never more than the vein's yield"), Ore->GetMined(Dep) <= D.Yield); TestTrue(TEXT("the carved point is water now"), T->Density(Mid) < 0.0); return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDescentTerrainCleanupTest, "Descent.Terrain.Cleanup", DESCENT_TEST_FLAGS) bool FDescentTerrainCleanupTest::RunTest(const FString& Parameters) { FDescentTestWorld TW; UDescentTerrainSubsystem* T = TW.Sub(); if (!TestNotNull(TEXT("terrain subsystem"), T)) { return false; } // Solid rock far from every void: hollow it, leave a 3 x 3 x 3 block of rock floating in the hollow. const FVector C(4000.0, 4000.0, -600.0); TestTrue(TEXT("the test spot is solid"), T->Density(C) > 0.0); T->Carve(C, 9.0, 999.0, 1.0, true); TestTrue(TEXT("the hollow is open"), T->Density(C) < 0.0); const int32 Gx = FMath::RoundToInt(C.X / TerrainTestTune::VoxelM), Gy = FMath::RoundToInt(C.Y / TerrainTestTune::VoxelM), Gz = FMath::RoundToInt(C.Z / TerrainTestTune::VoxelM); for (int32 Dz = -1; Dz <= 1; ++Dz) { for (int32 Dy = -1; Dy <= 1; ++Dy) { for (int32 Dx = -1; Dx <= 1; ++Dx) { T->SetSampleGlobal(Gx + Dx, Gy + Dy, Gz + Dz, 2.0f); } } } const int32 Before = T->CountFloatingRock(C, 9.0); TestEqual(TEXT("the block is floating (27 unconnected samples)"), Before, 27); const int32 Freed = T->CleanupFloating(C, 9.0); TestEqual(TEXT("cleanup frees exactly those samples"), Freed, 27); TestEqual(TEXT("and leaves no unconnected rock"), T->FloatingAfterCleanup, 0); TestTrue(TEXT("the freed block is water"), T->Density(C) < 0.0); return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDescentTerrainNoBoundsTest, "Descent.Terrain.NoBounds", DESCENT_TEST_FLAGS) bool FDescentTerrainNoBoundsTest::RunTest(const FString& Parameters) { FDescentTestWorld TW; UDescentTerrainSubsystem* T = TW.Sub(); if (!TestNotNull(TEXT("terrain subsystem"), T)) { return false; } const FVector P(100000.0, 100000.0, -100000.0); TestTrue(TEXT("a point 100 km out can be queried (rock)"), T->Density(P) > 0.0); FTerrainChunk* C = T->FindOrCreateChunk(UDescentTerrainSubsystem::KeyOfPoint(P)); T->EnsureData(*C); TestTrue(TEXT("its chunk can be filled"), C->HasData()); const int32 Removed = T->Carve(P, 3.0, 999.0, 1.0, true); TestTrue(FString::Printf(TEXT("and carved (%d samples removed)"), Removed), Removed > 0); TestTrue(TEXT("the carved point is open now"), T->Density(P) < 0.0); // The same far away in the other direction, and straight up in the sea. TestTrue(TEXT("-100 km in X,Y and +100 km up is open sea"), T->Density(FVector(-100000.0, -100000.0, 100000.0)) < 0.0); TestTrue(TEXT("a ray from the start pose into the open can run 5 km without a hit"), T->Raycast(FVector(0, 0, 500), FVector(0, 0, 1), 5000.0) < 0.0); return true; } IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDescentTerrainQueriesTest, "Descent.Terrain.Queries", DESCENT_TEST_FLAGS) bool FDescentTerrainQueriesTest::RunTest(const FString& Parameters) { FDescentTestWorld TW; UDescentTerrainSubsystem* T = TW.Sub(); if (!TestNotNull(TEXT("terrain subsystem"), T)) { return false; } // The canyon floor under the start pose: a ray straight down hits it, and the hit point is on the surface. const FVector O(0, 0, -30); TestTrue(TEXT("the start pose is in water"), T->Density(O) < 0.0); const double Hit = T->Raycast(O, FVector(0, 0, -1), 400.0); TestTrue(FString::Printf(TEXT("a ray down from the start hits the canyon floor (%.2f m)"), Hit), Hit > 50.0 && Hit < 150.0); if (Hit > 0.0) { const FVector H = O + FVector(0, 0, -1) * Hit; TestTrue(TEXT("the hit point lies on the surface"), FMath::Abs(T->Density(H)) < 0.3); TestTrue(TEXT("the gradient there points up out of ... into the rock, i.e. downward"), T->Gradient(H).Z < -0.3); // Push a sphere out of the floor: it ends up clear of the rock. FVector C = H + FVector(0, 0, -0.5); FVector N; const bool bMoved = T->PushOutSphere(C, 1.0, &N); TestTrue(TEXT("a sphere half inside the rock is pushed out"), bMoved); TestTrue(FString::Printf(TEXT("and ends up touching or clear (density %.2f)"), T->Density(C)), T->Density(C) <= -0.5); } return true; } #endif // WITH_DEV_AUTOMATION_TESTS