mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-04 16: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
|
|
@ -2,6 +2,7 @@
|
|||
#include "game/game_handler.hpp"
|
||||
#include "game/game_utils.hpp"
|
||||
#include "game/packet_parsers.hpp"
|
||||
#include "game/spline_packet.hpp"
|
||||
#include "game/transport_manager.hpp"
|
||||
#include "game/entity.hpp"
|
||||
#include "network/world_socket.hpp"
|
||||
|
|
@ -1572,52 +1573,17 @@ void MovementHandler::handleMonsterMoveTransport(network::Packet& packet) {
|
|||
if (packet.getReadPos() + 4 > packet.getSize()) return;
|
||||
uint32_t splineFlags = packet.readUInt32();
|
||||
|
||||
if (splineFlags & 0x00400000) {
|
||||
if (packet.getReadPos() + 5 > packet.getSize()) return;
|
||||
packet.readUInt8(); packet.readUInt32();
|
||||
}
|
||||
|
||||
if (packet.getReadPos() + 4 > packet.getSize()) return;
|
||||
uint32_t duration = packet.readUInt32();
|
||||
|
||||
if (splineFlags & 0x00000800) {
|
||||
if (packet.getReadPos() + 8 > packet.getSize()) return;
|
||||
packet.readFloat(); packet.readUInt32();
|
||||
}
|
||||
|
||||
if (packet.getReadPos() + 4 > packet.getSize()) return;
|
||||
uint32_t pointCount = packet.readUInt32();
|
||||
constexpr uint32_t kMaxTransportSplinePoints = 1000;
|
||||
if (pointCount > kMaxTransportSplinePoints) {
|
||||
LOG_WARNING("SMSG_MONSTER_MOVE_TRANSPORT: pointCount=", pointCount,
|
||||
" clamped to ", kMaxTransportSplinePoints);
|
||||
pointCount = kMaxTransportSplinePoints;
|
||||
}
|
||||
|
||||
float destLocalX = localX, destLocalY = localY, destLocalZ = localZ;
|
||||
bool hasDest = false;
|
||||
if (pointCount > 0) {
|
||||
const bool uncompressed = (splineFlags & (0x00080000 | 0x00002000)) != 0;
|
||||
if (uncompressed) {
|
||||
for (uint32_t i = 0; i < pointCount - 1; ++i) {
|
||||
if (packet.getReadPos() + 12 > packet.getSize()) break;
|
||||
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
||||
}
|
||||
if (packet.getReadPos() + 12 <= packet.getSize()) {
|
||||
destLocalX = packet.readFloat();
|
||||
destLocalY = packet.readFloat();
|
||||
destLocalZ = packet.readFloat();
|
||||
hasDest = true;
|
||||
}
|
||||
} else {
|
||||
if (packet.getReadPos() + 12 <= packet.getSize()) {
|
||||
destLocalX = packet.readFloat();
|
||||
destLocalY = packet.readFloat();
|
||||
destLocalZ = packet.readFloat();
|
||||
hasDest = true;
|
||||
}
|
||||
}
|
||||
// Consolidated spline body parser
|
||||
SplineBlockData spline;
|
||||
if (!parseMonsterMoveSplineBody(packet, spline, splineFlags,
|
||||
glm::vec3(localX, localY, localZ))) {
|
||||
return;
|
||||
}
|
||||
uint32_t duration = spline.duration;
|
||||
float destLocalX = spline.hasDest ? spline.destination.x : localX;
|
||||
float destLocalY = spline.hasDest ? spline.destination.y : localY;
|
||||
float destLocalZ = spline.hasDest ? spline.destination.z : localZ;
|
||||
bool hasDest = spline.hasDest;
|
||||
|
||||
if (!owner_.getTransportManager()) {
|
||||
LOG_WARNING("SMSG_MONSTER_MOVE_TRANSPORT: TransportManager not available for mover 0x",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "game/packet_parsers.hpp"
|
||||
#include "game/spline_packet.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include <cstdio>
|
||||
#include <functional>
|
||||
|
|
@ -258,45 +259,8 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo
|
|||
|
||||
// Spline data (Classic: SPLINE_ENABLED=0x00400000)
|
||||
if (moveFlags & ClassicMoveFlags::SPLINE_ENABLED) {
|
||||
if (rem() < 4) return false;
|
||||
uint32_t splineFlags = packet.readUInt32();
|
||||
LOG_DEBUG(" [Classic] Spline: flags=0x", std::hex, splineFlags, std::dec);
|
||||
|
||||
if (splineFlags & 0x00010000) { // FINAL_POINT
|
||||
if (rem() < 12) return false;
|
||||
/*float finalX =*/ packet.readFloat();
|
||||
/*float finalY =*/ packet.readFloat();
|
||||
/*float finalZ =*/ packet.readFloat();
|
||||
} else if (splineFlags & 0x00020000) { // FINAL_TARGET
|
||||
if (rem() < 8) return false;
|
||||
/*uint64_t finalTarget =*/ packet.readUInt64();
|
||||
} else if (splineFlags & 0x00040000) { // FINAL_ANGLE
|
||||
if (rem() < 4) return false;
|
||||
/*float finalAngle =*/ packet.readFloat();
|
||||
}
|
||||
|
||||
// Classic spline: timePassed, duration, id, pointCount
|
||||
if (rem() < 16) return false;
|
||||
/*uint32_t timePassed =*/ packet.readUInt32();
|
||||
/*uint32_t duration =*/ packet.readUInt32();
|
||||
/*uint32_t splineId =*/ packet.readUInt32();
|
||||
|
||||
uint32_t pointCount = packet.readUInt32();
|
||||
// Cap waypoints to prevent DoS from malformed packets allocating huge arrays
|
||||
if (pointCount > 256) return false;
|
||||
|
||||
// points + endPoint (no splineMode in Classic)
|
||||
if (rem() < 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();
|
||||
}
|
||||
|
||||
// Classic: NO splineMode byte
|
||||
/*float endPointX =*/ packet.readFloat();
|
||||
/*float endPointY =*/ packet.readFloat();
|
||||
/*float endPointZ =*/ packet.readFloat();
|
||||
SplineBlockData splineData;
|
||||
if (!parseClassicMoveUpdateSpline(packet, splineData)) return false;
|
||||
}
|
||||
}
|
||||
else if (updateFlags & UPDATEFLAG_HAS_POSITION) {
|
||||
|
|
@ -2004,45 +1968,8 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc
|
|||
bool hasSpline = (moveFlags & TurtleMoveFlags::SPLINE_CLASSIC) ||
|
||||
(moveFlags & TurtleMoveFlags::SPLINE_TBC);
|
||||
if (hasSpline) {
|
||||
if (rem() < 4) return false;
|
||||
uint32_t splineFlags = packet.readUInt32();
|
||||
LOG_DEBUG(" [Turtle] Spline: flags=0x", std::hex, splineFlags, std::dec);
|
||||
|
||||
if (splineFlags & 0x00010000) {
|
||||
if (rem() < 12) return false;
|
||||
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
||||
} else if (splineFlags & 0x00020000) {
|
||||
if (rem() < 8) return false;
|
||||
packet.readUInt64();
|
||||
} else if (splineFlags & 0x00040000) {
|
||||
if (rem() < 4) return false;
|
||||
packet.readFloat();
|
||||
}
|
||||
|
||||
// timePassed + duration + splineId + pointCount = 16 bytes
|
||||
if (rem() < 16) return false;
|
||||
/*uint32_t timePassed =*/ packet.readUInt32();
|
||||
/*uint32_t duration =*/ packet.readUInt32();
|
||||
/*uint32_t splineId =*/ packet.readUInt32();
|
||||
|
||||
uint32_t pointCount = packet.readUInt32();
|
||||
if (pointCount > 256) {
|
||||
static uint32_t badTurtleSplineCount = 0;
|
||||
++badTurtleSplineCount;
|
||||
if (badTurtleSplineCount <= 5 || (badTurtleSplineCount % 100) == 0) {
|
||||
LOG_WARNING(" [Turtle] Spline pointCount=", pointCount,
|
||||
" exceeds max (occurrence=", badTurtleSplineCount, ")");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// points + endPoint
|
||||
if (rem() < static_cast<size_t>(pointCount) * 12 + 12) return false;
|
||||
for (uint32_t i = 0; i < pointCount; i++) {
|
||||
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
||||
}
|
||||
|
||||
// End point
|
||||
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
||||
SplineBlockData splineData;
|
||||
if (!parseClassicMoveUpdateSpline(packet, splineData)) return false;
|
||||
}
|
||||
|
||||
LOG_DEBUG(" [Turtle] LIVING block consumed ", packet.getReadPos() - livingStart,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "game/packet_parsers.hpp"
|
||||
#include "game/spline_packet.hpp"
|
||||
#include "core/logger.hpp"
|
||||
|
||||
namespace wowee {
|
||||
|
|
@ -670,46 +671,20 @@ bool TbcPacketParsers::parseMonsterMove(network::Packet& packet, MonsterMoveData
|
|||
if (!packet.hasRemaining(4)) return false;
|
||||
data.splineFlags = packet.readUInt32();
|
||||
|
||||
// TBC 2.4.3 SplineFlags animation bit is same as WotLK: 0x00400000
|
||||
if (data.splineFlags & 0x00400000) {
|
||||
if (!packet.hasRemaining(5)) return false;
|
||||
packet.readUInt8(); // animationType
|
||||
packet.readUInt32(); // effectStartTime
|
||||
}
|
||||
|
||||
if (!packet.hasRemaining(4)) return false;
|
||||
data.duration = packet.readUInt32();
|
||||
|
||||
if (data.splineFlags & 0x00000800) {
|
||||
if (!packet.hasRemaining(8)) return false;
|
||||
packet.readFloat(); // verticalAcceleration
|
||||
packet.readUInt32(); // effectStartTime
|
||||
}
|
||||
|
||||
if (!packet.hasRemaining(4)) return false;
|
||||
uint32_t pointCount = packet.readUInt32();
|
||||
if (pointCount == 0) return true;
|
||||
if (pointCount > 16384) return false;
|
||||
|
||||
// Spline points are stored uncompressed when Catmull-Rom interpolation (0x80000)
|
||||
// or linear movement (0x2000) flags are set; otherwise they use packed delta format
|
||||
bool uncompressed = (data.splineFlags & (0x00080000 | 0x00002000)) != 0;
|
||||
if (uncompressed) {
|
||||
for (uint32_t i = 0; i < pointCount - 1; i++) {
|
||||
if (!packet.hasRemaining(12)) return true;
|
||||
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
||||
// Consolidated spline body parser (TBC uses different uncompressed mask)
|
||||
{
|
||||
SplineBlockData spline;
|
||||
if (!parseMonsterMoveSplineBody(packet, spline, data.splineFlags,
|
||||
glm::vec3(data.x, data.y, data.z), true)) {
|
||||
return false;
|
||||
}
|
||||
data.duration = spline.duration;
|
||||
if (spline.hasDest) {
|
||||
data.destX = spline.destination.x;
|
||||
data.destY = spline.destination.y;
|
||||
data.destZ = spline.destination.z;
|
||||
data.hasDest = true;
|
||||
}
|
||||
if (!packet.hasRemaining(12)) return true;
|
||||
data.destX = packet.readFloat();
|
||||
data.destY = packet.readFloat();
|
||||
data.destZ = packet.readFloat();
|
||||
data.hasDest = true;
|
||||
} else {
|
||||
if (!packet.hasRemaining(12)) return true;
|
||||
data.destX = packet.readFloat();
|
||||
data.destY = packet.readFloat();
|
||||
data.destZ = packet.readFloat();
|
||||
data.hasDest = true;
|
||||
}
|
||||
|
||||
LOG_DEBUG("[TBC] MonsterMove: guid=0x", std::hex, data.guid, std::dec,
|
||||
|
|
|
|||
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
|
||||
File diff suppressed because it is too large
Load diff
514
src/game/transport_path_repository.cpp
Normal file
514
src/game/transport_path_repository.cpp
Normal file
|
|
@ -0,0 +1,514 @@
|
|||
// src/game/transport_path_repository.cpp
|
||||
// Owns and manages transport path data — DBC, taxi, and custom paths.
|
||||
// Ported from TransportManager (path management subset).
|
||||
#include "game/transport_path_repository.hpp"
|
||||
#include "core/coordinates.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <cmath>
|
||||
|
||||
namespace wowee::game {
|
||||
|
||||
// ── Simple lookup methods ──────────────────────────────────────
|
||||
|
||||
const PathEntry* TransportPathRepository::findPath(uint32_t pathId) const {
|
||||
auto it = paths_.find(pathId);
|
||||
return it != paths_.end() ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
const PathEntry* TransportPathRepository::findTaxiPath(uint32_t taxiPathId) const {
|
||||
auto it = taxiPaths_.find(taxiPathId);
|
||||
return it != taxiPaths_.end() ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
bool TransportPathRepository::hasPathForEntry(uint32_t entry) const {
|
||||
auto* e = findPath(entry);
|
||||
return e != nullptr && e->fromDBC;
|
||||
}
|
||||
|
||||
bool TransportPathRepository::hasTaxiPath(uint32_t taxiPathId) const {
|
||||
return taxiPaths_.find(taxiPathId) != taxiPaths_.end();
|
||||
}
|
||||
|
||||
void TransportPathRepository::storePath(uint32_t pathId, PathEntry entry) {
|
||||
auto it = paths_.find(pathId);
|
||||
if (it != paths_.end()) {
|
||||
it->second = std::move(entry);
|
||||
} else {
|
||||
paths_.emplace(pathId, std::move(entry));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Query methods ──────────────────────────────────────────────
|
||||
|
||||
bool TransportPathRepository::hasUsableMovingPathForEntry(uint32_t entry, float minXYRange) const {
|
||||
auto* e = findPath(entry);
|
||||
if (!e) return false;
|
||||
if (!e->fromDBC || e->spline.keyCount() < 2 || e->spline.durationMs() == 0 || e->zOnly) {
|
||||
return false;
|
||||
}
|
||||
return e->spline.hasXYMovement(minXYRange);
|
||||
}
|
||||
|
||||
uint32_t TransportPathRepository::inferDbcPathForSpawn(const glm::vec3& spawnWorldPos,
|
||||
float maxDistance,
|
||||
bool allowZOnly) const {
|
||||
float bestD2 = maxDistance * maxDistance;
|
||||
uint32_t bestPathId = 0;
|
||||
|
||||
for (const auto& [pathId, entry] : paths_) {
|
||||
if (!entry.fromDBC || entry.spline.durationMs() == 0 || entry.spline.keyCount() == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!allowZOnly && entry.zOnly) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find nearest waypoint on this path to spawn
|
||||
size_t nearIdx = entry.spline.findNearestKey(spawnWorldPos);
|
||||
glm::vec3 diff = entry.spline.keys()[nearIdx].position - spawnWorldPos;
|
||||
float d2 = glm::dot(diff, diff);
|
||||
if (d2 < bestD2) {
|
||||
bestD2 = d2;
|
||||
bestPathId = pathId;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestPathId != 0) {
|
||||
LOG_INFO("TransportPathRepository: Inferred DBC path ", bestPathId,
|
||||
" (allowZOnly=", allowZOnly ? "yes" : "no",
|
||||
") for spawn at (", spawnWorldPos.x, ", ", spawnWorldPos.y, ", ", spawnWorldPos.z,
|
||||
"), dist=", std::sqrt(bestD2));
|
||||
}
|
||||
|
||||
return bestPathId;
|
||||
}
|
||||
|
||||
uint32_t TransportPathRepository::inferMovingPathForSpawn(const glm::vec3& spawnWorldPos, float maxDistance) const {
|
||||
return inferDbcPathForSpawn(spawnWorldPos, maxDistance, /*allowZOnly=*/false);
|
||||
}
|
||||
|
||||
uint32_t TransportPathRepository::pickFallbackMovingPath(uint32_t entry, uint32_t displayId) const {
|
||||
auto isUsableMovingPath = [this](uint32_t pathId) -> bool {
|
||||
auto* e = findPath(pathId);
|
||||
if (!e) return false;
|
||||
return e->fromDBC && !e->zOnly && e->spline.durationMs() > 0 && e->spline.keyCount() > 1;
|
||||
};
|
||||
|
||||
// Known AzerothCore transport entry remaps (WotLK): server entry -> moving DBC path id.
|
||||
// These entries commonly do not match TransportAnimation.dbc ids 1:1.
|
||||
static const std::unordered_map<uint32_t, uint32_t> kEntryRemap = {
|
||||
{176231u, 176080u}, // The Maiden's Fancy
|
||||
{176310u, 176081u}, // The Bravery
|
||||
{20808u, 176082u}, // The Black Princess
|
||||
{164871u, 193182u}, // The Thundercaller
|
||||
{176495u, 193183u}, // The Purple Princess
|
||||
{175080u, 193182u}, // The Iron Eagle
|
||||
{181689u, 193183u}, // Cloudkisser
|
||||
{186238u, 193182u}, // The Mighty Wind
|
||||
{181688u, 176083u}, // Northspear (icebreaker)
|
||||
{190536u, 176084u}, // Stormwind's Pride (icebreaker)
|
||||
};
|
||||
|
||||
auto itMapped = kEntryRemap.find(entry);
|
||||
if (itMapped != kEntryRemap.end() && isUsableMovingPath(itMapped->second)) {
|
||||
return itMapped->second;
|
||||
}
|
||||
|
||||
// Fallback by display model family.
|
||||
const bool looksLikeShip =
|
||||
(displayId == 3015u || displayId == 2454u || displayId == 7446u);
|
||||
const bool looksLikeZeppelin =
|
||||
(displayId == 3031u || displayId == 7546u || displayId == 1587u || displayId == 807u || displayId == 808u);
|
||||
|
||||
if (looksLikeShip) {
|
||||
static constexpr uint32_t kShipCandidates[] = {176080u, 176081u, 176082u, 176083u, 176084u, 176085u, 194675u};
|
||||
for (uint32_t id : kShipCandidates) {
|
||||
if (isUsableMovingPath(id)) return id;
|
||||
}
|
||||
}
|
||||
|
||||
if (looksLikeZeppelin) {
|
||||
static constexpr uint32_t kZeppelinCandidates[] = {193182u, 193183u, 188360u, 190587u};
|
||||
for (uint32_t id : kZeppelinCandidates) {
|
||||
if (isUsableMovingPath(id)) return id;
|
||||
}
|
||||
}
|
||||
|
||||
// Last-resort: pick any moving DBC path so transport does not remain stationary.
|
||||
for (const auto& [pathId, e] : paths_) {
|
||||
if (e.fromDBC && !e.zOnly && e.spline.durationMs() > 0 && e.spline.keyCount() > 1) {
|
||||
return pathId;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ── Path construction from waypoints ───────────────────────────
|
||||
|
||||
void TransportPathRepository::loadPathFromNodes(uint32_t pathId, const std::vector<glm::vec3>& waypoints, bool looping, float speed) {
|
||||
if (waypoints.empty()) {
|
||||
LOG_ERROR("TransportPathRepository: Cannot load empty path ", pathId);
|
||||
return;
|
||||
}
|
||||
|
||||
bool isZOnly = false; // Manually loaded paths are assumed to have XY movement
|
||||
|
||||
// Helper: compute segment duration from distance and speed
|
||||
auto segMsFromDist = [&](float dist) -> uint32_t {
|
||||
if (speed <= 0.0f) return 1000;
|
||||
return static_cast<uint32_t>((dist / speed) * 1000.0f);
|
||||
};
|
||||
|
||||
// Single point = stationary (durationMs = 0)
|
||||
if (waypoints.size() == 1) {
|
||||
std::vector<math::SplineKey> keys;
|
||||
keys.push_back({0, waypoints[0]});
|
||||
math::CatmullRomSpline spline(std::move(keys), false);
|
||||
paths_.emplace(pathId, PathEntry(std::move(spline), pathId, isZOnly, false, false));
|
||||
LOG_INFO("TransportPathRepository: Loaded stationary path ", pathId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Multiple points: calculate cumulative time based on distance and speed
|
||||
std::vector<math::SplineKey> keys;
|
||||
keys.reserve(waypoints.size() + (looping ? 1 : 0));
|
||||
uint32_t cumulativeMs = 0;
|
||||
keys.push_back({0, waypoints[0]});
|
||||
|
||||
for (size_t i = 1; i < waypoints.size(); i++) {
|
||||
float dist = glm::distance(waypoints[i-1], waypoints[i]);
|
||||
cumulativeMs += glm::max(1u, segMsFromDist(dist));
|
||||
keys.push_back({cumulativeMs, waypoints[i]});
|
||||
}
|
||||
|
||||
// Add explicit wrap segment (last → first) for looping paths.
|
||||
// By duplicating the first point at the end with cumulative time, the path
|
||||
// becomes time-closed and CatmullRomSpline handles wrap via modular time
|
||||
// without requiring special-case index wrapping during evaluation.
|
||||
if (looping && waypoints.size() >= 2) {
|
||||
float wrapDist = glm::distance(waypoints.back(), waypoints.front());
|
||||
cumulativeMs += glm::max(1u, segMsFromDist(wrapDist));
|
||||
keys.push_back({cumulativeMs, waypoints[0]});
|
||||
}
|
||||
|
||||
math::CatmullRomSpline spline(std::move(keys), false);
|
||||
paths_.emplace(pathId, PathEntry(std::move(spline), pathId, isZOnly, false, false));
|
||||
|
||||
auto* stored = findPath(pathId);
|
||||
LOG_INFO("TransportPathRepository: Loaded path ", pathId,
|
||||
" with ", waypoints.size(), " waypoints",
|
||||
(looping ? " + wrap segment" : ""),
|
||||
", duration=", stored ? stored->spline.durationMs() : 0, "ms, speed=", speed);
|
||||
}
|
||||
|
||||
// ── DBC: TransportAnimation ────────────────────────────────────
|
||||
|
||||
bool TransportPathRepository::loadTransportAnimationDBC(pipeline::AssetManager* assetMgr) {
|
||||
LOG_INFO("Loading TransportAnimation.dbc...");
|
||||
|
||||
if (!assetMgr) {
|
||||
LOG_ERROR("AssetManager is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load DBC file
|
||||
auto dbcData = assetMgr->readFile("DBFilesClient\\TransportAnimation.dbc");
|
||||
if (dbcData.empty()) {
|
||||
LOG_WARNING("TransportAnimation.dbc not found - transports will use fallback paths");
|
||||
return false;
|
||||
}
|
||||
|
||||
pipeline::DBCFile dbc;
|
||||
if (!dbc.load(dbcData)) {
|
||||
LOG_ERROR("Failed to parse TransportAnimation.dbc");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("TransportAnimation.dbc: ", dbc.getRecordCount(), " records, ",
|
||||
dbc.getFieldCount(), " fields per record");
|
||||
|
||||
// Debug: dump first 3 records to see all field values
|
||||
for (uint32_t i = 0; i < std::min(3u, dbc.getRecordCount()); i++) {
|
||||
LOG_INFO(" DEBUG Record ", i, ": ",
|
||||
" [0]=", dbc.getUInt32(i, 0),
|
||||
" [1]=", dbc.getUInt32(i, 1),
|
||||
" [2]=", dbc.getUInt32(i, 2),
|
||||
" [3]=", dbc.getFloat(i, 3),
|
||||
" [4]=", dbc.getFloat(i, 4),
|
||||
" [5]=", dbc.getFloat(i, 5),
|
||||
" [6]=", dbc.getUInt32(i, 6));
|
||||
}
|
||||
|
||||
// Group waypoints by transportEntry
|
||||
std::map<uint32_t, std::vector<std::pair<uint32_t, glm::vec3>>> waypointsByTransport;
|
||||
|
||||
for (uint32_t i = 0; i < dbc.getRecordCount(); i++) {
|
||||
// uint32_t id = dbc.getUInt32(i, 0); // Not needed
|
||||
uint32_t transportEntry = dbc.getUInt32(i, 1);
|
||||
uint32_t timeIndex = dbc.getUInt32(i, 2);
|
||||
float posX = dbc.getFloat(i, 3);
|
||||
float posY = dbc.getFloat(i, 4);
|
||||
float posZ = dbc.getFloat(i, 5);
|
||||
// uint32_t sequenceId = dbc.getUInt32(i, 6); // Not needed for basic paths
|
||||
|
||||
// RAW FLOAT SANITY CHECK: Log first 10 records to see if DBC has real data
|
||||
if (i < 10) {
|
||||
uint32_t ux = dbc.getUInt32(i, 3);
|
||||
uint32_t uy = dbc.getUInt32(i, 4);
|
||||
uint32_t uz = dbc.getUInt32(i, 5);
|
||||
LOG_INFO("TA raw rec ", i,
|
||||
" entry=", transportEntry,
|
||||
" t=", timeIndex,
|
||||
" raw=(", posX, ",", posY, ",", posZ, ")",
|
||||
" u32=(", ux, ",", uy, ",", uz, ")");
|
||||
}
|
||||
|
||||
// DIAGNOSTIC: Log ALL records for problematic ferries (20655, 20657, 149046)
|
||||
// AND first few records for known-good transports to verify DBC reading
|
||||
if (i < 5 || transportEntry == 2074 ||
|
||||
transportEntry == 20655 || transportEntry == 20657 || transportEntry == 149046) {
|
||||
LOG_INFO("RAW DBC [", i, "] entry=", transportEntry, " t=", timeIndex,
|
||||
" raw=(", posX, ",", posY, ",", posZ, ")");
|
||||
}
|
||||
|
||||
waypointsByTransport[transportEntry].push_back({timeIndex, glm::vec3(posX, posY, posZ)});
|
||||
}
|
||||
|
||||
// Create time-indexed paths from waypoints
|
||||
int pathsLoaded = 0;
|
||||
for (const auto& [transportEntry, waypoints] : waypointsByTransport) {
|
||||
if (waypoints.empty()) continue;
|
||||
|
||||
// Sort by timeIndex
|
||||
auto sortedWaypoints = waypoints;
|
||||
std::sort(sortedWaypoints.begin(), sortedWaypoints.end(),
|
||||
[](const auto& a, const auto& b) { return a.first < b.first; });
|
||||
|
||||
// CRITICAL: Normalize timeIndex to start at 0 (DBC records don't start at 0!)
|
||||
// This makes evaluatePosition(0) valid and stabilizes basePosition seeding
|
||||
uint32_t t0 = sortedWaypoints.front().first;
|
||||
|
||||
// Build SplineKey array with normalized time indices
|
||||
std::vector<math::SplineKey> keys;
|
||||
keys.reserve(sortedWaypoints.size() + 1); // +1 for wrap point
|
||||
|
||||
// Log DBC waypoints for tram entries
|
||||
if (transportEntry >= 176080 && transportEntry <= 176085) {
|
||||
size_t mid = sortedWaypoints.size() / 4; // ~quarter through
|
||||
size_t mid2 = sortedWaypoints.size() / 2; // ~halfway
|
||||
LOG_DEBUG("DBC path entry=", transportEntry, " nPts=", sortedWaypoints.size(),
|
||||
" [0] t=", sortedWaypoints[0].first, " raw=(", sortedWaypoints[0].second.x, ",", sortedWaypoints[0].second.y, ",", sortedWaypoints[0].second.z, ")",
|
||||
" [", mid, "] t=", sortedWaypoints[mid].first, " raw=(", sortedWaypoints[mid].second.x, ",", sortedWaypoints[mid].second.y, ",", sortedWaypoints[mid].second.z, ")",
|
||||
" [", mid2, "] t=", sortedWaypoints[mid2].first, " raw=(", sortedWaypoints[mid2].second.x, ",", sortedWaypoints[mid2].second.y, ",", sortedWaypoints[mid2].second.z, ")");
|
||||
}
|
||||
|
||||
for (size_t idx = 0; idx < sortedWaypoints.size(); idx++) {
|
||||
const auto& [tMs, pos] = sortedWaypoints[idx];
|
||||
|
||||
// TransportAnimation.dbc local offsets use a coordinate system where
|
||||
// the travel axis is negated relative to server world coords.
|
||||
// Negate X and Y before converting to canonical (Z=height stays the same).
|
||||
glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(-pos.x, -pos.y, pos.z));
|
||||
|
||||
// CRITICAL: Detect if serverToCanonical is zeroing nonzero inputs
|
||||
if ((pos.x != 0.0f || pos.y != 0.0f || pos.z != 0.0f) &&
|
||||
(canonical.x == 0.0f && canonical.y == 0.0f && canonical.z == 0.0f)) {
|
||||
LOG_ERROR("serverToCanonical ZEROED! entry=", transportEntry,
|
||||
" server=(", pos.x, ",", pos.y, ",", pos.z, ")",
|
||||
" → canon=(", canonical.x, ",", canonical.y, ",", canonical.z, ")");
|
||||
}
|
||||
|
||||
// Debug waypoint conversion for first transport (entry 2074)
|
||||
if (transportEntry == 2074 && idx < 5) {
|
||||
LOG_INFO("COORD CONVERT: entry=", transportEntry, " t=", tMs,
|
||||
" serverPos=(", pos.x, ", ", pos.y, ", ", pos.z, ")",
|
||||
" → canonical=(", canonical.x, ", ", canonical.y, ", ", canonical.z, ")");
|
||||
}
|
||||
|
||||
// DIAGNOSTIC: Log ALL conversions for problematic ferries
|
||||
if (transportEntry == 20655 || transportEntry == 20657 || transportEntry == 149046) {
|
||||
LOG_INFO("CONVERT ", transportEntry, " t=", tMs,
|
||||
" server=(", pos.x, ",", pos.y, ",", pos.z, ")",
|
||||
" → canon=(", canonical.x, ",", canonical.y, ",", canonical.z, ")");
|
||||
}
|
||||
|
||||
keys.push_back({tMs - t0, canonical}); // Normalize: subtract first timeIndex
|
||||
}
|
||||
|
||||
// Get base duration from last normalized timeIndex
|
||||
uint32_t lastTimeMs = sortedWaypoints.back().first - t0;
|
||||
|
||||
// Calculate wrap duration (last → first segment)
|
||||
// Use average segment duration as wrap duration
|
||||
uint32_t totalDelta = 0;
|
||||
int segmentCount = 0;
|
||||
for (size_t i = 1; i < sortedWaypoints.size(); i++) {
|
||||
uint32_t delta = sortedWaypoints[i].first - sortedWaypoints[i-1].first;
|
||||
if (delta > 0) {
|
||||
totalDelta += delta;
|
||||
segmentCount++;
|
||||
}
|
||||
}
|
||||
uint32_t wrapMs = (segmentCount > 0) ? (totalDelta / segmentCount) : 1000;
|
||||
|
||||
// Add duplicate first point at end with wrap duration
|
||||
// This makes the wrap segment (last → first) have proper duration
|
||||
const auto& fp = sortedWaypoints.front().second;
|
||||
glm::vec3 firstCanonical = core::coords::serverToCanonical(glm::vec3(-fp.x, -fp.y, fp.z));
|
||||
keys.push_back({lastTimeMs + wrapMs, firstCanonical});
|
||||
|
||||
// Build the spline (time-closed=false because we added explicit wrap point)
|
||||
math::CatmullRomSpline spline(std::move(keys), false);
|
||||
|
||||
// Detect Z-only paths (elevator/bobbing animation, not real XY travel)
|
||||
const auto& sk = spline.keys();
|
||||
float minX = sk[0].position.x, maxX = minX;
|
||||
float minY = sk[0].position.y, maxY = minY;
|
||||
float minZ = sk[0].position.z, maxZ = minZ;
|
||||
for (const auto& k : sk) {
|
||||
minX = std::min(minX, k.position.x); maxX = std::max(maxX, k.position.x);
|
||||
minY = std::min(minY, k.position.y); maxY = std::max(maxY, k.position.y);
|
||||
minZ = std::min(minZ, k.position.z); maxZ = std::max(maxZ, k.position.z);
|
||||
}
|
||||
float rangeX = maxX - minX;
|
||||
float rangeY = maxY - minY;
|
||||
float rangeZ = maxZ - minZ;
|
||||
float rangeXY = std::max(rangeX, rangeY);
|
||||
// Some elevator paths have tiny XY jitter. Treat them as z-only when horizontal travel
|
||||
// is negligible compared to vertical motion.
|
||||
bool isZOnly = (rangeXY < 0.01f) || (rangeXY < 1.0f && rangeZ > 2.0f);
|
||||
|
||||
// Log first, middle, and last points to verify path data
|
||||
glm::vec3 firstOffset = sk[0].position;
|
||||
size_t midIdx = sk.size() / 2;
|
||||
glm::vec3 midOffset = sk[midIdx].position;
|
||||
glm::vec3 lastOffset = sk[sk.size() - 2].position; // -2 to skip wrap duplicate
|
||||
uint32_t durationMs = spline.durationMs();
|
||||
LOG_INFO(" Transport ", transportEntry, ": ", sk.size() - 1, " waypoints + wrap, ",
|
||||
durationMs, "ms duration (wrap=", wrapMs, "ms, t0_normalized=", sk[0].timeMs, "ms)",
|
||||
" rangeXY=(", rangeX, ",", rangeY, ") rangeZ=", rangeZ, " ",
|
||||
(isZOnly ? "[Z-ONLY]" : "[XY-PATH]"),
|
||||
" firstOffset=(", firstOffset.x, ", ", firstOffset.y, ", ", firstOffset.z, ")",
|
||||
" midOffset=(", midOffset.x, ", ", midOffset.y, ", ", midOffset.z, ")",
|
||||
" lastOffset=(", lastOffset.x, ", ", lastOffset.y, ", ", lastOffset.z, ")");
|
||||
|
||||
// Store path
|
||||
paths_.emplace(transportEntry, PathEntry(std::move(spline), transportEntry, isZOnly, true, false));
|
||||
pathsLoaded++;
|
||||
}
|
||||
|
||||
LOG_INFO("Loaded ", pathsLoaded, " transport paths from TransportAnimation.dbc");
|
||||
return pathsLoaded > 0;
|
||||
}
|
||||
|
||||
// ── DBC: TaxiPathNode ──────────────────────────────────────────
|
||||
|
||||
bool TransportPathRepository::loadTaxiPathNodeDBC(pipeline::AssetManager* assetMgr) {
|
||||
LOG_INFO("Loading TaxiPathNode.dbc...");
|
||||
|
||||
if (!assetMgr) {
|
||||
LOG_ERROR("AssetManager is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto dbcData = assetMgr->readFile("DBFilesClient\\TaxiPathNode.dbc");
|
||||
if (dbcData.empty()) {
|
||||
LOG_WARNING("TaxiPathNode.dbc not found - MO_TRANSPORT will use fallback paths");
|
||||
return false;
|
||||
}
|
||||
|
||||
pipeline::DBCFile dbc;
|
||||
if (!dbc.load(dbcData)) {
|
||||
LOG_ERROR("Failed to parse TaxiPathNode.dbc");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("TaxiPathNode.dbc: ", dbc.getRecordCount(), " records, ",
|
||||
dbc.getFieldCount(), " fields per record");
|
||||
|
||||
// Group nodes by PathID, storing (NodeIndex, MapID, X, Y, Z)
|
||||
struct TaxiNode {
|
||||
uint32_t nodeIndex;
|
||||
uint32_t mapId;
|
||||
float x, y, z;
|
||||
};
|
||||
std::map<uint32_t, std::vector<TaxiNode>> nodesByPath;
|
||||
|
||||
for (uint32_t i = 0; i < dbc.getRecordCount(); i++) {
|
||||
uint32_t pathId = dbc.getUInt32(i, 1); // PathID
|
||||
uint32_t nodeIdx = dbc.getUInt32(i, 2); // NodeIndex
|
||||
uint32_t mapId = dbc.getUInt32(i, 3); // MapID
|
||||
float posX = dbc.getFloat(i, 4); // X (server coords)
|
||||
float posY = dbc.getFloat(i, 5); // Y (server coords)
|
||||
float posZ = dbc.getFloat(i, 6); // Z (server coords)
|
||||
|
||||
nodesByPath[pathId].push_back({nodeIdx, mapId, posX, posY, posZ});
|
||||
}
|
||||
|
||||
// Build world-coordinate transport paths
|
||||
int pathsLoaded = 0;
|
||||
for (auto& [pathId, nodes] : nodesByPath) {
|
||||
if (nodes.size() < 2) continue;
|
||||
|
||||
// Sort by NodeIndex
|
||||
std::sort(nodes.begin(), nodes.end(),
|
||||
[](const TaxiNode& a, const TaxiNode& b) { return a.nodeIndex < b.nodeIndex; });
|
||||
|
||||
// Skip flight-master paths (nodes on different maps are map teleports)
|
||||
// Transport paths stay on the same map
|
||||
bool sameMap = true;
|
||||
uint32_t firstMap = nodes[0].mapId;
|
||||
for (const auto& node : nodes) {
|
||||
if (node.mapId != firstMap) { sameMap = false; break; }
|
||||
}
|
||||
if (!sameMap) continue;
|
||||
|
||||
// Build timed points using distance-based timing (28 units/sec default boat speed)
|
||||
const float transportSpeed = 28.0f; // units per second
|
||||
std::vector<math::SplineKey> keys;
|
||||
keys.reserve(nodes.size() + 1);
|
||||
|
||||
uint32_t cumulativeMs = 0;
|
||||
for (size_t i = 0; i < nodes.size(); i++) {
|
||||
// Convert server coords to canonical
|
||||
glm::vec3 serverPos(nodes[i].x, nodes[i].y, nodes[i].z);
|
||||
glm::vec3 canonical = core::coords::serverToCanonical(serverPos);
|
||||
|
||||
keys.push_back({cumulativeMs, canonical});
|
||||
|
||||
if (i + 1 < nodes.size()) {
|
||||
float dx = nodes[i+1].x - nodes[i].x;
|
||||
float dy = nodes[i+1].y - nodes[i].y;
|
||||
float dz = nodes[i+1].z - nodes[i].z;
|
||||
float segDist = std::sqrt(dx*dx + dy*dy + dz*dz);
|
||||
uint32_t segMs = static_cast<uint32_t>((segDist / transportSpeed) * 1000.0f);
|
||||
if (segMs < 100) segMs = 100; // Minimum 100ms per segment
|
||||
cumulativeMs += segMs;
|
||||
}
|
||||
}
|
||||
|
||||
// Add wrap point (return to start) for looping
|
||||
float wrapDx = nodes.front().x - nodes.back().x;
|
||||
float wrapDy = nodes.front().y - nodes.back().y;
|
||||
float wrapDz = nodes.front().z - nodes.back().z;
|
||||
float wrapDist = std::sqrt(wrapDx*wrapDx + wrapDy*wrapDy + wrapDz*wrapDz);
|
||||
uint32_t wrapMs = static_cast<uint32_t>((wrapDist / transportSpeed) * 1000.0f);
|
||||
if (wrapMs < 100) wrapMs = 100;
|
||||
cumulativeMs += wrapMs;
|
||||
keys.push_back({cumulativeMs, keys[0].position});
|
||||
|
||||
math::CatmullRomSpline spline(std::move(keys), false);
|
||||
taxiPaths_.emplace(pathId, PathEntry(std::move(spline), pathId, false, true, true));
|
||||
pathsLoaded++;
|
||||
}
|
||||
|
||||
LOG_INFO("Loaded ", pathsLoaded, " TaxiPathNode transport paths (", nodesByPath.size(), " total taxi paths)");
|
||||
return pathsLoaded > 0;
|
||||
}
|
||||
|
||||
} // namespace wowee::game
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -595,96 +596,22 @@ bool MonsterMoveParser::parse(network::Packet& packet, MonsterMoveData& data) {
|
|||
if (!packet.hasRemaining(4)) return false;
|
||||
data.splineFlags = packet.readUInt32();
|
||||
|
||||
// WotLK 3.3.5a SplineFlags (from TrinityCore/MaNGOS MoveSplineFlag.h):
|
||||
// Animation = 0x00400000
|
||||
// Parabolic = 0x00000800
|
||||
// Catmullrom = 0x00080000 \ either means uncompressed (absolute) waypoints
|
||||
// Flying = 0x00002000 /
|
||||
|
||||
// [if Animation] uint8 animationType + int32 effectStartTime (5 bytes)
|
||||
if (data.splineFlags & 0x00400000) {
|
||||
if (!packet.hasRemaining(5)) return false;
|
||||
packet.readUInt8(); // animationType
|
||||
packet.readUInt32(); // effectStartTime (int32, read as uint32 same size)
|
||||
}
|
||||
|
||||
// uint32 duration
|
||||
if (!packet.hasRemaining(4)) return false;
|
||||
data.duration = packet.readUInt32();
|
||||
|
||||
// [if Parabolic] float verticalAcceleration + int32 effectStartTime (8 bytes)
|
||||
if (data.splineFlags & 0x00000800) {
|
||||
if (!packet.hasRemaining(8)) return false;
|
||||
packet.readFloat(); // verticalAcceleration
|
||||
packet.readUInt32(); // effectStartTime
|
||||
}
|
||||
|
||||
// uint32 pointCount
|
||||
if (!packet.hasRemaining(4)) return false;
|
||||
uint32_t pointCount = packet.readUInt32();
|
||||
|
||||
if (pointCount == 0) return true;
|
||||
|
||||
constexpr uint32_t kMaxSplinePoints = 1000;
|
||||
if (pointCount > kMaxSplinePoints) {
|
||||
LOG_WARNING("SMSG_MONSTER_MOVE: pointCount=", pointCount, " exceeds max ", kMaxSplinePoints,
|
||||
" (guid=0x", std::hex, data.guid, std::dec, ")");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Catmullrom or Flying → all waypoints stored as absolute float3 (uncompressed).
|
||||
// Otherwise: first float3 is final destination, remaining are packed deltas.
|
||||
bool uncompressed = (data.splineFlags & (0x00080000 | 0x00002000)) != 0;
|
||||
|
||||
if (uncompressed) {
|
||||
// All waypoints stored as absolute float3 (Catmullrom/Flying paths)
|
||||
// Read all intermediate points, then the final destination
|
||||
for (uint32_t i = 0; i < pointCount - 1; i++) {
|
||||
if (!packet.hasRemaining(12)) return true;
|
||||
MonsterMoveData::Point wp;
|
||||
wp.x = packet.readFloat();
|
||||
wp.y = packet.readFloat();
|
||||
wp.z = packet.readFloat();
|
||||
data.waypoints.push_back(wp);
|
||||
// Consolidated spline body parser
|
||||
{
|
||||
SplineBlockData spline;
|
||||
if (!parseMonsterMoveSplineBody(packet, spline, data.splineFlags,
|
||||
glm::vec3(data.x, data.y, data.z))) {
|
||||
return false;
|
||||
}
|
||||
if (!packet.hasRemaining(12)) return true;
|
||||
data.destX = packet.readFloat();
|
||||
data.destY = packet.readFloat();
|
||||
data.destZ = packet.readFloat();
|
||||
data.hasDest = true;
|
||||
} else {
|
||||
// Compressed: first 3 floats are the destination (final point)
|
||||
if (!packet.hasRemaining(12)) return true;
|
||||
data.destX = packet.readFloat();
|
||||
data.destY = packet.readFloat();
|
||||
data.destZ = packet.readFloat();
|
||||
data.hasDest = true;
|
||||
|
||||
// Remaining waypoints are packed as uint32 deltas from the midpoint
|
||||
// between the creature's start position and the destination.
|
||||
// Encoding matches TrinityCore MoveSpline::PackXYZ:
|
||||
// x = 11-bit signed (bits 0-10), y = 11-bit signed (bits 11-21),
|
||||
// z = 10-bit signed (bits 22-31), each scaled by 0.25 units.
|
||||
if (pointCount > 1) {
|
||||
float midX = (data.x + data.destX) * 0.5f;
|
||||
float midY = (data.y + data.destY) * 0.5f;
|
||||
float midZ = (data.z + data.destZ) * 0.5f;
|
||||
for (uint32_t i = 0; i < pointCount - 1; i++) {
|
||||
if (!packet.hasRemaining(4)) break;
|
||||
uint32_t packed = packet.readUInt32();
|
||||
// Sign-extend 11-bit x and y, 10-bit z (2's complement)
|
||||
int32_t sx = static_cast<int32_t>(packed & 0x7FF);
|
||||
if (sx & 0x400) sx |= static_cast<int32_t>(0xFFFFF800);
|
||||
int32_t sy = static_cast<int32_t>((packed >> 11) & 0x7FF);
|
||||
if (sy & 0x400) sy |= static_cast<int32_t>(0xFFFFF800);
|
||||
int32_t sz = static_cast<int32_t>((packed >> 22) & 0x3FF);
|
||||
if (sz & 0x200) sz |= static_cast<int32_t>(0xFFFFFC00);
|
||||
MonsterMoveData::Point wp;
|
||||
wp.x = midX - static_cast<float>(sx) * 0.25f;
|
||||
wp.y = midY - static_cast<float>(sy) * 0.25f;
|
||||
wp.z = midZ - static_cast<float>(sz) * 0.25f;
|
||||
data.waypoints.push_back(wp);
|
||||
}
|
||||
data.duration = spline.duration;
|
||||
if (spline.hasDest) {
|
||||
data.destX = spline.destination.x;
|
||||
data.destY = spline.destination.y;
|
||||
data.destZ = spline.destination.z;
|
||||
data.hasDest = true;
|
||||
}
|
||||
for (const auto& wp : spline.waypoints) {
|
||||
data.waypoints.push_back({wp.x, wp.y, wp.z});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -743,65 +670,22 @@ bool MonsterMoveParser::parseVanilla(network::Packet& packet, MonsterMoveData& d
|
|||
if (!packet.hasRemaining(4)) return false;
|
||||
data.splineFlags = packet.readUInt32();
|
||||
|
||||
// Animation flag (same bit as WotLK MoveSplineFlag::Animation)
|
||||
if (data.splineFlags & 0x00400000) {
|
||||
if (!packet.hasRemaining(5)) return false;
|
||||
packet.readUInt8();
|
||||
packet.readUInt32();
|
||||
}
|
||||
|
||||
if (!packet.hasRemaining(4)) return false;
|
||||
data.duration = packet.readUInt32();
|
||||
|
||||
// Parabolic flag (same bit as WotLK MoveSplineFlag::Parabolic)
|
||||
if (data.splineFlags & 0x00000800) {
|
||||
if (!packet.hasRemaining(8)) return false;
|
||||
packet.readFloat();
|
||||
packet.readUInt32();
|
||||
}
|
||||
|
||||
if (!packet.hasRemaining(4)) return false;
|
||||
uint32_t pointCount = packet.readUInt32();
|
||||
|
||||
if (pointCount == 0) return true;
|
||||
|
||||
// Reject extreme point counts from malformed packets.
|
||||
constexpr uint32_t kMaxSplinePoints = 1000;
|
||||
if (pointCount > kMaxSplinePoints) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t requiredBytes = 12;
|
||||
if (pointCount > 1) {
|
||||
requiredBytes += static_cast<size_t>(pointCount - 1) * 4ull;
|
||||
}
|
||||
if (!packet.hasRemaining(requiredBytes)) return false;
|
||||
|
||||
// First float[3] is destination.
|
||||
data.destX = packet.readFloat();
|
||||
data.destY = packet.readFloat();
|
||||
data.destZ = packet.readFloat();
|
||||
data.hasDest = true;
|
||||
|
||||
// Remaining waypoints are packed as uint32 deltas from midpoint.
|
||||
if (pointCount > 1) {
|
||||
float midX = (data.x + data.destX) * 0.5f;
|
||||
float midY = (data.y + data.destY) * 0.5f;
|
||||
float midZ = (data.z + data.destZ) * 0.5f;
|
||||
for (uint32_t i = 0; i < pointCount - 1; i++) {
|
||||
if (!packet.hasRemaining(4)) break;
|
||||
uint32_t packed = packet.readUInt32();
|
||||
int32_t sx = static_cast<int32_t>(packed & 0x7FF);
|
||||
if (sx & 0x400) sx |= static_cast<int32_t>(0xFFFFF800);
|
||||
int32_t sy = static_cast<int32_t>((packed >> 11) & 0x7FF);
|
||||
if (sy & 0x400) sy |= static_cast<int32_t>(0xFFFFF800);
|
||||
int32_t sz = static_cast<int32_t>((packed >> 22) & 0x3FF);
|
||||
if (sz & 0x200) sz |= static_cast<int32_t>(0xFFFFFC00);
|
||||
MonsterMoveData::Point wp;
|
||||
wp.x = midX - static_cast<float>(sx) * 0.25f;
|
||||
wp.y = midY - static_cast<float>(sy) * 0.25f;
|
||||
wp.z = midZ - static_cast<float>(sz) * 0.25f;
|
||||
data.waypoints.push_back(wp);
|
||||
// Consolidated Vanilla spline body parser (always compressed)
|
||||
{
|
||||
SplineBlockData spline;
|
||||
if (!parseMonsterMoveSplineBodyVanilla(packet, spline, data.splineFlags,
|
||||
glm::vec3(data.x, data.y, data.z))) {
|
||||
return false;
|
||||
}
|
||||
data.duration = spline.duration;
|
||||
if (spline.hasDest) {
|
||||
data.destX = spline.destination.x;
|
||||
data.destY = spline.destination.y;
|
||||
data.destZ = spline.destination.z;
|
||||
data.hasDest = true;
|
||||
}
|
||||
for (const auto& wp : spline.waypoints) {
|
||||
data.waypoints.push_back({wp.x, wp.y, wp.z});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -814,7 +698,7 @@ bool MonsterMoveParser::parseVanilla(network::Packet& packet, MonsterMoveData& d
|
|||
|
||||
|
||||
// ============================================================
|
||||
// Phase 2: Combat Core
|
||||
// Combat Core
|
||||
// ============================================================
|
||||
|
||||
bool AttackStartParser::parse(network::Packet& packet, AttackStartData& data) {
|
||||
|
|
@ -1035,7 +919,7 @@ bool XpGainParser::parse(network::Packet& packet, XpGainData& data) {
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 3: Spells, Action Bar, Auras
|
||||
// Spells, Action Bar, Auras
|
||||
// ============================================================
|
||||
|
||||
bool InitialSpellsParser::parse(network::Packet& packet, InitialSpellsData& data,
|
||||
|
|
|
|||
|
|
@ -376,7 +376,7 @@ bool ChannelNotifyParser::parse(network::Packet& packet, ChannelNotifyData& data
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 1: Foundation — Targeting, Name Queries
|
||||
// Foundation — Targeting, Name Queries
|
||||
// ============================================================
|
||||
|
||||
network::Packet SetSelectionPacket::build(uint64_t targetGuid) {
|
||||
|
|
|
|||
|
|
@ -321,7 +321,7 @@ bool SpellCooldownParser::parse(network::Packet& packet, SpellCooldownData& data
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 4: Group/Party System
|
||||
// Group/Party System
|
||||
// ============================================================
|
||||
|
||||
network::Packet GroupInvitePacket::build(const std::string& playerName) {
|
||||
|
|
@ -468,7 +468,7 @@ bool GroupDeclineResponseParser::parse(network::Packet& packet, GroupDeclineData
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: Loot System
|
||||
// Loot System
|
||||
// ============================================================
|
||||
|
||||
network::Packet LootPacket::build(uint64_t targetGuid) {
|
||||
|
|
@ -624,7 +624,7 @@ bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data,
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: NPC Gossip
|
||||
// NPC Gossip
|
||||
// ============================================================
|
||||
|
||||
network::Packet GossipHelloPacket::build(uint64_t npcGuid) {
|
||||
|
|
@ -1090,7 +1090,7 @@ network::Packet QuestgiverChooseRewardPacket::build(uint64_t npcGuid, uint32_t q
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: Vendor
|
||||
// Vendor
|
||||
// ============================================================
|
||||
|
||||
network::Packet ListInventoryPacket::build(uint64_t npcGuid) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue