Add separate draggable bag windows, fix dismount and player equipment

Bags are now individual draggable ImGui windows (backpack + each equipped
bag) with per-bag toggle from the bag bar. B key opens/closes all. A
settings toggle under Gameplay lets users switch back to the original
aggregate single-window mode. Window width adapts to bag item name length.

Fix dismount by clearing local mount state immediately (optimistic) instead
of waiting for server confirmation, and allow buff bar right-click dismount
regardless of the aura's buff flag.

Fix other players appearing naked by queuing them for auto-inspect when
the visible item field layout hasn't been detected yet.
This commit is contained in:
Kelsi 2026-02-13 22:51:49 -08:00
parent 89ccb0720a
commit 85864ab05b
5 changed files with 264 additions and 62 deletions

View file

@ -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;

View file

@ -5,6 +5,7 @@
#include "game/world_packets.hpp"
#include <GL/glew.h>
#include <imgui.h>
#include <array>
#include <functional>
#include <memory>
#include <unordered_map>
@ -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<bool, 4> 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);

View file

@ -5539,7 +5539,13 @@ void GameHandler::maybeDetectVisibleItemLayout() {
void GameHandler::updateOtherPlayerVisibleItems(uint64_t guid, const std::map<uint16_t, uint32_t>& 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<uint32_t, 19> 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);

View file

@ -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);

View file

@ -11,7 +11,9 @@
#include "core/logger.hpp"
#include <imgui.h>
#include <SDL2/SDL.h>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <unordered_set>
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<unsigned long long>(gold),
static_cast<unsigned long long>(silver),
static_cast<unsigned long long>(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<unsigned long long>(gold),
static_cast<unsigned long long>(silver),
static_cast<unsigned long long>(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<game::EquipSlot>(static_cast<int>(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<unsigned long long>(gold),
static_cast<unsigned long long>(silver),
static_cast<unsigned long long>(copper));
}
ImGui::End();
}
// ============================================================
// Character screen (C key) — equipment + model preview + stats
// ============================================================