Fix NPC textures, bag item interactions, and Classic item query parsing

Rebuild creature display lookups after expansion-specific DBC layout loads
(was using WotLK defaults before turtle layout was available). Add full
drag-and-drop support for bag items with server-side CMSG_SWAP_ITEM packets.
Add Classic-specific SMSG_ITEM_QUERY_SINGLE_RESPONSE parser for Vanilla
format differences (fewer damage types, no scaling stats, no Flags2).
This commit is contained in:
Kelsi 2026-02-17 01:00:04 -08:00
parent 97ea390969
commit 5ea41df8b5
10 changed files with 384 additions and 62 deletions

View file

@ -4396,6 +4396,9 @@ void GameHandler::handleChannelNotify(network::Packet& packet) {
}
void GameHandler::autoJoinDefaultChannels() {
LOG_INFO("autoJoinDefaultChannels: general=", chatAutoJoin.general,
" trade=", chatAutoJoin.trade, " localDefense=", chatAutoJoin.localDefense,
" lfg=", chatAutoJoin.lfg, " local=", chatAutoJoin.local);
if (chatAutoJoin.general) joinChannel("General");
if (chatAutoJoin.trade) joinChannel("Trade");
if (chatAutoJoin.localDefense) joinChannel("LocalDefense");
@ -5550,7 +5553,10 @@ void GameHandler::queryItemInfo(uint32_t entry, uint64_t guid) {
void GameHandler::handleItemQueryResponse(network::Packet& packet) {
ItemQueryResponseData data;
if (!ItemQueryResponseParser::parse(packet, data)) return;
bool parsed = packetParsers_
? packetParsers_->parseItemQueryResponse(packet, data)
: ItemQueryResponseParser::parse(packet, data);
if (!parsed) return;
pendingItemQueries_.erase(data.entry);
@ -6903,6 +6909,7 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) {
// Hearthstone is item-bound; use the item rather than direct spell cast.
if (spellId == 8690) {
LOG_INFO("Hearthstone spell intercepted, routing to useItemById(6948)");
useItemById(6948);
return;
}
@ -7904,6 +7911,46 @@ void GameHandler::autoEquipItemBySlot(int backpackIndex) {
}
}
void GameHandler::autoEquipItemInBag(int bagIndex, int slotIndex) {
if (bagIndex < 0 || bagIndex >= inventory.NUM_BAG_SLOTS) return;
if (slotIndex < 0 || slotIndex >= inventory.getBagSize(bagIndex)) return;
if (state == WorldState::IN_WORLD && socket) {
// Bag items: bag = equip slot 19+bagIndex, slot = index within bag
auto packet = AutoEquipItemPacket::build(
static_cast<uint8_t>(19 + bagIndex), static_cast<uint8_t>(slotIndex));
socket->send(packet);
}
}
void GameHandler::sellItemInBag(int bagIndex, int slotIndex) {
if (bagIndex < 0 || bagIndex >= inventory.NUM_BAG_SLOTS) return;
if (slotIndex < 0 || slotIndex >= inventory.getBagSize(bagIndex)) return;
const auto& slot = inventory.getBagSlot(bagIndex, slotIndex);
if (slot.empty()) return;
// Resolve item GUID from container contents
uint64_t itemGuid = 0;
uint64_t bagGuid = equipSlotGuids_[19 + bagIndex];
if (bagGuid != 0) {
auto it = containerContents_.find(bagGuid);
if (it != containerContents_.end() && slotIndex < static_cast<int>(it->second.numSlots)) {
itemGuid = it->second.slotGuids[slotIndex];
}
}
if (itemGuid == 0) {
itemGuid = resolveOnlineItemGuid(slot.item.itemId);
}
if (itemGuid != 0 && currentVendorItems.vendorGuid != 0) {
sellItem(currentVendorItems.vendorGuid, itemGuid, 1);
} else if (itemGuid == 0) {
addSystemChatMessage("Cannot sell: item not found.");
} else {
addSystemChatMessage("Cannot sell: no vendor.");
}
}
void GameHandler::unequipToBackpack(EquipSlot equipSlot) {
if (state != WorldState::IN_WORLD || !socket) return;
@ -7926,35 +7973,105 @@ void GameHandler::unequipToBackpack(EquipSlot equipSlot) {
socket->send(packet);
}
void GameHandler::swapContainerItems(uint8_t srcBag, uint8_t srcSlot, uint8_t dstBag, uint8_t dstSlot) {
if (!socket || !socket->isConnected()) return;
LOG_INFO("swapContainerItems: src(bag=", (int)srcBag, " slot=", (int)srcSlot,
") -> dst(bag=", (int)dstBag, " slot=", (int)dstSlot, ")");
auto packet = SwapItemPacket::build(dstBag, dstSlot, srcBag, srcSlot);
socket->send(packet);
}
void GameHandler::useItemBySlot(int backpackIndex) {
if (backpackIndex < 0 || backpackIndex >= inventory.getBackpackSize()) return;
const auto& slot = inventory.getBackpackSlot(backpackIndex);
if (slot.empty()) return;
if (slot.empty()) {
LOG_WARNING("useItemBySlot: slot ", backpackIndex, " is empty");
return;
}
LOG_INFO("useItemBySlot: backpackIndex=", backpackIndex, " itemId=", slot.item.itemId,
" wowSlot=", 23 + backpackIndex);
uint64_t itemGuid = backpackSlotGuids_[backpackIndex];
if (itemGuid == 0) {
itemGuid = resolveOnlineItemGuid(slot.item.itemId);
}
LOG_INFO("useItemBySlot: itemGuid=0x", std::hex, itemGuid, std::dec);
if (itemGuid != 0 && state == WorldState::IN_WORLD && socket) {
// WoW inventory: equipment 0-18, bags 19-22, backpack 23-38
auto packet = packetParsers_
? packetParsers_->buildUseItem(0xFF, static_cast<uint8_t>(23 + backpackIndex), itemGuid)
: UseItemPacket::build(0xFF, static_cast<uint8_t>(23 + backpackIndex), itemGuid);
LOG_INFO("useItemBySlot: sending CMSG_USE_ITEM, packetSize=", packet.getSize());
socket->send(packet);
} else if (itemGuid == 0) {
LOG_WARNING("Use item failed: missing item GUID for slot ", backpackIndex);
}
}
void GameHandler::useItemInBag(int bagIndex, int slotIndex) {
if (bagIndex < 0 || bagIndex >= inventory.NUM_BAG_SLOTS) return;
if (slotIndex < 0 || slotIndex >= inventory.getBagSize(bagIndex)) return;
const auto& slot = inventory.getBagSlot(bagIndex, slotIndex);
if (slot.empty()) return;
// Resolve item GUID from container contents
uint64_t itemGuid = 0;
uint64_t bagGuid = equipSlotGuids_[19 + bagIndex];
if (bagGuid != 0) {
auto it = containerContents_.find(bagGuid);
if (it != containerContents_.end() && slotIndex < static_cast<int>(it->second.numSlots)) {
itemGuid = it->second.slotGuids[slotIndex];
}
}
if (itemGuid == 0) {
itemGuid = resolveOnlineItemGuid(slot.item.itemId);
}
LOG_INFO("useItemInBag: bag=", bagIndex, " slot=", slotIndex, " itemId=", slot.item.itemId,
" itemGuid=0x", std::hex, itemGuid, std::dec);
if (itemGuid != 0 && state == WorldState::IN_WORLD && socket) {
// WoW bag addressing: bagIndex = equip slot of bag container (19-22)
// For CMSG_USE_ITEM: bag = 19+bagIndex, slot = slot within bag
uint8_t wowBag = static_cast<uint8_t>(19 + bagIndex);
auto packet = packetParsers_
? packetParsers_->buildUseItem(wowBag, static_cast<uint8_t>(slotIndex), itemGuid)
: UseItemPacket::build(wowBag, static_cast<uint8_t>(slotIndex), itemGuid);
LOG_INFO("useItemInBag: sending CMSG_USE_ITEM, bag=", (int)wowBag, " slot=", slotIndex,
" packetSize=", packet.getSize());
socket->send(packet);
} else if (itemGuid == 0) {
LOG_WARNING("Use item in bag failed: missing item GUID for bag ", bagIndex, " slot ", slotIndex);
}
}
void GameHandler::useItemById(uint32_t itemId) {
if (itemId == 0) return;
LOG_INFO("useItemById: searching for itemId=", itemId, " in backpack (", inventory.getBackpackSize(), " slots)");
// Search backpack first
for (int i = 0; i < inventory.getBackpackSize(); i++) {
const auto& slot = inventory.getBackpackSlot(i);
if (!slot.empty() && slot.item.itemId == itemId) {
LOG_INFO("useItemById: found itemId=", itemId, " at backpack slot ", i);
useItemBySlot(i);
return;
}
}
// Search bags
for (int bag = 0; bag < inventory.NUM_BAG_SLOTS; bag++) {
int bagSize = inventory.getBagSize(bag);
for (int slot = 0; slot < bagSize; slot++) {
const auto& bagSlot = inventory.getBagSlot(bag, slot);
if (!bagSlot.empty() && bagSlot.item.itemId == itemId) {
LOG_INFO("useItemById: found itemId=", itemId, " in bag ", bag, " slot ", slot);
useItemInBag(bag, slot);
return;
}
}
}
LOG_WARNING("useItemById: itemId=", itemId, " not found in inventory");
}
void GameHandler::unstuck() {

View file

@ -68,6 +68,13 @@ bool Inventory::setBagSlot(int bagIndex, int slotIndex, const ItemDef& item) {
return true;
}
bool Inventory::clearBagSlot(int bagIndex, int slotIndex) {
if (bagIndex < 0 || bagIndex >= NUM_BAG_SLOTS) return false;
if (slotIndex < 0 || slotIndex >= bags[bagIndex].size) return false;
bags[bagIndex].slots[slotIndex].item = ItemDef{};
return true;
}
const ItemSlot& Inventory::getBankSlot(int index) const {
if (index < 0 || index >= BANK_SLOTS) return EMPTY_SLOT;
return bankSlots_[index];

View file

@ -841,5 +841,91 @@ network::Packet ClassicPacketParsers::buildMailDelete(uint64_t mailboxGuid,
return packet;
}
// ============================================================================
// Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE
// Vanilla has NO SoundOverrideSubclass, NO Flags2, NO ScalingStatDistribution,
// NO ScalingStatValue, and only 2 damage types (not 5).
// ============================================================================
bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQueryResponseData& data) {
data.entry = packet.readUInt32();
// High bit set means item not found
if (data.entry & 0x80000000) {
data.entry &= ~0x80000000;
return true;
}
uint32_t itemClass = packet.readUInt32();
uint32_t subClass = packet.readUInt32();
// Vanilla: NO SoundOverrideSubclass
(void)itemClass;
(void)subClass;
// 4 name strings
data.name = packet.readString();
packet.readString(); // name2
packet.readString(); // name3
packet.readString(); // name4
data.displayInfoId = packet.readUInt32();
data.quality = packet.readUInt32();
packet.readUInt32(); // Flags
// Vanilla: NO Flags2
packet.readUInt32(); // BuyPrice
data.sellPrice = packet.readUInt32(); // SellPrice
data.inventoryType = packet.readUInt32();
packet.readUInt32(); // AllowableClass
packet.readUInt32(); // AllowableRace
packet.readUInt32(); // ItemLevel
packet.readUInt32(); // RequiredLevel
packet.readUInt32(); // RequiredSkill
packet.readUInt32(); // RequiredSkillRank
packet.readUInt32(); // RequiredSpell
packet.readUInt32(); // RequiredHonorRank
packet.readUInt32(); // RequiredCityRank
packet.readUInt32(); // RequiredReputationFaction
packet.readUInt32(); // RequiredReputationRank
packet.readUInt32(); // MaxCount
data.maxStack = static_cast<int32_t>(packet.readUInt32()); // Stackable
data.containerSlots = packet.readUInt32();
// Vanilla: 10 stat pairs (same as WotLK)
uint32_t statsCount = packet.readUInt32();
for (uint32_t i = 0; i < 10; 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;
}
}
}
// Vanilla: NO ScalingStatDistribution, NO ScalingStatValue
// Vanilla: only 2 damage types (not 5)
for (int i = 0; i < 2; i++) {
packet.readFloat(); // DamageMin
packet.readFloat(); // DamageMax
packet.readUInt32(); // DamageType
}
data.armor = static_cast<int32_t>(packet.readUInt32());
data.valid = !data.name.empty();
LOG_DEBUG("[Classic] Item query response: ", data.name, " (quality=", data.quality,
" invType=", data.inventoryType, " stack=", data.maxStack, ")");
return true;
}
} // namespace game
} // namespace wowee