diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 20c0d19e..5e398b4c 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -82,6 +82,7 @@ private: int pendingUiOpacity = 65; bool pendingMinimapRotate = false; bool pendingMinimapSquare = false; + bool pendingSeparateBags = true; // UI element transparency (0.0 = fully transparent, 1.0 = fully opaque) float uiOpacity_ = 0.65f; diff --git a/include/ui/inventory_screen.hpp b/include/ui/inventory_screen.hpp index d979dc9e..204d9687 100644 --- a/include/ui/inventory_screen.hpp +++ b/include/ui/inventory_screen.hpp @@ -5,6 +5,7 @@ #include "game/world_packets.hpp" #include #include +#include #include #include #include @@ -29,6 +30,14 @@ public: void toggle() { open = !open; } void setOpen(bool o) { open = o; } + // Separate bag window controls + void toggleBackpack(); + void toggleBag(int idx); + void openAllBags(); + void closeAllBags(); + void setSeparateBags(bool sep) { separateBags_ = sep; } + bool isSeparateBags() const { return separateBags_; } + bool isCharacterOpen() const { return characterOpen; } void toggleCharacter() { characterOpen = !characterOpen; } void setCharacterOpen(bool o) { characterOpen = o; } @@ -64,6 +73,9 @@ private: bool open = false; bool characterOpen = false; bool bKeyWasDown = false; + bool separateBags_ = true; + bool backpackOpen_ = false; + std::array bagOpen_{}; bool cKeyWasDown = false; bool equipmentDirty = false; bool inventoryDirty = false; @@ -106,6 +118,10 @@ private: int heldBackpackIndex = -1; game::EquipSlot heldEquipSlot = game::EquipSlot::NUM_SLOTS; + void renderSeparateBags(game::Inventory& inventory, uint64_t moneyCopper); + void renderAggregateBags(game::Inventory& inventory, uint64_t moneyCopper); + void renderBagWindow(const char* title, bool& isOpen, game::Inventory& inventory, + int bagIndex, float defaultX, float defaultY, uint64_t moneyCopper); void renderEquipmentPanel(game::Inventory& inventory); void renderBackpackPanel(game::Inventory& inventory); void renderStatsPanel(game::Inventory& inventory, uint32_t playerLevel); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index a3f6d17f..61d5777d 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -5539,7 +5539,13 @@ void GameHandler::maybeDetectVisibleItemLayout() { void GameHandler::updateOtherPlayerVisibleItems(uint64_t guid, const std::map& fields) { if (guid == 0 || guid == playerGuid) return; - if (visibleItemEntryBase_ < 0 || visibleItemStride_ <= 0) return; + if (visibleItemEntryBase_ < 0 || visibleItemStride_ <= 0) { + // Layout not detected yet — queue this player for inspect as fallback. + if (socket && state == WorldState::IN_WORLD) { + pendingAutoInspect_.insert(guid); + } + return; + } std::array newEntries{}; for (int s = 0; s < 19; s++) { @@ -5713,18 +5719,16 @@ void GameHandler::handleAttackStop(network::Packet& packet) { void GameHandler::dismount() { if (!socket) return; - if (!isMounted()) { - // Local/server desync guard: clear visual mount even when server says unmounted. + // Clear local mount state immediately (optimistic dismount). + // Server will confirm via SMSG_UPDATE_OBJECT with mountDisplayId=0. + if (currentMountDisplayId_ != 0 || taxiMountActive_) { if (mountCallback_) { mountCallback_(0); } currentMountDisplayId_ = 0; taxiMountActive_ = false; taxiMountDisplayId_ = 0; - onTaxiFlight_ = false; - taxiActivatePending_ = false; - taxiClientActive_ = false; - LOG_INFO("Dismount desync recovery: force-cleared local mount state"); + LOG_INFO("Dismount: cleared local mount state"); } network::Packet pkt(wireOpcode(Opcode::CMSG_CANCEL_MOUNT_AURA)); socket->send(pkt); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 146423f4..cfd24ab1 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -2860,8 +2860,10 @@ void GameScreen::renderBagBar(game::GameHandler& gameHandler) { ImVec2(0, 0), ImVec2(1, 1), ImVec4(0.1f, 0.1f, 0.1f, 0.9f), ImVec4(1, 1, 1, 1))) { - // TODO: Open specific bag - inventoryScreen.toggle(); + if (inventoryScreen.isSeparateBags()) + inventoryScreen.toggleBag(i); + else + inventoryScreen.toggle(); } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("%s", bagItem.item.name.c_str()); @@ -2870,7 +2872,7 @@ void GameScreen::renderBagBar(game::GameHandler& gameHandler) { // 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 - maybe show equipment to find a bag? + // Empty slot - no bag equipped } ImGui::PopStyleColor(); if (ImGui::IsItemHovered()) { @@ -2902,11 +2904,17 @@ void GameScreen::renderBagBar(game::GameHandler& gameHandler) { ImVec2(0, 0), ImVec2(1, 1), ImVec4(0.1f, 0.1f, 0.1f, 0.9f), ImVec4(1, 1, 1, 1))) { - inventoryScreen.toggle(); + if (inventoryScreen.isSeparateBags()) + inventoryScreen.toggleBackpack(); + else + inventoryScreen.toggle(); } } else { if (ImGui::Button("B", ImVec2(slotSize, slotSize))) { - inventoryScreen.toggle(); + if (inventoryScreen.isSeparateBags()) + inventoryScreen.toggleBackpack(); + else + inventoryScreen.toggle(); } } if (ImGui::IsItemHovered()) { @@ -3398,10 +3406,10 @@ void GameScreen::renderBuffBar(game::GameHandler& gameHandler) { } // Right-click to cancel buffs / dismount - if (ImGui::IsItemClicked(ImGuiMouseButton_Right) && isBuff) { + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { if (gameHandler.isMounted()) { gameHandler.dismount(); - } else { + } else if (isBuff) { gameHandler.cancelAura(aura.spellId); } } @@ -4981,6 +4989,14 @@ void GameScreen::renderSettingsWindow() { } } + ImGui::Spacing(); + ImGui::Text("Bags"); + ImGui::Separator(); + if (ImGui::Checkbox("Separate Bag Windows", &pendingSeparateBags)) { + inventoryScreen.setSeparateBags(pendingSeparateBags); + saveSettings(); + } + ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); @@ -4991,6 +5007,8 @@ void GameScreen::renderSettingsWindow() { pendingUiOpacity = 65; pendingMinimapRotate = false; pendingMinimapSquare = false; + pendingSeparateBags = true; + inventoryScreen.setSeparateBags(true); uiOpacity_ = 0.65f; minimapRotate_ = false; minimapSquare_ = false; @@ -5438,6 +5456,7 @@ void GameScreen::saveSettings() { out << "ui_opacity=" << pendingUiOpacity << "\n"; out << "minimap_rotate=" << (pendingMinimapRotate ? 1 : 0) << "\n"; out << "minimap_square=" << (pendingMinimapSquare ? 1 : 0) << "\n"; + out << "separate_bags=" << (pendingSeparateBags ? 1 : 0) << "\n"; // Audio out << "master_volume=" << pendingMasterVolume << "\n"; @@ -5487,6 +5506,9 @@ void GameScreen::loadSettings() { int v = std::stoi(val); minimapSquare_ = (v != 0); pendingMinimapSquare = minimapSquare_; + } else if (key == "separate_bags") { + pendingSeparateBags = (std::stoi(val) != 0); + inventoryScreen.setSeparateBags(pendingSeparateBags); } // Audio else if (key == "master_volume") pendingMasterVolume = std::clamp(std::stoi(val), 0, 100); diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index ebaf98d3..3fa66600 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -11,7 +11,9 @@ #include "core/logger.hpp" #include #include +#include #include +#include #include namespace wowee { @@ -529,13 +531,30 @@ void InventoryScreen::renderHeldItem() { // Bags window (B key) — bottom of screen, no equipment panel // ============================================================ +void InventoryScreen::toggleBackpack() { + backpackOpen_ = !backpackOpen_; +} + +void InventoryScreen::toggleBag(int idx) { + if (idx >= 0 && idx < 4) + bagOpen_[idx] = !bagOpen_[idx]; +} + +void InventoryScreen::openAllBags() { + backpackOpen_ = true; + for (auto& b : bagOpen_) b = true; +} + +void InventoryScreen::closeAllBags() { + backpackOpen_ = false; + for (auto& b : bagOpen_) b = false; +} + void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) { // B key toggle (edge-triggered) bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard; bool bDown = !uiWantsKeyboard && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_B); - if (bDown && !bKeyWasDown) { - open = !open; - } + bool bToggled = bDown && !bKeyWasDown; bKeyWasDown = bDown; // C key toggle for character screen (edge-triggered) @@ -545,6 +564,18 @@ void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) { } cKeyWasDown = cDown; + if (separateBags_) { + if (bToggled) { + bool anyOpen = backpackOpen_; + for (auto b : bagOpen_) anyOpen |= b; + if (anyOpen) closeAllBags(); + else openAllBags(); + } + open = backpackOpen_ || std::any_of(bagOpen_.begin(), bagOpen_.end(), [](bool b){ return b; }); + } else { + if (bToggled) open = !open; + } + if (!open) { if (holdingItem) cancelPickup(inventory); return; @@ -560,53 +591,12 @@ void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) { cancelPickup(inventory); } - ImGuiIO& io = ImGui::GetIO(); - float screenW = io.DisplaySize.x; - float screenH = io.DisplaySize.y; - - // Calculate bag window size - constexpr float slotSize = 40.0f; - constexpr int columns = 4; - int rows = (inventory.getBackpackSize() + columns - 1) / columns; - float bagContentH = rows * (slotSize + 4.0f) + 40.0f; // slots + header + money - - // Check for extra bags and add space - for (int bag = 0; bag < game::Inventory::NUM_BAG_SLOTS; bag++) { - int bagSize = inventory.getBagSize(bag); - if (bagSize <= 0) continue; - int bagRows = (bagSize + columns - 1) / columns; - bagContentH += bagRows * (slotSize + 4.0f) + 30.0f; // slots + header + if (separateBags_) { + renderSeparateBags(inventory, moneyCopper); + } else { + renderAggregateBags(inventory, moneyCopper); } - float windowW = columns * (slotSize + 4.0f) + 30.0f; - float windowH = bagContentH + 50.0f; // padding - - // Position at bottom-right of screen - float posX = screenW - windowW - 10.0f; - float posY = screenH - windowH - 60.0f; // above action bar area - - ImGui::SetNextWindowPos(ImVec2(posX, posY), ImGuiCond_Always); - ImGui::SetNextWindowSize(ImVec2(windowW, windowH), ImGuiCond_Always); - - ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; - if (!ImGui::Begin("Bags", &open, flags)) { - ImGui::End(); - return; - } - - renderBackpackPanel(inventory); - - // Money display - ImGui::Spacing(); - uint64_t gold = moneyCopper / 10000; - uint64_t silver = (moneyCopper / 100) % 100; - uint64_t copper = moneyCopper % 100; - ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "%llug %llus %lluc", - static_cast(gold), - static_cast(silver), - static_cast(copper)); - ImGui::End(); - // Detect held item dropped outside inventory windows → drop confirmation if (holdingItem && heldItem.itemId != 6948 && ImGui::IsMouseReleased(ImGuiMouseButton_Left) && !ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow) && @@ -646,6 +636,175 @@ void InventoryScreen::render(game::Inventory& inventory, uint64_t moneyCopper) { renderHeldItem(); } +// ============================================================ +// Aggregate mode — original single-window bags +// ============================================================ + +void InventoryScreen::renderAggregateBags(game::Inventory& inventory, uint64_t moneyCopper) { + ImGuiIO& io = ImGui::GetIO(); + float screenW = io.DisplaySize.x; + float screenH = io.DisplaySize.y; + + constexpr float slotSize = 40.0f; + constexpr int columns = 4; + int rows = (inventory.getBackpackSize() + columns - 1) / columns; + float bagContentH = rows * (slotSize + 4.0f) + 40.0f; + + for (int bag = 0; bag < game::Inventory::NUM_BAG_SLOTS; bag++) { + int bagSize = inventory.getBagSize(bag); + if (bagSize <= 0) continue; + int bagRows = (bagSize + columns - 1) / columns; + bagContentH += bagRows * (slotSize + 4.0f) + 30.0f; + } + + float windowW = columns * (slotSize + 4.0f) + 30.0f; + float windowH = bagContentH + 50.0f; + + float posX = screenW - windowW - 10.0f; + float posY = screenH - windowH - 60.0f; + + ImGui::SetNextWindowPos(ImVec2(posX, posY), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(windowW, windowH), ImGuiCond_Always); + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; + if (!ImGui::Begin("Bags", &open, flags)) { + ImGui::End(); + return; + } + + renderBackpackPanel(inventory); + + ImGui::Spacing(); + uint64_t gold = moneyCopper / 10000; + uint64_t silver = (moneyCopper / 100) % 100; + uint64_t copper = moneyCopper % 100; + ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "%llug %llus %lluc", + static_cast(gold), + static_cast(silver), + static_cast(copper)); + ImGui::End(); +} + +// ============================================================ +// Separate mode — individual draggable bag windows +// ============================================================ + +void InventoryScreen::renderSeparateBags(game::Inventory& inventory, uint64_t moneyCopper) { + ImGuiIO& io = ImGui::GetIO(); + float screenW = io.DisplaySize.x; + float screenH = io.DisplaySize.y; + + constexpr float slotSize = 40.0f; + constexpr int columns = 4; + constexpr float baseWindowW = columns * (slotSize + 4.0f) + 30.0f; + + // Backpack window (rightmost, bottom-right) + if (backpackOpen_) { + int bpRows = (inventory.getBackpackSize() + columns - 1) / columns; + float bpH = bpRows * (slotSize + 4.0f) + 80.0f; // header + money + padding + float defaultX = screenW - baseWindowW - 10.0f; + float defaultY = screenH - bpH - 60.0f; + renderBagWindow("Backpack", backpackOpen_, inventory, -1, defaultX, defaultY, moneyCopper); + } + + // Extra bag windows (stacked to the left of backpack) + for (int bag = 0; bag < game::Inventory::NUM_BAG_SLOTS; bag++) { + if (!bagOpen_[bag]) continue; + int bagSize = inventory.getBagSize(bag); + if (bagSize <= 0) { + bagOpen_[bag] = false; + continue; + } + + int bagRows = (bagSize + columns - 1) / columns; + float bagH = bagRows * (slotSize + 4.0f) + 60.0f; + float defaultX = screenW - (baseWindowW + 10.0f) * (bag + 2) - 10.0f; + float defaultY = screenH - bagH - 60.0f; + + // Build title from equipped bag item name + char title[64]; + game::EquipSlot bagSlot = static_cast(static_cast(game::EquipSlot::BAG1) + bag); + const auto& bagItem = inventory.getEquipSlot(bagSlot); + if (!bagItem.empty() && !bagItem.item.name.empty()) { + snprintf(title, sizeof(title), "%s##bag%d", bagItem.item.name.c_str(), bag); + } else { + snprintf(title, sizeof(title), "Bag %d##bag%d", bag + 1, bag); + } + + renderBagWindow(title, bagOpen_[bag], inventory, bag, defaultX, defaultY, 0); + } + + // Update open state based on individual windows + open = backpackOpen_ || std::any_of(bagOpen_.begin(), bagOpen_.end(), [](bool b){ return b; }); +} + +void InventoryScreen::renderBagWindow(const char* title, bool& isOpen, + game::Inventory& inventory, int bagIndex, + float defaultX, float defaultY, uint64_t moneyCopper) { + constexpr float slotSize = 40.0f; + constexpr int columns = 4; + + int numSlots = (bagIndex < 0) ? inventory.getBackpackSize() : inventory.getBagSize(bagIndex); + if (numSlots <= 0) return; + + int rows = (numSlots + columns - 1) / columns; + float contentH = rows * (slotSize + 4.0f) + 10.0f; + if (bagIndex < 0) contentH += 25.0f; // money display for backpack + float gridW = columns * (slotSize + 4.0f) + 30.0f; + // Ensure window is wide enough for the title + close button + const char* displayTitle = title; + const char* hashPos = strstr(title, "##"); + float titleW = ImGui::CalcTextSize(displayTitle, hashPos).x + 50.0f; // close button + padding + float windowW = std::max(gridW, titleW); + float windowH = contentH + 40.0f; // title bar + padding + + ImGui::SetNextWindowPos(ImVec2(defaultX, defaultY), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(windowW, windowH), ImGuiCond_Always); + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize; + if (!ImGui::Begin(title, &isOpen, flags)) { + ImGui::End(); + return; + } + + // Render item slots in 4-column grid + for (int i = 0; i < numSlots; i++) { + if (i % columns != 0) ImGui::SameLine(); + + const game::ItemSlot& slot = (bagIndex < 0) + ? inventory.getBackpackSlot(i) + : inventory.getBagSlot(bagIndex, i); + + char id[32]; + if (bagIndex < 0) { + snprintf(id, sizeof(id), "##sbp_%d", i); + } else { + snprintf(id, sizeof(id), "##sb%d_%d", bagIndex, i); + } + 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); + ImGui::PopID(); + } + + // Money display at bottom of backpack + if (bagIndex < 0 && moneyCopper > 0) { + ImGui::Spacing(); + uint64_t gold = moneyCopper / 10000; + uint64_t silver = (moneyCopper / 100) % 100; + uint64_t copper = moneyCopper % 100; + ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "%llug %llus %lluc", + static_cast(gold), + static_cast(silver), + static_cast(copper)); + } + + ImGui::End(); +} + // ============================================================ // Character screen (C key) — equipment + model preview + stats // ============================================================