diff --git a/Data/expansions/turtle/dbc_layouts.json b/Data/expansions/turtle/dbc_layouts.json index 5a38f46c..45517b04 100644 --- a/Data/expansions/turtle/dbc_layouts.json +++ b/Data/expansions/turtle/dbc_layouts.json @@ -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, diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index b6005824..c480877e 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -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; } diff --git a/include/game/inventory.hpp b/include/game/inventory.hpp index bb36e3bd..c4bc7334 100644 --- a/include/game/inventory.hpp +++ b/include/game/inventory.hpp @@ -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; diff --git a/include/game/packet_parsers.hpp b/include/game/packet_parsers.hpp index 34ed09c5..9bc6b72a 100644 --- a/include/game/packet_parsers.hpp +++ b/include/game/packet_parsers.hpp @@ -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& 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; }; /** diff --git a/include/ui/inventory_screen.hpp b/include/ui/inventory_screen.hpp index 204d9687..decaa70c 100644 --- a/include/ui/inventory_screen.hpp +++ b/include/ui/inventory_screen.hpp @@ -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); diff --git a/src/core/application.cpp b/src/core/application.cpp index ee8bb4a0..2255c6a8 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -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(cdie->getUInt32(i, cdieL ? (*cdieL)["HairStyleID"] : 5)); extra.hairColorId = static_cast(cdie->getUInt32(i, cdieL ? (*cdieL)["HairColorID"] : 6)); extra.facialHairId = static_cast(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) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 9d6f0e30..0a61e1e0 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -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(19 + bagIndex), static_cast(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(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(23 + backpackIndex), itemGuid) : UseItemPacket::build(0xFF, static_cast(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(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(19 + bagIndex); + auto packet = packetParsers_ + ? packetParsers_->buildUseItem(wowBag, static_cast(slotIndex), itemGuid) + : UseItemPacket::build(wowBag, static_cast(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() { diff --git a/src/game/inventory.cpp b/src/game/inventory.cpp index f42a42e5..a447d444 100644 --- a/src/game/inventory.cpp +++ b/src/game/inventory.cpp @@ -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]; diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index ef7a121d..086a4863 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -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(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(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(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 diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index f5e4804f..fdf4eb0b 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -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(23 + index); + uint8_t srcBag = 0xFF; + uint8_t srcSlot = 0; + if (heldSource == HeldSource::BACKPACK && heldBackpackIndex >= 0) { + srcSlot = static_cast(23 + heldBackpackIndex); + } else if (heldSource == HeldSource::BAG) { + srcBag = static_cast(19 + heldBagIndex); + srcSlot = static_cast(heldBagSlotIndex); + } else if (heldSource == HeldSource::EQUIPMENT) { + srcSlot = static_cast(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(19 + bagIndex); + uint8_t dstSlot = static_cast(slotIndex); + uint8_t srcBag = 0xFF; + uint8_t srcSlot = 0; + if (heldSource == HeldSource::BACKPACK && heldBackpackIndex >= 0) { + srcSlot = static_cast(23 + heldBackpackIndex); + } else if (heldSource == HeldSource::BAG) { + srcBag = static_cast(19 + heldBagIndex); + srcSlot = static_cast(heldBagSlotIndex); + } else if (heldSource == HeldSource::EQUIPMENT) { + srcSlot = static_cast(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); } } }