Kelsidavis-WoWee/src/game/spline_packet.cpp
Kelsi 3be40c3b69 fix: resolve 7 code quality issues across PRs #59-63
- Remove stale kVOffset (-0.15) from zone_highlight_layer hover detection;
  the offset was removed from rendering but left in the hit-test path,
  shifting hover ~15% vertically
- Add null guard for cachedGameHandler_ in ChatPanel::inputTextCallback
  to prevent dereference before first render frame
- Zero WindowBorderSize in world map ImGui window to eliminate gap
  between window edge and map content
- Replace hardcoded cosmic highlight multipliers with displayH×displayH
  square rendering, preserving 1:1 aspect ratio at any resolution
- Skip transport waypoints where serverToCanonical zeroes nonzero input
  instead of silently building paths with broken (0,0,0) coordinates
- Use length-squared check (posLenSq > 1.0) for spline endpoint
  validation instead of per-component != 0 comparison, so entities
  near the world origin are no longer skipped
- Fix off-by-one in ChatPanel::insertChatLink buffer capacity check
2026-04-14 02:41:55 -07:00

451 lines
18 KiB
C++

// 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 (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