Fix item use (CMSG_USE_ITEM), mount tab, and inventory right-click

- Fix SMSG_ITEM_QUERY_SINGLE_RESPONSE parsing: read statsCount stat
  pairs instead of always 10, use 2 damage entries (MAX_ITEM_PROTO_DAMAGES),
  and parse item spell data (spellId + spellTrigger per slot)
- Pass item spell ID in CMSG_USE_ITEM packet so server processes item
  use requests (spellId=0 caused silent server rejection)
- Add spellId parameter to buildUseItem interface across all expansions
- Fix spellbook mount tab to use SkillLine 777 (Mounts) instead of 762
  (Riding), so known mount summon spells appear correctly
- Fix inventory right-click: use IsItemHovered+IsMouseClicked instead
  of IsItemClicked for InvisibleButton (which only tracks left-clicks)
- Fix SlotKind enum declaration order in inventory_screen.hpp
This commit is contained in:
Kelsi 2026-02-26 00:59:07 -08:00
parent 7982815a67
commit c919477e74
9 changed files with 233 additions and 152 deletions

View file

@ -2398,94 +2398,62 @@ bool ItemQueryResponseParser::parse(network::Packet& packet, ItemQueryResponseDa
data.containerSlots = packet.readUInt32();
uint32_t statsCount = packet.readUInt32();
// Server always sends 10 stat pairs; statsCount tells how many are meaningful
for (uint32_t i = 0; i < 10; i++) {
// Server sends exactly statsCount stat pairs (not always 10).
uint32_t statsToRead = std::min(statsCount, 10u);
for (uint32_t i = 0; i < statsToRead; i++) {
uint32_t statType = packet.readUInt32();
int32_t statValue = static_cast<int32_t>(packet.readUInt32());
if (i < statsCount) {
switch (statType) {
case 3: data.agility = statValue; break;
case 4: data.strength = statValue; break;
case 5: data.intellect = statValue; break;
case 6: data.spirit = statValue; break;
case 7: data.stamina = statValue; break;
default: break;
}
switch (statType) {
case 3: data.agility = statValue; break;
case 4: data.strength = statValue; break;
case 5: data.intellect = statValue; break;
case 6: data.spirit = statValue; break;
case 7: data.stamina = statValue; break;
default: break;
}
}
packet.readUInt32(); // ScalingStatDistribution
packet.readUInt32(); // ScalingStatValue
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);
}
// WotLK 3.3.5a: MAX_ITEM_PROTO_DAMAGES = 2
bool haveWeaponDamage = false;
for (int i = 0; i < 2; i++) {
float dmgMin = packet.readFloat();
float dmgMax = packet.readFloat();
uint32_t damageType = packet.readUInt32();
if (!haveWeaponDamage && dmgMax > 0.0f) {
if (damageType == 0 || data.damageMax <= 0.0f) {
data.damageMin = dmgMin;
data.damageMax = dmgMax;
haveWeaponDamage = (damageType == 0);
}
}
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;
};
// All WoW versions (Classic, TBC, WotLK) use exactly 5 damage entries in
// SMSG_ITEM_QUERY_SINGLE_RESPONSE. Default to 5. Fall back to 2 only if
// the 5-entry parse fails or yields clearly implausible results for weapons.
DamageParseResult parsed2 = parseDamageBlock(2);
DamageParseResult parsed5 = parseDamageBlock(5);
auto looksWeaponItem = [&](const DamageParseResult& r) {
return (data.itemClass == 2) && (r.damageMax > 0.0f) && (r.delayMs > 0);
};
const DamageParseResult* chosen = &parsed5;
if (parsed5.ok && parsed2.ok) {
// Only prefer parsed2 if it identifies as a weapon and parsed5 doesn't.
// This handles non-standard 2-entry servers for weapon items.
if (looksWeaponItem(parsed2) && !looksWeaponItem(parsed5)) chosen = &parsed2;
} else if (!parsed5.ok && parsed2.ok) {
chosen = &parsed2;
}
int chosenDamageEntries = (chosen == &parsed5) ? 5 : 2;
data.damageMin = chosen->damageMin;
data.damageMax = chosen->damageMax;
data.armor = chosen->armor;
data.delayMs = chosen->delayMs;
data.armor = static_cast<int32_t>(packet.readUInt32());
packet.readUInt32(); // HolyRes
packet.readUInt32(); // FireRes
packet.readUInt32(); // NatureRes
packet.readUInt32(); // FrostRes
packet.readUInt32(); // ShadowRes
packet.readUInt32(); // ArcaneRes
data.delayMs = packet.readUInt32();
packet.readUInt32(); // AmmoType
packet.readFloat(); // RangedModRange
// 5 item spells: SpellId, SpellTrigger, SpellCharges, SpellCooldown, SpellCategory, SpellCategoryCooldown
for (int i = 0; i < 5; i++) {
if (packet.getReadPos() + 24 > packet.getSize()) break;
data.spells[i].spellId = packet.readUInt32();
data.spells[i].spellTrigger = packet.readUInt32();
packet.readUInt32(); // SpellCharges
packet.readUInt32(); // SpellCooldown
packet.readUInt32(); // SpellCategory
packet.readUInt32(); // SpellCategoryCooldown
}
data.valid = !data.name.empty();
LOG_INFO("Item query: '", data.name, "' class=", data.itemClass,
" invType=", data.inventoryType, " quality=", data.quality,
" armor=", data.armor, " dmgEntries=", chosenDamageEntries,
" statsCount=", statsCount, " sellPrice=", data.sellPrice);
return true;
}
@ -3142,13 +3110,13 @@ network::Packet AutostoreLootItemPacket::build(uint8_t slotIndex) {
return packet;
}
network::Packet UseItemPacket::build(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid) {
network::Packet UseItemPacket::build(uint8_t bagIndex, uint8_t slotIndex, uint64_t itemGuid, uint32_t spellId) {
network::Packet packet(wireOpcode(Opcode::CMSG_USE_ITEM));
packet.writeUInt8(bagIndex);
packet.writeUInt8(slotIndex);
packet.writeUInt8(0); // cast count
packet.writeUInt32(0); // spell id
packet.writeUInt64(itemGuid);
packet.writeUInt32(spellId); // spell id from item data
packet.writeUInt64(itemGuid); // full 8-byte GUID
packet.writeUInt32(0); // glyph index
packet.writeUInt8(0); // cast flags
// SpellCastTargets: self