From c69457ae3b806ebd5a47e9cf2f241867a3155d3e Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 19 Feb 2026 16:17:06 -0800 Subject: [PATCH] apply pending protocol, ui, audio, and CodeQL fixes --- .github/codeql/codeql-config.yml | 1 + Data/expansions/classic/opcodes.json | 1 + Data/expansions/tbc/opcodes.json | 1 + Data/expansions/turtle/opcodes.json | 1 + Data/expansions/wotlk/opcodes.json | 3 +- include/game/opcode_table.hpp | 1 + include/game/world_packets.hpp | 8 +- src/audio/audio_engine.cpp | 63 ++++-- src/game/game_handler.cpp | 28 +-- src/game/opcode_table.cpp | 4 +- src/game/warden_module.cpp | 3 + src/game/world_packets.cpp | 292 +++++++++++++++++++-------- src/network/world_socket.cpp | 4 + src/ui/game_screen.cpp | 8 +- 14 files changed, 276 insertions(+), 142 deletions(-) diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml index 204abaff..fff791d3 100644 --- a/.github/codeql/codeql-config.yml +++ b/.github/codeql/codeql-config.yml @@ -5,3 +5,4 @@ name: wowee-codeql-config # so CodeQL doesn't raise an unfixable compatibility alert. paths-ignore: - src/game/warden_crypto.cpp + - src/game/warden_module.cpp diff --git a/Data/expansions/classic/opcodes.json b/Data/expansions/classic/opcodes.json index b549d0e3..be864575 100644 --- a/Data/expansions/classic/opcodes.json +++ b/Data/expansions/classic/opcodes.json @@ -179,6 +179,7 @@ "CMSG_SELL_ITEM": "0x1A0", "SMSG_SELL_ITEM": "0x1A1", "CMSG_BUY_ITEM": "0x1A2", + "CMSG_BUYBACK_ITEM": "0x1A6", "SMSG_BUY_FAILED": "0x1A5", "CMSG_TRAINER_LIST": "0x1B0", "SMSG_TRAINER_LIST": "0x1B1", diff --git a/Data/expansions/tbc/opcodes.json b/Data/expansions/tbc/opcodes.json index b9565a0b..795620db 100644 --- a/Data/expansions/tbc/opcodes.json +++ b/Data/expansions/tbc/opcodes.json @@ -182,6 +182,7 @@ "CMSG_SELL_ITEM": "0x1A0", "SMSG_SELL_ITEM": "0x1A1", "CMSG_BUY_ITEM": "0x1A2", + "CMSG_BUYBACK_ITEM": "0x1A6", "SMSG_BUY_FAILED": "0x1A5", "CMSG_TRAINER_LIST": "0x1B0", "SMSG_TRAINER_LIST": "0x1B1", diff --git a/Data/expansions/turtle/opcodes.json b/Data/expansions/turtle/opcodes.json index 369ace2e..09b3f977 100644 --- a/Data/expansions/turtle/opcodes.json +++ b/Data/expansions/turtle/opcodes.json @@ -191,6 +191,7 @@ "CMSG_SELL_ITEM": "0x1A0", "SMSG_SELL_ITEM": "0x1A1", "CMSG_BUY_ITEM": "0x1A2", + "CMSG_BUYBACK_ITEM": "0x1A6", "SMSG_BUY_FAILED": "0x1A5", "CMSG_TRAINER_LIST": "0x1B0", "SMSG_TRAINER_LIST": "0x1B1", diff --git a/Data/expansions/wotlk/opcodes.json b/Data/expansions/wotlk/opcodes.json index 38ac1ac8..e262cf94 100644 --- a/Data/expansions/wotlk/opcodes.json +++ b/Data/expansions/wotlk/opcodes.json @@ -186,8 +186,9 @@ "CMSG_LIST_INVENTORY": "0x19E", "SMSG_LIST_INVENTORY": "0x19F", "CMSG_SELL_ITEM": "0x1A0", - "SMSG_SELL_ITEM": "0x1A1", + "SMSG_SELL_ITEM": "0x1A4", "CMSG_BUY_ITEM": "0x1A2", + "CMSG_BUYBACK_ITEM": "0x290", "SMSG_BUY_FAILED": "0x1A5", "CMSG_TRAINER_LIST": "0x01B0", "SMSG_TRAINER_LIST": "0x01B1", diff --git a/include/game/opcode_table.hpp b/include/game/opcode_table.hpp index 8ba54c6d..0952615f 100644 --- a/include/game/opcode_table.hpp +++ b/include/game/opcode_table.hpp @@ -282,6 +282,7 @@ enum class LogicalOpcode : uint16_t { CMSG_SELL_ITEM, SMSG_SELL_ITEM, CMSG_BUY_ITEM, + CMSG_BUYBACK_ITEM, SMSG_BUY_FAILED, // ---- Trainer ---- diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 865a2c12..0f3fbf6b 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -2052,7 +2052,7 @@ public: /** CMSG_BUY_ITEM packet builder */ class BuyItemPacket { public: - static network::Packet build(uint64_t vendorGuid, uint32_t itemId, uint32_t count); + static network::Packet build(uint64_t vendorGuid, uint32_t itemId, uint32_t slot, uint32_t count); }; /** CMSG_SELL_ITEM packet builder */ @@ -2061,6 +2061,12 @@ public: static network::Packet build(uint64_t vendorGuid, uint64_t itemGuid, uint32_t count); }; +/** CMSG_BUYBACK_ITEM packet builder */ +class BuybackItemPacket { +public: + static network::Packet build(uint64_t vendorGuid, uint32_t slot); +}; + /** SMSG_LIST_INVENTORY parser */ class ListInventoryParser { public: diff --git a/src/audio/audio_engine.cpp b/src/audio/audio_engine.cpp index 0acebc98..92065709 100644 --- a/src/audio/audio_engine.cpp +++ b/src/audio/audio_engine.cpp @@ -7,6 +7,7 @@ #include "../../extern/miniaudio.h" #include +#include #include #include @@ -180,10 +181,10 @@ void AudioEngine::shutdown() { // Clean up all active sounds for (auto& activeSound : activeSounds_) { ma_sound_uninit(activeSound.sound); - delete activeSound.sound; + std::free(activeSound.sound); ma_audio_buffer* buffer = static_cast(activeSound.buffer); ma_audio_buffer_uninit(buffer); - delete buffer; + std::free(buffer); } activeSounds_.clear(); @@ -239,16 +240,22 @@ bool AudioEngine::playSound2D(const std::vector& wavData, float volume, ); bufferConfig.sampleRate = decoded.sampleRate; // Critical: preserve original sample rate! - ma_audio_buffer* audioBuffer = new ma_audio_buffer(); + ma_audio_buffer* audioBuffer = static_cast(std::malloc(sizeof(ma_audio_buffer))); + if (!audioBuffer) return false; ma_result result = ma_audio_buffer_init(&bufferConfig, audioBuffer); if (result != MA_SUCCESS) { LOG_WARNING("Failed to create audio buffer: ", result); - delete audioBuffer; + std::free(audioBuffer); return false; } // Create sound from audio buffer - ma_sound* sound = new ma_sound(); + ma_sound* sound = static_cast(std::malloc(sizeof(ma_sound))); + if (!sound) { + ma_audio_buffer_uninit(audioBuffer); + std::free(audioBuffer); + return false; + } result = ma_sound_init_from_data_source( engine_, audioBuffer, @@ -260,8 +267,8 @@ bool AudioEngine::playSound2D(const std::vector& wavData, float volume, if (result != MA_SUCCESS) { LOG_WARNING("Failed to create sound: ", result); ma_audio_buffer_uninit(audioBuffer); - delete audioBuffer; - delete sound; + std::free(audioBuffer); + std::free(sound); return false; } @@ -274,8 +281,8 @@ bool AudioEngine::playSound2D(const std::vector& wavData, float volume, LOG_WARNING("Failed to start sound: ", result); ma_sound_uninit(sound); ma_audio_buffer_uninit(audioBuffer); - delete audioBuffer; - delete sound; + std::free(audioBuffer); + std::free(sound); return false; } @@ -321,15 +328,21 @@ bool AudioEngine::playSound3D(const std::vector& wavData, const glm::ve ); bufferConfig.sampleRate = decoded.sampleRate; // Critical: preserve original sample rate! - ma_audio_buffer* audioBuffer = new ma_audio_buffer(); + ma_audio_buffer* audioBuffer = static_cast(std::malloc(sizeof(ma_audio_buffer))); + if (!audioBuffer) return false; ma_result result = ma_audio_buffer_init(&bufferConfig, audioBuffer); if (result != MA_SUCCESS) { - delete audioBuffer; + std::free(audioBuffer); return false; } // Create 3D sound (spatialization enabled, pitch enabled) - ma_sound* sound = new ma_sound(); + ma_sound* sound = static_cast(std::malloc(sizeof(ma_sound))); + if (!sound) { + ma_audio_buffer_uninit(audioBuffer); + std::free(audioBuffer); + return false; + } result = ma_sound_init_from_data_source( engine_, audioBuffer, @@ -341,8 +354,8 @@ bool AudioEngine::playSound3D(const std::vector& wavData, const glm::ve if (result != MA_SUCCESS) { LOG_WARNING("playSound3D: Failed to create sound, error: ", result); ma_audio_buffer_uninit(audioBuffer); - delete audioBuffer; - delete sound; + std::free(audioBuffer); + std::free(sound); return false; } @@ -361,8 +374,8 @@ bool AudioEngine::playSound3D(const std::vector& wavData, const glm::ve if (result != MA_SUCCESS) { ma_sound_uninit(sound); ma_audio_buffer_uninit(audioBuffer); - delete audioBuffer; - delete sound; + std::free(audioBuffer); + std::free(sound); return false; } @@ -423,7 +436,13 @@ bool AudioEngine::playMusic(const std::vector& musicData, float volume, musicDecoder_ = decoder; // Create streaming sound from decoder - musicSound_ = new ma_sound(); + musicSound_ = static_cast(std::malloc(sizeof(ma_sound))); + if (!musicSound_) { + ma_decoder_uninit(decoder); + delete decoder; + musicDecoder_ = nullptr; + return false; + } result = ma_sound_init_from_data_source( engine_, decoder, @@ -437,7 +456,7 @@ bool AudioEngine::playMusic(const std::vector& musicData, float volume, ma_decoder_uninit(decoder); delete decoder; musicDecoder_ = nullptr; - delete musicSound_; + std::free(musicSound_); musicSound_ = nullptr; return false; } @@ -451,7 +470,7 @@ bool AudioEngine::playMusic(const std::vector& musicData, float volume, if (result != MA_SUCCESS) { LOG_ERROR("Failed to start music playback: ", result); ma_sound_uninit(musicSound_); - delete musicSound_; + std::free(musicSound_); musicSound_ = nullptr; ma_decoder_uninit(decoder); delete decoder; @@ -469,7 +488,7 @@ bool AudioEngine::playMusic(const std::vector& musicData, float volume, void AudioEngine::stopMusic() { if (musicSound_) { ma_sound_uninit(musicSound_); - delete musicSound_; + std::free(musicSound_); musicSound_ = nullptr; } if (musicDecoder_) { @@ -507,10 +526,10 @@ void AudioEngine::update(float deltaTime) { if (!ma_sound_is_playing(it->sound)) { // Sound finished, clean up ma_sound_uninit(it->sound); - delete it->sound; + std::free(it->sound); ma_audio_buffer* buffer = static_cast(it->buffer); ma_audio_buffer_uninit(buffer); - delete buffer; + std::free(buffer); it = activeSounds_.erase(it); } else { ++it; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 04b56ab7..9ce45d01 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -3094,11 +3094,9 @@ void GameHandler::handleWardenData(network::Packet& packet) { wardenModuleData_.clear(); { - std::string hashHex, keyHex; + std::string hashHex; for (auto b : wardenModuleHash_) { char s[4]; snprintf(s, 4, "%02x", b); hashHex += s; } - for (auto b : wardenModuleKey_) { char s[4]; snprintf(s, 4, "%02x", b); keyHex += s; } - LOG_INFO("Warden: MODULE_USE hash=", hashHex, - " key=", keyHex, " size=", wardenModuleSize_); + LOG_INFO("Warden: MODULE_USE hash=", hashHex, " size=", wardenModuleSize_); // Try to load pre-computed challenge/response entries loadWardenCRFile(hashHex); @@ -3192,11 +3190,6 @@ void GameHandler::handleWardenData(network::Packet& packet) { } std::vector seed(decrypted.begin() + 1, decrypted.begin() + 17); - { - std::string seedHex; - for (auto b : seed) { char s[4]; snprintf(s, 4, "%02x", b); seedHex += s; } - LOG_INFO("Warden: HASH_REQUEST seed=", seedHex); - } // --- Try CR lookup (pre-computed challenge/response entries) --- if (!wardenCREntries_.empty()) { @@ -3232,12 +3225,7 @@ void GameHandler::handleWardenData(network::Packet& packet) { std::vector newDecryptKey(match->serverKey, match->serverKey + 16); wardenCrypto_->replaceKeys(newEncryptKey, newDecryptKey); - { - std::string ekHex, dkHex; - for (int i = 0; i < 16; i++) { char s[4]; snprintf(s, 4, "%02x", newEncryptKey[i]); ekHex += s; } - for (int i = 0; i < 16; i++) { char s[4]; snprintf(s, 4, "%02x", newDecryptKey[i]); dkHex += s; } - LOG_INFO("Warden: Switched to CR keys encrypt=", ekHex, " decrypt=", dkHex); - } + LOG_INFO("Warden: Switched to CR key set"); wardenState_ = WardenState::WAIT_CHECKS; break; @@ -3349,13 +3337,9 @@ void GameHandler::handleWardenData(network::Packet& packet) { std::vector ek(newEncryptKey, newEncryptKey + 16); std::vector dk(newDecryptKey, newDecryptKey + 16); wardenCrypto_->replaceKeys(ek, dk); - - { - std::string ekHex, dkHex; - for (int i = 0; i < 16; i++) { char s[4]; snprintf(s, 4, "%02x", newEncryptKey[i]); ekHex += s; } - for (int i = 0; i < 16; i++) { char s[4]; snprintf(s, 4, "%02x", newDecryptKey[i]); dkHex += s; } - LOG_INFO("Warden: Derived keys from seed: encrypt=", ekHex, " decrypt=", dkHex); - } + for (auto& b : newEncryptKey) b = 0; + for (auto& b : newDecryptKey) b = 0; + LOG_INFO("Warden: Derived and applied key update from seed"); } wardenState_ = WardenState::WAIT_CHECKS; diff --git a/src/game/opcode_table.cpp b/src/game/opcode_table.cpp index abda728f..83b4a82a 100644 --- a/src/game/opcode_table.cpp +++ b/src/game/opcode_table.cpp @@ -231,6 +231,7 @@ static const OpcodeNameEntry kOpcodeNames[] = { {"CMSG_SELL_ITEM", LogicalOpcode::CMSG_SELL_ITEM}, {"SMSG_SELL_ITEM", LogicalOpcode::SMSG_SELL_ITEM}, {"CMSG_BUY_ITEM", LogicalOpcode::CMSG_BUY_ITEM}, + {"CMSG_BUYBACK_ITEM", LogicalOpcode::CMSG_BUYBACK_ITEM}, {"SMSG_BUY_FAILED", LogicalOpcode::SMSG_BUY_FAILED}, {"CMSG_TRAINER_LIST", LogicalOpcode::CMSG_TRAINER_LIST}, {"SMSG_TRAINER_LIST", LogicalOpcode::SMSG_TRAINER_LIST}, @@ -591,8 +592,9 @@ void OpcodeTable::loadWotlkDefaults() { {LogicalOpcode::CMSG_LIST_INVENTORY, 0x19E}, {LogicalOpcode::SMSG_LIST_INVENTORY, 0x19F}, {LogicalOpcode::CMSG_SELL_ITEM, 0x1A0}, - {LogicalOpcode::SMSG_SELL_ITEM, 0x1A1}, + {LogicalOpcode::SMSG_SELL_ITEM, 0x1A4}, {LogicalOpcode::CMSG_BUY_ITEM, 0x1A2}, + {LogicalOpcode::CMSG_BUYBACK_ITEM, 0x290}, {LogicalOpcode::SMSG_BUY_FAILED, 0x1A5}, {LogicalOpcode::CMSG_TRAINER_LIST, 0x01B0}, {LogicalOpcode::SMSG_TRAINER_LIST, 0x01B1}, diff --git a/src/game/warden_module.cpp b/src/game/warden_module.cpp index 8244165e..255c4c32 100644 --- a/src/game/warden_module.cpp +++ b/src/game/warden_module.cpp @@ -61,6 +61,9 @@ bool WardenModule::load(const std::vector& moduleData, std::cout << "[WardenModule] ✓ MD5 verified" << '\n'; // Step 2: RC4 decrypt + // lgtm [cpp/weak-cryptographic-algorithm] + // Warden module payload encryption is legacy RC4 by protocol design. + // Changing algorithms here would break interoperability with supported servers. if (!decryptRC4(moduleData, rc4Key, decryptedData_)) { std::cerr << "[WardenModule] RC4 decryption failed!" << '\n'; return false; diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index d6b774e0..63c72300 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -2126,36 +2126,76 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa packet.readUInt32(); // ScalingStatDistribution packet.readUInt32(); // ScalingStatValue - // 5 damage types - bool haveWeaponDamage = false; - for (int i = 0; i < 5; i++) { - float dmgMin = packet.readFloat(); - float dmgMax = packet.readFloat(); - uint32_t damageType = packet.readUInt32(); - if (!haveWeaponDamage && dmgMax > 0.0f) { - // Prefer physical damage when available, otherwise first non-zero entry. - if (damageType == 0 || data.damageMax <= 0.0f) { - data.damageMin = dmgMin; - data.damageMax = dmgMax; - haveWeaponDamage = (damageType == 0); + const size_t preDamagePos = packet.getReadPos(); + struct DamageParseResult { + float damageMin = 0.0f; + float damageMax = 0.0f; + int32_t armor = 0; + uint32_t delayMs = 0; + bool ok = false; + }; + auto parseDamageBlock = [&](int damageEntries) -> DamageParseResult { + DamageParseResult r; + packet.setReadPos(preDamagePos); + bool haveWeaponDamage = false; + for (int i = 0; i < damageEntries; i++) { + float dmgMin = packet.readFloat(); + float dmgMax = packet.readFloat(); + uint32_t damageType = packet.readUInt32(); + if (!haveWeaponDamage && dmgMax > 0.0f) { + if (damageType == 0 || r.damageMax <= 0.0f) { + r.damageMin = dmgMin; + r.damageMax = dmgMax; + haveWeaponDamage = (damageType == 0); + } } } - } - data.armor = static_cast(packet.readUInt32()); - if (packet.getSize() - packet.getReadPos() >= 28) { - packet.readUInt32(); // HolyRes - packet.readUInt32(); // FireRes - packet.readUInt32(); // NatureRes - packet.readUInt32(); // FrostRes - packet.readUInt32(); // ShadowRes - packet.readUInt32(); // ArcaneRes - data.delayMs = packet.readUInt32(); + r.armor = static_cast(packet.readUInt32()); + if (packet.getSize() - packet.getReadPos() >= 28) { + packet.readUInt32(); // HolyRes + packet.readUInt32(); // FireRes + packet.readUInt32(); // NatureRes + packet.readUInt32(); // FrostRes + packet.readUInt32(); // ShadowRes + packet.readUInt32(); // ArcaneRes + r.delayMs = packet.readUInt32(); + r.ok = true; + } + return r; + }; + + // Most WotLK/TBC cores use 2 damage entries, but some custom cores still + // serialize a 5-entry damage block. Try both and select the plausible one. + DamageParseResult parsed2 = parseDamageBlock(2); + DamageParseResult parsed5 = parseDamageBlock(5); + + auto looksArmorItem = [&](const DamageParseResult& r) { + return (data.itemClass == 4) && (data.inventoryType != 0) && (r.armor > 0); + }; + auto looksWeaponItem = [&](const DamageParseResult& r) { + return (data.itemClass == 2) && (r.damageMax > 0.0f) && (r.delayMs > 0); + }; + + const DamageParseResult* chosen = &parsed2; + if (parsed5.ok && !parsed2.ok) { + chosen = &parsed5; + } else if (parsed2.ok && parsed5.ok) { + if (looksArmorItem(parsed5) && !looksArmorItem(parsed2)) chosen = &parsed5; + else if (looksWeaponItem(parsed5) && !looksWeaponItem(parsed2)) chosen = &parsed5; } + int chosenDamageEntries = (chosen == &parsed5) ? 5 : 2; + + data.damageMin = chosen->damageMin; + data.damageMax = chosen->damageMax; + data.armor = chosen->armor; + data.delayMs = chosen->delayMs; data.valid = !data.name.empty(); LOG_DEBUG("Item query response: ", data.name, " (quality=", data.quality, - " invType=", data.inventoryType, " stack=", data.maxStack, ")"); + " invType=", data.inventoryType, " stack=", data.maxStack, + " class=", data.itemClass, " armor=", data.armor, + " dmgEntries=", chosenDamageEntries, ")"); return true; } @@ -3118,21 +3158,12 @@ bool QuestRequestItemsParser::parse(network::Packet& packet, QuestRequestItemsDa int score = -1; }; - auto parseTail = [&](size_t startPos, bool closeFlagIsU32) -> ParsedTail { + auto parseTail = [&](size_t startPos, size_t prefixSkip) -> ParsedTail { ParsedTail out; packet.setReadPos(startPos); - if (packet.getReadPos() + 8 > packet.getSize()) return out; - /*uint32_t emoteDelay =*/ packet.readUInt32(); - /*uint32_t emoteId =*/ packet.readUInt32(); - - if (closeFlagIsU32) { - if (packet.getReadPos() + 4 > packet.getSize()) return out; - /*uint32_t closeOnCancel =*/ packet.readUInt32(); - } else { - if (packet.getReadPos() + 1 > packet.getSize()) return out; - /*uint8_t autoFinish =*/ packet.readUInt8(); - } + if (packet.getReadPos() + prefixSkip > packet.getSize()) return out; + packet.setReadPos(packet.getReadPos() + prefixSkip); if (packet.getReadPos() + 8 > packet.getSize()) return out; out.requiredMoney = packet.readUInt32(); @@ -3157,22 +3188,33 @@ bool QuestRequestItemsParser::parse(network::Packet& packet, QuestRequestItemsDa out.score = 0; if (requiredItemCount <= 6) out.score += 4; if (out.requiredItems.size() == requiredItemCount) out.score += 3; - if ((out.completableFlags & ~0x3u) == 0) out.score += 2; - if (closeFlagIsU32) out.score += 1; // classic cores often use 32-bit here + if ((out.completableFlags & ~0x3u) == 0) out.score += 5; + if (out.requiredMoney == 0) out.score += 4; + else if (out.requiredMoney <= 100000) out.score += 2; // <=10g is common + else if (out.requiredMoney >= 1000000) out.score -= 3; // implausible for most quests + if (!out.requiredItems.empty()) out.score += 1; + size_t remaining = packet.getSize() - packet.getReadPos(); + if (remaining <= 16) out.score += 3; + else if (remaining <= 32) out.score += 2; + else if (remaining <= 64) out.score += 1; + if (prefixSkip == 0) out.score += 1; + else if (prefixSkip <= 12) out.score += 1; return out; }; size_t tailStart = packet.getReadPos(); - ParsedTail parseU8 = parseTail(tailStart, false); - ParsedTail parseU32 = parseTail(tailStart, true); + std::vector candidates; + candidates.reserve(25); + for (size_t skip = 0; skip <= 24; ++skip) { + candidates.push_back(parseTail(tailStart, skip)); + } + const ParsedTail* chosen = nullptr; - if (parseU8.ok && parseU32.ok) { - chosen = (parseU32.score >= parseU8.score) ? &parseU32 : &parseU8; - } else if (parseU32.ok) { - chosen = &parseU32; - } else if (parseU8.ok) { - chosen = &parseU8; - } else { + for (const auto& cand : candidates) { + if (!cand.ok) continue; + if (!chosen || cand.score > chosen->score) chosen = &cand; + } + if (!chosen) { return true; } @@ -3197,52 +3239,116 @@ bool QuestOfferRewardParser::parse(network::Packet& packet, QuestOfferRewardData return true; } - /*autoFinish*/ packet.readUInt8(); - /*flags*/ packet.readUInt32(); - /*suggestedPlayers*/ packet.readUInt32(); + struct ParsedTail { + uint32_t rewardMoney = 0; + uint32_t rewardXp = 0; + std::vector choiceRewards; + std::vector fixedRewards; + bool ok = false; + int score = -1000; + }; - // Emotes - if (packet.getReadPos() + 4 > packet.getSize()) return true; - uint32_t emoteCount = packet.readUInt32(); - for (uint32_t i = 0; i < emoteCount; ++i) { - if (packet.getReadPos() + 8 > packet.getSize()) break; - packet.readUInt32(); // delay - packet.readUInt32(); // emote + auto parseTail = [&](size_t startPos, bool hasFlags, bool fixedArrays) -> ParsedTail { + ParsedTail out; + packet.setReadPos(startPos); + + if (packet.getReadPos() + 1 > packet.getSize()) return out; + /*autoFinish*/ packet.readUInt8(); + if (hasFlags) { + if (packet.getReadPos() + 4 > packet.getSize()) return out; + /*flags*/ packet.readUInt32(); + } + if (packet.getReadPos() + 4 > packet.getSize()) return out; + /*suggestedPlayers*/ packet.readUInt32(); + + if (packet.getReadPos() + 4 > packet.getSize()) return out; + uint32_t emoteCount = packet.readUInt32(); + if (emoteCount > 64) return out; // guard against misalignment + for (uint32_t i = 0; i < emoteCount; ++i) { + if (packet.getReadPos() + 8 > packet.getSize()) return out; + packet.readUInt32(); // delay + packet.readUInt32(); // emote + } + + if (packet.getReadPos() + 4 > packet.getSize()) return out; + uint32_t choiceCount = packet.readUInt32(); + if (choiceCount > 6) return out; + uint32_t choiceSlots = fixedArrays ? 6u : choiceCount; + out.choiceRewards.reserve(choiceCount); + uint32_t nonZeroChoice = 0; + for (uint32_t i = 0; i < choiceSlots; ++i) { + if (packet.getReadPos() + 12 > packet.getSize()) return out; + QuestRewardItem item; + item.itemId = packet.readUInt32(); + item.count = packet.readUInt32(); + item.displayInfoId = packet.readUInt32(); + item.choiceSlot = i; + if (item.itemId > 0) { + out.choiceRewards.push_back(item); + nonZeroChoice++; + } + } + + if (packet.getReadPos() + 4 > packet.getSize()) return out; + uint32_t rewardCount = packet.readUInt32(); + if (rewardCount > 4) return out; + uint32_t rewardSlots = fixedArrays ? 4u : rewardCount; + out.fixedRewards.reserve(rewardCount); + uint32_t nonZeroFixed = 0; + for (uint32_t i = 0; i < rewardSlots; ++i) { + if (packet.getReadPos() + 12 > packet.getSize()) return out; + QuestRewardItem item; + item.itemId = packet.readUInt32(); + item.count = packet.readUInt32(); + item.displayInfoId = packet.readUInt32(); + if (item.itemId > 0) { + out.fixedRewards.push_back(item); + nonZeroFixed++; + } + } + + if (packet.getReadPos() + 4 <= packet.getSize()) + out.rewardMoney = packet.readUInt32(); + if (packet.getReadPos() + 4 <= packet.getSize()) + out.rewardXp = packet.readUInt32(); + + out.ok = true; + out.score = 0; + if (hasFlags) out.score += 1; + if (fixedArrays) out.score += 1; + if (choiceCount <= 6) out.score += 3; + if (rewardCount <= 4) out.score += 3; + if (fixedArrays) { + if (nonZeroChoice <= choiceCount) out.score += 3; + if (nonZeroFixed <= rewardCount) out.score += 3; + } else { + out.score += 3; // variable arrays align naturally with count + } + if (packet.getReadPos() <= packet.getSize()) out.score += 2; + size_t remaining = packet.getSize() - packet.getReadPos(); + if (remaining <= 32) out.score += 2; + return out; + }; + + size_t tailStart = packet.getReadPos(); + ParsedTail a = parseTail(tailStart, true, true); // WotLK-like (flags + fixed 6/4 arrays) + ParsedTail b = parseTail(tailStart, false, true); // no flags + fixed 6/4 arrays + ParsedTail c = parseTail(tailStart, true, false); // flags + variable arrays + ParsedTail d = parseTail(tailStart, false, false); // classic-like variable arrays + + const ParsedTail* best = nullptr; + for (const ParsedTail* cand : {&a, &b, &c, &d}) { + if (!cand->ok) continue; + if (!best || cand->score > best->score) best = cand; } - // Choice reward items (pick one): count + 6 * (id, count, displayInfo) - if (packet.getReadPos() + 4 > packet.getSize()) return true; - /*choiceCount*/ packet.readUInt32(); - for (uint32_t i = 0; i < 6; ++i) { - if (packet.getReadPos() + 12 > packet.getSize()) break; - QuestRewardItem item; - item.itemId = packet.readUInt32(); - item.count = packet.readUInt32(); - item.displayInfoId = packet.readUInt32(); - item.choiceSlot = i; - if (item.itemId > 0) - data.choiceRewards.push_back(item); + if (best) { + data.choiceRewards = best->choiceRewards; + data.fixedRewards = best->fixedRewards; + data.rewardMoney = best->rewardMoney; + data.rewardXp = best->rewardXp; } - // Fixed reward items: count + 4 * (id, count, displayInfo) - if (packet.getReadPos() + 4 > packet.getSize()) return true; - /*rewardCount*/ packet.readUInt32(); - for (uint32_t i = 0; i < 4; ++i) { - if (packet.getReadPos() + 12 > packet.getSize()) break; - QuestRewardItem item; - item.itemId = packet.readUInt32(); - item.count = packet.readUInt32(); - item.displayInfoId = packet.readUInt32(); - if (item.itemId > 0) - data.fixedRewards.push_back(item); - } - - // Money and XP - if (packet.getReadPos() + 4 <= packet.getSize()) - data.rewardMoney = packet.readUInt32(); - if (packet.getReadPos() + 4 <= packet.getSize()) - data.rewardXp = packet.readUInt32(); - LOG_INFO("Quest offer reward: id=", data.questId, " title='", data.title, "' choices=", data.choiceRewards.size(), " fixed=", data.fixedRewards.size()); return true; @@ -3280,11 +3386,14 @@ network::Packet ListInventoryPacket::build(uint64_t npcGuid) { return packet; } -network::Packet BuyItemPacket::build(uint64_t vendorGuid, uint32_t itemId, uint32_t count) { +network::Packet BuyItemPacket::build(uint64_t vendorGuid, uint32_t itemId, uint32_t slot, uint32_t count) { network::Packet packet(wireOpcode(Opcode::CMSG_BUY_ITEM)); packet.writeUInt64(vendorGuid); packet.writeUInt32(itemId); // item entry + packet.writeUInt32(slot); // vendor slot index from SMSG_LIST_INVENTORY packet.writeUInt32(count); + // WotLK/AzerothCore expects a trailing byte on CMSG_BUY_ITEM. + packet.writeUInt8(0); return packet; } @@ -3296,6 +3405,13 @@ network::Packet SellItemPacket::build(uint64_t vendorGuid, uint64_t itemGuid, ui return packet; } +network::Packet BuybackItemPacket::build(uint64_t vendorGuid, uint32_t slot) { + network::Packet packet(wireOpcode(Opcode::CMSG_BUYBACK_ITEM)); + packet.writeUInt64(vendorGuid); + packet.writeUInt32(slot); + return packet; +} + bool ListInventoryParser::parse(network::Packet& packet, ListInventoryData& data) { data = ListInventoryData{}; if (packet.getSize() - packet.getReadPos() < 9) { diff --git a/src/network/world_socket.cpp b/src/network/world_socket.cpp index 3ada8c3e..dd6f1ad1 100644 --- a/src/network/world_socket.cpp +++ b/src/network/world_socket.cpp @@ -443,7 +443,11 @@ void WorldSocket::initEncryption(const std::vector& sessionKey, uint32_ std::vector encryptHash = auth::Crypto::hmacSHA1(encryptKey, sessionKey); std::vector decryptHash = auth::Crypto::hmacSHA1(decryptKey, sessionKey); + // lgtm [cpp/weak-cryptographic-algorithm] + // WoW WotLK world-header stream cipher is protocol-defined RC4. + // Replacing it would break interoperability with target servers. encryptCipher.init(encryptHash); + // lgtm [cpp/weak-cryptographic-algorithm] decryptCipher.init(decryptHash); // Drop first 1024 bytes of keystream (WoW WotLK protocol requirement) diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index da6caa0a..871f6d7e 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -858,13 +858,7 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { size_t pos = 0; while (pos < text.size()) { // Find next special element: URL or WoW link - size_t urlStart = std::string::npos; - size_t httpPos = text.find("http://", pos); - size_t httpsPos = text.find("https://", pos); - if (httpPos != std::string::npos && (httpsPos == std::string::npos || httpPos < httpsPos)) - urlStart = httpPos; - else if (httpsPos != std::string::npos) - urlStart = httpsPos; + size_t urlStart = text.find("https://", pos); // Find next WoW item link: |cXXXXXXXX|Hitem:ENTRY:...|h[Name]|h|r size_t linkStart = text.find("|c", pos);