diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index e219e917..fae92812 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -44,6 +44,7 @@ public: } void reset(); + void resetAngles(); void teleportTo(const glm::vec3& pos); void setOnlineMode(bool online) { onlineMode = online; } diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 7b25d257..e04832f6 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -520,23 +520,20 @@ bool ClassicPacketParsers::parseSpellStart(network::Packet& packet, SpellStartDa data.castFlags = packet.readUInt16(); // uint16 in Vanilla (uint32 in TBC/WotLK) data.castTime = packet.readUInt32(); - // SpellCastTargets: uint16 targetFlags in Vanilla (uint32 in TBC/WotLK) - if (rem() < 2) { - LOG_WARNING("[Classic] Spell start: missing targetFlags"); - packet.setReadPos(startPos); - return false; - } - uint16_t targetFlags = packet.readUInt16(); - // TARGET_FLAG_UNIT (0x02) or TARGET_FLAG_OBJECT (0x800) carry a packed GUID - if ((targetFlags & 0x02) || (targetFlags & 0x800)) { - if (!hasFullPackedGuid(packet)) { - packet.setReadPos(startPos); - return false; - } - data.targetGuid = UpdateObjectParser::readPackedGuid(packet); + // SpellCastTargets: consume ALL target payload types so subsequent reads stay aligned. + // Previously only UNIT(0x02)/OBJECT(0x800) were handled; DEST_LOCATION(0x40), + // SOURCE_LOCATION(0x20), and ITEM(0x10) bytes were silently skipped, corrupting + // castFlags/castTime for every AOE/ground-targeted spell (Rain of Fire, Blizzard, etc.). + { + uint64_t targetGuid = 0; + // skipClassicSpellCastTargets reads uint16 targetFlags and all payloads. + // Non-fatal on truncation: self-cast spells have zero-byte targets. + skipClassicSpellCastTargets(packet, &targetGuid); + data.targetGuid = targetGuid; } - LOG_DEBUG("[Classic] Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms"); + LOG_DEBUG("[Classic] Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms", + " targetGuid=0x", std::hex, data.targetGuid, std::dec); return true; } diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index a29a0beb..3b0a545c 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -1232,6 +1232,66 @@ bool TbcPacketParsers::parseMailList(network::Packet& packet, std::vector bool { + if (!(targetFlags & flag)) return true; + // Packed GUID: 1-byte mask + up to 8 data bytes + if (packet.getReadPos() >= packet.getSize()) return false; + uint8_t mask = packet.getData()[packet.getReadPos()]; + size_t needed = 1; + for (int b = 0; b < 8; ++b) if (mask & (1u << b)) ++needed; + if (packet.getSize() - packet.getReadPos() < needed) return false; + uint64_t g = UpdateObjectParser::readPackedGuid(packet); + if (capture && primaryTargetGuid && *primaryTargetGuid == 0) *primaryTargetGuid = g; + return true; + }; + auto skipFloats3 = [&](uint32_t flag) -> bool { + if (!(targetFlags & flag)) return true; + if (packet.getSize() - packet.getReadPos() < 12) return false; + (void)packet.readFloat(); (void)packet.readFloat(); (void)packet.readFloat(); + return true; + }; + + // Process in wire order matching cmangos-tbc SpellCastTargets::write() + if (!readPackedGuidCond(0x0002, true)) return false; // UNIT + if (!readPackedGuidCond(0x0004, false)) return false; // UNIT_MINIPET + if (!readPackedGuidCond(0x0010, false)) return false; // ITEM + if (!skipFloats3(0x0020)) return false; // SOURCE_LOCATION + if (!skipFloats3(0x0040)) return false; // DEST_LOCATION + + if (targetFlags & 0x1000) { // TRADE_ITEM: uint8 + if (packet.getReadPos() >= packet.getSize()) return false; + (void)packet.readUInt8(); + } + if (targetFlags & 0x2000) { // STRING: null-terminated + const auto& raw = packet.getData(); + size_t pos = packet.getReadPos(); + while (pos < raw.size() && raw[pos] != 0) ++pos; + if (pos >= raw.size()) return false; + packet.setReadPos(pos + 1); + } + if (!readPackedGuidCond(0x8200, false)) return false; // CORPSE / PVP_CORPSE + if (!readPackedGuidCond(0x0800, true)) return false; // OBJECT + + return true; +} + // TbcPacketParsers::parseSpellStart — TBC 2.4.3 SMSG_SPELL_START // // TBC uses full uint64 GUIDs for casterGuid and casterUnit. @@ -1243,7 +1303,6 @@ bool TbcPacketParsers::parseMailList(network::Packet& packet, std::vector packet.getSize()) { - LOG_WARNING("[TBC] Spell start: missing targetFlags"); - packet.setReadPos(startPos); - return false; + // SpellCastTargets: consume ALL target payload types to keep the read position + // aligned for any bytes the caller may parse after this (ammo, etc.). + // The previous code only read UNIT(0x02)/OBJECT(0x800) target GUIDs and left + // DEST_LOCATION(0x40)/SOURCE_LOCATION(0x20)/ITEM(0x10) bytes unconsumed, + // corrupting subsequent reads for every AOE/ground-targeted spell cast. + { + uint64_t targetGuid = 0; + skipTbcSpellCastTargets(packet, &targetGuid); // non-fatal on truncation + data.targetGuid = targetGuid; } - uint32_t targetFlags = packet.readUInt32(); - const bool needsTargetGuid = (targetFlags & 0x02) || (targetFlags & 0x800); // UNIT/OBJECT - if (needsTargetGuid) { - if (packet.getReadPos() + 8 > packet.getSize()) { - packet.setReadPos(startPos); - return false; - } - data.targetGuid = packet.readUInt64(); // full GUID in TBC - } - - LOG_DEBUG("[TBC] Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms"); + LOG_DEBUG("[TBC] Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms", + " targetGuid=0x", std::hex, data.targetGuid, std::dec); return true; } diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 22c5304f..d16fa26c 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -388,10 +388,11 @@ void CameraController::update(float deltaTime) { if (mounted_) sitting = false; xKeyWasDown = xDown; - // Reset camera with R key (edge-triggered) — only when UI doesn't want keyboard + // Reset camera angles with R key (edge-triggered) — only when UI doesn't want keyboard + // Does NOT move the player; full reset() is reserved for world-entry/respawn. bool rDown = !uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_R); if (rDown && !rKeyWasDown) { - reset(); + resetAngles(); } rKeyWasDown = rDown; @@ -1941,6 +1942,14 @@ void CameraController::processMouseButton(const SDL_MouseButtonEvent& event) { mouseButtonDown = anyDown; } +void CameraController::resetAngles() { + if (!camera) return; + yaw = defaultYaw; + facingYaw = defaultYaw; + pitch = defaultPitch; + camera->setRotation(yaw, pitch); +} + void CameraController::reset() { if (!camera) { return;