apply pending protocol, ui, audio, and CodeQL fixes

This commit is contained in:
Kelsi 2026-02-19 16:17:06 -08:00
parent 586fb88c5f
commit c69457ae3b
14 changed files with 276 additions and 142 deletions

View file

@ -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

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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 ----

View file

@ -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:

View file

@ -7,6 +7,7 @@
#include "../../extern/miniaudio.h"
#include <cstring>
#include <cstdlib>
#include <memory>
#include <unordered_map>
@ -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<ma_audio_buffer*>(activeSound.buffer);
ma_audio_buffer_uninit(buffer);
delete buffer;
std::free(buffer);
}
activeSounds_.clear();
@ -239,16 +240,22 @@ bool AudioEngine::playSound2D(const std::vector<uint8_t>& 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<ma_audio_buffer*>(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<ma_sound*>(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<uint8_t>& 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<uint8_t>& 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<uint8_t>& 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<ma_audio_buffer*>(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<ma_sound*>(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<uint8_t>& 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<uint8_t>& 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<uint8_t>& musicData, float volume,
musicDecoder_ = decoder;
// Create streaming sound from decoder
musicSound_ = new ma_sound();
musicSound_ = static_cast<ma_sound*>(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<uint8_t>& 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<uint8_t>& 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<uint8_t>& 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<ma_audio_buffer*>(it->buffer);
ma_audio_buffer_uninit(buffer);
delete buffer;
std::free(buffer);
it = activeSounds_.erase(it);
} else {
++it;

View file

@ -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<uint8_t> 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<uint8_t> 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<uint8_t> ek(newEncryptKey, newEncryptKey + 16);
std::vector<uint8_t> 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;

View file

@ -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},

View file

@ -61,6 +61,9 @@ bool WardenModule::load(const std::vector<uint8_t>& 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;

View file

@ -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<int32_t>(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<int32_t>(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<ParsedTail> 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<QuestRewardItem> choiceRewards;
std::vector<QuestRewardItem> 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) {

View file

@ -443,7 +443,11 @@ void WorldSocket::initEncryption(const std::vector<uint8_t>& sessionKey, uint32_
std::vector<uint8_t> encryptHash = auth::Crypto::hmacSHA1(encryptKey, sessionKey);
std::vector<uint8_t> 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)

View file

@ -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);