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:
Pavel Okhlopkov 2026-04-11 08:30:28 +03:00
parent 535cc20afe
commit de0383aa6b
32 changed files with 2198 additions and 1293 deletions

View file

@ -1,5 +1,6 @@
#include "game/world_packets.hpp"
#include "game/packet_parsers.hpp"
#include "game/spline_packet.hpp"
#include "game/opcodes.hpp"
#include "game/character.hpp"
#include "auth/crypto.hpp"
@ -959,196 +960,10 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
// Spline data
if (moveFlags & 0x08000000) { // MOVEMENTFLAG_SPLINE_ENABLED
auto bytesAvailable = [&](size_t n) -> bool { return packet.hasRemaining(n); };
if (!bytesAvailable(4)) return false;
uint32_t splineFlags = packet.readUInt32();
LOG_DEBUG(" Spline: flags=0x", std::hex, splineFlags, std::dec);
if (splineFlags & 0x00010000) { // SPLINEFLAG_FINAL_POINT
if (!bytesAvailable(12)) return false;
/*float finalX =*/ packet.readFloat();
/*float finalY =*/ packet.readFloat();
/*float finalZ =*/ packet.readFloat();
} else if (splineFlags & 0x00020000) { // SPLINEFLAG_FINAL_TARGET
if (!bytesAvailable(8)) return false;
/*uint64_t finalTarget =*/ packet.readUInt64();
} else if (splineFlags & 0x00040000) { // SPLINEFLAG_FINAL_ANGLE
if (!bytesAvailable(4)) return false;
/*float finalAngle =*/ packet.readFloat();
}
// WotLK spline data layout:
// timePassed(4)+duration(4)+splineId(4)+durationMod(4)+durationModNext(4)
// +[ANIMATION(5)]+verticalAccel(4)+effectStartTime(4)+pointCount(4)+points+mode(1)+endPoint(12)
if (!bytesAvailable(12)) return false;
/*uint32_t timePassed =*/ packet.readUInt32();
/*uint32_t duration =*/ packet.readUInt32();
/*uint32_t splineId =*/ packet.readUInt32();
// Helper: parse spline points + splineMode + endPoint.
// WotLK uses compressed points by default (first=12 bytes, rest=4 bytes packed).
auto tryParseSplinePoints = [&](bool compressed, const char* tag) -> bool {
if (!bytesAvailable(4)) return false;
size_t prePointCount = packet.getReadPos();
uint32_t pc = packet.readUInt32();
if (pc > 256) return false;
size_t pointsBytes;
if (compressed && pc > 0) {
// First point = 3 floats (12 bytes), rest = packed uint32 (4 bytes each)
pointsBytes = 12ull + (pc > 1 ? static_cast<size_t>(pc - 1) * 4ull : 0ull);
} else {
// All uncompressed: 3 floats each
pointsBytes = static_cast<size_t>(pc) * 12ull;
}
size_t needed = pointsBytes + 13ull; // + splineMode(1) + endPoint(12)
if (!bytesAvailable(needed)) {
packet.setReadPos(prePointCount);
return false;
}
packet.setReadPos(packet.getReadPos() + pointsBytes);
uint8_t splineMode = packet.readUInt8();
if (splineMode > 3) {
packet.setReadPos(prePointCount);
return false;
}
float epX = packet.readFloat();
float epY = packet.readFloat();
float epZ = packet.readFloat();
// Validate endPoint: garbage bytes rarely produce finite world coords
if (!std::isfinite(epX) || !std::isfinite(epY) || !std::isfinite(epZ) ||
std::fabs(epX) > 65000.0f || std::fabs(epY) > 65000.0f ||
std::fabs(epZ) > 65000.0f) {
packet.setReadPos(prePointCount);
return false;
}
LOG_DEBUG(" Spline pointCount=", pc, " compressed=", compressed,
" endPt=(", epX, ",", epY, ",", epZ, ") (", tag, ")");
return true;
};
// Save position before WotLK spline header for fallback
size_t beforeSplineHeader = packet.getReadPos();
// AzerothCore MoveSplineFlag constants:
// CATMULLROM = 0x00080000 — uncompressed Catmull-Rom interpolation
// CYCLIC = 0x00100000 — cyclic path
// ENTER_CYCLE = 0x00200000 — entering cyclic path
// ANIMATION = 0x00400000 — animation spline with animType+effectStart
// PARABOLIC = 0x00000008 — vertical_acceleration+effectStartTime
constexpr uint32_t SF_PARABOLIC = 0x00000008;
constexpr uint32_t SF_CATMULLROM = 0x00080000;
constexpr uint32_t SF_CYCLIC = 0x00100000;
constexpr uint32_t SF_ENTER_CYCLE = 0x00200000;
constexpr uint32_t SF_ANIMATION = 0x00400000;
constexpr uint32_t SF_UNCOMPRESSED_MASK = SF_CATMULLROM | SF_CYCLIC | SF_ENTER_CYCLE;
// Try 1: WotLK format (durationMod+durationModNext+[ANIMATION]+vertAccel+effectStart+points)
// Some servers (ChromieCraft) always write vertAccel+effectStart unconditionally.
bool splineParsed = false;
if (bytesAvailable(8)) {
/*float durationMod =*/ packet.readFloat();
/*float durationModNext =*/ packet.readFloat();
bool wotlkOk = true;
if (splineFlags & SF_ANIMATION) {
if (!bytesAvailable(5)) { wotlkOk = false; }
else { packet.readUInt8(); packet.readUInt32(); }
}
// Unconditional vertAccel+effectStart (ChromieCraft/some AzerothCore builds)
if (wotlkOk) {
if (!bytesAvailable(8)) { wotlkOk = false; }
else { /*float vertAccel =*/ packet.readFloat(); /*uint32_t effectStart =*/ packet.readUInt32(); }
}
if (wotlkOk) {
bool useCompressed = (splineFlags & SF_UNCOMPRESSED_MASK) == 0;
splineParsed = tryParseSplinePoints(useCompressed, "wotlk-compressed");
if (!splineParsed) {
splineParsed = tryParseSplinePoints(false, "wotlk-uncompressed");
}
}
}
// Try 2: ANIMATION present but vertAccel+effectStart gated by PARABOLIC
// (standard AzerothCore: only writes vertAccel+effectStart when PARABOLIC is set)
if (!splineParsed && (splineFlags & SF_ANIMATION)) {
packet.setReadPos(beforeSplineHeader);
if (bytesAvailable(8)) {
packet.readFloat(); // durationMod
packet.readFloat(); // durationModNext
bool ok = true;
if (!bytesAvailable(5)) { ok = false; }
else { packet.readUInt8(); packet.readUInt32(); } // animType + effectStart
if (ok && (splineFlags & SF_PARABOLIC)) {
if (!bytesAvailable(8)) { ok = false; }
else { packet.readFloat(); packet.readUInt32(); }
}
if (ok) {
bool useCompressed = (splineFlags & SF_UNCOMPRESSED_MASK) == 0;
splineParsed = tryParseSplinePoints(useCompressed, "wotlk-anim-conditional");
if (!splineParsed) {
splineParsed = tryParseSplinePoints(false, "wotlk-anim-conditional-uncomp");
}
}
}
}
// Try 3: No ANIMATION — vertAccel+effectStart only when PARABOLIC set
if (!splineParsed) {
packet.setReadPos(beforeSplineHeader);
if (bytesAvailable(8)) {
packet.readFloat(); // durationMod
packet.readFloat(); // durationModNext
bool ok = true;
if (splineFlags & SF_PARABOLIC) {
if (!bytesAvailable(8)) { ok = false; }
else { packet.readFloat(); packet.readUInt32(); }
}
if (ok) {
bool useCompressed = (splineFlags & SF_UNCOMPRESSED_MASK) == 0;
splineParsed = tryParseSplinePoints(useCompressed, "wotlk-parabolic-gated");
if (!splineParsed) {
splineParsed = tryParseSplinePoints(false, "wotlk-parabolic-gated-uncomp");
}
}
}
}
// Try 4: No header at all — just durationMod+durationModNext then points
if (!splineParsed) {
packet.setReadPos(beforeSplineHeader);
if (bytesAvailable(8)) {
packet.readFloat(); // durationMod
packet.readFloat(); // durationModNext
splineParsed = tryParseSplinePoints(false, "wotlk-no-parabolic");
if (!splineParsed) {
bool useComp = (splineFlags & SF_UNCOMPRESSED_MASK) == 0;
splineParsed = tryParseSplinePoints(useComp, "wotlk-no-parabolic-compressed");
}
}
}
// Try 5: bare points (no WotLK header at all — some spline types skip everything)
if (!splineParsed) {
packet.setReadPos(beforeSplineHeader);
splineParsed = tryParseSplinePoints(false, "bare-uncompressed");
if (!splineParsed) {
packet.setReadPos(beforeSplineHeader);
bool useComp = (splineFlags & SF_UNCOMPRESSED_MASK) == 0;
splineParsed = tryParseSplinePoints(useComp, "bare-compressed");
}
}
if (!splineParsed) {
// Dump first 5 uint32s at beforeSplineHeader for format diagnosis
packet.setReadPos(beforeSplineHeader);
uint32_t d[5] = {};
for (int di = 0; di < 5 && packet.hasRemaining(4); ++di)
d[di] = packet.readUInt32();
packet.setReadPos(beforeSplineHeader);
LOG_WARNING("WotLK spline parse failed for guid=0x", std::hex, block.guid, std::dec,
" splineFlags=0x", std::hex, splineFlags, std::dec,
" remaining=", packet.getRemainingSize(),
" header=[0x", std::hex, d[0], " 0x", d[1], " 0x", d[2],
" 0x", d[3], " 0x", d[4], "]", std::dec);
SplineBlockData splineData;
glm::vec3 entityPos(block.x, block.y, block.z);
if (!parseWotlkMoveUpdateSpline(packet, splineData, entityPos)) {
LOG_WARNING("WotLK spline parse failed for guid=0x", std::hex, block.guid, std::dec);
return false;
}
}