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:
@@ -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: <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]));
|
||||
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<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)
|
||||
{
|
||||
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 <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.
|
||||
|
||||
Reference in New Issue
Block a user