Add bag bar drag-to-reorder, fix three wrong WotLK opcodes

Bag bar: left-click drag bags to swap positions using CMSG_SWAP_ITEM
with INVENTORY_SLOT_BAG_0 (255). Local optimistic swap for instant
feedback. Camera controller now respects ImGui WantCaptureMouse.
Vendor auto-open bags only triggers once per session.

Fix opcodes: CMSG_GAMEOBJECT_USE 0x01B→0x0B1 (typo caused
SMSG_FORCEACTIONSHOW spam), CMSG_CANCEL_AURA 0x033→0x136,
SMSG_SELL_ITEM 0x1A4→0x1A1.
This commit is contained in:
Kelsi 2026-02-19 22:34:22 -08:00
parent 328ec9ea78
commit 38ad368c82
9 changed files with 168 additions and 48 deletions

View file

@ -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",

View file

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

View file

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

View file

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

View file

@ -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<game::EquipSlot>(static_cast<int>(game::EquipSlot::BAG1) + srcBagIndex);
auto dstEquip = static_cast<game::EquipSlot>(static_cast<int>(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<uint8_t>(19 + srcBagIndex);
uint8_t dstSlot = static_cast<uint8_t>(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;

View file

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

View file

@ -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},

View file

@ -1,5 +1,6 @@
#include "rendering/camera_controller.hpp"
#include <algorithm>
#include <imgui.h>
#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;

View file

@ -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<game::EquipSlot>(static_cast<int>(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<game::EquipSlot>(
static_cast<int>(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);
}
}
}
// ============================================================