Playerbot: bot character & inventory controller (gear, bags, destroy, move)

Server (AddonControl.cpp):
- GEAR_BAGS_REQ now returns gold, the four equipped bag containers (entry+size),
  and items from the backpack AND all bags (bagNum 0..4).
- GEAR_EQUIP_ITEM accepts items from any bag slot (not just the backpack).
- New GEAR_DESTROY_ITEM <guid> <bag> <slot> — permanent item destruction.
- New GEAR_MOVE_ITEM <guid> <srcBag> <srcSlot> <dstBag> <dstSlot> — rearrange
  items between bag slots (empty destination only).

Addon (BotGear.lua):
- Full 3D character preview (ModelScene) of the targeted bot, live-updating.
- Renders backpack + real bag containers with gold display.
- Left-click pick up / left-click place to move items between slots.
- Right-click bag item to equip; shift+right-click to destroy; right-click
  equipped item to unequip.
This commit is contained in:
devbox
2026-08-16 22:11:06 +10:00
parent 2638d9ac61
commit 07d2a49395
2 changed files with 422 additions and 139 deletions
+279 -126
View File
@@ -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
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: <guid>|G|<gold>|B|<bag>|<entry>|<size>...|I|<bag>|<slot>|<entry>|<count>|<quality>...
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)
@@ -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 <guid> — return gold, bag containers, and every item in the
// backpack + the four equipped bags.
// Payload: <guid>|G|<goldCopper>|B|<bagNum>|<bagEntry>|<bagSize>|I|<bagNum>|<slot>|<entry>|<count>|<quality>
// bagNum 0 = backpack, 1..4 = equipped bags (in bag-slot order).
void HandleGearBagsReq(uint32 client_seq, WorldSession* sess,
std::vector<std::string> const& fields)
{
@@ -943,15 +948,59 @@ void HandleGearBagsReq(uint32 client_seq, WorldSession* sess,
std::vector<std::string> out;
out.push_back(Escape(fields[0]));
// Gold (copper)
{
char buf[32];
std::snprintf(buf, sizeof(buf), "G|%llu",
static_cast<unsigned long long>(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)
{
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[48];
std::snprintf(buf, sizeof(buf), "%u,%u,%u,%u",
slot, item->GetEntry(), item->GetCount(),
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(std::string(buf));
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 <guid> <srcSlot> — equip from backpack slot
// GEAR_EQUIP_ITEM <guid> [<srcBag>] <srcSlot> — equip from a bag slot.
// srcBag 0 = backpack (default when omitted), 1..4 = equipped bags.
void HandleGearEquipItem(uint32 /*client_seq*/, WorldSession* sess,
std::vector<std::string> 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<HighGuid::Player>(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 <guid> <bagNum> <slot> — 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<std::string> 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<HighGuid::Player>(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 <guid> <srcBag> <srcSlot> <dstBag> <dstSlot> — 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<std::string> 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<HighGuid::Player>(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 <name>`. 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.