mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-07 01:23:52 +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
450
src/game/spline_packet.cpp
Normal file
450
src/game/spline_packet.cpp
Normal file
|
|
@ -0,0 +1,450 @@
|
|||
// src/game/spline_packet.cpp
|
||||
// Consolidated spline packet parsing — replaces 7 duplicated parsing locations.
|
||||
// Ported from: world_packets.cpp, world_packets_entity.cpp, packet_parsers_classic.cpp,
|
||||
// packet_parsers_tbc.cpp, movement_handler.cpp.
|
||||
#include "game/spline_packet.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <cmath>
|
||||
|
||||
namespace wowee::game {
|
||||
|
||||
// ── Packed-delta decoding ───────────────────────────────────────
|
||||
|
||||
glm::vec3 decodePackedDelta(uint32_t packed, const glm::vec3& midpoint) {
|
||||
// 11-bit signed X, 11-bit signed Y, 10-bit signed Z
|
||||
// Scaled by 0.25, subtracted from midpoint
|
||||
int32_t sx = static_cast<int32_t>(packed & 0x7FF);
|
||||
if (sx & 0x400) sx |= static_cast<int32_t>(0xFFFFF800); // sign-extend 11-bit
|
||||
|
||||
int32_t sy = static_cast<int32_t>((packed >> 11) & 0x7FF);
|
||||
if (sy & 0x400) sy |= static_cast<int32_t>(0xFFFFF800); // sign-extend 11-bit
|
||||
|
||||
int32_t sz = static_cast<int32_t>((packed >> 22) & 0x3FF);
|
||||
if (sz & 0x200) sz |= static_cast<int32_t>(0xFFFFFC00); // sign-extend 10-bit
|
||||
|
||||
return glm::vec3(
|
||||
midpoint.x - static_cast<float>(sx) * 0.25f,
|
||||
midpoint.y - static_cast<float>(sy) * 0.25f,
|
||||
midpoint.z - static_cast<float>(sz) * 0.25f
|
||||
);
|
||||
}
|
||||
|
||||
// ── MonsterMove spline body (post-splineFlags) ─────────────────
|
||||
|
||||
bool parseMonsterMoveSplineBody(
|
||||
network::Packet& packet,
|
||||
SplineBlockData& out,
|
||||
uint32_t splineFlags,
|
||||
const glm::vec3& startPos,
|
||||
bool useTbcUncompressedMask)
|
||||
{
|
||||
out.splineFlags = splineFlags;
|
||||
|
||||
// Animation (0x00400000): uint8 animType + uint32 animStartTime
|
||||
if (splineFlags & SplineFlag::ANIMATION) {
|
||||
if (!packet.hasRemaining(5)) return false;
|
||||
out.hasAnimation = true;
|
||||
out.animationType = packet.readUInt8();
|
||||
out.animationStartTime = packet.readUInt32();
|
||||
}
|
||||
|
||||
// Duration
|
||||
if (!packet.hasRemaining(4)) return false;
|
||||
out.duration = packet.readUInt32();
|
||||
|
||||
// Parabolic (0x00000800 in MonsterMove): float vertAccel + uint32 startTime
|
||||
if (splineFlags & SplineFlag::PARABOLIC_MM) {
|
||||
if (!packet.hasRemaining(8)) return false;
|
||||
out.hasParabolic = true;
|
||||
out.verticalAcceleration = packet.readFloat();
|
||||
out.parabolicStartTime = packet.readUInt32();
|
||||
}
|
||||
|
||||
// Point count
|
||||
if (!packet.hasRemaining(4)) return false;
|
||||
uint32_t pointCount = packet.readUInt32();
|
||||
if (pointCount == 0) return true;
|
||||
if (pointCount > 1000) return false;
|
||||
|
||||
// Determine compressed vs uncompressed
|
||||
uint32_t uncompMask = useTbcUncompressedMask
|
||||
? SplineFlag::UNCOMPRESSED_MASK_TBC
|
||||
: SplineFlag::UNCOMPRESSED_MASK;
|
||||
bool uncompressed = (splineFlags & uncompMask) != 0;
|
||||
|
||||
if (uncompressed) {
|
||||
// All waypoints as absolute float3, last one is destination
|
||||
for (uint32_t i = 0; i + 1 < pointCount; ++i) {
|
||||
if (!packet.hasRemaining(12)) return true; // Partial parse OK
|
||||
float wx = packet.readFloat();
|
||||
float wy = packet.readFloat();
|
||||
float wz = packet.readFloat();
|
||||
out.waypoints.push_back(glm::vec3(wx, wy, wz));
|
||||
}
|
||||
if (!packet.hasRemaining(12)) return true;
|
||||
out.destination.x = packet.readFloat();
|
||||
out.destination.y = packet.readFloat();
|
||||
out.destination.z = packet.readFloat();
|
||||
out.hasDest = true;
|
||||
} else {
|
||||
// Compressed: first float3 is destination, rest are packed deltas from midpoint
|
||||
if (!packet.hasRemaining(12)) return true;
|
||||
out.destination.x = packet.readFloat();
|
||||
out.destination.y = packet.readFloat();
|
||||
out.destination.z = packet.readFloat();
|
||||
out.hasDest = true;
|
||||
|
||||
if (pointCount > 1) {
|
||||
glm::vec3 mid = (startPos + out.destination) * 0.5f;
|
||||
for (uint32_t i = 0; i + 1 < pointCount; ++i) {
|
||||
if (!packet.hasRemaining(4)) break;
|
||||
uint32_t packed = packet.readUInt32();
|
||||
out.waypoints.push_back(decodePackedDelta(packed, mid));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Vanilla MonsterMove spline body (always compressed) ─────────
|
||||
|
||||
bool parseMonsterMoveSplineBodyVanilla(
|
||||
network::Packet& packet,
|
||||
SplineBlockData& out,
|
||||
uint32_t splineFlags,
|
||||
const glm::vec3& startPos)
|
||||
{
|
||||
out.splineFlags = splineFlags;
|
||||
|
||||
// Animation (0x00400000): uint8 animType + uint32 animStartTime
|
||||
if (splineFlags & SplineFlag::ANIMATION) {
|
||||
if (!packet.hasRemaining(5)) return false;
|
||||
out.hasAnimation = true;
|
||||
out.animationType = packet.readUInt8();
|
||||
out.animationStartTime = packet.readUInt32();
|
||||
}
|
||||
|
||||
// Duration
|
||||
if (!packet.hasRemaining(4)) return false;
|
||||
out.duration = packet.readUInt32();
|
||||
|
||||
// Parabolic (0x00000800)
|
||||
if (splineFlags & SplineFlag::PARABOLIC_MM) {
|
||||
if (!packet.hasRemaining(8)) return false;
|
||||
out.hasParabolic = true;
|
||||
out.verticalAcceleration = packet.readFloat();
|
||||
out.parabolicStartTime = packet.readUInt32();
|
||||
}
|
||||
|
||||
// Point count
|
||||
if (!packet.hasRemaining(4)) return false;
|
||||
uint32_t pointCount = packet.readUInt32();
|
||||
if (pointCount == 0) return true;
|
||||
if (pointCount > 1000) return false;
|
||||
|
||||
// Always compressed in Vanilla: dest (12 bytes) + packed deltas (4 bytes each)
|
||||
size_t requiredBytes = 12;
|
||||
if (pointCount > 1) requiredBytes += static_cast<size_t>(pointCount - 1) * 4ull;
|
||||
if (!packet.hasRemaining(requiredBytes)) return false;
|
||||
|
||||
out.destination.x = packet.readFloat();
|
||||
out.destination.y = packet.readFloat();
|
||||
out.destination.z = packet.readFloat();
|
||||
out.hasDest = true;
|
||||
|
||||
if (pointCount > 1) {
|
||||
glm::vec3 mid = (startPos + out.destination) * 0.5f;
|
||||
for (uint32_t i = 0; i + 1 < pointCount; ++i) {
|
||||
uint32_t packed = packet.readUInt32();
|
||||
out.waypoints.push_back(decodePackedDelta(packed, mid));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Classic/Turtle movement update spline block ─────────────────
|
||||
|
||||
bool parseClassicMoveUpdateSpline(
|
||||
network::Packet& packet,
|
||||
SplineBlockData& out)
|
||||
{
|
||||
// splineFlags
|
||||
if (!packet.hasRemaining(4)) return false;
|
||||
out.splineFlags = packet.readUInt32();
|
||||
LOG_DEBUG(" [Classic] Spline: flags=0x", std::hex, out.splineFlags, std::dec);
|
||||
|
||||
// FINAL_POINT / FINAL_TARGET / FINAL_ANGLE
|
||||
if (out.splineFlags & SplineFlag::FINAL_POINT) {
|
||||
if (!packet.hasRemaining(12)) return false;
|
||||
out.hasFinalPoint = true;
|
||||
out.finalPoint.x = packet.readFloat();
|
||||
out.finalPoint.y = packet.readFloat();
|
||||
out.finalPoint.z = packet.readFloat();
|
||||
} else if (out.splineFlags & SplineFlag::FINAL_TARGET) {
|
||||
if (!packet.hasRemaining(8)) return false;
|
||||
out.hasFinalTarget = true;
|
||||
out.finalTarget = packet.readUInt64();
|
||||
} else if (out.splineFlags & SplineFlag::FINAL_ANGLE) {
|
||||
if (!packet.hasRemaining(4)) return false;
|
||||
out.hasFinalAngle = true;
|
||||
out.finalAngle = packet.readFloat();
|
||||
}
|
||||
|
||||
// timePassed + duration + splineId + pointCount = 16 bytes
|
||||
if (!packet.hasRemaining(16)) return false;
|
||||
out.timePassed = packet.readUInt32();
|
||||
out.duration = packet.readUInt32();
|
||||
out.splineId = packet.readUInt32();
|
||||
|
||||
uint32_t pointCount = packet.readUInt32();
|
||||
if (pointCount > 256) return false;
|
||||
|
||||
// All points uncompressed (12 bytes each) + endPoint (12 bytes)
|
||||
// Classic: NO splineMode byte
|
||||
if (!packet.hasRemaining(static_cast<size_t>(pointCount) * 12 + 12)) return false;
|
||||
for (uint32_t i = 0; i < pointCount; ++i) {
|
||||
float px = packet.readFloat();
|
||||
float py = packet.readFloat();
|
||||
float pz = packet.readFloat();
|
||||
out.waypoints.push_back(glm::vec3(px, py, pz));
|
||||
}
|
||||
|
||||
out.endPoint.x = packet.readFloat();
|
||||
out.endPoint.y = packet.readFloat();
|
||||
out.endPoint.z = packet.readFloat();
|
||||
out.hasEndPoint = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── WotLK movement update spline block ──────────────────────────
|
||||
// Complex multi-try parser for different server variations.
|
||||
|
||||
bool parseWotlkMoveUpdateSpline(
|
||||
network::Packet& packet,
|
||||
SplineBlockData& out,
|
||||
const glm::vec3& entityPos)
|
||||
{
|
||||
auto bytesAvailable = [&](size_t n) -> bool { return packet.hasRemaining(n); };
|
||||
|
||||
// splineFlags
|
||||
if (!bytesAvailable(4)) return false;
|
||||
out.splineFlags = packet.readUInt32();
|
||||
LOG_DEBUG(" Spline: flags=0x", std::hex, out.splineFlags, std::dec);
|
||||
|
||||
// FINAL_POINT / FINAL_TARGET / FINAL_ANGLE
|
||||
if (out.splineFlags & SplineFlag::FINAL_POINT) {
|
||||
if (!bytesAvailable(12)) return false;
|
||||
out.hasFinalPoint = true;
|
||||
out.finalPoint.x = packet.readFloat();
|
||||
out.finalPoint.y = packet.readFloat();
|
||||
out.finalPoint.z = packet.readFloat();
|
||||
} else if (out.splineFlags & SplineFlag::FINAL_TARGET) {
|
||||
if (!bytesAvailable(8)) return false;
|
||||
out.hasFinalTarget = true;
|
||||
out.finalTarget = packet.readUInt64();
|
||||
} else if (out.splineFlags & SplineFlag::FINAL_ANGLE) {
|
||||
if (!bytesAvailable(4)) return false;
|
||||
out.hasFinalAngle = true;
|
||||
out.finalAngle = packet.readFloat();
|
||||
}
|
||||
|
||||
// timePassed + duration + splineId
|
||||
if (!bytesAvailable(12)) return false;
|
||||
out.timePassed = packet.readUInt32();
|
||||
out.duration = packet.readUInt32();
|
||||
out.splineId = packet.readUInt32();
|
||||
|
||||
// ── Helper: try to 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;
|
||||
// Zero-point splines (e.g. FINAL_TARGET "follow" splines) have no
|
||||
// splineMode or endPoint written — return immediately.
|
||||
if (pc == 0) {
|
||||
LOG_DEBUG(" Spline pointCount=0 (", tag, ")");
|
||||
return true;
|
||||
}
|
||||
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 mode = packet.readUInt8();
|
||||
if (mode > 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;
|
||||
}
|
||||
// Proximity check: if entity position is known, reject endpoints that
|
||||
// are implausibly far from it (catches misinterpreted compressed data).
|
||||
if (entityPos.x != 0.0f || entityPos.y != 0.0f || entityPos.z != 0.0f) {
|
||||
float dx = epX - entityPos.x;
|
||||
float dy = epY - entityPos.y;
|
||||
float dz = epZ - entityPos.z;
|
||||
float distSq = dx * dx + dy * dy + dz * dz;
|
||||
if (distSq > 5000.0f * 5000.0f) {
|
||||
packet.setReadPos(prePointCount);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
out.splineMode = mode;
|
||||
out.endPoint = glm::vec3(epX, epY, epZ);
|
||||
out.hasEndPoint = true;
|
||||
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();
|
||||
|
||||
// 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 (out.splineFlags & SplineFlag::ANIMATION) {
|
||||
if (!bytesAvailable(5)) { wotlkOk = false; }
|
||||
else {
|
||||
out.hasAnimation = true;
|
||||
out.animationType = packet.readUInt8();
|
||||
out.animationStartTime = 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 = (out.splineFlags & SplineFlag::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
|
||||
if (!splineParsed && (out.splineFlags & SplineFlag::ANIMATION)) {
|
||||
packet.setReadPos(beforeSplineHeader);
|
||||
out.hasAnimation = false; // Reset from failed try
|
||||
if (bytesAvailable(8)) {
|
||||
packet.readFloat(); // durationMod
|
||||
packet.readFloat(); // durationModNext
|
||||
bool ok = true;
|
||||
if (!bytesAvailable(5)) { ok = false; }
|
||||
else {
|
||||
out.hasAnimation = true;
|
||||
out.animationType = packet.readUInt8();
|
||||
out.animationStartTime = packet.readUInt32();
|
||||
}
|
||||
if (ok && (out.splineFlags & SplineFlag::PARABOLIC_MU)) {
|
||||
if (!bytesAvailable(8)) { ok = false; }
|
||||
else { packet.readFloat(); packet.readUInt32(); }
|
||||
}
|
||||
if (ok) {
|
||||
bool useCompressed = (out.splineFlags & SplineFlag::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);
|
||||
out.hasAnimation = false;
|
||||
if (bytesAvailable(8)) {
|
||||
packet.readFloat(); // durationMod
|
||||
packet.readFloat(); // durationModNext
|
||||
bool ok = true;
|
||||
if (out.splineFlags & SplineFlag::PARABOLIC_MU) {
|
||||
if (!bytesAvailable(8)) { ok = false; }
|
||||
else { packet.readFloat(); packet.readUInt32(); }
|
||||
}
|
||||
if (ok) {
|
||||
bool useCompressed = (out.splineFlags & SplineFlag::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 = (out.splineFlags & SplineFlag::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 = (out.splineFlags & SplineFlag::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"
|
||||
" splineFlags=0x", std::hex, out.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);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace wowee::game
|
||||
Loading…
Add table
Add a link
Reference in a new issue