mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-06 09:03:52 +00:00
- 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
451 lines
18 KiB
C++
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
|