// 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 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(packed & 0x7FF); if (sx & 0x400) sx |= static_cast(0xFFFFF800); // sign-extend 11-bit int32_t sy = static_cast((packed >> 11) & 0x7FF); if (sy & 0x400) sy |= static_cast(0xFFFFF800); // sign-extend 11-bit int32_t sz = static_cast((packed >> 22) & 0x3FF); if (sz & 0x200) sz |= static_cast(0xFFFFFC00); // sign-extend 10-bit return glm::vec3( midpoint.x - static_cast(sx) * 0.25f, midpoint.y - static_cast(sy) * 0.25f, midpoint.z - static_cast(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(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(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(pc - 1) * 4ull : 0ull); } else { // All uncompressed: 3 floats each pointsBytes = static_cast(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 (not the default 0,0,0 // sentinel), reject endpoints that are implausibly far from it. float posLenSq = entityPos.x * entityPos.x + entityPos.y * entityPos.y + entityPos.z * entityPos.z; if (posLenSq > 1.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