diff --git a/src/unittest/test_serveractiveobjectmgr.cpp b/src/unittest/test_serveractiveobjectmgr.cpp index 85cfd119fff3..9e75f7f19033 100644 --- a/src/unittest/test_serveractiveobjectmgr.cpp +++ b/src/unittest/test_serveractiveobjectmgr.cpp @@ -19,8 +19,12 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "activeobjectmgr.h" #include "catch.h" +#include "irrTypes.h" +#include "irr_aabb3d.h" #include "mock_serveractiveobject.h" #include +#include +#include #include #include "server/activeobjectmgr.h" @@ -28,6 +32,7 @@ with this program; if not, write to the Free Software Foundation, Inc., class TestServerActiveObjectMgr { server::ActiveObjectMgr saomgr; + std::vector ids; public: @@ -36,15 +41,30 @@ class TestServerActiveObjectMgr { } bool registerObject(std::unique_ptr obj) { - return saomgr.registerObject(std::move(obj)); + auto *ptr = obj.get(); + if (!saomgr.registerObject(std::move(obj))) + return false; + ids.push_back(ptr->getId()); + return true; } void removeObject(u16 id) { + const auto it = std::find(ids.begin(), ids.end(), id); + REQUIRE(it != ids.end()); + ids.erase(it); saomgr.removeObject(id); } + void updatePos(u16 id, const v3f &pos) { + auto *obj = saomgr.getActiveObject(id); + REQUIRE(obj != nullptr); + obj->setPos(pos); + saomgr.updatePos(pos, id); // HACK work around m_env == nullptr + } + void clear() { saomgr.clear(); + ids.clear(); } ServerActiveObject *getActiveObject(u16 id) { @@ -60,6 +80,82 @@ class TestServerActiveObjectMgr { void getAddedActiveObjectsAroundPos(T&& arg) { saomgr.getAddedActiveObjectsAroundPos(std::forward(arg)); } + + // Testing + + bool empty() { + return ids.empty(); + } + + template + u16 randomId(T &random) { + REQUIRE(!ids.empty()); + std::uniform_int_distribution index(0, ids.size() - 1); + return ids[index(random)]; + } + + void getObjectsInsideRadiusNaive(const v3f &pos, float radius, + std::vector &result) + { + for (const auto &[id, obj] : saomgr.m_active_objects.iter()) { + if (obj->getBasePosition().getDistanceFromSQ(pos) <= radius * radius) { + result.push_back(obj.get()); + } + } + } + + void getObjectsInAreaNaive(const aabb3f &box, + std::vector &result) + { + for (const auto &[id, obj] : saomgr.m_active_objects.iter()) { + if (box.isPointInside(obj->getBasePosition())) { + result.push_back(obj.get()); + } + } + } + + constexpr static auto compare_by_id = [](auto *sao1, auto *sao2) -> bool { + return sao1->getId() < sao2->getId(); + }; + + static void sortById(std::vector &saos) { + std::sort(saos.begin(), saos.end(), compare_by_id); + } + + void compareObjects(std::vector &actual, + std::vector &expected) + { + std::vector unexpected, missing; + sortById(actual); + sortById(expected); + + std::set_difference(actual.begin(), actual.end(), + expected.begin(), expected.end(), + std::back_inserter(unexpected), compare_by_id); + + assert(unexpected.empty()); + + std::set_difference(expected.begin(), expected.end(), + actual.begin(), actual.end(), + std::back_inserter(missing), compare_by_id); + assert(missing.empty()); + } + + void compareObjectsInsideRadius(const v3f &pos, float radius) + { + std::vector actual, expected; + saomgr.getObjectsInsideRadius(pos, radius, actual, nullptr); + getObjectsInsideRadiusNaive(pos, radius, expected); + compareObjects(actual, expected); + } + + void compareObjectsInArea(const aabb3f &box) + { + std::vector actual, expected; + saomgr.getObjectsInArea(box, actual, nullptr); + getObjectsInAreaNaive(box, expected); + compareObjects(actual, expected); + } }; @@ -190,4 +286,54 @@ TEST_CASE("server active object manager") saomgr.clear(); } + + SECTION("spatial index") { + TestServerActiveObjectMgr saomgr; + std::mt19937 gen(0xABCDEF); + std::uniform_int_distribution coordinate(-1000, 1000); + const auto random_pos = [&]() { + return v3f(coordinate(gen), coordinate(gen), coordinate(gen)); + }; + + std::uniform_int_distribution percent(0, 99); + const auto modify = [&](u32 p_insert, u32 p_delete, u32 p_update) { + const auto p = percent(gen); + if (p < p_insert) { + saomgr.registerObject(std::make_unique(nullptr, random_pos())); + } else if (p < p_insert + p_delete) { + if (!saomgr.empty()) + saomgr.removeObject(saomgr.randomId(gen)); + } else if (p < p_insert + p_delete + p_update) { + if (!saomgr.empty()) + saomgr.updatePos(saomgr.randomId(gen), random_pos()); + } + }; + + const auto test_queries = [&]() { + std::uniform_real_distribution radius(0, 100); + saomgr.compareObjectsInsideRadius(random_pos(), radius(gen)); + + aabb3f box(random_pos(), random_pos()); + box.repair(); + saomgr.compareObjectsInArea(box); + }; + + // Grow: Insertion twice as likely as deletion + for (u32 i = 0; i < 3000; ++i) { + modify(50, 25, 25); + test_queries(); + } + + // Stagnate: Insertion and deletion equally likely + for (u32 i = 0; i < 3000; ++i) { + modify(25, 25, 50); + test_queries(); + } + + // Shrink: Deletion twice as likely as insertion + while (!saomgr.empty()) { + modify(25, 50, 25); + test_queries(); + } + } } \ No newline at end of file