diff --git a/Data/expansions/wotlk/opcodes.json b/Data/expansions/wotlk/opcodes.json index e262cf94..a37c38f9 100644 --- a/Data/expansions/wotlk/opcodes.json +++ b/Data/expansions/wotlk/opcodes.json @@ -111,7 +111,7 @@ "SMSG_ENVIRONMENTALDAMAGELOG": "0x1FC", "CMSG_CAST_SPELL": "0x12E", "CMSG_CANCEL_CAST": "0x12F", - "CMSG_CANCEL_AURA": "0x033", + "CMSG_CANCEL_AURA": "0x136", "SMSG_CAST_FAILED": "0x130", "SMSG_SPELL_START": "0x131", "SMSG_SPELL_GO": "0x132", @@ -162,7 +162,7 @@ "SMSG_GOSSIP_MESSAGE": "0x17D", "SMSG_GOSSIP_COMPLETE": "0x17E", "SMSG_NPC_TEXT_UPDATE": "0x180", - "CMSG_GAMEOBJECT_USE": "0x01B", + "CMSG_GAMEOBJECT_USE": "0x0B1", "CMSG_QUESTGIVER_STATUS_QUERY": "0x182", "SMSG_QUESTGIVER_STATUS": "0x183", "SMSG_QUESTGIVER_STATUS_MULTIPLE": "0x198", @@ -186,7 +186,7 @@ "CMSG_LIST_INVENTORY": "0x19E", "SMSG_LIST_INVENTORY": "0x19F", "CMSG_SELL_ITEM": "0x1A0", - "SMSG_SELL_ITEM": "0x1A4", + "SMSG_SELL_ITEM": "0x1A1", "CMSG_BUY_ITEM": "0x1A2", "CMSG_BUYBACK_ITEM": "0x290", "SMSG_BUY_FAILED": "0x1A5", diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index bb9d5e8d..4699f501 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -833,6 +833,7 @@ public: void useItemInBag(int bagIndex, int slotIndex); void destroyItem(uint8_t bag, uint8_t slot, uint8_t count = 1); void swapContainerItems(uint8_t srcBag, uint8_t srcSlot, uint8_t dstBag, uint8_t dstSlot); + void swapBagSlots(int srcBagIndex, int dstBagIndex); 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 8be8a2b3..4ec0b418 100644 --- a/include/game/inventory.hpp +++ b/include/game/inventory.hpp @@ -95,6 +95,9 @@ public: uint8_t getPurchasedBankBagSlots() const { return purchasedBankBagSlots_; } void setPurchasedBankBagSlots(uint8_t count) { purchasedBankBagSlots_ = count; } + // Swap two bag slots (equip items + contents) + void swapBagContents(int bagA, int bagB); + // Utility int findFreeBackpackSlot() const; bool addItem(const ItemDef& item); diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index aae34324..dd4021a0 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -69,6 +69,7 @@ private: bool editingOfficerNote_ = false; char guildNoteEditBuffer_[256] = {0}; bool refocusChatInput = false; + bool vendorBagsOpened_ = false; // Track if bags were auto-opened for current vendor session bool chatWindowLocked = true; ImVec2 chatWindowPos_ = ImVec2(0.0f, 0.0f); bool chatWindowPosInit_ = false; @@ -223,9 +224,11 @@ private: int actionBarDragSlot_ = -1; GLuint actionBarDragIcon_ = 0; - // Bag bar textures + // Bag bar state GLuint backpackIconTexture_ = 0; GLuint emptyBagSlotTexture_ = 0; + int bagBarPickedSlot_ = -1; // Visual drag in progress (-1 = none) + int bagBarDragSource_ = -1; // Mouse pressed on this slot, waiting for drag or click (-1 = none) // Chat settings bool chatShowTimestamps_ = false; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 241bec39..a86500ef 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -9476,6 +9476,33 @@ void GameHandler::swapContainerItems(uint8_t srcBag, uint8_t srcSlot, uint8_t ds socket->send(packet); } +void GameHandler::swapBagSlots(int srcBagIndex, int dstBagIndex) { + if (srcBagIndex < 0 || srcBagIndex > 3 || dstBagIndex < 0 || dstBagIndex > 3) return; + if (srcBagIndex == dstBagIndex) return; + + // Local swap for immediate visual feedback + auto srcEquip = static_cast(static_cast(game::EquipSlot::BAG1) + srcBagIndex); + auto dstEquip = static_cast(static_cast(game::EquipSlot::BAG1) + dstBagIndex); + auto srcItem = inventory.getEquipSlot(srcEquip).item; + auto dstItem = inventory.getEquipSlot(dstEquip).item; + inventory.setEquipSlot(srcEquip, dstItem); + inventory.setEquipSlot(dstEquip, srcItem); + + // Also swap bag contents locally + inventory.swapBagContents(srcBagIndex, dstBagIndex); + + // Send to server using CMSG_SWAP_ITEM with INVENTORY_SLOT_BAG_0 (255) + // CMSG_SWAP_INV_ITEM doesn't support bag equip slots (19-22) + if (socket && socket->isConnected()) { + uint8_t srcSlot = static_cast(19 + srcBagIndex); + uint8_t dstSlot = static_cast(19 + dstBagIndex); + LOG_INFO("swapBagSlots: bag ", srcBagIndex, " (slot ", (int)srcSlot, + ") <-> bag ", dstBagIndex, " (slot ", (int)dstSlot, ")"); + auto packet = SwapItemPacket::build(255, dstSlot, 255, srcSlot); + socket->send(packet); + } +} + void GameHandler::destroyItem(uint8_t bag, uint8_t slot, uint8_t count) { if (state != WorldState::IN_WORLD || !socket) return; if (count == 0) count = 1; diff --git a/src/game/inventory.cpp b/src/game/inventory.cpp index a447d444..57806ebf 100644 --- a/src/game/inventory.cpp +++ b/src/game/inventory.cpp @@ -115,6 +115,12 @@ void Inventory::setBankBagSize(int bagIndex, int size) { bankBags_[bagIndex].size = std::min(size, MAX_BAG_SIZE); } +void Inventory::swapBagContents(int bagA, int bagB) { + if (bagA < 0 || bagA >= NUM_BAG_SLOTS || bagB < 0 || bagB >= NUM_BAG_SLOTS) return; + if (bagA == bagB) return; + std::swap(bags[bagA], bags[bagB]); +} + int Inventory::findFreeBackpackSlot() const { for (int i = 0; i < BACKPACK_SLOTS; i++) { if (backpack[i].empty()) return i; diff --git a/src/game/opcode_table.cpp b/src/game/opcode_table.cpp index 83b4a82a..6c609c19 100644 --- a/src/game/opcode_table.cpp +++ b/src/game/opcode_table.cpp @@ -513,7 +513,7 @@ void OpcodeTable::loadWotlkDefaults() { {LogicalOpcode::SMSG_ENVIRONMENTALDAMAGELOG, 0x1FC}, {LogicalOpcode::CMSG_CAST_SPELL, 0x12E}, {LogicalOpcode::CMSG_CANCEL_CAST, 0x12F}, - {LogicalOpcode::CMSG_CANCEL_AURA, 0x033}, + {LogicalOpcode::CMSG_CANCEL_AURA, 0x136}, {LogicalOpcode::SMSG_CAST_FAILED, 0x130}, {LogicalOpcode::SMSG_SPELL_START, 0x131}, {LogicalOpcode::SMSG_SPELL_GO, 0x132}, @@ -592,7 +592,7 @@ void OpcodeTable::loadWotlkDefaults() { {LogicalOpcode::CMSG_LIST_INVENTORY, 0x19E}, {LogicalOpcode::SMSG_LIST_INVENTORY, 0x19F}, {LogicalOpcode::CMSG_SELL_ITEM, 0x1A0}, - {LogicalOpcode::SMSG_SELL_ITEM, 0x1A4}, + {LogicalOpcode::SMSG_SELL_ITEM, 0x1A1}, {LogicalOpcode::CMSG_BUY_ITEM, 0x1A2}, {LogicalOpcode::CMSG_BUYBACK_ITEM, 0x290}, {LogicalOpcode::SMSG_BUY_FAILED, 0x1A5}, diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 5539f7a2..a1b7463c 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -1,5 +1,6 @@ #include "rendering/camera_controller.hpp" #include +#include #include "rendering/terrain_manager.hpp" #include "rendering/wmo_renderer.hpp" #include "rendering/m2_renderer.hpp" @@ -1416,14 +1417,17 @@ void CameraController::processMouseButton(const SDL_MouseButtonEvent& event) { return; } + // Don't capture mouse when ImGui wants it (hovering UI windows) + bool uiWantsMouse = ImGui::GetIO().WantCaptureMouse; + if (event.button == SDL_BUTTON_LEFT) { - leftMouseDown = (event.state == SDL_PRESSED); + leftMouseDown = (event.state == SDL_PRESSED) && !uiWantsMouse; if (event.state == SDL_PRESSED && event.clicks >= 2) { autoRunning = false; } } if (event.button == SDL_BUTTON_RIGHT) { - rightMouseDown = (event.state == SDL_PRESSED); + rightMouseDown = (event.state == SDL_PRESSED) && !uiWantsMouse; } bool anyDown = leftMouseDown || rightMouseDown; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 397dacb3..332b0c03 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -360,19 +360,18 @@ void GameScreen::render(game::GameHandler& gameHandler) { // Set vendor mode before rendering inventory inventoryScreen.setVendorMode(gameHandler.isVendorWindowOpen(), &gameHandler); - // Auto-open bags when vendor window opens + // Auto-open bags once when vendor window first opens if (gameHandler.isVendorWindowOpen()) { - if (inventoryScreen.isSeparateBags()) { - if (!inventoryScreen.isBackpackOpen() && - !inventoryScreen.isBagOpen(0) && - !inventoryScreen.isBagOpen(1) && - !inventoryScreen.isBagOpen(2) && - !inventoryScreen.isBagOpen(3)) { + if (!vendorBagsOpened_) { + vendorBagsOpened_ = true; + if (inventoryScreen.isSeparateBags()) { inventoryScreen.openAllBags(); + } else if (!inventoryScreen.isOpen()) { + inventoryScreen.setOpen(true); } - } else if (!inventoryScreen.isOpen()) { - inventoryScreen.setOpen(true); } + } else { + vendorBagsOpened_ = false; } // Bags (B key toggle handled inside) @@ -3591,6 +3590,10 @@ void GameScreen::renderBagBar(game::GameHandler& gameHandler) { } } + // Track bag slot screen rects for drop detection + ImVec2 bagSlotMins[4], bagSlotMaxs[4]; + GLuint bagIcons[4] = {}; + // Slots 1-4: Bag slots (leftmost) for (int i = 0; i < 4; ++i) { if (i > 0) ImGui::SameLine(0, spacing); @@ -3603,47 +3606,59 @@ void GameScreen::renderBagBar(game::GameHandler& gameHandler) { if (!bagItem.empty() && bagItem.item.displayInfoId != 0) { bagIcon = inventoryScreen.getItemIcon(bagItem.item.displayInfoId); } + bagIcons[i] = bagIcon; + // Render the slot as an invisible button so we control all interaction + ImVec2 cpos = ImGui::GetCursorScreenPos(); + ImGui::InvisibleButton("##bagSlot", ImVec2(slotSize, slotSize)); + bagSlotMins[i] = cpos; + bagSlotMaxs[i] = ImVec2(cpos.x + slotSize, cpos.y + slotSize); + + ImDrawList* dl = ImGui::GetWindowDrawList(); + + // Draw background + icon if (bagIcon) { - if (ImGui::ImageButton("##bag", (ImTextureID)(uintptr_t)bagIcon, - ImVec2(slotSize, slotSize), - ImVec2(0, 0), ImVec2(1, 1), - ImVec4(0.1f, 0.1f, 0.1f, 0.9f), - ImVec4(1, 1, 1, 1))) { - if (inventoryScreen.isSeparateBags()) - inventoryScreen.toggleBag(i); - else - inventoryScreen.toggle(); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("%s", bagItem.item.name.c_str()); - } + dl->AddRectFilled(bagSlotMins[i], bagSlotMaxs[i], IM_COL32(25, 25, 25, 230)); + dl->AddImage((ImTextureID)(uintptr_t)bagIcon, bagSlotMins[i], bagSlotMaxs[i]); } else { - // Empty bag slot - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.15f, 0.15f, 0.8f)); - if (ImGui::Button("##empty", ImVec2(slotSize, slotSize))) { - // Empty slot - no bag equipped - } - ImGui::PopStyleColor(); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Empty Bag Slot"); - } + dl->AddRectFilled(bagSlotMins[i], bagSlotMaxs[i], IM_COL32(38, 38, 38, 204)); } - if (inventoryScreen.isSeparateBags() && - inventoryScreen.isBagOpen(i)) { - ImDrawList* dl = ImGui::GetWindowDrawList(); - ImVec2 r0 = ImGui::GetItemRectMin(); - ImVec2 r1 = ImGui::GetItemRectMax(); - dl->AddRect(r0, r1, IM_COL32(255, 255, 255, 255), 3.0f, 0, 2.0f); + // Hover highlight + bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); + if (hovered && bagBarPickedSlot_ < 0) { + dl->AddRect(bagSlotMins[i], bagSlotMaxs[i], IM_COL32(255, 255, 255, 100)); + } + + // Track which slot was pressed for drag detection + if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && bagBarPickedSlot_ < 0 && bagIcon) { + bagBarDragSource_ = i; + } + + // Click toggles bag open/close (handled in mouse release section below) + + // Dim the slot being dragged + if (bagBarPickedSlot_ == i) { + dl->AddRectFilled(bagSlotMins[i], bagSlotMaxs[i], IM_COL32(0, 0, 0, 150)); + } + + // Tooltip + if (hovered && bagBarPickedSlot_ < 0) { + if (bagIcon) + ImGui::SetTooltip("%s", bagItem.item.name.c_str()); + else + ImGui::SetTooltip("Empty Bag Slot"); + } + + // Open bag indicator + if (inventoryScreen.isSeparateBags() && inventoryScreen.isBagOpen(i)) { + dl->AddRect(bagSlotMins[i], bagSlotMaxs[i], IM_COL32(255, 255, 255, 255), 3.0f, 0, 2.0f); } // Accept dragged item from inventory - if (ImGui::IsItemHovered() && inventoryScreen.isHoldingItem()) { + if (hovered && inventoryScreen.isHoldingItem()) { const auto& heldItem = inventoryScreen.getHeldItem(); - // Check if held item is a bag (bagSlots > 0) if (heldItem.bagSlots > 0 && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { - // Equip the bag to inventory auto& inventory = gameHandler.getInventory(); inventory.setEquipSlot(bagSlot, heldItem); inventoryScreen.returnHeldItem(inventory); @@ -3653,6 +3668,46 @@ void GameScreen::renderBagBar(game::GameHandler& gameHandler) { ImGui::PopID(); } + // Drag lifecycle: press on a slot sets bagBarDragSource_, + // dragging 3+ pixels promotes to bagBarPickedSlot_ (visual drag), + // releasing completes swap or click + if (bagBarDragSource_ >= 0) { + if (ImGui::IsMouseDragging(ImGuiMouseButton_Left, 3.0f) && bagBarPickedSlot_ < 0) { + // Mouse moved enough — start visual drag + bagBarPickedSlot_ = bagBarDragSource_; + } + if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { + if (bagBarPickedSlot_ >= 0) { + // Was dragging — check for drop target + ImVec2 mousePos = ImGui::GetIO().MousePos; + int dropTarget = -1; + for (int j = 0; j < 4; ++j) { + if (j == bagBarPickedSlot_) continue; + if (mousePos.x >= bagSlotMins[j].x && mousePos.x <= bagSlotMaxs[j].x && + mousePos.y >= bagSlotMins[j].y && mousePos.y <= bagSlotMaxs[j].y) { + dropTarget = j; + break; + } + } + if (dropTarget >= 0) { + gameHandler.swapBagSlots(bagBarPickedSlot_, dropTarget); + } + bagBarPickedSlot_ = -1; + } else { + // Was just a click (no drag) — toggle bag + int slot = bagBarDragSource_; + auto equip = static_cast(static_cast(game::EquipSlot::BAG1) + slot); + if (!inv.getEquipSlot(equip).empty()) { + if (inventoryScreen.isSeparateBags()) + inventoryScreen.toggleBag(slot); + else + inventoryScreen.toggle(); + } + } + bagBarDragSource_ = -1; + } + } + // Backpack (rightmost slot) ImGui::SameLine(0, spacing); ImGui::PushID(0); @@ -3692,6 +3747,27 @@ void GameScreen::renderBagBar(game::GameHandler& gameHandler) { ImGui::PopStyleColor(); ImGui::PopStyleVar(4); + + // Draw dragged bag icon following cursor + if (bagBarPickedSlot_ >= 0) { + auto& inv2 = gameHandler.getInventory(); + auto pickedEquip = static_cast( + static_cast(game::EquipSlot::BAG1) + bagBarPickedSlot_); + const auto& pickedItem = inv2.getEquipSlot(pickedEquip); + GLuint pickedIcon = 0; + if (!pickedItem.empty() && pickedItem.item.displayInfoId != 0) { + pickedIcon = inventoryScreen.getItemIcon(pickedItem.item.displayInfoId); + } + if (pickedIcon) { + ImVec2 mousePos = ImGui::GetIO().MousePos; + float sz = 40.0f; + ImVec2 p0(mousePos.x - sz * 0.5f, mousePos.y - sz * 0.5f); + ImVec2 p1(mousePos.x + sz * 0.5f, mousePos.y + sz * 0.5f); + ImDrawList* fg = ImGui::GetForegroundDrawList(); + fg->AddImage((ImTextureID)(uintptr_t)pickedIcon, p0, p1); + fg->AddRect(p0, p1, IM_COL32(200, 200, 200, 255), 0.0f, 0, 2.0f); + } + } } // ============================================================