Files
playerbot-v2/src/modules/PlayerbotV2/Addon/BotGear/BotGear.lua
T
devbox 41ebd14db9 Playerbot: BotGear addon — use a non-zero sequence number
The server rejects any PBC frame with seq == 0 (AddonControl envelope
check), so GEAR_BAGS_REQ/GEAR_EQUIP_REQ were silently dropped and the panel
rendered with no data. Send uses an incrementing seq like PlayerbotControl.
2026-08-17 18:15:46 +10:00

399 lines
14 KiB
Lua

--[[
BotGear.lua — Altbot character & inventory controller.
Uses PBC addon protocol, no dependency on PlayerbotControl.
/botgear — opens panel for targeted bot.
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"
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 bagScroll, bagContent, goldText
local EQUIP_SLOT_NAMES = {
[0]="Head",[1]="Neck",[2]="Shoulder",[3]="Shirt",[4]="Chest",
[5]="Waist",[6]="Legs",[7]="Feet",[8]="Wrist",[9]="Hands",
[10]="Finger1",[11]="Finger2",[12]="Trinket1",[13]="Trinket2",
[14]="Back",[15]="MainHand",[16]="OffHand",[17]="Ranged",[18]="Tabard",
}
-- Comms -------------------------------------------------------------
local seqCounter = 1 -- server rejects frames with seq == 0
local function Send(mtype, ...)
local args = {...}
local body = table.concat(args, "|")
local f = string.format("1|%08x|1|1|%s|%s", seqCounter, mtype, body)
seqCounter = (seqCounter % 0xFFFFFFFF) + 1
C_ChatInfo.SendAddonMessage(PREFIX, f, "WHISPER", botName or UnitName("player"))
end
local function ResolveTarget()
if not UnitExists("target") then return nil, nil end
local guid = UnitGUID("target")
if not guid then return nil, nil end
local low = tonumber(string.match(guid, "-(%d+)-%x+$"))
return low, UnitName("target")
end
local function RequestBags()
if not botGuid then return end
Send("GEAR_BAGS_REQ", tostring(botGuid))
end
local function RequestEquip()
if not botGuid then return end
Send("GEAR_EQUIP_REQ", tostring(botGuid))
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
local function CreateSlotButton(parent, x, y, slotType, bag, slot)
local btn = CreateFrame("Button", nil, parent, "BackdropTemplate")
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.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)
btn:SetScript("OnEnter", function(self)
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)
btn:SetScript("OnClick", function(self, button)
if self.itemEntry == 0 then return end
if slotType == "bag" then
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
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 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, "BackdropTemplate")
frame:SetSize(520, 700)
frame:SetPoint("CENTER")
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)
frame:RegisterForDrag("LeftButton")
frame:SetScript("OnDragStart", frame.StartMoving)
frame:SetScript("OnDragStop", frame.StopMovingOrSizing)
tinsert(UISpecialFrames, "BotGearFrame")
local close = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
close:SetPoint("TOPRIGHT", -5, -5)
frame.title = frame:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
frame.title:SetPoint("TOP", 0, -12)
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, "BackdropTemplate")
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)
local refresh = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
refresh:SetSize(80, 22)
refresh:SetPoint("BOTTOM", 0, 8)
refresh:SetText("Refresh")
refresh:SetScript("OnClick", RefreshTarget)
end
-- 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
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("^([^|]+)|(.*)$")
ParseBagsResp(guidStr, rest)
elseif mtype == "GEAR_EQUIP_RESP" then
local guidStr, rest = body:match("^([^|]+)|(.*)$")
ParseEquipResp(guidStr, rest)
end
end)
-- Slash command
SLASH_BOTGEAR1 = "/botgear"
SlashCmdList["BOTGEAR"] = function()
BuildFrame()
if frame:IsVisible() then
frame:Hide()
else
frame:Show()
RefreshTarget()
end
end