mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 09:33:51 +00:00
refactor: extract spline math, consolidate packet parsing, decompose TransportManager
Extract CatmullRomSpline (include/math/spline.hpp, src/math/spline.cpp) as a
standalone, immutable, thread-safe spline module with O(log n) binary segment
search and fused position+tangent evaluation — replacing the duplicated O(n)
evalTimedCatmullRom/orientationFromTangent pair in TransportManager.
Consolidate 7 copies of spline packet parsing into shared functions in
game/spline_packet.{hpp,cpp}: parseMonsterMoveSplineBody (WotLK/TBC),
parseMonsterMoveSplineBodyVanilla, parseClassicMoveUpdateSpline,
parseWotlkMoveUpdateSpline, and decodePackedDelta. Named SplineFlag constants
replace magic hex literals throughout.
Extract TransportPathRepository (game/transport_path_repository.{hpp,cpp}) from
TransportManager — owns path data, DBC loading, and path inference. Paths stored
as PathEntry wrapping CatmullRomSpline + metadata (zOnly, fromDBC, worldCoords).
TransportManager reduced from ~1200 to ~500 lines, focused on transport lifecycle
and server sync.
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
This commit is contained in:
parent
535cc20afe
commit
de0383aa6b
32 changed files with 2198 additions and 1293 deletions
|
|
@ -59,6 +59,32 @@ target_link_libraries(test_srp PRIVATE catch2_main OpenSSL::SSL OpenSSL::Crypto)
|
|||
add_test(NAME srp COMMAND test_srp)
|
||||
register_test_target(test_srp)
|
||||
|
||||
# ── test_spline ──────────────────────────────────────────────
|
||||
add_executable(test_spline
|
||||
test_spline.cpp
|
||||
${TEST_COMMON_SOURCES}
|
||||
${CMAKE_SOURCE_DIR}/src/math/spline.cpp
|
||||
)
|
||||
target_include_directories(test_spline PRIVATE ${TEST_INCLUDE_DIRS})
|
||||
target_include_directories(test_spline SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
|
||||
target_link_libraries(test_spline PRIVATE catch2_main)
|
||||
add_test(NAME spline COMMAND test_spline)
|
||||
register_test_target(test_spline)
|
||||
|
||||
# ── test_transport_path_repo ─────────────────────────────────
|
||||
add_executable(test_transport_path_repo
|
||||
test_transport_path_repo.cpp
|
||||
${TEST_COMMON_SOURCES}
|
||||
${CMAKE_SOURCE_DIR}/src/game/transport_path_repository.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/math/spline.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/pipeline/dbc_loader.cpp
|
||||
)
|
||||
target_include_directories(test_transport_path_repo PRIVATE ${TEST_INCLUDE_DIRS})
|
||||
target_include_directories(test_transport_path_repo SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
|
||||
target_link_libraries(test_transport_path_repo PRIVATE catch2_main)
|
||||
add_test(NAME transport_path_repo COMMAND test_transport_path_repo)
|
||||
register_test_target(test_transport_path_repo)
|
||||
|
||||
# ── test_opcode_table ────────────────────────────────────────
|
||||
add_executable(test_opcode_table
|
||||
test_opcode_table.cpp
|
||||
|
|
|
|||
240
tests/test_spline.cpp
Normal file
240
tests/test_spline.cpp
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
// tests/test_spline.cpp
|
||||
// Unit tests for wowee::math::CatmullRomSpline
|
||||
#include <catch_amalgamated.hpp>
|
||||
#include "math/spline.hpp"
|
||||
#include <cmath>
|
||||
|
||||
using namespace wowee::math;
|
||||
|
||||
// ── Helper: build a simple 4-point linear path ─────────────────────
|
||||
static std::vector<SplineKey> linearKeys() {
|
||||
// Straight line along X axis: (0,0,0) → (10,0,0) → (20,0,0) → (30,0,0)
|
||||
return {
|
||||
{0, glm::vec3(0.0f, 0.0f, 0.0f)},
|
||||
{1000, glm::vec3(10.0f, 0.0f, 0.0f)},
|
||||
{2000, glm::vec3(20.0f, 0.0f, 0.0f)},
|
||||
{3000, glm::vec3(30.0f, 0.0f, 0.0f)},
|
||||
};
|
||||
}
|
||||
|
||||
// ── Helper: build a square looping path ─────────────────────────────
|
||||
static std::vector<SplineKey> squareKeys() {
|
||||
// Square path: (0,0,0) → (10,0,0) → (10,10,0) → (0,10,0) → (0,0,0)
|
||||
return {
|
||||
{0, glm::vec3(0.0f, 0.0f, 0.0f)},
|
||||
{1000, glm::vec3(10.0f, 0.0f, 0.0f)},
|
||||
{2000, glm::vec3(10.0f, 10.0f, 0.0f)},
|
||||
{3000, glm::vec3(0.0f, 10.0f, 0.0f)},
|
||||
{4000, glm::vec3(0.0f, 0.0f, 0.0f)}, // Wrap back to start
|
||||
};
|
||||
}
|
||||
|
||||
// ── Construction ────────────────────────────────────────────────────
|
||||
|
||||
TEST_CASE("CatmullRomSpline empty construction", "[spline]") {
|
||||
CatmullRomSpline spline({});
|
||||
REQUIRE(spline.keyCount() == 0);
|
||||
REQUIRE(spline.durationMs() == 0);
|
||||
REQUIRE(spline.evaluatePosition(0) == glm::vec3(0.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("CatmullRomSpline single key", "[spline]") {
|
||||
CatmullRomSpline spline({{500, glm::vec3(1.0f, 2.0f, 3.0f)}});
|
||||
REQUIRE(spline.keyCount() == 1);
|
||||
REQUIRE(spline.durationMs() == 0);
|
||||
|
||||
auto pos = spline.evaluatePosition(0);
|
||||
REQUIRE(pos.x == Catch::Approx(1.0f));
|
||||
REQUIRE(pos.y == Catch::Approx(2.0f));
|
||||
REQUIRE(pos.z == Catch::Approx(3.0f));
|
||||
|
||||
// Any time returns the same position
|
||||
pos = spline.evaluatePosition(9999);
|
||||
REQUIRE(pos.x == Catch::Approx(1.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("CatmullRomSpline duration calculation", "[spline]") {
|
||||
CatmullRomSpline spline(linearKeys());
|
||||
REQUIRE(spline.durationMs() == 3000);
|
||||
REQUIRE(spline.keyCount() == 4);
|
||||
REQUIRE_FALSE(spline.isTimeClosed());
|
||||
}
|
||||
|
||||
// ── Position evaluation ─────────────────────────────────────────────
|
||||
|
||||
TEST_CASE("CatmullRomSpline evaluates at key positions", "[spline]") {
|
||||
auto keys = linearKeys();
|
||||
CatmullRomSpline spline(keys);
|
||||
|
||||
// At exact key times, Catmull-Rom passes through the control point
|
||||
auto pos0 = spline.evaluatePosition(0);
|
||||
REQUIRE(pos0.x == Catch::Approx(0.0f).margin(0.01f));
|
||||
|
||||
auto pos1 = spline.evaluatePosition(1000);
|
||||
REQUIRE(pos1.x == Catch::Approx(10.0f).margin(0.01f));
|
||||
|
||||
auto pos2 = spline.evaluatePosition(2000);
|
||||
REQUIRE(pos2.x == Catch::Approx(20.0f).margin(0.01f));
|
||||
|
||||
auto pos3 = spline.evaluatePosition(3000);
|
||||
REQUIRE(pos3.x == Catch::Approx(30.0f).margin(0.01f));
|
||||
}
|
||||
|
||||
TEST_CASE("CatmullRomSpline midpoint evaluation", "[spline]") {
|
||||
CatmullRomSpline spline(linearKeys());
|
||||
|
||||
// For a straight line, midpoint should be approximately halfway.
|
||||
// Catmull-Rom with clamped endpoints at segment boundaries
|
||||
// has some overshoot, so use a wider tolerance.
|
||||
auto mid = spline.evaluatePosition(500);
|
||||
REQUIRE(mid.x == Catch::Approx(5.0f).margin(1.0f));
|
||||
REQUIRE(mid.y == Catch::Approx(0.0f).margin(0.1f));
|
||||
REQUIRE(mid.z == Catch::Approx(0.0f).margin(0.1f));
|
||||
}
|
||||
|
||||
TEST_CASE("CatmullRomSpline clamping at boundaries", "[spline]") {
|
||||
CatmullRomSpline spline(linearKeys());
|
||||
|
||||
// Before start should clamp to first segment start
|
||||
auto before = spline.evaluatePosition(0);
|
||||
REQUIRE(before.x == Catch::Approx(0.0f).margin(0.01f));
|
||||
|
||||
// After end should clamp to last segment end
|
||||
auto after = spline.evaluatePosition(5000);
|
||||
REQUIRE(after.x == Catch::Approx(30.0f).margin(0.01f));
|
||||
}
|
||||
|
||||
// ── Time-closed (looping) path ──────────────────────────────────────
|
||||
|
||||
TEST_CASE("CatmullRomSpline time-closed path", "[spline]") {
|
||||
CatmullRomSpline spline(squareKeys(), true);
|
||||
REQUIRE(spline.durationMs() == 4000);
|
||||
REQUIRE(spline.isTimeClosed());
|
||||
|
||||
// Start position
|
||||
auto pos0 = spline.evaluatePosition(0);
|
||||
REQUIRE(pos0.x == Catch::Approx(0.0f).margin(0.1f));
|
||||
REQUIRE(pos0.y == Catch::Approx(0.0f).margin(0.1f));
|
||||
|
||||
// Quarter way — should be near (10, 0, 0)
|
||||
auto pos1 = spline.evaluatePosition(1000);
|
||||
REQUIRE(pos1.x == Catch::Approx(10.0f).margin(0.1f));
|
||||
REQUIRE(pos1.y == Catch::Approx(0.0f).margin(0.1f));
|
||||
}
|
||||
|
||||
// ── Tangent / evaluate() ────────────────────────────────────────────
|
||||
|
||||
TEST_CASE("CatmullRomSpline evaluate returns tangent", "[spline]") {
|
||||
CatmullRomSpline spline(linearKeys());
|
||||
|
||||
auto result = spline.evaluate(1500);
|
||||
// For a straight line along X, tangent should be predominantly in X
|
||||
REQUIRE(std::abs(result.tangent.x) > std::abs(result.tangent.y));
|
||||
REQUIRE(std::abs(result.tangent.x) > std::abs(result.tangent.z));
|
||||
}
|
||||
|
||||
// ── orientationFromTangent ──────────────────────────────────────────
|
||||
|
||||
TEST_CASE("orientationFromTangent identity for zero tangent", "[spline]") {
|
||||
auto q = CatmullRomSpline::orientationFromTangent(glm::vec3(0.0f));
|
||||
// Should return identity quaternion
|
||||
REQUIRE(q.w == Catch::Approx(1.0f).margin(0.01f));
|
||||
}
|
||||
|
||||
TEST_CASE("orientationFromTangent for forward direction", "[spline]") {
|
||||
auto q = CatmullRomSpline::orientationFromTangent(glm::vec3(1.0f, 0.0f, 0.0f));
|
||||
// Should return a valid quaternion (unit length)
|
||||
float length = glm::length(q);
|
||||
REQUIRE(length == Catch::Approx(1.0f).margin(0.01f));
|
||||
}
|
||||
|
||||
TEST_CASE("orientationFromTangent for vertical tangent", "[spline]") {
|
||||
// Nearly vertical tangent — tests the fallback up vector
|
||||
auto q = CatmullRomSpline::orientationFromTangent(glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
float length = glm::length(q);
|
||||
REQUIRE(length == Catch::Approx(1.0f).margin(0.01f));
|
||||
}
|
||||
|
||||
// ── hasXYMovement ───────────────────────────────────────────────────
|
||||
|
||||
TEST_CASE("hasXYMovement detects horizontal movement", "[spline]") {
|
||||
CatmullRomSpline spline(linearKeys());
|
||||
REQUIRE(spline.hasXYMovement(1.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("hasXYMovement detects Z-only (elevator)", "[spline]") {
|
||||
std::vector<SplineKey> elevator = {
|
||||
{0, glm::vec3(5.0f, 5.0f, 0.0f)},
|
||||
{1000, glm::vec3(5.0f, 5.0f, 10.0f)},
|
||||
{2000, glm::vec3(5.0f, 5.0f, 20.0f)},
|
||||
};
|
||||
CatmullRomSpline spline(elevator);
|
||||
REQUIRE_FALSE(spline.hasXYMovement(1.0f));
|
||||
}
|
||||
|
||||
// ── findNearestKey ──────────────────────────────────────────────────
|
||||
|
||||
TEST_CASE("findNearestKey returns closest key", "[spline]") {
|
||||
CatmullRomSpline spline(linearKeys());
|
||||
|
||||
// Closest to (9, 0, 0) should be key at (10, 0, 0) = index 1
|
||||
size_t idx = spline.findNearestKey(glm::vec3(9.0f, 0.0f, 0.0f));
|
||||
REQUIRE(idx == 1);
|
||||
|
||||
// Closest to (0, 0, 0) should be key 0
|
||||
idx = spline.findNearestKey(glm::vec3(0.0f, 0.0f, 0.0f));
|
||||
REQUIRE(idx == 0);
|
||||
|
||||
// Closest to (25, 0, 0) should be key at (20, 0, 0) = index 2 or (30,0,0) = index 3
|
||||
idx = spline.findNearestKey(glm::vec3(25.0f, 0.0f, 0.0f));
|
||||
REQUIRE((idx == 2 || idx == 3));
|
||||
}
|
||||
|
||||
// ── Binary search segment lookup ────────────────────────────────────
|
||||
|
||||
TEST_CASE("CatmullRomSpline segment lookup is correct", "[spline]") {
|
||||
// Build a path with uneven timing
|
||||
std::vector<SplineKey> keys = {
|
||||
{0, glm::vec3(0.0f)},
|
||||
{100, glm::vec3(1.0f, 0.0f, 0.0f)},
|
||||
{500, glm::vec3(2.0f, 0.0f, 0.0f)},
|
||||
{2000, glm::vec3(3.0f, 0.0f, 0.0f)},
|
||||
{5000, glm::vec3(4.0f, 0.0f, 0.0f)},
|
||||
};
|
||||
CatmullRomSpline spline(keys);
|
||||
|
||||
// At t=50, should be in first segment → position near key 0
|
||||
auto pos50 = spline.evaluatePosition(50);
|
||||
REQUIRE(pos50.x == Catch::Approx(0.5f).margin(0.5f)); // Somewhere between 0 and 1
|
||||
|
||||
// At t=300, should be in second segment → between key 1 and key 2
|
||||
auto pos300 = spline.evaluatePosition(300);
|
||||
REQUIRE(pos300.x > 1.0f);
|
||||
REQUIRE(pos300.x < 2.5f);
|
||||
|
||||
// At t=3000, should be in fourth segment → between key 3 and key 4
|
||||
auto pos3000 = spline.evaluatePosition(3000);
|
||||
REQUIRE(pos3000.x > 2.5f);
|
||||
REQUIRE(pos3000.x < 4.5f);
|
||||
}
|
||||
|
||||
// ── Two-point spline (minimum viable path) ──────────────────────────
|
||||
|
||||
TEST_CASE("CatmullRomSpline with two points", "[spline]") {
|
||||
std::vector<SplineKey> keys = {
|
||||
{0, glm::vec3(0.0f, 0.0f, 0.0f)},
|
||||
{1000, glm::vec3(10.0f, 0.0f, 0.0f)},
|
||||
};
|
||||
CatmullRomSpline spline(keys);
|
||||
REQUIRE(spline.durationMs() == 1000);
|
||||
|
||||
auto start = spline.evaluatePosition(0);
|
||||
REQUIRE(start.x == Catch::Approx(0.0f).margin(0.01f));
|
||||
|
||||
auto end = spline.evaluatePosition(1000);
|
||||
REQUIRE(end.x == Catch::Approx(10.0f).margin(0.01f));
|
||||
|
||||
// Midpoint should be near 5
|
||||
auto mid = spline.evaluatePosition(500);
|
||||
REQUIRE(mid.x == Catch::Approx(5.0f).margin(1.0f));
|
||||
}
|
||||
315
tests/test_transport_path_repo.cpp
Normal file
315
tests/test_transport_path_repo.cpp
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
// Tests for TransportPathRepository (Phase 3 of spline refactoring).
|
||||
// Verifies PathEntry wrapping, path operations, and CatmullRomSpline integration.
|
||||
#include <catch2/catch_amalgamated.hpp>
|
||||
#include "game/transport_path_repository.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "math/spline.hpp"
|
||||
#include <glm/glm.hpp>
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
|
||||
// ── Minimal stubs for pipeline symbols referenced by DBC loading functions ──
|
||||
// The test never calls loadTransportAnimationDBC / loadTaxiPathNodeDBC,
|
||||
// but the linker still needs these symbols from the compiled translation unit.
|
||||
namespace wowee::pipeline {
|
||||
AssetManager::AssetManager() {}
|
||||
AssetManager::~AssetManager() {}
|
||||
std::vector<uint8_t> AssetManager::readFile(const std::string&) const { return {}; }
|
||||
std::vector<uint8_t> AssetManager::readFileOptional(const std::string&) const { return {}; }
|
||||
} // namespace wowee::pipeline
|
||||
|
||||
using namespace wowee;
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────
|
||||
|
||||
static constexpr float kEps = 0.001f;
|
||||
|
||||
static void requireVec3Near(const glm::vec3& v, float x, float y, float z,
|
||||
float eps = kEps) {
|
||||
REQUIRE(std::abs(v.x - x) < eps);
|
||||
REQUIRE(std::abs(v.y - y) < eps);
|
||||
REQUIRE(std::abs(v.z - z) < eps);
|
||||
}
|
||||
|
||||
// ── PathEntry construction ─────────────────────────────────────
|
||||
|
||||
TEST_CASE("PathEntry wraps CatmullRomSpline with metadata", "[transport_path_repo]") {
|
||||
std::vector<math::SplineKey> keys = {
|
||||
{0, {0.0f, 0.0f, 0.0f}},
|
||||
{1000, {10.0f, 0.0f, 0.0f}},
|
||||
{2000, {20.0f, 0.0f, 0.0f}},
|
||||
};
|
||||
math::CatmullRomSpline spline(std::move(keys), false);
|
||||
game::PathEntry entry(std::move(spline), 42, false, true, false);
|
||||
|
||||
REQUIRE(entry.pathId == 42);
|
||||
REQUIRE(entry.fromDBC == true);
|
||||
REQUIRE(entry.zOnly == false);
|
||||
REQUIRE(entry.worldCoords == false);
|
||||
REQUIRE(entry.spline.keyCount() == 3);
|
||||
REQUIRE(entry.spline.durationMs() == 2000);
|
||||
}
|
||||
|
||||
TEST_CASE("PathEntry is move-constructible", "[transport_path_repo]") {
|
||||
std::vector<math::SplineKey> keys = {
|
||||
{0, {0.0f, 0.0f, 0.0f}},
|
||||
{500, {5.0f, 0.0f, 0.0f}},
|
||||
};
|
||||
math::CatmullRomSpline spline(std::move(keys), false);
|
||||
game::PathEntry entry(std::move(spline), 99, true, false, true);
|
||||
|
||||
game::PathEntry moved(std::move(entry));
|
||||
REQUIRE(moved.pathId == 99);
|
||||
REQUIRE(moved.zOnly == true);
|
||||
REQUIRE(moved.worldCoords == true);
|
||||
REQUIRE(moved.spline.keyCount() == 2);
|
||||
}
|
||||
|
||||
// ── Repository: loadPathFromNodes ──────────────────────────────
|
||||
|
||||
TEST_CASE("loadPathFromNodes stores a retrievable path", "[transport_path_repo]") {
|
||||
game::TransportPathRepository repo;
|
||||
std::vector<glm::vec3> waypoints = {
|
||||
{0.0f, 0.0f, 0.0f},
|
||||
{100.0f, 0.0f, 0.0f},
|
||||
{200.0f, 0.0f, 0.0f},
|
||||
};
|
||||
|
||||
repo.loadPathFromNodes(1001, waypoints, true, 10.0f);
|
||||
auto* entry = repo.findPath(1001);
|
||||
REQUIRE(entry != nullptr);
|
||||
REQUIRE(entry->pathId == 1001);
|
||||
REQUIRE(entry->fromDBC == false);
|
||||
REQUIRE(entry->zOnly == false);
|
||||
// 3 waypoints + 1 wrap point = 4 keys
|
||||
REQUIRE(entry->spline.keyCount() == 4);
|
||||
REQUIRE(entry->spline.durationMs() > 0);
|
||||
}
|
||||
|
||||
TEST_CASE("loadPathFromNodes single waypoint creates stationary path", "[transport_path_repo]") {
|
||||
game::TransportPathRepository repo;
|
||||
repo.loadPathFromNodes(2001, {{5.0f, 6.0f, 7.0f}}, false, 10.0f);
|
||||
|
||||
auto* entry = repo.findPath(2001);
|
||||
REQUIRE(entry != nullptr);
|
||||
REQUIRE(entry->spline.keyCount() == 1);
|
||||
// Stationary: duration is the last key's time (0ms for single key)
|
||||
requireVec3Near(entry->spline.evaluatePosition(0), 5.0f, 6.0f, 7.0f);
|
||||
}
|
||||
|
||||
TEST_CASE("loadPathFromNodes non-looping omits wrap point", "[transport_path_repo]") {
|
||||
game::TransportPathRepository repo;
|
||||
std::vector<glm::vec3> waypoints = {
|
||||
{0.0f, 0.0f, 0.0f},
|
||||
{50.0f, 0.0f, 0.0f},
|
||||
};
|
||||
|
||||
repo.loadPathFromNodes(3001, waypoints, false, 10.0f);
|
||||
auto* entry = repo.findPath(3001);
|
||||
REQUIRE(entry != nullptr);
|
||||
// Non-looping: 2 waypoints, no wrap point
|
||||
REQUIRE(entry->spline.keyCount() == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("loadPathFromNodes looping adds wrap point", "[transport_path_repo]") {
|
||||
game::TransportPathRepository repo;
|
||||
std::vector<glm::vec3> waypoints = {
|
||||
{0.0f, 0.0f, 0.0f},
|
||||
{50.0f, 0.0f, 0.0f},
|
||||
};
|
||||
|
||||
repo.loadPathFromNodes(3002, waypoints, true, 10.0f);
|
||||
auto* entry = repo.findPath(3002);
|
||||
REQUIRE(entry != nullptr);
|
||||
// Looping: 2 waypoints + 1 wrap point = 3 keys
|
||||
REQUIRE(entry->spline.keyCount() == 3);
|
||||
// Wrap point should match first waypoint
|
||||
const auto& keys = entry->spline.keys();
|
||||
requireVec3Near(keys.back().position, 0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
// ── Repository: findPath / storePath / hasPathForEntry ──────────
|
||||
|
||||
TEST_CASE("findPath returns nullptr for missing paths", "[transport_path_repo]") {
|
||||
game::TransportPathRepository repo;
|
||||
REQUIRE(repo.findPath(999) == nullptr);
|
||||
}
|
||||
|
||||
TEST_CASE("storePath overwrites existing paths", "[transport_path_repo]") {
|
||||
game::TransportPathRepository repo;
|
||||
|
||||
// Store first path
|
||||
std::vector<math::SplineKey> keys1 = {{0, {0, 0, 0}}, {1000, {10, 0, 0}}};
|
||||
math::CatmullRomSpline spline1(std::move(keys1), false);
|
||||
repo.storePath(500, game::PathEntry(std::move(spline1), 500, false, true, false));
|
||||
|
||||
auto* e1 = repo.findPath(500);
|
||||
REQUIRE(e1 != nullptr);
|
||||
REQUIRE(e1->spline.keyCount() == 2);
|
||||
|
||||
// Overwrite with different path
|
||||
std::vector<math::SplineKey> keys2 = {{0, {0, 0, 0}}, {500, {5, 0, 0}}, {1000, {10, 5, 0}}};
|
||||
math::CatmullRomSpline spline2(std::move(keys2), false);
|
||||
repo.storePath(500, game::PathEntry(std::move(spline2), 500, true, false, true));
|
||||
|
||||
auto* e2 = repo.findPath(500);
|
||||
REQUIRE(e2 != nullptr);
|
||||
REQUIRE(e2->spline.keyCount() == 3);
|
||||
REQUIRE(e2->zOnly == true);
|
||||
REQUIRE(e2->worldCoords == true);
|
||||
}
|
||||
|
||||
TEST_CASE("hasPathForEntry checks fromDBC flag", "[transport_path_repo]") {
|
||||
game::TransportPathRepository repo;
|
||||
|
||||
// Non-DBC path
|
||||
repo.loadPathFromNodes(700, {{0, 0, 0}, {10, 0, 0}}, true, 10.0f);
|
||||
REQUIRE(repo.hasPathForEntry(700) == false); // fromDBC=false
|
||||
|
||||
// DBC path (via storePath)
|
||||
std::vector<math::SplineKey> keys = {{0, {0, 0, 0}}, {1000, {10, 0, 0}}};
|
||||
math::CatmullRomSpline spline(std::move(keys), false);
|
||||
repo.storePath(701, game::PathEntry(std::move(spline), 701, false, true, false));
|
||||
REQUIRE(repo.hasPathForEntry(701) == true);
|
||||
}
|
||||
|
||||
// ── Repository: hasUsableMovingPathForEntry ─────────────────────
|
||||
|
||||
TEST_CASE("hasUsableMovingPathForEntry rejects stationary/z-only", "[transport_path_repo]") {
|
||||
game::TransportPathRepository repo;
|
||||
|
||||
// Single-point path (stationary)
|
||||
std::vector<math::SplineKey> keys1 = {{0, {0, 0, 0}}};
|
||||
math::CatmullRomSpline sp1(std::move(keys1), false);
|
||||
repo.storePath(800, game::PathEntry(std::move(sp1), 800, false, true, false));
|
||||
REQUIRE(repo.hasUsableMovingPathForEntry(800) == false);
|
||||
|
||||
// Z-only path (flagged)
|
||||
std::vector<math::SplineKey> keys2 = {{0, {0, 0, 0}}, {1000, {0, 0, 5}}};
|
||||
math::CatmullRomSpline sp2(std::move(keys2), false);
|
||||
repo.storePath(801, game::PathEntry(std::move(sp2), 801, true, true, false));
|
||||
REQUIRE(repo.hasUsableMovingPathForEntry(801) == false);
|
||||
|
||||
// Moving XY path
|
||||
std::vector<math::SplineKey> keys3 = {{0, {0, 0, 0}}, {1000, {100, 0, 0}}};
|
||||
math::CatmullRomSpline sp3(std::move(keys3), false);
|
||||
repo.storePath(802, game::PathEntry(std::move(sp3), 802, false, true, false));
|
||||
REQUIRE(repo.hasUsableMovingPathForEntry(802) == true);
|
||||
}
|
||||
|
||||
// ── Repository: inferDbcPathForSpawn ────────────────────────────
|
||||
|
||||
TEST_CASE("inferDbcPathForSpawn finds nearest DBC path", "[transport_path_repo]") {
|
||||
game::TransportPathRepository repo;
|
||||
|
||||
// Path A at (100, 0, 0)
|
||||
std::vector<math::SplineKey> keysA = {{0, {100, 0, 0}}, {1000, {200, 0, 0}}};
|
||||
math::CatmullRomSpline spA(std::move(keysA), false);
|
||||
repo.storePath(10, game::PathEntry(std::move(spA), 10, false, true, false));
|
||||
|
||||
// Path B at (500, 0, 0)
|
||||
std::vector<math::SplineKey> keysB = {{0, {500, 0, 0}}, {1000, {600, 0, 0}}};
|
||||
math::CatmullRomSpline spB(std::move(keysB), false);
|
||||
repo.storePath(20, game::PathEntry(std::move(spB), 20, false, true, false));
|
||||
|
||||
// Spawn near Path A
|
||||
uint32_t result = repo.inferDbcPathForSpawn({105.0f, 0.0f, 0.0f}, 200.0f, true);
|
||||
REQUIRE(result == 10);
|
||||
|
||||
// Spawn near Path B
|
||||
result = repo.inferDbcPathForSpawn({510.0f, 0.0f, 0.0f}, 200.0f, true);
|
||||
REQUIRE(result == 20);
|
||||
|
||||
// Spawn too far from both
|
||||
result = repo.inferDbcPathForSpawn({9999.0f, 0.0f, 0.0f}, 200.0f, true);
|
||||
REQUIRE(result == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("inferMovingPathForSpawn skips z-only paths", "[transport_path_repo]") {
|
||||
game::TransportPathRepository repo;
|
||||
|
||||
// Z-only path near spawn
|
||||
std::vector<math::SplineKey> keys = {{0, {10, 0, 0}}, {1000, {10, 0, 5}}};
|
||||
math::CatmullRomSpline sp(std::move(keys), false);
|
||||
repo.storePath(30, game::PathEntry(std::move(sp), 30, true, true, false));
|
||||
|
||||
// inferMovingPathForSpawn passes allowZOnly=false
|
||||
uint32_t result = repo.inferMovingPathForSpawn({10.0f, 0.0f, 0.0f}, 200.0f);
|
||||
REQUIRE(result == 0);
|
||||
}
|
||||
|
||||
// ── Repository: taxi paths ─────────────────────────────────────
|
||||
|
||||
TEST_CASE("hasTaxiPath and findTaxiPath", "[transport_path_repo]") {
|
||||
game::TransportPathRepository repo;
|
||||
REQUIRE(repo.hasTaxiPath(100) == false);
|
||||
REQUIRE(repo.findTaxiPath(100) == nullptr);
|
||||
|
||||
// loadTaxiPathNodeDBC would populate this, but we can't test it without AssetManager.
|
||||
// This just verifies the API works with empty data.
|
||||
}
|
||||
|
||||
// ── Spline evaluation through PathEntry (Phase 3 integration) ──
|
||||
|
||||
TEST_CASE("PathEntry spline evaluates position at midpoint", "[transport_path_repo]") {
|
||||
std::vector<math::SplineKey> keys = {
|
||||
{0, {0.0f, 0.0f, 0.0f}},
|
||||
{1000, {100.0f, 0.0f, 0.0f}},
|
||||
{2000, {200.0f, 0.0f, 0.0f}},
|
||||
};
|
||||
math::CatmullRomSpline spline(std::move(keys), false);
|
||||
game::PathEntry entry(std::move(spline), 1, false, true, false);
|
||||
|
||||
// At t=1000ms, should be near (100, 0, 0) — exactly at key 1
|
||||
glm::vec3 pos = entry.spline.evaluatePosition(1000);
|
||||
requireVec3Near(pos, 100.0f, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
TEST_CASE("PathEntry spline evaluates position at interpolated time", "[transport_path_repo]") {
|
||||
std::vector<math::SplineKey> keys = {
|
||||
{0, {0.0f, 0.0f, 0.0f}},
|
||||
{1000, {100.0f, 0.0f, 0.0f}},
|
||||
{2000, {200.0f, 0.0f, 0.0f}},
|
||||
};
|
||||
math::CatmullRomSpline spline(std::move(keys), false);
|
||||
game::PathEntry entry(std::move(spline), 1, false, true, false);
|
||||
|
||||
// At t=500ms, should be approximately (50, 0, 0)
|
||||
glm::vec3 pos = entry.spline.evaluatePosition(500);
|
||||
REQUIRE(pos.x > 40.0f);
|
||||
REQUIRE(pos.x < 60.0f);
|
||||
REQUIRE(std::abs(pos.y) < 1.0f);
|
||||
}
|
||||
|
||||
TEST_CASE("PathEntry spline evaluate returns tangent for orientation", "[transport_path_repo]") {
|
||||
std::vector<math::SplineKey> keys = {
|
||||
{0, {0.0f, 0.0f, 0.0f}},
|
||||
{1000, {100.0f, 0.0f, 0.0f}},
|
||||
{2000, {200.0f, 0.0f, 0.0f}},
|
||||
};
|
||||
math::CatmullRomSpline spline(std::move(keys), false);
|
||||
game::PathEntry entry(std::move(spline), 1, false, true, false);
|
||||
|
||||
// Tangent at midpoint should point roughly in +X direction
|
||||
auto result = entry.spline.evaluate(1000);
|
||||
REQUIRE(result.tangent.x > 0.0f);
|
||||
REQUIRE(std::abs(result.tangent.y) < 1.0f);
|
||||
}
|
||||
|
||||
TEST_CASE("PathEntry findNearestKey finds closest waypoint", "[transport_path_repo]") {
|
||||
std::vector<math::SplineKey> keys = {
|
||||
{0, {0.0f, 0.0f, 0.0f}},
|
||||
{1000, {100.0f, 0.0f, 0.0f}},
|
||||
{2000, {200.0f, 0.0f, 0.0f}},
|
||||
};
|
||||
math::CatmullRomSpline spline(std::move(keys), false);
|
||||
game::PathEntry entry(std::move(spline), 1, false, true, false);
|
||||
|
||||
// Point near key 1 (100, 0, 0)
|
||||
size_t nearest = entry.spline.findNearestKey({105.0f, 0.0f, 0.0f});
|
||||
REQUIRE(nearest == 1);
|
||||
|
||||
// Point near key 2 (200, 0, 0)
|
||||
nearest = entry.spline.findNearestKey({195.0f, 0.0f, 0.0f});
|
||||
REQUIRE(nearest == 2);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue