fix: R key resets camera angles only; consume all SpellCastTargets bytes

- CameraController::resetAngles(): new method that only resets yaw/pitch
  without teleporting the player. R key now calls resetAngles() instead
  of reset() so pressing R no longer moves the character to spawn.
  The full reset() (position + angles) is still used on world-entry and
  respawn via application.cpp.

- packet_parsers_classic: parseSpellStart now calls
  skipClassicSpellCastTargets() to consume all target payload bytes
  (UNIT, ITEM, SOURCE_LOCATION, DEST_LOCATION, etc.) instead of only
  handling UNIT/OBJECT. Prevents packet-read corruption for ground-
  targeted AoE spells.

- packet_parsers_tbc: added skipTbcSpellCastTargets() static helper
  (uint32 targetFlags, full payload coverage including TRADE_ITEM and
  STRING targets). parseSpellStart now uses it.
This commit is contained in:
Kelsi 2026-03-17 21:52:45 -07:00
parent a731223e47
commit 32497552d1
4 changed files with 95 additions and 33 deletions

View file

@ -44,6 +44,7 @@ public:
} }
void reset(); void reset();
void resetAngles();
void teleportTo(const glm::vec3& pos); void teleportTo(const glm::vec3& pos);
void setOnlineMode(bool online) { onlineMode = online; } void setOnlineMode(bool online) { onlineMode = online; }

View file

@ -520,23 +520,20 @@ bool ClassicPacketParsers::parseSpellStart(network::Packet& packet, SpellStartDa
data.castFlags = packet.readUInt16(); // uint16 in Vanilla (uint32 in TBC/WotLK) data.castFlags = packet.readUInt16(); // uint16 in Vanilla (uint32 in TBC/WotLK)
data.castTime = packet.readUInt32(); data.castTime = packet.readUInt32();
// SpellCastTargets: uint16 targetFlags in Vanilla (uint32 in TBC/WotLK) // SpellCastTargets: consume ALL target payload types so subsequent reads stay aligned.
if (rem() < 2) { // Previously only UNIT(0x02)/OBJECT(0x800) were handled; DEST_LOCATION(0x40),
LOG_WARNING("[Classic] Spell start: missing targetFlags"); // SOURCE_LOCATION(0x20), and ITEM(0x10) bytes were silently skipped, corrupting
packet.setReadPos(startPos); // castFlags/castTime for every AOE/ground-targeted spell (Rain of Fire, Blizzard, etc.).
return false; {
} uint64_t targetGuid = 0;
uint16_t targetFlags = packet.readUInt16(); // skipClassicSpellCastTargets reads uint16 targetFlags and all payloads.
// TARGET_FLAG_UNIT (0x02) or TARGET_FLAG_OBJECT (0x800) carry a packed GUID // Non-fatal on truncation: self-cast spells have zero-byte targets.
if ((targetFlags & 0x02) || (targetFlags & 0x800)) { skipClassicSpellCastTargets(packet, &targetGuid);
if (!hasFullPackedGuid(packet)) { data.targetGuid = targetGuid;
packet.setReadPos(startPos);
return false;
}
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
} }
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; return true;
} }

View file

@ -1232,6 +1232,66 @@ bool TbcPacketParsers::parseMailList(network::Packet& packet, std::vector<MailMe
} }
// ============================================================================ // ============================================================================
// ---------------------------------------------------------------------------
// skipTbcSpellCastTargets — consume all SpellCastTargets payload bytes for TBC.
//
// TBC uses uint32 targetFlags (Classic: uint16). Unit/item/object/corpse targets
// are PackedGuid (same as Classic). Source/dest location is 3 floats (12 bytes)
// with no transport guid (Classic: same; WotLK adds a transport PackedGuid).
//
// This helper is used by parseSpellStart to ensure the read position advances
// past ALL target payload fields so subsequent fields (e.g. those parsed by the
// caller after spell targets) are not corrupted.
// ---------------------------------------------------------------------------
static bool skipTbcSpellCastTargets(network::Packet& packet, uint64_t* primaryTargetGuid = nullptr) {
if (packet.getSize() - packet.getReadPos() < 4) return false;
const uint32_t targetFlags = packet.readUInt32();
// Returns false if the packed guid can't be read, otherwise reads and optionally captures it.
auto readPackedGuidCond = [&](uint32_t flag, bool capture) -> 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 // TbcPacketParsers::parseSpellStart — TBC 2.4.3 SMSG_SPELL_START
// //
// TBC uses full uint64 GUIDs for casterGuid and casterUnit. // TBC uses full uint64 GUIDs for casterGuid and casterUnit.
@ -1243,7 +1303,6 @@ bool TbcPacketParsers::parseMailList(network::Packet& packet, std::vector<MailMe
// ============================================================================ // ============================================================================
bool TbcPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData& data) { bool TbcPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData& data) {
data = SpellStartData{}; data = SpellStartData{};
const size_t startPos = packet.getReadPos();
if (packet.getSize() - packet.getReadPos() < 22) return false; if (packet.getSize() - packet.getReadPos() < 22) return false;
data.casterGuid = packet.readUInt64(); // full GUID (object) data.casterGuid = packet.readUInt64(); // full GUID (object)
@ -1253,23 +1312,19 @@ bool TbcPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData&
data.castFlags = packet.readUInt32(); data.castFlags = packet.readUInt32();
data.castTime = packet.readUInt32(); data.castTime = packet.readUInt32();
if (packet.getReadPos() + 4 > packet.getSize()) { // SpellCastTargets: consume ALL target payload types to keep the read position
LOG_WARNING("[TBC] Spell start: missing targetFlags"); // aligned for any bytes the caller may parse after this (ammo, etc.).
packet.setReadPos(startPos); // The previous code only read UNIT(0x02)/OBJECT(0x800) target GUIDs and left
return false; // 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(); LOG_DEBUG("[TBC] Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms",
const bool needsTargetGuid = (targetFlags & 0x02) || (targetFlags & 0x800); // UNIT/OBJECT " targetGuid=0x", std::hex, data.targetGuid, std::dec);
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");
return true; return true;
} }

View file

@ -388,10 +388,11 @@ void CameraController::update(float deltaTime) {
if (mounted_) sitting = false; if (mounted_) sitting = false;
xKeyWasDown = xDown; 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); bool rDown = !uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_R);
if (rDown && !rKeyWasDown) { if (rDown && !rKeyWasDown) {
reset(); resetAngles();
} }
rKeyWasDown = rDown; rKeyWasDown = rDown;
@ -1941,6 +1942,14 @@ void CameraController::processMouseButton(const SDL_MouseButtonEvent& event) {
mouseButtonDown = anyDown; mouseButtonDown = anyDown;
} }
void CameraController::resetAngles() {
if (!camera) return;
yaw = defaultYaw;
facingYaw = defaultYaw;
pitch = defaultPitch;
camera->setRotation(yaw, pitch);
}
void CameraController::reset() { void CameraController::reset() {
if (!camera) { if (!camera) {
return; return;