diff --git a/include/omath/pathfinding/navigation_mesh.hpp b/include/omath/pathfinding/navigation_mesh.hpp index 35cc6e6..f9f5e27 100644 --- a/include/omath/pathfinding/navigation_mesh.hpp +++ b/include/omath/pathfinding/navigation_mesh.hpp @@ -7,6 +7,7 @@ #include "omath/linear_algebra/vector3.hpp" #include #include +#include #include namespace omath::pathfinding @@ -28,9 +29,9 @@ namespace omath::pathfinding [[nodiscard]] bool empty() const; - [[nodiscard]] std::vector serialize() const noexcept; + [[nodiscard]] std::string serialize() const noexcept; - void deserialize(const std::vector& raw) noexcept; + void deserialize(const std::string& raw); std::unordered_map, std::vector>> m_vertex_map; }; diff --git a/source/pathfinding/navigation_mesh.cpp b/source/pathfinding/navigation_mesh.cpp index 370bc8f..8ee17a2 100644 --- a/source/pathfinding/navigation_mesh.cpp +++ b/source/pathfinding/navigation_mesh.cpp @@ -3,8 +3,7 @@ // #include "omath/pathfinding/navigation_mesh.hpp" #include -#include -#include +#include #include namespace omath::pathfinding { @@ -30,76 +29,37 @@ namespace omath::pathfinding return m_vertex_map.empty(); } - std::vector NavigationMesh::serialize() const noexcept + std::string NavigationMesh::serialize() const noexcept { - std::vector raw; - - // Pre-calculate total size for better performance - std::size_t total_size = 0; + std::ostringstream oss; for (const auto& [vertex, neighbors] : m_vertex_map) { - total_size += sizeof(vertex) + sizeof(std::uint16_t) + sizeof(Vector3) * neighbors.size(); + oss << vertex.x << ' ' << vertex.y << ' ' << vertex.z + << ' ' << neighbors.size() << '\n'; + for (const auto& n : neighbors) + oss << n.x << ' ' << n.y << ' ' << n.z << '\n'; } - raw.reserve(total_size); - - auto dump_to_vector = [&raw](const T& t) - { - const auto* byte_ptr = reinterpret_cast(&t); - raw.insert(raw.end(), byte_ptr, byte_ptr + sizeof(T)); - }; - - for (const auto& [vertex, neighbors] : m_vertex_map) - { - // Clamp neighbors count to fit in uint16_t (prevents silent data corruption) - // NOTE: If neighbors.size() > 65535, only the first 65535 neighbors will be serialized. - // This is a limitation of the current serialization format using uint16_t for count. - const auto clamped_count = - std::min(neighbors.size(), std::numeric_limits::max()); - const auto neighbors_count = static_cast(clamped_count); - - dump_to_vector(vertex); - dump_to_vector(neighbors_count); - - // Only serialize up to the clamped count - for (std::size_t i = 0; i < clamped_count; ++i) - dump_to_vector(neighbors[i]); - } - return raw; + return oss.str(); } - void NavigationMesh::deserialize(const std::vector& raw) noexcept + void NavigationMesh::deserialize(const std::string& raw) { - auto load_from_vector = [](const std::vector& vec, std::size_t& offset, auto& value) - { - if (offset + sizeof(value) > vec.size()) - throw std::runtime_error("Deserialize: Invalid input data size."); - - std::copy_n(vec.data() + offset, sizeof(value), reinterpret_cast(&value)); - offset += sizeof(value); - }; - m_vertex_map.clear(); + std::istringstream iss(raw); - std::size_t offset = 0; - - while (offset < raw.size()) + Vector3 vertex; + std::size_t neighbors_count; + while (iss >> vertex.x >> vertex.y >> vertex.z >> neighbors_count) { - Vector3 vertex; - load_from_vector(raw, offset, vertex); - - std::uint16_t neighbors_count; - load_from_vector(raw, offset, neighbors_count); - std::vector> neighbors; neighbors.reserve(neighbors_count); - for (std::size_t i = 0; i < neighbors_count; ++i) { - Vector3 neighbor; - load_from_vector(raw, offset, neighbor); - neighbors.push_back(neighbor); + Vector3 n; + if (!(iss >> n.x >> n.y >> n.z)) + throw std::runtime_error("Deserialize: Unexpected end of data."); + neighbors.push_back(n); } - m_vertex_map.emplace(vertex, std::move(neighbors)); } } diff --git a/tests/general/unit_test_navigation_mesh.cpp b/tests/general/unit_test_navigation_mesh.cpp index b5ba7b0..c3c9314 100644 --- a/tests/general/unit_test_navigation_mesh.cpp +++ b/tests/general/unit_test_navigation_mesh.cpp @@ -7,19 +7,18 @@ using namespace omath::pathfinding; TEST(NavigationMeshTests, SerializeDeserializeRoundTrip) { NavigationMesh nav; - Vector3 a{0.f,0.f,0.f}; - Vector3 b{1.f,0.f,0.f}; - Vector3 c{0.f,1.f,0.f}; + Vector3 a{0.f, 0.f, 0.f}; + Vector3 b{1.f, 0.f, 0.f}; + Vector3 c{0.f, 1.f, 0.f}; - nav.m_vertex_map.emplace(a, std::vector>{b,c}); + nav.m_vertex_map.emplace(a, std::vector>{b, c}); nav.m_vertex_map.emplace(b, std::vector>{a}); nav.m_vertex_map.emplace(c, std::vector>{a}); - auto data = nav.serialize(); + std::string data = nav.serialize(); NavigationMesh nav2; EXPECT_NO_THROW(nav2.deserialize(data)); - // verify neighbors preserved EXPECT_EQ(nav2.m_vertex_map.size(), nav.m_vertex_map.size()); EXPECT_EQ(nav2.get_neighbors(a).size(), 2u); } @@ -27,7 +26,117 @@ TEST(NavigationMeshTests, SerializeDeserializeRoundTrip) TEST(NavigationMeshTests, GetClosestVertexWhenEmpty) { const NavigationMesh nav; - constexpr Vector3 p{5.f,5.f,5.f}; + constexpr Vector3 p{5.f, 5.f, 5.f}; const auto res = nav.get_closest_vertex(p); EXPECT_FALSE(res.has_value()); } + +TEST(NavigationMeshTests, SerializeEmptyMesh) +{ + const NavigationMesh nav; + const std::string data = nav.serialize(); + EXPECT_TRUE(data.empty()); +} + +TEST(NavigationMeshTests, DeserializeEmptyString) +{ + NavigationMesh nav; + EXPECT_NO_THROW(nav.deserialize("")); + EXPECT_TRUE(nav.empty()); +} + +TEST(NavigationMeshTests, SerializeProducesHumanReadableText) +{ + NavigationMesh nav; + nav.m_vertex_map.emplace(Vector3{1.f, 2.f, 3.f}, std::vector>{{4.f, 5.f, 6.f}}); + + const std::string data = nav.serialize(); + + // Must contain the vertex and neighbor coords as plain text + EXPECT_NE(data.find("1"), std::string::npos); + EXPECT_NE(data.find("2"), std::string::npos); + EXPECT_NE(data.find("3"), std::string::npos); + EXPECT_NE(data.find("4"), std::string::npos); + EXPECT_NE(data.find("5"), std::string::npos); + EXPECT_NE(data.find("6"), std::string::npos); +} + +TEST(NavigationMeshTests, DeserializeRestoresNeighborValues) +{ + NavigationMesh nav; + const Vector3 v{1.f, 2.f, 3.f}; + const Vector3 n1{4.f, 5.f, 6.f}; + const Vector3 n2{7.f, 8.f, 9.f}; + nav.m_vertex_map.emplace(v, std::vector>{n1, n2}); + + NavigationMesh nav2; + nav2.deserialize(nav.serialize()); + + ASSERT_EQ(nav2.m_vertex_map.count(v), 1u); + const auto& neighbors = nav2.get_neighbors(v); + ASSERT_EQ(neighbors.size(), 2u); + EXPECT_EQ(neighbors[0], n1); + EXPECT_EQ(neighbors[1], n2); +} + +TEST(NavigationMeshTests, DeserializeOverwritesPreviousData) +{ + NavigationMesh nav; + const Vector3 v{1.f, 0.f, 0.f}; + nav.m_vertex_map.emplace(v, std::vector>{}); + + // Load a different mesh into the same object + NavigationMesh other; + const Vector3 a{10.f, 20.f, 30.f}; + other.m_vertex_map.emplace(a, std::vector>{}); + + nav.deserialize(other.serialize()); + + EXPECT_EQ(nav.m_vertex_map.size(), 1u); + EXPECT_EQ(nav.m_vertex_map.count(v), 0u); + EXPECT_EQ(nav.m_vertex_map.count(a), 1u); +} + +TEST(NavigationMeshTests, RoundTripNegativeAndFractionalCoords) +{ + NavigationMesh nav; + const Vector3 v{-1.5f, 0.25f, -3.75f}; + const Vector3 n{100.f, -200.f, 0.001f}; + nav.m_vertex_map.emplace(v, std::vector>{n}); + + NavigationMesh nav2; + nav2.deserialize(nav.serialize()); + + ASSERT_EQ(nav2.m_vertex_map.count(v), 1u); + const auto& neighbors = nav2.get_neighbors(v); + ASSERT_EQ(neighbors.size(), 1u); + EXPECT_NEAR(neighbors[0].x, n.x, 1e-3f); + EXPECT_NEAR(neighbors[0].y, n.y, 1e-3f); + EXPECT_NEAR(neighbors[0].z, n.z, 1e-3f); +} + +TEST(NavigationMeshTests, GetClosestVertexReturnsNearest) +{ + NavigationMesh nav; + const Vector3 a{0.f, 0.f, 0.f}; + const Vector3 b{10.f, 0.f, 0.f}; + nav.m_vertex_map.emplace(a, std::vector>{}); + nav.m_vertex_map.emplace(b, std::vector>{}); + + const auto res = nav.get_closest_vertex({1.f, 0.f, 0.f}); + ASSERT_TRUE(res.has_value()); + EXPECT_EQ(res.value(), a); +} + +TEST(NavigationMeshTests, VertexWithNoNeighborsRoundTrip) +{ + NavigationMesh nav; + const Vector3 v{5.f, 5.f, 5.f}; + nav.m_vertex_map.emplace(v, std::vector>{}); + + NavigationMesh nav2; + nav2.deserialize(nav.serialize()); + + ASSERT_EQ(nav2.m_vertex_map.count(v), 1u); + EXPECT_TRUE(nav2.get_neighbors(v).empty()); +}