mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-25 00:20:16 +00:00
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:
parent
44d1431b60
commit
a1457ee801
10 changed files with 384 additions and 62 deletions
|
|
@ -34,7 +34,7 @@
|
|||
"EquipDisplay0": 8, "EquipDisplay1": 9, "EquipDisplay2": 10,
|
||||
"EquipDisplay3": 11, "EquipDisplay4": 12, "EquipDisplay5": 13,
|
||||
"EquipDisplay6": 14, "EquipDisplay7": 15, "EquipDisplay8": 16,
|
||||
"EquipDisplay9": 17, "EquipDisplay10": 18, "BakeName": 20
|
||||
"EquipDisplay9": 17, "BakeName": 18
|
||||
},
|
||||
"CreatureDisplayInfo": {
|
||||
"ID": 0, "ModelID": 1, "ExtraDisplayId": 3,
|
||||
|
|
|
|||
|
|
@ -762,8 +762,12 @@ public:
|
|||
void buyItem(uint64_t vendorGuid, uint32_t itemId, uint32_t slot, uint32_t count);
|
||||
void sellItem(uint64_t vendorGuid, uint64_t itemGuid, uint32_t count);
|
||||
void sellItemBySlot(int backpackIndex);
|
||||
void sellItemInBag(int bagIndex, int slotIndex);
|
||||
void autoEquipItemBySlot(int backpackIndex);
|
||||
void autoEquipItemInBag(int bagIndex, int slotIndex);
|
||||
void useItemBySlot(int backpackIndex);
|
||||
void useItemInBag(int bagIndex, int slotIndex);
|
||||
void swapContainerItems(uint8_t srcBag, uint8_t srcSlot, uint8_t dstBag, uint8_t dstSlot);
|
||||
void useItemById(uint32_t itemId);
|
||||
bool isVendorWindowOpen() const { return vendorWindowOpen; }
|
||||
const ListInventoryData& getVendorItems() const { return currentVendorItems; }
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ public:
|
|||
void setBagSize(int bagIndex, int size);
|
||||
const ItemSlot& getBagSlot(int bagIndex, int slotIndex) const;
|
||||
bool setBagSlot(int bagIndex, int slotIndex, const ItemDef& item);
|
||||
bool clearBagSlot(int bagIndex, int slotIndex);
|
||||
|
||||
// Bank slots (28 main + 7 bank bags)
|
||||
const ItemSlot& getBankSlot(int index) const;
|
||||
|
|
|
|||
|
|
@ -122,6 +122,13 @@ public:
|
|||
return NameQueryResponseParser::parse(packet, data);
|
||||
}
|
||||
|
||||
// --- Item Query ---
|
||||
|
||||
/** Parse SMSG_ITEM_QUERY_SINGLE_RESPONSE */
|
||||
virtual bool parseItemQueryResponse(network::Packet& packet, ItemQueryResponseData& data) {
|
||||
return ItemQueryResponseParser::parse(packet, data);
|
||||
}
|
||||
|
||||
// --- GameObject Query ---
|
||||
|
||||
/** Parse SMSG_GAMEOBJECT_QUERY_RESPONSE */
|
||||
|
|
@ -280,6 +287,7 @@ public:
|
|||
bool parseMailList(network::Packet& packet, std::vector<MailMessage>& inbox) override;
|
||||
network::Packet buildMailTakeItem(uint64_t mailboxGuid, uint32_t mailId, uint32_t itemSlot) override;
|
||||
network::Packet buildMailDelete(uint64_t mailboxGuid, uint32_t mailId, uint32_t mailTemplateId) override;
|
||||
bool parseItemQueryResponse(network::Packet& packet, ItemQueryResponseData& data) override;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -113,9 +113,11 @@ private:
|
|||
// Drag-and-drop held item state
|
||||
bool holdingItem = false;
|
||||
game::ItemDef heldItem;
|
||||
enum class HeldSource { NONE, BACKPACK, EQUIPMENT };
|
||||
enum class HeldSource { NONE, BACKPACK, BAG, EQUIPMENT };
|
||||
HeldSource heldSource = HeldSource::NONE;
|
||||
int heldBackpackIndex = -1;
|
||||
int heldBagIndex = -1;
|
||||
int heldBagSlotIndex = -1;
|
||||
game::EquipSlot heldEquipSlot = game::EquipSlot::NUM_SLOTS;
|
||||
|
||||
void renderSeparateBags(game::Inventory& inventory, uint64_t moneyCopper);
|
||||
|
|
@ -131,13 +133,16 @@ private:
|
|||
void renderItemSlot(game::Inventory& inventory, const game::ItemSlot& slot,
|
||||
float size, const char* label,
|
||||
SlotKind kind, int backpackIndex,
|
||||
game::EquipSlot equipSlot);
|
||||
game::EquipSlot equipSlot,
|
||||
int bagIndex = -1, int bagSlotIndex = -1);
|
||||
void renderItemTooltip(const game::ItemDef& item);
|
||||
|
||||
// Held item helpers
|
||||
void pickupFromBackpack(game::Inventory& inv, int index);
|
||||
void pickupFromBag(game::Inventory& inv, int bagIndex, int slotIndex);
|
||||
void pickupFromEquipment(game::Inventory& inv, game::EquipSlot slot);
|
||||
void placeInBackpack(game::Inventory& inv, int index);
|
||||
void placeInBag(game::Inventory& inv, int bagIndex, int slotIndex);
|
||||
void placeInEquipment(game::Inventory& inv, game::EquipSlot slot);
|
||||
void cancelPickup(game::Inventory& inv);
|
||||
game::EquipSlot getEquipSlotForType(uint8_t inventoryType, game::Inventory& inv);
|
||||
|
|
|
|||
|
|
@ -480,6 +480,13 @@ void Application::reloadExpansionData() {
|
|||
if (gameHandler) {
|
||||
gameHandler->resetDbcCaches();
|
||||
}
|
||||
|
||||
// Rebuild creature display lookups with the new expansion's DBC layout
|
||||
creatureLookupsBuilt_ = false;
|
||||
displayDataMap_.clear();
|
||||
humanoidExtraMap_.clear();
|
||||
creatureModelIds_.clear();
|
||||
buildCreatureDisplayLookups();
|
||||
}
|
||||
|
||||
void Application::logoutToLogin() {
|
||||
|
|
@ -2757,12 +2764,19 @@ void Application::buildCreatureDisplayLookups() {
|
|||
// Col 5: HairStyleID
|
||||
// Col 6: HairColorID
|
||||
// Col 7: FacialHairID
|
||||
// Col 8-18: Item display IDs (equipment slots)
|
||||
// Col 19: Flags
|
||||
// Col 20: BakeName (pre-baked texture path)
|
||||
// Turtle/Vanilla: 19 fields — 10 equip slots (8-17), BakeName=18 (no Flags field)
|
||||
// WotLK/TBC/Classic: 21 fields — 11 equip slots (8-18), Flags=19, BakeName=20
|
||||
if (auto cdie = assetManager->loadDBC("CreatureDisplayInfoExtra.dbc"); cdie && cdie->isLoaded()) {
|
||||
const auto* cdieL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CreatureDisplayInfoExtra") : nullptr;
|
||||
const uint32_t cdieEquip0 = cdieL ? (*cdieL)["EquipDisplay0"] : 8;
|
||||
const uint32_t bakeField = cdieL ? (*cdieL)["BakeName"] : 20;
|
||||
// Count equipment slots: Vanilla/Turtle has 10, WotLK/TBC has 11
|
||||
int numEquipSlots = 10;
|
||||
if (cdieL && cdieL->field("EquipDisplay10") != 0xFFFFFFFF) {
|
||||
numEquipSlots = 11;
|
||||
} else if (!cdieL) {
|
||||
numEquipSlots = 11; // Default (WotLK) has 11
|
||||
}
|
||||
uint32_t withBakeName = 0;
|
||||
for (uint32_t i = 0; i < cdie->getRecordCount(); i++) {
|
||||
HumanoidDisplayExtra extra;
|
||||
|
|
@ -2773,14 +2787,15 @@ void Application::buildCreatureDisplayLookups() {
|
|||
extra.hairStyleId = static_cast<uint8_t>(cdie->getUInt32(i, cdieL ? (*cdieL)["HairStyleID"] : 5));
|
||||
extra.hairColorId = static_cast<uint8_t>(cdie->getUInt32(i, cdieL ? (*cdieL)["HairColorID"] : 6));
|
||||
extra.facialHairId = static_cast<uint8_t>(cdie->getUInt32(i, cdieL ? (*cdieL)["FacialHairID"] : 7));
|
||||
for (int eq = 0; eq < 11; eq++) {
|
||||
for (int eq = 0; eq < numEquipSlots; eq++) {
|
||||
extra.equipDisplayId[eq] = cdie->getUInt32(i, cdieEquip0 + eq);
|
||||
}
|
||||
extra.bakeName = cdie->getString(i, cdieL ? (*cdieL)["BakeName"] : 20);
|
||||
extra.bakeName = cdie->getString(i, bakeField);
|
||||
if (!extra.bakeName.empty()) withBakeName++;
|
||||
humanoidExtraMap_[cdie->getUInt32(i, cdieL ? (*cdieL)["ID"] : 0)] = extra;
|
||||
}
|
||||
LOG_INFO("Loaded ", humanoidExtraMap_.size(), " humanoid display extra entries (", withBakeName, " with baked textures)");
|
||||
LOG_INFO("Loaded ", humanoidExtraMap_.size(), " humanoid display extra entries (",
|
||||
withBakeName, " with baked textures, ", numEquipSlots, " equip slots)");
|
||||
}
|
||||
|
||||
// CreatureModelData.dbc: modelId (col 0) → modelPath (col 2, .mdx → .m2)
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -361,6 +361,20 @@ void InventoryScreen::pickupFromBackpack(game::Inventory& inv, int index) {
|
|||
inventoryDirty = true;
|
||||
}
|
||||
|
||||
void InventoryScreen::pickupFromBag(game::Inventory& inv, int bagIndex, int slotIndex) {
|
||||
const auto& slot = inv.getBagSlot(bagIndex, slotIndex);
|
||||
if (slot.empty()) return;
|
||||
holdingItem = true;
|
||||
heldItem = slot.item;
|
||||
heldSource = HeldSource::BAG;
|
||||
heldBackpackIndex = -1;
|
||||
heldBagIndex = bagIndex;
|
||||
heldBagSlotIndex = slotIndex;
|
||||
heldEquipSlot = game::EquipSlot::NUM_SLOTS;
|
||||
inv.clearBagSlot(bagIndex, slotIndex);
|
||||
inventoryDirty = true;
|
||||
}
|
||||
|
||||
void InventoryScreen::pickupFromEquipment(game::Inventory& inv, game::EquipSlot slot) {
|
||||
const auto& es = inv.getEquipSlot(slot);
|
||||
if (es.empty()) return;
|
||||
|
|
@ -376,8 +390,24 @@ void InventoryScreen::pickupFromEquipment(game::Inventory& inv, game::EquipSlot
|
|||
|
||||
void InventoryScreen::placeInBackpack(game::Inventory& inv, int index) {
|
||||
if (!holdingItem) return;
|
||||
if (gameHandler_ && heldSource == HeldSource::EQUIPMENT) {
|
||||
// Online mode: avoid client-side unequip; wait for server update.
|
||||
if (gameHandler_) {
|
||||
// Online mode: send server swap packet for all container moves
|
||||
uint8_t dstBag = 0xFF;
|
||||
uint8_t dstSlot = static_cast<uint8_t>(23 + index);
|
||||
uint8_t srcBag = 0xFF;
|
||||
uint8_t srcSlot = 0;
|
||||
if (heldSource == HeldSource::BACKPACK && heldBackpackIndex >= 0) {
|
||||
srcSlot = static_cast<uint8_t>(23 + heldBackpackIndex);
|
||||
} else if (heldSource == HeldSource::BAG) {
|
||||
srcBag = static_cast<uint8_t>(19 + heldBagIndex);
|
||||
srcSlot = static_cast<uint8_t>(heldBagSlotIndex);
|
||||
} else if (heldSource == HeldSource::EQUIPMENT) {
|
||||
srcSlot = static_cast<uint8_t>(heldEquipSlot);
|
||||
} else {
|
||||
cancelPickup(inv);
|
||||
return;
|
||||
}
|
||||
gameHandler_->swapContainerItems(srcBag, srcSlot, dstBag, dstSlot);
|
||||
cancelPickup(inv);
|
||||
return;
|
||||
}
|
||||
|
|
@ -396,17 +426,58 @@ void InventoryScreen::placeInBackpack(game::Inventory& inv, int index) {
|
|||
inventoryDirty = true;
|
||||
}
|
||||
|
||||
void InventoryScreen::placeInBag(game::Inventory& inv, int bagIndex, int slotIndex) {
|
||||
if (!holdingItem) return;
|
||||
if (gameHandler_) {
|
||||
// Online mode: send server swap packet
|
||||
uint8_t dstBag = static_cast<uint8_t>(19 + bagIndex);
|
||||
uint8_t dstSlot = static_cast<uint8_t>(slotIndex);
|
||||
uint8_t srcBag = 0xFF;
|
||||
uint8_t srcSlot = 0;
|
||||
if (heldSource == HeldSource::BACKPACK && heldBackpackIndex >= 0) {
|
||||
srcSlot = static_cast<uint8_t>(23 + heldBackpackIndex);
|
||||
} else if (heldSource == HeldSource::BAG) {
|
||||
srcBag = static_cast<uint8_t>(19 + heldBagIndex);
|
||||
srcSlot = static_cast<uint8_t>(heldBagSlotIndex);
|
||||
} else if (heldSource == HeldSource::EQUIPMENT) {
|
||||
srcSlot = static_cast<uint8_t>(heldEquipSlot);
|
||||
} else {
|
||||
cancelPickup(inv);
|
||||
return;
|
||||
}
|
||||
gameHandler_->swapContainerItems(srcBag, srcSlot, dstBag, dstSlot);
|
||||
cancelPickup(inv);
|
||||
return;
|
||||
}
|
||||
const auto& target = inv.getBagSlot(bagIndex, slotIndex);
|
||||
if (target.empty()) {
|
||||
inv.setBagSlot(bagIndex, slotIndex, heldItem);
|
||||
holdingItem = false;
|
||||
} else {
|
||||
game::ItemDef targetItem = target.item;
|
||||
inv.setBagSlot(bagIndex, slotIndex, heldItem);
|
||||
heldItem = targetItem;
|
||||
heldSource = HeldSource::BAG;
|
||||
heldBagIndex = bagIndex;
|
||||
heldBagSlotIndex = slotIndex;
|
||||
}
|
||||
inventoryDirty = true;
|
||||
}
|
||||
|
||||
void InventoryScreen::placeInEquipment(game::Inventory& inv, game::EquipSlot slot) {
|
||||
if (!holdingItem) return;
|
||||
if (gameHandler_) {
|
||||
if (heldSource == HeldSource::BACKPACK && heldBackpackIndex >= 0) {
|
||||
// Online mode: request server auto-equip and keep local state intact.
|
||||
gameHandler_->autoEquipItemBySlot(heldBackpackIndex);
|
||||
cancelPickup(inv);
|
||||
return;
|
||||
}
|
||||
if (heldSource == HeldSource::BAG) {
|
||||
gameHandler_->autoEquipItemInBag(heldBagIndex, heldBagSlotIndex);
|
||||
cancelPickup(inv);
|
||||
return;
|
||||
}
|
||||
if (heldSource == HeldSource::EQUIPMENT) {
|
||||
// Online mode: avoid client-side equipment swaps.
|
||||
cancelPickup(inv);
|
||||
return;
|
||||
}
|
||||
|
|
@ -470,6 +541,12 @@ void InventoryScreen::cancelPickup(game::Inventory& inv) {
|
|||
} else {
|
||||
inv.addItem(heldItem);
|
||||
}
|
||||
} else if (heldSource == HeldSource::BAG && heldBagIndex >= 0 && heldBagSlotIndex >= 0) {
|
||||
if (inv.getBagSlot(heldBagIndex, heldBagSlotIndex).empty()) {
|
||||
inv.setBagSlot(heldBagIndex, heldBagSlotIndex, heldItem);
|
||||
} else {
|
||||
inv.addItem(heldItem);
|
||||
}
|
||||
} else if (heldSource == HeldSource::EQUIPMENT && heldEquipSlot != game::EquipSlot::NUM_SLOTS) {
|
||||
if (inv.getEquipSlot(heldEquipSlot).empty()) {
|
||||
inv.setEquipSlot(heldEquipSlot, heldItem);
|
||||
|
|
@ -784,10 +861,16 @@ void InventoryScreen::renderBagWindow(const char* title, bool& isOpen,
|
|||
}
|
||||
ImGui::PushID(id);
|
||||
|
||||
// For backpack slots, pass actual backpack index for drag/drop
|
||||
int bpIdx = (bagIndex < 0) ? i : -1;
|
||||
renderItemSlot(inventory, slot, slotSize, nullptr,
|
||||
SlotKind::BACKPACK, bpIdx, game::EquipSlot::NUM_SLOTS);
|
||||
if (bagIndex < 0) {
|
||||
// Backpack slot
|
||||
renderItemSlot(inventory, slot, slotSize, nullptr,
|
||||
SlotKind::BACKPACK, i, game::EquipSlot::NUM_SLOTS);
|
||||
} else {
|
||||
// Bag slot - pass bag index info for interactions
|
||||
renderItemSlot(inventory, slot, slotSize, nullptr,
|
||||
SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS,
|
||||
bagIndex, i);
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
|
|
@ -1151,7 +1234,8 @@ void InventoryScreen::renderBackpackPanel(game::Inventory& inventory) {
|
|||
snprintf(sid, sizeof(sid), "##bag%d_%d", bag, s);
|
||||
ImGui::PushID(sid);
|
||||
renderItemSlot(inventory, slot, slotSize, nullptr,
|
||||
SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS);
|
||||
SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS,
|
||||
bag, s);
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
|
@ -1160,7 +1244,10 @@ void InventoryScreen::renderBackpackPanel(game::Inventory& inventory) {
|
|||
void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::ItemSlot& slot,
|
||||
float size, const char* label,
|
||||
SlotKind kind, int backpackIndex,
|
||||
game::EquipSlot equipSlot) {
|
||||
game::EquipSlot equipSlot,
|
||||
int bagIndex, int bagSlotIndex) {
|
||||
// Bag items are valid inventory slots even though backpackIndex is -1
|
||||
bool isBagSlot = (bagIndex >= 0 && bagSlotIndex >= 0);
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
|
||||
|
|
@ -1169,7 +1256,7 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
// Determine if this is a valid drop target for held item
|
||||
bool validDrop = false;
|
||||
if (holdingItem) {
|
||||
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
if (kind == SlotKind::BACKPACK && (backpackIndex >= 0 || isBagSlot)) {
|
||||
validDrop = true;
|
||||
} else if (kind == SlotKind::EQUIPMENT && heldItem.inventoryType > 0) {
|
||||
game::EquipSlot validSlot = getEquipSlotForType(heldItem.inventoryType, inventory);
|
||||
|
|
@ -1207,6 +1294,8 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
if (ImGui::IsItemClicked(ImGuiMouseButton_Left) && holdingItem && validDrop) {
|
||||
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
placeInBackpack(inventory, backpackIndex);
|
||||
} else if (kind == SlotKind::BACKPACK && isBagSlot) {
|
||||
placeInBag(inventory, bagIndex, bagSlotIndex);
|
||||
} else if (kind == SlotKind::EQUIPMENT) {
|
||||
placeInEquipment(inventory, equipSlot);
|
||||
}
|
||||
|
|
@ -1266,60 +1355,50 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
|||
if (!holdingItem) {
|
||||
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
pickupFromBackpack(inventory, backpackIndex);
|
||||
} else if (kind == SlotKind::BACKPACK && isBagSlot) {
|
||||
pickupFromBag(inventory, bagIndex, bagSlotIndex);
|
||||
} else if (kind == SlotKind::EQUIPMENT) {
|
||||
if (gameHandler_) {
|
||||
// Online mode: don't mutate local equipment state.
|
||||
game::MessageChatData msg{};
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Moving equipped items not supported yet (online mode).";
|
||||
gameHandler_->addLocalChatMessage(msg);
|
||||
} else {
|
||||
pickupFromEquipment(inventory, equipSlot);
|
||||
}
|
||||
game::MessageChatData msg{};
|
||||
msg.type = game::ChatType::SYSTEM;
|
||||
msg.language = game::ChatLanguage::UNIVERSAL;
|
||||
msg.message = "Moving equipped items not supported yet (online mode).";
|
||||
if (gameHandler_) gameHandler_->addLocalChatMessage(msg);
|
||||
}
|
||||
} else {
|
||||
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
placeInBackpack(inventory, backpackIndex);
|
||||
} else if (kind == SlotKind::BACKPACK && isBagSlot) {
|
||||
placeInBag(inventory, bagIndex, bagSlotIndex);
|
||||
} else if (kind == SlotKind::EQUIPMENT && validDrop) {
|
||||
placeInEquipment(inventory, equipSlot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Right-click: vendor sell (if vendor mode) or auto-equip/unequip
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Right) && !holdingItem) {
|
||||
LOG_DEBUG("Right-click slot: kind=", (int)kind,
|
||||
" backpackIndex=", backpackIndex,
|
||||
" vendorMode=", vendorMode_,
|
||||
" hasHandler=", (gameHandler_ != nullptr));
|
||||
if (vendorMode_ && gameHandler_ && kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
// Sell to vendor
|
||||
// Right-click: vendor sell (if vendor mode) or auto-equip/use
|
||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Right) && !holdingItem && gameHandler_) {
|
||||
LOG_INFO("Right-click slot: kind=", (int)kind,
|
||||
" backpackIndex=", backpackIndex,
|
||||
" bagIndex=", bagIndex, " bagSlotIndex=", bagSlotIndex,
|
||||
" vendorMode=", vendorMode_);
|
||||
if (vendorMode_ && kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
gameHandler_->sellItemBySlot(backpackIndex);
|
||||
} else if (vendorMode_ && kind == SlotKind::BACKPACK && isBagSlot) {
|
||||
gameHandler_->sellItemInBag(bagIndex, bagSlotIndex);
|
||||
} else if (kind == SlotKind::EQUIPMENT) {
|
||||
if (gameHandler_) {
|
||||
// Online mode: request server-side unequip (move to first free backpack slot).
|
||||
LOG_INFO("UI unequip request: equipSlot=", (int)equipSlot);
|
||||
gameHandler_->unequipToBackpack(equipSlot);
|
||||
} else {
|
||||
// Offline mode: Unequip: move to free backpack slot
|
||||
int freeSlot = inventory.findFreeBackpackSlot();
|
||||
if (freeSlot >= 0) {
|
||||
inventory.setBackpackSlot(freeSlot, item);
|
||||
inventory.clearEquipSlot(equipSlot);
|
||||
equipmentDirty = true;
|
||||
inventoryDirty = true;
|
||||
}
|
||||
}
|
||||
LOG_INFO("UI unequip request: equipSlot=", (int)equipSlot);
|
||||
gameHandler_->unequipToBackpack(equipSlot);
|
||||
} else if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||
if (gameHandler_) {
|
||||
if (item.inventoryType > 0) {
|
||||
// Auto-equip (online)
|
||||
gameHandler_->autoEquipItemBySlot(backpackIndex);
|
||||
} else {
|
||||
// Use consumable (online)
|
||||
gameHandler_->useItemBySlot(backpackIndex);
|
||||
}
|
||||
if (item.inventoryType > 0) {
|
||||
gameHandler_->autoEquipItemBySlot(backpackIndex);
|
||||
} else {
|
||||
gameHandler_->useItemBySlot(backpackIndex);
|
||||
}
|
||||
} else if (kind == SlotKind::BACKPACK && isBagSlot) {
|
||||
if (item.inventoryType > 0) {
|
||||
gameHandler_->autoEquipItemInBag(bagIndex, bagSlotIndex);
|
||||
} else {
|
||||
gameHandler_->useItemInBag(bagIndex, bagSlotIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue