diff --git a/src/modules/PlayerbotV2/Addon/BotGear/BotGear.lua b/src/modules/PlayerbotV2/Addon/BotGear/BotGear.lua index 32a98dcdd8..2808a3d631 100644 --- a/src/modules/PlayerbotV2/Addon/BotGear/BotGear.lua +++ b/src/modules/PlayerbotV2/Addon/BotGear/BotGear.lua @@ -1,21 +1,34 @@ --[[ - BotGear.lua — Standalone altbot gear inspector. + BotGear.lua — Altbot character & inventory controller. Uses PBC addon protocol, no dependency on PlayerbotControl. /botgear — opens panel for targeted bot. - Right-click bag slot → equip. Right-click equipment slot → unequip. + Left-click bag item → pick up for move; left-click another slot → move + Right-click bag item → equip to correct equipment slot + Shift+right-click bag item → destroy (permanent) + Right-click equipped item → unequip to first free bag slot + Shows the bot's full 3D character (ModelScene), equipment, all bags, gold. ]] local ADDON = "BotGear" local PREFIX = "PBC" --- State -local frame, botGuid, botName -local bagButtons = {} -- [slot] = button -local equipButtons = {} -- [slot] = button -local bagData = {} -- [slot] = {entry, count, quality} -local equipData = {} -- [slot] = {entry, quality} - local SLOT_SIZE = 36 +local GAP = 4 +local BAG_COLS = 4 +local BACKPACK_SLOTS = 16 +local MAX_BAGS = 4 + +-- State ------------------------------------------------------------- +local frame, botGuid, botName +local equipButtons = {} -- [equipSlot] = button +local bagButtons = {} -- [bagNum] = { [slot] = button } +local equipData = {} -- [equipSlot] = {entry, quality} +local bagData = {} -- [bagNum] = { [slot] = {entry,count,quality} } +local bagLayout = {} -- [bagNum] = {entry, size} +local gold = 0 +local selected = nil -- {bag=, slot=} item picked up for move +local modelActor = nil + local EQUIP_SLOT_NAMES = { [0]="Head",[1]="Neck",[2]="Shoulder",[3]="Shirt",[4]="Chest", [5]="Waist",[6]="Legs",[7]="Feet",[8]="Wrist",[9]="Hands", @@ -23,41 +36,20 @@ local EQUIP_SLOT_NAMES = { [14]="Back",[15]="MainHand",[16]="OffHand",[17]="Ranged",[18]="Tabard", } --- Send addon message +-- Comms ------------------------------------------------------------- local function Send(mtype, ...) local args = {...} local body = table.concat(args, "|") - local frame = string.format("1|00000000|1|1|%s|%s", mtype, body) - C_ChatInfo.SendAddonMessage(PREFIX, frame, "WHISPER", - botName or UnitName("player")) + local f = string.format("1|00000000|1|1|%s|%s", mtype, body) + C_ChatInfo.SendAddonMessage(PREFIX, f, "WHISPER", botName or UnitName("player")) end --- Resolve target local function ResolveTarget() if not UnitExists("target") then return nil, nil end local guid = UnitGUID("target") if not guid then return nil, nil end - -- Format: Player-0-XXXX-YYYYYY local low = tonumber(string.match(guid, "-(%d+)-%x+$")) - local name = UnitName("target") - return low, name -end - --- Slot rendering -local function RefreshSlot(btn, entry, count, quality) - if not btn then return end - if entry and entry > 0 then - local _, _, _, _, _, _, _, _, _, tex = GetItemInfo(entry) - btn.itemEntry = entry - btn.icon:SetTexture(tex or "Interface\\Icons\\INV_Misc_QuestionMark") - btn.countText:SetText(count and count > 1 and count or "") - btn:SetAlpha(1.0) - else - btn.itemEntry = 0 - btn.icon:SetTexture(nil) - btn.countText:SetText("") - btn:SetAlpha(0.3) - end + return low, UnitName("target") end local function RequestBags() @@ -70,67 +62,249 @@ local function RequestEquip() Send("GEAR_EQUIP_REQ", tostring(botGuid)) end --- Reset to targeted bot -local function RefreshTarget() - local g, n = ResolveTarget() - if g then - botGuid = g - botName = n - if frame and frame.title then - frame.title:SetText("Gear - " .. (n or "?")) - end - RequestBags() - C_Timer.After(0.15, RequestEquip) +local function RefreshAll() + RequestBags() + C_Timer.After(0.15, RequestEquip) +end + +-- Rendering --------------------------------------------------------- +local function RefreshSlot(btn, entry, count, quality, isSelected) + if not btn then return end + if entry and entry > 0 then + local _, _, _, _, _, _, _, _, _, tex = GetItemInfo(entry) + btn.itemEntry = entry + btn.icon:SetTexture(tex or "Interface\\Icons\\INV_Misc_QuestionMark") + btn.icon:SetAlpha(1.0) + btn.countText:SetText(count and count > 1 and count or "") + btn:SetAlpha(1.0) + else + btn.itemEntry = 0 + btn.icon:SetTexture(nil) + btn.icon:SetAlpha(0) + btn.countText:SetText("") + btn:SetAlpha(0.35) + end + if isSelected then + btn:SetBackdropColor(1, 0.82, 0.2, 0.35) + else + btn:SetBackdropColor(0, 0, 0, 0) end end --- Create a slot button -local function CreateSlot(parent, x, y, slotType, slotIdx) +local function CreateSlotButton(parent, x, y, slotType, bag, slot) local btn = CreateFrame("Button", nil, parent) btn:SetSize(SLOT_SIZE, SLOT_SIZE) btn:SetPoint("TOPLEFT", x, -y) btn:SetNormalTexture("Interface\\Buttons\\UI-Quickslot2") btn:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square") + btn:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8", edgeFile = "Interface\\Buttons\\WHITE8x8", edgeSize = 1 }) btn.slotType = slotType - btn.slotIdx = slotIdx + btn.bag = bag + btn.slotIdx = slot btn.itemEntry = 0 + btn.icon = btn:CreateTexture(nil, "ARTWORK") btn.icon:SetAllPoints() btn.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) + btn.icon:SetAlpha(0) btn.countText = btn:CreateFontString(nil, "OVERLAY", "NumberFontNormal") btn.countText:SetPoint("BOTTOMRIGHT", -1, 2) - -- Tooltip + btn:SetScript("OnEnter", function(self) - if self.itemEntry > 0 then + if self.itemEntry and self.itemEntry > 0 then GameTooltip:SetOwner(self, "ANCHOR_RIGHT") GameTooltip:SetItemByID(self.itemEntry) GameTooltip:Show() end end) btn:SetScript("OnLeave", function() GameTooltip:Hide() end) - -- Right-click action + btn:SetScript("OnClick", function(self, button) - if button ~= "RightButton" or self.itemEntry == 0 then return end + if self.itemEntry == 0 then return end if slotType == "bag" then - Send("GEAR_EQUIP_ITEM", tostring(botGuid), tostring(slotIdx)) - C_Timer.After(0.3, RequestBags) - C_Timer.After(0.4, RequestEquip) - elseif slotType == "equip" then - Send("GEAR_UNEQUIP_ITEM", tostring(botGuid), tostring(slotIdx)) - C_Timer.After(0.3, RequestBags) - C_Timer.After(0.4, RequestEquip) + if button == "RightButton" and IsShiftKeyDown() then + -- Destroy (permanent) + Send("GEAR_DESTROY_ITEM", tostring(botGuid), tostring(bag), tostring(slot)) + selected = nil + C_Timer.After(0.3, RefreshAll) + elseif button == "RightButton" then + -- Equip + Send("GEAR_EQUIP_ITEM", tostring(botGuid), tostring(bag), tostring(slot)) + C_Timer.After(0.3, RefreshAll) + elseif button == "LeftButton" then + -- Move: pick up / place + local key = bag * 1000 + slot + if not selected then + selected = { bag = bag, slot = slot } + RefreshBags() + elseif selected.bag == bag and selected.slot == slot then + selected = nil + RefreshBags() + else + Send("GEAR_MOVE_ITEM", tostring(botGuid), + tostring(selected.bag), tostring(selected.slot), + tostring(bag), tostring(slot)) + selected = nil + C_Timer.After(0.3, RefreshAll) + end + end + elseif slotType == "equip" and button == "RightButton" then + Send("GEAR_UNEQUIP_ITEM", tostring(botGuid), tostring(slot)) + C_Timer.After(0.3, RefreshAll) end end) + return btn end --- Build UI +local function RefreshEquip() + for i = 0, 18 do + local d = equipData[i] + RefreshSlot(equipButtons[i], d and d.entry, 1, d and d.quality, false) + end +end + +local function RefreshBags() + for bag = 0, MAX_BAGS do + local btns = bagButtons[bag] + if btns then + for slot, btn in pairs(btns) do + local d = bagData[bag] and bagData[bag][slot] + local isSel = selected and selected.bag == bag and selected.slot == slot + RefreshSlot(btn, d and d.entry, d and d.count, d and d.quality, isSel) + end + end + end + if goldText then + local g = math.floor(gold / 10000) + local s = math.floor((gold % 10000) / 100) + local c = gold % 100 + goldText:SetText(string.format("Gold: %dg %ds %dc", g, s, c)) + end +end + +-- Rebuild the bag section layout inside the scroll content. +local bagScroll, bagContent, goldText + +local function BuildBagLayout() + local content = bagContent + if not content then return end + + -- Compute total content height: backpack + each bag, header lines included. + local y = 0 + local function RowOfSlots(bag, size, title) + local header = content:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") + header:SetPoint("TOPLEFT", content, "TOPLEFT", 4, -y) + header:SetText(title) + y = y + 18 + local rows = math.ceil(size / BAG_COLS) + for slot = 0, size - 1 do + local col = slot % BAG_COLS + local row = math.floor(slot / BAG_COLS) + if not bagButtons[bag] then bagButtons[bag] = {} end + if not bagButtons[bag][slot] then + bagButtons[bag][slot] = CreateSlotButton(content, + col * (SLOT_SIZE + GAP), row * (SLOT_SIZE + GAP), "bag", bag, slot) + end + bagButtons[bag][slot]:Show() + bagButtons[bag][slot]:ClearAllPoints() + bagButtons[bag][slot]:SetPoint("TOPLEFT", content, "TOPLEFT", + col * (SLOT_SIZE + GAP), -(y + row * (SLOT_SIZE + GAP))) + end + y = y + rows * (SLOT_SIZE + GAP) + 8 + end + + RowOfSlots(0, BACKPACK_SLOTS, "Backpack") + for bag = 1, MAX_BAGS do + local info = bagLayout[bag] + if info and info.size > 0 then + RowOfSlots(bag, info.size, "Bag " .. bag) + else + -- hide leftover buttons for a removed bag + if bagButtons[bag] then + for _, btn in pairs(bagButtons[bag]) do btn:Hide() end + end + end + end + + content:SetHeight(y + 20) + bagScroll:UpdateScrollChildRect() +end + +-- Wire protocol: |G||B|||...|I|||||... +local function ParseBagsResp(guidStr, body) + if not guidStr or tonumber(guidStr) ~= botGuid then return end + bagData = {} + bagLayout = {} + gold = 0 + local parts = { strsplit("|", body) } + local i = 1 + while parts[i] do + local kind = parts[i] + if kind == "G" then + gold = tonumber(parts[i + 1]) or 0 + i = i + 2 + elseif kind == "B" then + local num = tonumber(parts[i + 1]) + bagLayout[num] = { entry = tonumber(parts[i + 2]), size = tonumber(parts[i + 3]) or 0 } + i = i + 4 + elseif kind == "I" then + local b = tonumber(parts[i + 1]) + local s = tonumber(parts[i + 2]) + if not bagData[b] then bagData[b] = {} end + bagData[b][s] = { + entry = tonumber(parts[i + 3]), + count = tonumber(parts[i + 4]), + quality = tonumber(parts[i + 5]) or 1, + } + i = i + 6 + else + break + end + end + BuildBagLayout() + RefreshBags() +end + +local function ParseEquipResp(guidStr, body) + if not guidStr or tonumber(guidStr) ~= botGuid then return end + equipData = {} + if body then + for part in body:gmatch("[^|]+") do + local slot, entry, qual = part:match("([^,]+),([^,]+),([^,]*)") + if slot then + equipData[tonumber(slot)] = { + entry = tonumber(entry), + quality = tonumber(qual) or 1, + } + end + end + end + RefreshEquip() +end + +-- UI ---------------------------------------------------------------- +local function RefreshTarget() + local g, n = ResolveTarget() + if g then + botGuid = g + botName = n + if frame and frame.title then + frame.title:SetText("Bot - " .. (n or "?")) + end + if modelActor then + modelActor:SetUnit("target") + end + RefreshAll() + end +end + local function BuildFrame() if frame then return end frame = CreateFrame("Frame", "BotGearFrame", UIParent) - frame:SetSize(330, 500) + frame:SetSize(520, 700) frame:SetPoint("CENTER") - frame:SetBackdrop({bgFile="Interface\\DialogFrame\\Background-DialogFrame", + frame:SetBackdrop({ bgFile="Interface\\DialogFrame\\Background-DialogFrame", edgeFile="Interface\\Tooltips\\UI-Tooltip-Border", tile=1,tileSize=16,edgeSize=16, insets={left=3,right=3,top=3,bottom=3}}) frame:SetMovable(true) @@ -139,95 +313,74 @@ local function BuildFrame() frame:SetScript("OnDragStop", frame.StopMovingOrSizing) tinsert(UISpecialFrames, "BotGearFrame") - -- Close button local close = CreateFrame("Button", nil, frame, "UIPanelCloseButton") close:SetPoint("TOPRIGHT", -5, -5) - -- Title frame.title = frame:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge") frame.title:SetPoint("TOP", 0, -12) - frame.title:SetText("Gear - /target a bot") + frame.title:SetText("Bot - /target a bot") + + -- 3D character model + local modelScene = CreateFrame("ModelScene", nil, frame) + modelScene:SetSize(180, 250) + modelScene:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, -40) + modelScene:SetPortraitZoom(1.0) + modelActor = modelScene:CreateActor() + modelActor:SetPosition(0, 0, 0) + modelActor:SetFacing(-1.8) + modelActor:SetUnit("player") + + -- Gold + goldText = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal") + goldText:SetPoint("TOPLEFT", frame, "TOPLEFT", 14, -300) + goldText:SetText("Gold: 0g 0s 0c") + + -- Equipment + local equipLbl = frame:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") + equipLbl:SetPoint("TOPLEFT", frame, "TOPLEFT", 14, -320) + equipLbl:SetText("Equipment") + + for i = 0, 18 do + local col = i < 10 and 12 or 102 + local row = (i < 10 and i or (i - 10)) * (SLOT_SIZE + GAP) + equipButtons[i] = CreateSlotButton(frame, col, row + 335, "equip", nil, i) + local lbl = frame:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") + local lcol = i < 10 and 50 or 140 + local lrow = (i < 10 and i or (i - 10)) * (SLOT_SIZE + GAP) + 350 + lbl:SetPoint("TOPLEFT", lcol, -lrow) + lbl:SetText(EQUIP_SLOT_NAMES[i] or ("Slot" .. i)) + end + + -- Bags (scroll) + bagScroll = CreateFrame("ScrollFrame", nil, frame) + bagScroll:SetPoint("TOPLEFT", frame, "TOPLEFT", 205, -40) + bagScroll:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -12, 34) + bagScroll:SetBackdrop({ bgFile="Interface\\DialogFrame\\UI-DialogBox-Background" }) + bagContent = CreateFrame("Frame", nil, bagScroll) + bagContent:SetSize(300, 400) + bagScroll:SetScrollChild(bagContent) - -- Refresh button local refresh = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") refresh:SetSize(80, 22) refresh:SetPoint("BOTTOM", 0, 8) refresh:SetText("Refresh") - refresh:SetScript("OnClick", function() - RefreshTarget() - end) - - -- Equip slots (left column) - for i = 0, 18 do - local col = i < 10 and 10 or 160 - local row = (i < 10 and i or (i - 10)) * (SLOT_SIZE + 4) - equipButtons[i] = CreateSlot(frame, col, row + 25, "equip", i) - end - -- Slot labels - for i = 0, 18 do - local lbl = frame:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") - local col2 = i < 10 and 48 or 198 - local row2 = (i < 10 and i or (i - 10)) * (SLOT_SIZE + 4) + 35 - lbl:SetPoint("TOPLEFT", col2, -row2) - lbl:SetText(EQUIP_SLOT_NAMES[i] or ("Slot"..i)) - end - - -- Bag slots (4x10 grid, right side starts at row 25) - for i = 0, 39 do - local col = 10 + (i % 4) * (SLOT_SIZE + 4) - local row = 25 + math.floor(i / 4) * (SLOT_SIZE + 4) + 210 - bagButtons[i] = CreateSlot(frame, col, row, "bag", i) - end + refresh:SetScript("OnClick", RefreshTarget) end --- Handle inbound PBC messages +-- Inbound PBC messages local f = CreateFrame("Frame") f:RegisterEvent("CHAT_MSG_ADDON") f:SetScript("OnEvent", function(_, _, prefix, msg, channel, sender) if prefix ~= PREFIX then return end - -- Parse: 1|seq|idx|tot|MTYPE|body local mtype, body = msg:match("^1|[^|]+|%d+|%d+|([^|]+)|(.+)$") if not mtype then return end if mtype == "GEAR_BAGS_RESP" then local guidStr, rest = body:match("^([^|]+)|(.*)$") - if not guidStr or tonumber(guidStr) ~= botGuid then return end - bagData = {} - if rest then - for part in rest:gmatch("[^|]+") do - local slot, entry, count, qual = part:match("([^,]+),([^,]+),([^,]+),([^,]*)") - if slot then - bagData[tonumber(slot)] = { - entry = tonumber(entry), - count = tonumber(count), - quality = tonumber(qual) or 1 - } - end - end - end - for i = 0, 39 do - local d = bagData[i] - RefreshSlot(bagButtons[i], d and d.entry, d and d.count, d and d.quality) - end + ParseBagsResp(guidStr, rest) elseif mtype == "GEAR_EQUIP_RESP" then local guidStr, rest = body:match("^([^|]+)|(.*)$") - if not guidStr or tonumber(guidStr) ~= botGuid then return end - equipData = {} - if rest then - for part in rest:gmatch("[^|]+") do - local slot, entry, qual = part:match("([^,]+),([^,]+),([^,]*)") - if slot then - equipData[tonumber(slot)] = { - entry = tonumber(entry), - quality = tonumber(qual) or 1 - } - end - end - end - for i = 0, 18 do - local d = equipData[i] - RefreshSlot(equipButtons[i], d and d.entry, 1, d and d.quality) - end + ParseEquipResp(guidStr, rest) end end) diff --git a/src/modules/PlayerbotV2/Session/AddonControl.cpp b/src/modules/PlayerbotV2/Session/AddonControl.cpp index 351ab23748..1c56b64e03 100644 --- a/src/modules/PlayerbotV2/Session/AddonControl.cpp +++ b/src/modules/PlayerbotV2/Session/AddonControl.cpp @@ -22,6 +22,7 @@ #include "DatabaseEnv.h" #include "GameTime.h" #include "Item.h" +#include "Bag.h" #include "ItemTemplate.h" #include "Language.h" #include "Log.h" @@ -929,6 +930,10 @@ void HandleSelfToggle(uint32 client_seq, WorldSession* sess, // ---------------------------------------------------------------------------- // GEAR_BAGS_REQ — return the bot's backpack contents. +// GEAR_BAGS_REQ — return gold, bag containers, and every item in the +// backpack + the four equipped bags. +// Payload: |G||B||||I||||| +// bagNum 0 = backpack, 1..4 = equipped bags (in bag-slot order). void HandleGearBagsReq(uint32 client_seq, WorldSession* sess, std::vector const& fields) { @@ -943,15 +948,59 @@ void HandleGearBagsReq(uint32 client_seq, WorldSession* sess, std::vector out; out.push_back(Escape(fields[0])); - for (uint8 slot = INVENTORY_SLOT_ITEM_START; slot < INVENTORY_SLOT_ITEM_END; ++slot) + + // Gold (copper) { - if (Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot)) + char buf[32]; + std::snprintf(buf, sizeof(buf), "G|%llu", + static_cast(bot->GetMoney())); + out.push_back(buf); + } + + // Equipped bag containers (bag slots 19..22 → bagNum 1..4) + for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag) + { + Bag* b = bot->GetBagByPos(bag); + if (!b) continue; + char buf[48]; + std::snprintf(buf, sizeof(buf), "B|%u|%u|%u", + uint32(bag - INVENTORY_SLOT_BAG_START + 1), + b->GetEntry(), b->GetBagSize()); + out.push_back(buf); + } + + // Items — backpack (bagNum 0) then each equipped bag + for (uint8 bagNum = 0; bagNum <= INVENTORY_SLOT_BAG_END - INVENTORY_SLOT_BAG_START; ++bagNum) + { + if (bagNum == 0) { - char buf[48]; - std::snprintf(buf, sizeof(buf), "%u,%u,%u,%u", - slot, item->GetEntry(), item->GetCount(), - uint32(item->GetTemplate()->GetQuality())); - out.push_back(std::string(buf)); + for (uint8 slot = INVENTORY_SLOT_ITEM_START; slot < INVENTORY_SLOT_ITEM_END; ++slot) + { + if (Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot)) + { + char buf[64]; + std::snprintf(buf, sizeof(buf), "I|0|%u|%u|%u|%u", + uint32(slot), item->GetEntry(), item->GetCount(), + uint32(item->GetTemplate()->GetQuality())); + out.push_back(buf); + } + } + } + else + { + Bag* b = bot->GetBagByPos(INVENTORY_SLOT_BAG_START + bagNum - 1); + if (!b) continue; + for (uint8 slot = 0; slot < b->GetBagSize(); ++slot) + { + if (Item* item = b->GetItemByPos(slot)) + { + char buf[64]; + std::snprintf(buf, sizeof(buf), "I|%u|%u|%u|%u|%u", + uint32(bagNum), uint32(slot), item->GetEntry(), item->GetCount(), + uint32(item->GetTemplate()->GetQuality())); + out.push_back(buf); + } + } } } SendFields(sess, client_seq, "GEAR_BAGS_RESP", out); @@ -986,13 +1035,23 @@ void HandleGearEquipReq(uint32 client_seq, WorldSession* sess, SendFields(sess, client_seq, "GEAR_EQUIP_RESP", out); } -// GEAR_EQUIP_ITEM — equip from backpack slot +// GEAR_EQUIP_ITEM [] — equip from a bag slot. +// srcBag 0 = backpack (default when omitted), 1..4 = equipped bags. void HandleGearEquipItem(uint32 /*client_seq*/, WorldSession* sess, std::vector const& fields) { if (fields.size() < 2) return; uint32 const guidLow = uint32(std::strtoul(fields[0].c_str(), nullptr, 10)); - uint8 const srcSlot = uint8(std::strtoul(fields[1].c_str(), nullptr, 10)); + uint8 srcBag = 0; + uint8 srcSlot = 0; + if (fields.size() >= 3) + { + srcBag = uint8(std::strtoul(fields[1].c_str(), nullptr, 10)); + srcSlot = uint8(std::strtoul(fields[2].c_str(), nullptr, 10)); + } + else + srcSlot = uint8(std::strtoul(fields[1].c_str(), nullptr, 10)); + ObjectGuid const botGuid = ObjectGuid::Create(guidLow); Player* bot = ObjectAccessor::FindConnectedPlayer(botGuid); if (!bot) return; @@ -1000,7 +1059,9 @@ void HandleGearEquipItem(uint32 /*client_seq*/, WorldSession* sess, sess->GetPlayer() ? sess->GetPlayer()->GetGUID().GetCounter() : 0u)) return; - Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, srcSlot); + uint8 const srcBagPos = (srcBag == 0) ? INVENTORY_SLOT_BAG_0 + : INVENTORY_SLOT_BAG_START + srcBag - 1; + Item* item = bot->GetItemByPos(srcBagPos, srcSlot); if (!item) return; // Determine destination equipment slot from inventory type @@ -1042,7 +1103,7 @@ void HandleGearEquipItem(uint32 /*client_seq*/, WorldSession* sess, bot->SwapItem(uint16(INVENTORY_SLOT_BAG_0 << 8) | destSlot, uint16(INVENTORY_SLOT_BAG_0 << 8) | freeSlot); } - bot->SwapItem(uint16(INVENTORY_SLOT_BAG_0 << 8) | srcSlot, + bot->SwapItem(uint16(srcBagPos << 8) | srcSlot, uint16(INVENTORY_SLOT_BAG_0 << 8) | destSlot); } @@ -1076,6 +1137,67 @@ void HandleGearUnequipItem(uint32 /*client_seq*/, WorldSession* sess, uint16(INVENTORY_SLOT_BAG_0 << 8) | freeSlot); } +// GEAR_DESTROY_ITEM — permanently destroy a bag item. +// bagNum 0 = backpack, 1..4 = equipped bags. Owner-authorized; shift-right- +// click in the addon is the UI guard. +void HandleGearDestroyItem(uint32 /*client_seq*/, WorldSession* sess, + std::vector const& fields) +{ + if (fields.size() < 3) return; + uint32 const guidLow = uint32(std::strtoul(fields[0].c_str(), nullptr, 10)); + uint8 const bagNum = uint8(std::strtoul(fields[1].c_str(), nullptr, 10)); + uint8 const slot = uint8(std::strtoul(fields[2].c_str(), nullptr, 10)); + ObjectGuid const botGuid = ObjectGuid::Create(guidLow); + Player* bot = ObjectAccessor::FindConnectedPlayer(botGuid); + if (!bot) return; + if (!Services::Owners().IsOwner(guidLow, sess->GetAccountId(), + sess->GetPlayer() ? sess->GetPlayer()->GetGUID().GetCounter() : 0u)) + return; + + uint8 const bagPos = (bagNum == 0) ? INVENTORY_SLOT_BAG_0 + : INVENTORY_SLOT_BAG_START + bagNum - 1; + if (!bot->GetItemByPos(bagPos, slot)) + return; + bot->DestroyItem(bagPos, slot, /*update*/ true); +} + +// GEAR_MOVE_ITEM — rearrange an +// item between bag slots (backpack or bags). Addon enforces empty destination; +// server bails if the item is gone or the move is a no-op. +void HandleGearMoveItem(uint32 /*client_seq*/, WorldSession* sess, + std::vector const& fields) +{ + if (fields.size() < 5) return; + uint32 const guidLow = uint32(std::strtoul(fields[0].c_str(), nullptr, 10)); + uint8 const srcBag = uint8(std::strtoul(fields[1].c_str(), nullptr, 10)); + uint8 const srcSlot = uint8(std::strtoul(fields[2].c_str(), nullptr, 10)); + uint8 const dstBag = uint8(std::strtoul(fields[3].c_str(), nullptr, 10)); + uint8 const dstSlot = uint8(std::strtoul(fields[4].c_str(), nullptr, 10)); + ObjectGuid const botGuid = ObjectGuid::Create(guidLow); + Player* bot = ObjectAccessor::FindConnectedPlayer(botGuid); + if (!bot) return; + if (!Services::Owners().IsOwner(guidLow, sess->GetAccountId(), + sess->GetPlayer() ? sess->GetPlayer()->GetGUID().GetCounter() : 0u)) + return; + + uint8 const srcBagPos = (srcBag == 0) ? INVENTORY_SLOT_BAG_0 + : INVENTORY_SLOT_BAG_START + srcBag - 1; + uint8 const dstBagPos = (dstBag == 0) ? INVENTORY_SLOT_BAG_0 + : INVENTORY_SLOT_BAG_START + dstBag - 1; + + if (srcBagPos == dstBagPos && srcSlot == dstSlot) + return; + if (!bot->GetItemByPos(srcBagPos, srcSlot)) + return; + + // Destination must be empty (same-entry stack merging is not offered in v1). + if (bot->GetItemByPos(dstBagPos, dstSlot)) + return; + + bot->SwapItem(uint16(srcBagPos << 8) | srcSlot, + uint16(dstBagPos << 8) | dstSlot); +} + // ---------------------------------------------------------------------------- // SUMMON — addon counterpart to `.playerbot summon `. Enforces the // SAME guards as the chat command (account match, cap, not-online refusal, @@ -1478,6 +1600,14 @@ void DispatchAssembled(WorldSession* sess, uint32 seq, { HandleGearUnequipItem(seq, sess, fields); } + else if (mtype == "GEAR_DESTROY_ITEM") + { + HandleGearDestroyItem(seq, sess, fields); + } + else if (mtype == "GEAR_MOVE_ITEM") + { + HandleGearMoveItem(seq, sess, fields); + } else if (mtype == "ACK") { // Retry spool not implemented in v1 — silent drop.