diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 16374768..40c4d34b 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -116,6 +116,25 @@ bool hasFullPackedGuid(const network::Packet& packet) { return packet.getSize() - packet.getReadPos() >= guidBytes; } +CombatTextEntry::Type combatTextTypeFromSpellMissInfo(uint8_t missInfo) { + switch (missInfo) { + case 0: return CombatTextEntry::MISS; + case 1: return CombatTextEntry::DODGE; + case 2: return CombatTextEntry::PARRY; + case 3: return CombatTextEntry::BLOCK; + case 4: return CombatTextEntry::EVADE; + case 5: return CombatTextEntry::IMMUNE; + case 6: return CombatTextEntry::DEFLECT; + case 7: return CombatTextEntry::ABSORB; + case 8: return CombatTextEntry::RESIST; + case 9: // Some cores encode SPELL_MISS_IMMUNE2 as 9. + case 10: // Others encode SPELL_MISS_IMMUNE2 as 10. + return CombatTextEntry::IMMUNE; + case 11: return CombatTextEntry::REFLECT; + default: return CombatTextEntry::MISS; + } +} + std::string formatCopperAmount(uint32_t amount) { uint32_t gold = amount / 10000; uint32_t silver = (amount / 100) % 100; @@ -2741,39 +2760,56 @@ void GameHandler::handlePacket(network::Packet& packet) { uint64_t casterGuid = readSpellMissGuid(); if (packet.getSize() - packet.getReadPos() < 5) break; /*uint8_t unk =*/ packet.readUInt8(); - uint32_t count = packet.readUInt32(); - count = std::min(count, 32u); - for (uint32_t i = 0; i < count; ++i) { + const uint32_t rawCount = packet.readUInt32(); + if (rawCount > 128) { + LOG_WARNING("SMSG_SPELLLOGMISS: miss count capped (requested=", rawCount, ")"); + } + const uint32_t storedLimit = std::min(rawCount, 128u); + + struct SpellMissLogEntry { + uint64_t victimGuid = 0; + uint8_t missInfo = 0; + }; + std::vector parsedMisses; + parsedMisses.reserve(storedLimit); + + bool truncated = false; + for (uint32_t i = 0; i < rawCount; ++i) { if (packet.getSize() - packet.getReadPos() < (spellMissUsesFullGuid ? 9u : 2u) || (!spellMissUsesFullGuid && !hasFullPackedGuid(packet))) { - packet.setReadPos(packet.getSize()); break; + truncated = true; + break; } - uint64_t victimGuid = readSpellMissGuid(); - if (packet.getSize() - packet.getReadPos() < 1) break; - uint8_t missInfo = packet.readUInt8(); + const uint64_t victimGuid = readSpellMissGuid(); + if (packet.getSize() - packet.getReadPos() < 1) { + truncated = true; + break; + } + const uint8_t missInfo = packet.readUInt8(); // REFLECT (11): extra uint32 reflectSpellId + uint8 reflectResult if (missInfo == 11) { if (packet.getSize() - packet.getReadPos() >= 5) { /*uint32_t reflectSpellId =*/ packet.readUInt32(); /*uint8_t reflectResult =*/ packet.readUInt8(); } else { - packet.setReadPos(packet.getSize()); + truncated = true; break; } } - static const CombatTextEntry::Type missTypes[] = { - CombatTextEntry::MISS, // 0=MISS - CombatTextEntry::DODGE, // 1=DODGE - CombatTextEntry::PARRY, // 2=PARRY - CombatTextEntry::BLOCK, // 3=BLOCK - CombatTextEntry::EVADE, // 4=EVADE - CombatTextEntry::IMMUNE, // 5=IMMUNE - CombatTextEntry::DEFLECT, // 6=DEFLECT - CombatTextEntry::ABSORB, // 7=ABSORB - CombatTextEntry::RESIST, // 8=RESIST - }; - CombatTextEntry::Type ct = (missInfo < 9) ? missTypes[missInfo] - : (missInfo == 11 ? CombatTextEntry::REFLECT : CombatTextEntry::MISS); + if (i < storedLimit) { + parsedMisses.push_back({victimGuid, missInfo}); + } + } + + if (truncated) { + packet.setReadPos(packet.getSize()); + break; + } + + for (const auto& miss : parsedMisses) { + const uint64_t victimGuid = miss.victimGuid; + const uint8_t missInfo = miss.missInfo; + CombatTextEntry::Type ct = combatTextTypeFromSpellMissInfo(missInfo); if (casterGuid == playerGuid) { // We cast a spell and it missed the target addCombatText(ct, 0, spellId, true, 0, casterGuid, victimGuid); @@ -4090,11 +4126,13 @@ void GameHandler::handlePacket(network::Packet& packet) { return (packet.getSize() - packet.getReadPos() >= 8) ? packet.readUInt64() : 0; return UpdateObjectParser::readPackedGuid(packet); }; - if (packet.getSize() - packet.getReadPos() < (energizeTbc ? 8u : 1u)) { + if (packet.getSize() - packet.getReadPos() < (energizeTbc ? 8u : 1u) + || (!energizeTbc && !hasFullPackedGuid(packet))) { packet.setReadPos(packet.getSize()); break; } uint64_t victimGuid = readEnergizeGuid(); - if (packet.getSize() - packet.getReadPos() < (energizeTbc ? 8u : 1u)) { + if (packet.getSize() - packet.getReadPos() < (energizeTbc ? 8u : 1u) + || (!energizeTbc && !hasFullPackedGuid(packet))) { packet.setReadPos(packet.getSize()); break; } uint64_t casterGuid = readEnergizeGuid(); @@ -6489,7 +6527,10 @@ void GameHandler::handlePacket(network::Packet& packet) { } uint64_t ikVictim = ikUsesFullGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - uint32_t ikSpell = (ik_rem() >= 4) ? packet.readUInt32() : 0; + if (ik_rem() < 4) { + packet.setReadPos(packet.getSize()); break; + } + uint32_t ikSpell = packet.readUInt32(); // Show kill/death feedback for the local player if (ikCaster == playerGuid) { addCombatText(CombatTextEntry::INSTAKILL, 0, ikSpell, true, 0, ikCaster, ikVictim); @@ -7105,11 +7146,19 @@ void GameHandler::handlePacket(network::Packet& packet) { ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); if (rl_rem() < 4) { packet.setReadPos(packet.getSize()); break; } uint32_t spellId = packet.readUInt32(); + // Resist payload includes: + // float resistFactor + uint32 targetResistance + uint32 resistedValue. + // Require the full payload so truncated packets cannot synthesize + // zero-value resist events. + if (rl_rem() < 12) { packet.setReadPos(packet.getSize()); break; } + /*float resistFactor =*/ packet.readFloat(); + /*uint32_t targetRes =*/ packet.readUInt32(); + int32_t resistedAmount = static_cast(packet.readUInt32()); // Show RESIST when the player is involved on either side. - if (victimGuid == playerGuid) { - addCombatText(CombatTextEntry::RESIST, 0, spellId, false, 0, attackerGuid, victimGuid); - } else if (attackerGuid == playerGuid) { - addCombatText(CombatTextEntry::RESIST, 0, spellId, true, 0, attackerGuid, victimGuid); + if (resistedAmount > 0 && victimGuid == playerGuid) { + addCombatText(CombatTextEntry::RESIST, resistedAmount, spellId, false, 0, attackerGuid, victimGuid); + } else if (resistedAmount > 0 && attackerGuid == playerGuid) { + addCombatText(CombatTextEntry::RESIST, resistedAmount, spellId, true, 0, attackerGuid, victimGuid); } packet.setReadPos(packet.getSize()); break; @@ -14381,8 +14430,11 @@ void GameHandler::addCombatText(CombatTextEntry::Type type, int32_t amount, uint log.spellId = spellId; log.isPlayerSource = isPlayerSource; log.timestamp = std::time(nullptr); + // If the caller provided an explicit destination GUID but left source GUID as 0, + // preserve "unknown/no source" (e.g. environmental damage) instead of + // backfilling from current target. uint64_t effectiveSrc = (srcGuid != 0) ? srcGuid - : (isPlayerSource ? playerGuid : targetGuid); + : ((dstGuid != 0) ? 0 : (isPlayerSource ? playerGuid : targetGuid)); uint64_t effectiveDst = (dstGuid != 0) ? dstGuid : (isPlayerSource ? targetGuid : playerGuid); log.sourceName = lookupName(effectiveSrc); @@ -17224,17 +17276,6 @@ void GameHandler::handleSpellGo(network::Packet& packet) { // Preserve spellId and actual participants for spell-go miss results. // This keeps the persistent combat log aligned with the later GUID fixes. if (!data.missTargets.empty()) { - static const CombatTextEntry::Type missTypes[] = { - CombatTextEntry::MISS, // 0=MISS - CombatTextEntry::DODGE, // 1=DODGE - CombatTextEntry::PARRY, // 2=PARRY - CombatTextEntry::BLOCK, // 3=BLOCK - CombatTextEntry::EVADE, // 4=EVADE - CombatTextEntry::IMMUNE, // 5=IMMUNE - CombatTextEntry::DEFLECT, // 6=DEFLECT - CombatTextEntry::ABSORB, // 7=ABSORB - CombatTextEntry::RESIST, // 8=RESIST - }; const uint64_t spellCasterGuid = data.casterUnit != 0 ? data.casterUnit : data.casterGuid; const bool playerIsCaster = (spellCasterGuid == playerGuid); @@ -17242,8 +17283,7 @@ void GameHandler::handleSpellGo(network::Packet& packet) { if (!playerIsCaster && m.targetGuid != playerGuid) { continue; } - CombatTextEntry::Type ct = (m.missType < 9) ? missTypes[m.missType] - : (m.missType == 11 ? CombatTextEntry::REFLECT : CombatTextEntry::MISS); + CombatTextEntry::Type ct = combatTextTypeFromSpellMissInfo(m.missType); addCombatText(ct, 0, data.spellId, playerIsCaster, 0, spellCasterGuid, m.targetGuid); } } diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 046839d0..74935162 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -355,11 +355,21 @@ network::Packet ClassicPacketParsers::buildUseItem(uint8_t bagIndex, uint8_t slo // + uint16(targetFlags) [+ PackedGuid(unitTarget) if TARGET_FLAG_UNIT] // ============================================================================ bool ClassicPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData& data) { + data = SpellStartData{}; + auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; + const size_t startPos = packet.getReadPos(); if (rem() < 2) return false; + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.casterGuid = UpdateObjectParser::readPackedGuid(packet); - if (rem() < 1) return false; + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.casterUnit = UpdateObjectParser::readPackedGuid(packet); // uint8 castCount + uint32 spellId + uint16 castFlags + uint32 castTime = 11 bytes @@ -370,10 +380,18 @@ bool ClassicPacketParsers::parseSpellStart(network::Packet& packet, SpellStartDa data.castTime = packet.readUInt32(); // SpellCastTargets: uint16 targetFlags in Vanilla (uint32 in TBC/WotLK) - if (rem() < 2) return true; + 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)) && rem() >= 1) { + if ((targetFlags & 0x02) || (targetFlags & 0x800)) { + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.targetGuid = UpdateObjectParser::readPackedGuid(packet); } @@ -395,11 +413,16 @@ bool ClassicPacketParsers::parseSpellStart(network::Packet& packet, SpellStartDa // + uint8(missCount) + [PackedGuid(missTarget) + uint8(missType)] × missCount // ============================================================================ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) { + // Always reset output to avoid stale targets when callers reuse buffers. + data = SpellGoData{}; + auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; + const size_t startPos = packet.getReadPos(); if (rem() < 2) return false; + if (!hasFullPackedGuid(packet)) return false; data.casterGuid = UpdateObjectParser::readPackedGuid(packet); - if (rem() < 1) return false; + if (!hasFullPackedGuid(packet)) return false; data.casterUnit = UpdateObjectParser::readPackedGuid(packet); // uint8 castCount + uint32 spellId + uint16 castFlags = 7 bytes @@ -408,52 +431,84 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da data.spellId = packet.readUInt32(); data.castFlags = packet.readUInt16(); // uint16 in Vanilla (uint32 in TBC/WotLK) - // Hit targets - if (rem() < 1) return true; - data.hitCount = packet.readUInt8(); - // Cap hit count to prevent OOM from huge target lists - if (data.hitCount > 128) { - LOG_WARNING("[Classic] Spell go: hitCount capped (requested=", (int)data.hitCount, ")"); - data.hitCount = 128; + // hitCount is mandatory in SMSG_SPELL_GO. Missing byte means truncation. + if (rem() < 1) { + LOG_WARNING("[Classic] Spell go: missing hitCount after fixed fields"); + packet.setReadPos(startPos); + return false; } - data.hitTargets.reserve(data.hitCount); - for (uint8_t i = 0; i < data.hitCount && rem() >= 1; ++i) { - data.hitTargets.push_back(UpdateObjectParser::readPackedGuid(packet)); + const uint8_t rawHitCount = packet.readUInt8(); + if (rawHitCount > 128) { + LOG_WARNING("[Classic] Spell go: hitCount capped (requested=", (int)rawHitCount, ")"); } - // Check if we read all expected hits - if (data.hitTargets.size() < data.hitCount) { - LOG_WARNING("[Classic] Spell go: truncated hit targets at index ", (int)data.hitTargets.size(), - "/", (int)data.hitCount); - data.hitCount = data.hitTargets.size(); + const uint8_t storedHitLimit = std::min(rawHitCount, 128); + data.hitTargets.reserve(storedHitLimit); + bool truncatedTargets = false; + for (uint16_t i = 0; i < rawHitCount; ++i) { + if (!hasFullPackedGuid(packet)) { + LOG_WARNING("[Classic] Spell go: truncated hit targets at index ", i, + "/", (int)rawHitCount); + truncatedTargets = true; + break; + } + const uint64_t targetGuid = UpdateObjectParser::readPackedGuid(packet); + if (i < storedHitLimit) { + data.hitTargets.push_back(targetGuid); + } } + if (truncatedTargets) { + packet.setReadPos(startPos); + return false; + } + data.hitCount = static_cast(data.hitTargets.size()); - // Miss targets - if (rem() < 1) return true; - data.missCount = packet.readUInt8(); - // Cap miss count to prevent OOM - if (data.missCount > 128) { - LOG_WARNING("[Classic] Spell go: missCount capped (requested=", (int)data.missCount, ")"); - data.missCount = 128; + // missCount is mandatory in SMSG_SPELL_GO. Missing byte means truncation. + if (rem() < 1) { + LOG_WARNING("[Classic] Spell go: missing missCount after hit target list"); + packet.setReadPos(startPos); + return false; } - data.missTargets.reserve(data.missCount); - for (uint8_t i = 0; i < data.missCount && rem() >= 2; ++i) { + const uint8_t rawMissCount = packet.readUInt8(); + if (rawMissCount > 128) { + LOG_WARNING("[Classic] Spell go: missCount capped (requested=", (int)rawMissCount, ")"); + } + const uint8_t storedMissLimit = std::min(rawMissCount, 128); + data.missTargets.reserve(storedMissLimit); + for (uint16_t i = 0; i < rawMissCount; ++i) { + if (!hasFullPackedGuid(packet)) { + LOG_WARNING("[Classic] Spell go: truncated miss targets at index ", i, + "/", (int)rawMissCount); + truncatedTargets = true; + break; + } SpellGoMissEntry m; m.targetGuid = UpdateObjectParser::readPackedGuid(packet); - if (rem() < 1) break; + if (rem() < 1) { + LOG_WARNING("[Classic] Spell go: missing missType at miss index ", i, + "/", (int)rawMissCount); + truncatedTargets = true; + break; + } m.missType = packet.readUInt8(); if (m.missType == 11) { - if (rem() < 5) break; + if (rem() < 5) { + LOG_WARNING("[Classic] Spell go: truncated reflect payload at miss index ", i, + "/", (int)rawMissCount); + truncatedTargets = true; + break; + } (void)packet.readUInt32(); (void)packet.readUInt8(); } - data.missTargets.push_back(m); + if (i < storedMissLimit) { + data.missTargets.push_back(m); + } } - // Check if we read all expected misses - if (data.missTargets.size() < data.missCount) { - LOG_WARNING("[Classic] Spell go: truncated miss targets at index ", (int)data.missTargets.size(), - "/", (int)data.missCount); - data.missCount = data.missTargets.size(); + if (truncatedTargets) { + packet.setReadPos(startPos); + return false; } + data.missCount = static_cast(data.missTargets.size()); LOG_DEBUG("[Classic] Spell go: spell=", data.spellId, " hits=", (int)data.hitCount, " misses=", (int)data.missCount); @@ -471,19 +526,42 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da // + uint32(victimState) + int32(overkill) [+ uint32(blocked)] // ============================================================================ bool ClassicPacketParsers::parseAttackerStateUpdate(network::Packet& packet, AttackerStateUpdateData& data) { + data = AttackerStateUpdateData{}; + auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; if (rem() < 5) return false; // hitInfo(4) + at least GUID mask byte(1) + const size_t startPos = packet.getReadPos(); data.hitInfo = packet.readUInt32(); + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.attackerGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla - if (rem() < 1) return false; + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.targetGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla - if (rem() < 5) return false; // int32 totalDamage + uint8 subDamageCount + if (rem() < 5) { + packet.setReadPos(startPos); + return false; + } // int32 totalDamage + uint8 subDamageCount data.totalDamage = static_cast(packet.readUInt32()); data.subDamageCount = packet.readUInt8(); - for (uint8_t i = 0; i < data.subDamageCount && rem() >= 20; ++i) { + const uint8_t maxSubDamageCount = static_cast(std::min(rem() / 20, 64)); + if (data.subDamageCount > maxSubDamageCount) { + data.subDamageCount = maxSubDamageCount; + } + + data.subDamages.reserve(data.subDamageCount); + for (uint8_t i = 0; i < data.subDamageCount; ++i) { + if (rem() < 20) { + packet.setReadPos(startPos); + return false; + } SubDamage sub; sub.schoolMask = packet.readUInt32(); sub.damage = packet.readFloat(); @@ -492,8 +570,12 @@ bool ClassicPacketParsers::parseAttackerStateUpdate(network::Packet& packet, Att sub.resisted = packet.readUInt32(); data.subDamages.push_back(sub); } + data.subDamageCount = static_cast(data.subDamages.size()); - if (rem() < 8) return true; + if (rem() < 8) { + packet.setReadPos(startPos); + return false; + } data.victimState = packet.readUInt32(); data.overkill = static_cast(packet.readUInt32()); diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index d218926a..308873f1 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -1234,6 +1234,8 @@ bool TbcPacketParsers::parseMailList(network::Packet& packet, std::vector packet.getSize()) { + LOG_WARNING("[TBC] Spell start: missing targetFlags"); + packet.setReadPos(startPos); + return false; + } + + 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"); @@ -1261,6 +1272,10 @@ bool TbcPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData& // WotLK uses packed GUIDs and adds a timestamp (u32) after castFlags. // ============================================================================ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) { + // Always reset output to avoid stale targets when callers reuse buffers. + data = SpellGoData{}; + + const size_t startPos = packet.getReadPos(); // Fixed header before hit/miss lists: // casterGuid(u64) + casterUnit(u64) + castCount(u8) + spellId(u32) + castFlags(u32) if (packet.getSize() - packet.getReadPos() < 25) return false; @@ -1273,55 +1288,77 @@ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) // NOTE: NO timestamp field here in TBC (WotLK added packet.readUInt32()) if (packet.getReadPos() >= packet.getSize()) { - LOG_DEBUG("[TBC] Spell go: spell=", data.spellId, " (no hit data)"); - return true; + LOG_WARNING("[TBC] Spell go: missing hitCount after fixed fields"); + packet.setReadPos(startPos); + return false; } - data.hitCount = packet.readUInt8(); - // Cap hit count to prevent OOM from huge target lists - if (data.hitCount > 128) { - LOG_WARNING("[TBC] Spell go: hitCount capped (requested=", (int)data.hitCount, ")"); - data.hitCount = 128; + const uint8_t rawHitCount = packet.readUInt8(); + if (rawHitCount > 128) { + LOG_WARNING("[TBC] Spell go: hitCount capped (requested=", (int)rawHitCount, ")"); } - data.hitTargets.reserve(data.hitCount); - for (uint8_t i = 0; i < data.hitCount && packet.getReadPos() + 8 <= packet.getSize(); ++i) { - data.hitTargets.push_back(packet.readUInt64()); // full GUID in TBC - } - // Check if we read all expected hits - if (data.hitTargets.size() < data.hitCount) { - LOG_WARNING("[TBC] Spell go: truncated hit targets at index ", (int)data.hitTargets.size(), - "/", (int)data.hitCount); - data.hitCount = data.hitTargets.size(); - } - - if (packet.getReadPos() < packet.getSize()) { - data.missCount = packet.readUInt8(); - // Cap miss count to prevent OOM - if (data.missCount > 128) { - LOG_WARNING("[TBC] Spell go: missCount capped (requested=", (int)data.missCount, ")"); - data.missCount = 128; + const uint8_t storedHitLimit = std::min(rawHitCount, 128); + data.hitTargets.reserve(storedHitLimit); + bool truncatedTargets = false; + for (uint16_t i = 0; i < rawHitCount; ++i) { + if (packet.getReadPos() + 8 > packet.getSize()) { + LOG_WARNING("[TBC] Spell go: truncated hit targets at index ", i, + "/", (int)rawHitCount); + truncatedTargets = true; + break; } - data.missTargets.reserve(data.missCount); - for (uint8_t i = 0; i < data.missCount && packet.getReadPos() + 9 <= packet.getSize(); ++i) { - SpellGoMissEntry m; - m.targetGuid = packet.readUInt64(); // full GUID in TBC - m.missType = packet.readUInt8(); - if (m.missType == 11) { - if (packet.getReadPos() + 5 > packet.getSize()) { - break; - } - (void)packet.readUInt32(); - (void)packet.readUInt8(); + const uint64_t targetGuid = packet.readUInt64(); // full GUID in TBC + if (i < storedHitLimit) { + data.hitTargets.push_back(targetGuid); + } + } + if (truncatedTargets) { + packet.setReadPos(startPos); + return false; + } + data.hitCount = static_cast(data.hitTargets.size()); + + if (packet.getReadPos() >= packet.getSize()) { + LOG_WARNING("[TBC] Spell go: missing missCount after hit target list"); + packet.setReadPos(startPos); + return false; + } + + const uint8_t rawMissCount = packet.readUInt8(); + if (rawMissCount > 128) { + LOG_WARNING("[TBC] Spell go: missCount capped (requested=", (int)rawMissCount, ")"); + } + const uint8_t storedMissLimit = std::min(rawMissCount, 128); + data.missTargets.reserve(storedMissLimit); + for (uint16_t i = 0; i < rawMissCount; ++i) { + if (packet.getReadPos() + 9 > packet.getSize()) { + LOG_WARNING("[TBC] Spell go: truncated miss targets at index ", i, + "/", (int)rawMissCount); + truncatedTargets = true; + break; + } + SpellGoMissEntry m; + m.targetGuid = packet.readUInt64(); // full GUID in TBC + m.missType = packet.readUInt8(); + if (m.missType == 11) { + if (packet.getReadPos() + 5 > packet.getSize()) { + LOG_WARNING("[TBC] Spell go: truncated reflect payload at miss index ", i, + "/", (int)rawMissCount); + truncatedTargets = true; + break; } + (void)packet.readUInt32(); + (void)packet.readUInt8(); + } + if (i < storedMissLimit) { data.missTargets.push_back(m); } - // Check if we read all expected misses - if (data.missTargets.size() < data.missCount) { - LOG_WARNING("[TBC] Spell go: truncated miss targets at index ", (int)data.missTargets.size(), - "/", (int)data.missCount); - data.missCount = data.missTargets.size(); - } } + if (truncatedTargets) { + packet.setReadPos(startPos); + return false; + } + data.missCount = static_cast(data.missTargets.size()); LOG_DEBUG("[TBC] Spell go: spell=", data.spellId, " hits=", (int)data.hitCount, " misses=", (int)data.missCount); @@ -1369,15 +1406,33 @@ bool TbcPacketParsers::parseCastFailed(network::Packet& packet, CastFailedData& // would mis-parse TBC's GUIDs and corrupt all subsequent damage fields. // ============================================================================ bool TbcPacketParsers::parseAttackerStateUpdate(network::Packet& packet, AttackerStateUpdateData& data) { - if (packet.getSize() - packet.getReadPos() < 21) return false; + data = AttackerStateUpdateData{}; - data.hitInfo = packet.readUInt32(); - data.attackerGuid = packet.readUInt64(); // full GUID in TBC - data.targetGuid = packet.readUInt64(); // full GUID in TBC - data.totalDamage = static_cast(packet.readUInt32()); + const size_t startPos = packet.getReadPos(); + auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; + + // Fixed fields before sub-damage list: + // hitInfo(4) + attackerGuid(8) + targetGuid(8) + totalDamage(4) + subDamageCount(1) = 25 bytes + if (rem() < 25) return false; + + data.hitInfo = packet.readUInt32(); + data.attackerGuid = packet.readUInt64(); // full GUID in TBC + data.targetGuid = packet.readUInt64(); // full GUID in TBC + data.totalDamage = static_cast(packet.readUInt32()); data.subDamageCount = packet.readUInt8(); + // Clamp to what can fit in the remaining payload (20 bytes per sub-damage entry). + const uint8_t maxSubDamageCount = static_cast(std::min(rem() / 20, 64)); + if (data.subDamageCount > maxSubDamageCount) { + data.subDamageCount = maxSubDamageCount; + } + + data.subDamages.reserve(data.subDamageCount); for (uint8_t i = 0; i < data.subDamageCount; ++i) { + if (rem() < 20) { + packet.setReadPos(startPos); + return false; + } SubDamage sub; sub.schoolMask = packet.readUInt32(); sub.damage = packet.readFloat(); @@ -1387,10 +1442,17 @@ bool TbcPacketParsers::parseAttackerStateUpdate(network::Packet& packet, Attacke data.subDamages.push_back(sub); } + data.subDamageCount = static_cast(data.subDamages.size()); + + // victimState + overkill are part of the expected payload. + if (rem() < 8) { + packet.setReadPos(startPos); + return false; + } data.victimState = packet.readUInt32(); data.overkill = static_cast(packet.readUInt32()); - if (packet.getReadPos() < packet.getSize()) { + if (rem() >= 4) { data.blocked = packet.readUInt32(); } @@ -1406,20 +1468,28 @@ bool TbcPacketParsers::parseAttackerStateUpdate(network::Packet& packet, Attacke // TBC uses full uint64 GUIDs; WotLK uses packed GUIDs. // ============================================================================ bool TbcPacketParsers::parseSpellDamageLog(network::Packet& packet, SpellDamageLogData& data) { - if (packet.getSize() - packet.getReadPos() < 29) return false; + // Fixed TBC payload size: + // targetGuid(8) + attackerGuid(8) + spellId(4) + damage(4) + schoolMask(1) + // + absorbed(4) + resisted(4) + periodicLog(1) + unused(1) + blocked(4) + flags(4) + // = 43 bytes + // Some servers append additional trailing fields; consume the canonical minimum + // and leave any extension bytes unread. + if (packet.getSize() - packet.getReadPos() < 43) return false; - data.targetGuid = packet.readUInt64(); // full GUID in TBC + data = SpellDamageLogData{}; + + data.targetGuid = packet.readUInt64(); // full GUID in TBC data.attackerGuid = packet.readUInt64(); // full GUID in TBC - data.spellId = packet.readUInt32(); - data.damage = packet.readUInt32(); - data.schoolMask = packet.readUInt8(); - data.absorbed = packet.readUInt32(); - data.resisted = packet.readUInt32(); + data.spellId = packet.readUInt32(); + data.damage = packet.readUInt32(); + data.schoolMask = packet.readUInt8(); + data.absorbed = packet.readUInt32(); + data.resisted = packet.readUInt32(); uint8_t periodicLog = packet.readUInt8(); (void)periodicLog; - packet.readUInt8(); // unused - packet.readUInt32(); // blocked + packet.readUInt8(); // unused + packet.readUInt32(); // blocked uint32_t flags = packet.readUInt32(); data.isCrit = (flags & 0x02) != 0; @@ -1437,13 +1507,17 @@ bool TbcPacketParsers::parseSpellDamageLog(network::Packet& packet, SpellDamageL // TBC uses full uint64 GUIDs; WotLK uses packed GUIDs. // ============================================================================ bool TbcPacketParsers::parseSpellHealLog(network::Packet& packet, SpellHealLogData& data) { - if (packet.getSize() - packet.getReadPos() < 25) return false; + // Fixed payload is 28 bytes; many cores append crit flag (1 byte). + // targetGuid(8) + casterGuid(8) + spellId(4) + heal(4) + overheal(4) + if (packet.getSize() - packet.getReadPos() < 28) return false; - data.targetGuid = packet.readUInt64(); // full GUID in TBC - data.casterGuid = packet.readUInt64(); // full GUID in TBC - data.spellId = packet.readUInt32(); - data.heal = packet.readUInt32(); - data.overheal = packet.readUInt32(); + data = SpellHealLogData{}; + + data.targetGuid = packet.readUInt64(); // full GUID in TBC + data.casterGuid = packet.readUInt64(); // full GUID in TBC + data.spellId = packet.readUInt32(); + data.heal = packet.readUInt32(); + data.overheal = packet.readUInt32(); // TBC has no absorbed field in SMSG_SPELLHEALLOG; skip crit flag if (packet.getReadPos() < packet.getSize()) { uint8_t critFlag = packet.readUInt8(); diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 659003d7..23efb3bc 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -19,6 +19,22 @@ namespace { inline uint16_t bswap16(uint16_t v) { return static_cast(((v & 0xFF00u) >> 8) | ((v & 0x00FFu) << 8)); } + + bool hasFullPackedGuid(const wowee::network::Packet& packet) { + if (packet.getReadPos() >= packet.getSize()) { + return false; + } + + const auto& rawData = packet.getData(); + const uint8_t mask = rawData[packet.getReadPos()]; + size_t guidBytes = 1; + for (int bit = 0; bit < 8; ++bit) { + if ((mask & (1u << bit)) != 0) { + ++guidBytes; + } + } + return packet.getSize() - packet.getReadPos() >= guidBytes; + } } namespace wowee { @@ -3156,12 +3172,10 @@ bool MonsterMoveParser::parse(network::Packet& packet, MonsterMoveData& data) { if (pointCount == 0) return true; - // Cap pointCount to prevent excessive iteration from malformed packets + // Reject extreme point counts from malformed packets. constexpr uint32_t kMaxSplinePoints = 1000; if (pointCount > kMaxSplinePoints) { - LOG_WARNING("SMSG_MONSTER_MOVE: pointCount=", pointCount, " exceeds max ", kMaxSplinePoints, - " (guid=0x", std::hex, data.guid, std::dec, "), capping"); - pointCount = kMaxSplinePoints; + return false; } // Catmullrom or Flying → all waypoints stored as absolute float3 (uncompressed). @@ -3169,20 +3183,27 @@ bool MonsterMoveParser::parse(network::Packet& packet, MonsterMoveData& data) { bool uncompressed = (data.splineFlags & (0x00080000 | 0x00002000)) != 0; if (uncompressed) { + const size_t requiredBytes = static_cast(pointCount) * 12ull; + if (packet.getReadPos() + requiredBytes > packet.getSize()) return false; + // Read last point as destination // Skip to last point: each point is 12 bytes for (uint32_t i = 0; i < pointCount - 1; i++) { - if (packet.getReadPos() + 12 > packet.getSize()) return true; + if (packet.getReadPos() + 12 > packet.getSize()) return false; packet.readFloat(); packet.readFloat(); packet.readFloat(); } - if (packet.getReadPos() + 12 > packet.getSize()) return true; + if (packet.getReadPos() + 12 > packet.getSize()) return false; data.destX = packet.readFloat(); data.destY = packet.readFloat(); data.destZ = packet.readFloat(); data.hasDest = true; } else { // Compressed: first 3 floats are the destination (final point) - if (packet.getReadPos() + 12 > packet.getSize()) return true; + size_t requiredBytes = 12; + if (pointCount > 1) { + requiredBytes += static_cast(pointCount - 1) * 4ull; + } + if (packet.getReadPos() + requiredBytes > packet.getSize()) return false; data.destX = packet.readFloat(); data.destY = packet.readFloat(); data.destZ = packet.readFloat(); @@ -3266,16 +3287,19 @@ bool MonsterMoveParser::parseVanilla(network::Packet& packet, MonsterMoveData& d if (pointCount == 0) return true; - // Cap pointCount to prevent excessive iteration from malformed packets + // Reject extreme point counts from malformed packets. constexpr uint32_t kMaxSplinePoints = 1000; if (pointCount > kMaxSplinePoints) { - LOG_WARNING("SMSG_MONSTER_MOVE(Vanilla): pointCount=", pointCount, " exceeds max ", kMaxSplinePoints, - " (guid=0x", std::hex, data.guid, std::dec, "), capping"); - pointCount = kMaxSplinePoints; + return false; } + size_t requiredBytes = 12; + if (pointCount > 1) { + requiredBytes += static_cast(pointCount - 1) * 4ull; + } + if (packet.getReadPos() + requiredBytes > packet.getSize()) return false; + // First float[3] is destination. - if (packet.getReadPos() + 12 > packet.getSize()) return true; data.destX = packet.readFloat(); data.destY = packet.readFloat(); data.destZ = packet.readFloat(); @@ -3285,9 +3309,8 @@ bool MonsterMoveParser::parseVanilla(network::Packet& packet, MonsterMoveData& d if (pointCount > 1) { size_t skipBytes = static_cast(pointCount - 1) * 4; size_t newPos = packet.getReadPos() + skipBytes; - if (newPos <= packet.getSize()) { - packet.setReadPos(newPos); - } + if (newPos > packet.getSize()) return false; + packet.setReadPos(newPos); } LOG_DEBUG("MonsterMove(turtle): guid=0x", std::hex, data.guid, std::dec, @@ -3327,7 +3350,15 @@ bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpda size_t startPos = packet.getReadPos(); data.hitInfo = packet.readUInt32(); + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.attackerGuid = UpdateObjectParser::readPackedGuid(packet); + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.targetGuid = UpdateObjectParser::readPackedGuid(packet); // Validate totalDamage + subDamageCount can be read (5 bytes) @@ -3339,15 +3370,15 @@ bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpda data.totalDamage = static_cast(packet.readUInt32()); data.subDamageCount = packet.readUInt8(); - // Cap subDamageCount: each entry is 20 bytes. If the claimed count + // Cap subDamageCount: each entry is 20 bytes. If the claimed count // exceeds what the remaining bytes can hold, a GUID was mis-parsed // (off by one byte), causing the school-mask byte to be read as count. - // In that case silently clamp to the number of full entries that fit. + // In that case clamp to the number of full entries that fit. { size_t remaining = packet.getSize() - packet.getReadPos(); size_t maxFit = remaining / 20; if (data.subDamageCount > maxFit) { - data.subDamageCount = static_cast(maxFit > 0 ? 1 : 0); + data.subDamageCount = static_cast(std::min(maxFit, 64)); } else if (data.subDamageCount > 64) { data.subDamageCount = 64; } @@ -3399,11 +3430,22 @@ bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpda } bool SpellDamageLogParser::parse(network::Packet& packet, SpellDamageLogData& data) { - // Upfront validation: packed GUIDs(1-8 each) + spellId(4) + damage(4) + overkill(4) + schoolMask(1) + absorbed(4) + resisted(4) = 30 bytes minimum - if (packet.getSize() - packet.getReadPos() < 30) return false; + // Upfront validation: + // packed GUIDs(1-8 each) + spellId(4) + damage(4) + overkill(4) + schoolMask(1) + // + absorbed(4) + resisted(4) + periodicLog(1) + unused(1) + blocked(4) + flags(4) + // = 33 bytes minimum. + if (packet.getSize() - packet.getReadPos() < 33) return false; size_t startPos = packet.getReadPos(); + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.targetGuid = UpdateObjectParser::readPackedGuid(packet); + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.attackerGuid = UpdateObjectParser::readPackedGuid(packet); // Validate core fields (spellId + damage + overkill + schoolMask + absorbed + resisted = 21 bytes) @@ -3419,11 +3461,11 @@ bool SpellDamageLogParser::parse(network::Packet& packet, SpellDamageLogData& da data.absorbed = packet.readUInt32(); data.resisted = packet.readUInt32(); - // Skip remaining fields (periodicLog + unused + blocked + flags = 10 bytes) + // Remaining fields are required for a complete event. + // Reject truncated packets so we do not emit partial/incorrect combat entries. if (packet.getSize() - packet.getReadPos() < 10) { - LOG_WARNING("SpellDamageLog: truncated trailing fields"); - data.isCrit = false; - return true; + packet.setReadPos(startPos); + return false; } uint8_t periodicLog = packet.readUInt8(); @@ -3445,7 +3487,15 @@ bool SpellHealLogParser::parse(network::Packet& packet, SpellHealLogData& data) if (packet.getSize() - packet.getReadPos() < 21) return false; size_t startPos = packet.getReadPos(); + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.targetGuid = UpdateObjectParser::readPackedGuid(packet); + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.casterGuid = UpdateObjectParser::readPackedGuid(packet); // Validate remaining fields (spellId + heal + overheal + absorbed + critFlag = 17 bytes) @@ -3663,15 +3713,25 @@ bool CastFailedParser::parse(network::Packet& packet, CastFailedData& data) { } bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) { - // Upfront validation: packed GUID(1-8) + packed GUID(1-8) + castCount(1) + spellId(4) + castFlags(4) + castTime(4) = 22 bytes minimum - if (packet.getSize() - packet.getReadPos() < 22) return false; + data = SpellStartData{}; + + // Packed GUIDs are variable-length; only require minimal packet shape up front: + // two GUID masks + castCount(1) + spellId(4) + castFlags(4) + castTime(4). + if (packet.getSize() - packet.getReadPos() < 15) return false; size_t startPos = packet.getReadPos(); + if (!hasFullPackedGuid(packet)) { + return false; + } data.casterGuid = UpdateObjectParser::readPackedGuid(packet); + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.casterUnit = UpdateObjectParser::readPackedGuid(packet); - // Validate remaining fixed fields (castCount + spellId + castFlags + castTime = 9 bytes) - if (packet.getSize() - packet.getReadPos() < 9) { + // Validate remaining fixed fields (castCount + spellId + castFlags + castTime = 13 bytes) + if (packet.getSize() - packet.getReadPos() < 13) { packet.setReadPos(startPos); return false; } @@ -3681,12 +3741,21 @@ bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) { data.castFlags = packet.readUInt32(); data.castTime = packet.readUInt32(); - // Read target flags and target (simplified) - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t targetFlags = packet.readUInt32(); - if ((targetFlags & 0x02) && packet.getSize() - packet.getReadPos() >= 1) { // TARGET_FLAG_UNIT, validate packed GUID read - data.targetGuid = UpdateObjectParser::readPackedGuid(packet); + // SpellCastTargets starts with target flags and is mandatory. + if (packet.getSize() - packet.getReadPos() < 4) { + LOG_WARNING("Spell start: missing targetFlags"); + packet.setReadPos(startPos); + return false; + } + + uint32_t targetFlags = packet.readUInt32(); + const bool needsTargetGuid = (targetFlags & 0x02) || (targetFlags & 0x800); // UNIT/OBJECT + if (needsTargetGuid) { + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; } + data.targetGuid = UpdateObjectParser::readPackedGuid(packet); } LOG_DEBUG("Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms"); @@ -3694,12 +3763,22 @@ bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) { } bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { + // Always reset output to avoid stale targets when callers reuse buffers. + data = SpellGoData{}; + // Packed GUIDs are variable-length, so only require the smallest possible - // shape up front: 2 GUID masks + fixed fields through missCount. - if (packet.getSize() - packet.getReadPos() < 17) return false; + // shape up front: 2 GUID masks + fixed fields through hitCount. + if (packet.getSize() - packet.getReadPos() < 16) return false; size_t startPos = packet.getReadPos(); + if (!hasFullPackedGuid(packet)) { + return false; + } data.casterGuid = UpdateObjectParser::readPackedGuid(packet); + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.casterUnit = UpdateObjectParser::readPackedGuid(packet); // Validate remaining fixed fields up to hitCount/missCount @@ -3714,59 +3793,81 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { // Timestamp in 3.3.5a packet.readUInt32(); - data.hitCount = packet.readUInt8(); - // Cap hit count to prevent DoS via massive arrays - if (data.hitCount > 128) { - LOG_WARNING("Spell go: hitCount capped (requested=", (int)data.hitCount, ")"); - data.hitCount = 128; + const uint8_t rawHitCount = packet.readUInt8(); + if (rawHitCount > 128) { + LOG_WARNING("Spell go: hitCount capped (requested=", (int)rawHitCount, ")"); } + const uint8_t storedHitLimit = std::min(rawHitCount, 128); - data.hitTargets.reserve(data.hitCount); - for (uint8_t i = 0; i < data.hitCount; ++i) { + bool truncatedTargets = false; + + data.hitTargets.reserve(storedHitLimit); + for (uint16_t i = 0; i < rawHitCount; ++i) { // WotLK hit targets are packed GUIDs, like the caster and miss targets. - if (packet.getSize() - packet.getReadPos() < 1) { - LOG_WARNING("Spell go: truncated hit targets at index ", (int)i, "/", (int)data.hitCount); - data.hitCount = i; + if (!hasFullPackedGuid(packet)) { + LOG_WARNING("Spell go: truncated hit targets at index ", i, "/", (int)rawHitCount); + truncatedTargets = true; break; } - data.hitTargets.push_back(UpdateObjectParser::readPackedGuid(packet)); + const uint64_t targetGuid = UpdateObjectParser::readPackedGuid(packet); + if (i < storedHitLimit) { + data.hitTargets.push_back(targetGuid); + } } + if (truncatedTargets) { + packet.setReadPos(startPos); + return false; + } + data.hitCount = static_cast(data.hitTargets.size()); - // Validate missCount field exists + // missCount is mandatory in SMSG_SPELL_GO. Missing byte means truncation. if (packet.getSize() - packet.getReadPos() < 1) { - return true; // Valid, just no misses + LOG_WARNING("Spell go: missing missCount after hit target list"); + packet.setReadPos(startPos); + return false; } - data.missCount = packet.readUInt8(); - // Cap miss count to prevent DoS - if (data.missCount > 128) { - LOG_WARNING("Spell go: missCount capped (requested=", (int)data.missCount, ")"); - data.missCount = 128; + const uint8_t rawMissCount = packet.readUInt8(); + if (rawMissCount > 128) { + LOG_WARNING("Spell go: missCount capped (requested=", (int)rawMissCount, ")"); } + const uint8_t storedMissLimit = std::min(rawMissCount, 128); - data.missTargets.reserve(data.missCount); - for (uint8_t i = 0; i < data.missCount; ++i) { + data.missTargets.reserve(storedMissLimit); + for (uint16_t i = 0; i < rawMissCount; ++i) { // Each miss entry: packed GUID(1-8 bytes) + missType(1 byte). // REFLECT additionally appends uint32 reflectSpellId + uint8 reflectResult. - if (packet.getSize() - packet.getReadPos() < 2) { - LOG_WARNING("Spell go: truncated miss targets at index ", (int)i, "/", (int)data.missCount); - data.missCount = i; + if (!hasFullPackedGuid(packet)) { + LOG_WARNING("Spell go: truncated miss targets at index ", i, "/", (int)rawMissCount); + truncatedTargets = true; break; } SpellGoMissEntry m; m.targetGuid = UpdateObjectParser::readPackedGuid(packet); // packed GUID in WotLK - m.missType = (packet.getSize() - packet.getReadPos() >= 1) ? packet.readUInt8() : 0; + if (packet.getSize() - packet.getReadPos() < 1) { + LOG_WARNING("Spell go: missing missType at miss index ", i, "/", (int)rawMissCount); + truncatedTargets = true; + break; + } + m.missType = packet.readUInt8(); if (m.missType == 11) { if (packet.getSize() - packet.getReadPos() < 5) { - LOG_WARNING("Spell go: truncated reflect payload at miss index ", (int)i, "/", (int)data.missCount); - data.missCount = i; + LOG_WARNING("Spell go: truncated reflect payload at miss index ", i, "/", (int)rawMissCount); + truncatedTargets = true; break; } (void)packet.readUInt32(); (void)packet.readUInt8(); } - data.missTargets.push_back(m); + if (i < storedMissLimit) { + data.missTargets.push_back(m); + } } + if (truncatedTargets) { + packet.setReadPos(startPos); + return false; + } + data.missCount = static_cast(data.missTargets.size()); LOG_DEBUG("Spell go: spell=", data.spellId, " hits=", (int)data.hitCount, " misses=", (int)data.missCount);