Playerbot: BotGear uses bot name end-to-end + retry first-open resolution

The client's UnitGUID format is unreliable across builds (the parser kept
extracting the realm/sub field), so the request resolved the wrong player and
no GEAR response was ever returned. The bot's NAME is stable, so:
- Server: GEAR_* handlers resolve fields[0] as a name OR numeric guid-low via
  ResolveGearBot(), and authorize via the resolved bot's guid.
- Addon: ResolveTarget returns UnitName('target'); all requests send the name;
  responses are matched by name; slash handler retries RefreshTarget after
  0.25s so the first open still populates when the unit is transiently
  unavailable right after targeting.
This commit is contained in:
devbox
2026-08-17 18:42:08 +10:00
parent d137727d3c
commit 1f25f119ec
2 changed files with 50 additions and 51 deletions
@@ -19,7 +19,7 @@ local BACKPACK_SLOTS = 16
local MAX_BAGS = 4
-- State -------------------------------------------------------------
local frame, botGuid, botName
local frame, botName
local equipButtons = {} -- [equipSlot] = button
local bagButtons = {} -- [bagNum] = { [slot] = button }
local equipData = {} -- [equipSlot] = {entry, quality}
@@ -52,25 +52,18 @@ local function Send(mtype, ...)
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
-- Player guid format ends with the low counter as hex, e.g.
-- "Player-1-00000258" or "Player-0000-00000258" / "Player-0-1-00000258".
-- The character counter is the LAST '-' group (hex).
local low = tonumber(string.match(guid, "%-([%x]+)$"), 16)
if not low or low == 0 then return nil, nil end
return low, UnitName("target")
if not UnitExists("target") then return nil end
return UnitName("target")
end
local function RequestBags()
if not botGuid then return end
Send("GEAR_BAGS_REQ", tostring(botGuid))
if not botName then return end
Send("GEAR_BAGS_REQ", botName)
end
local function RequestEquip()
if not botGuid then return end
Send("GEAR_EQUIP_REQ", tostring(botGuid))
if not botName then return end
Send("GEAR_EQUIP_REQ", botName)
end
local function RefreshAll()
@@ -135,12 +128,12 @@ local function CreateSlotButton(parent, x, y, slotType, bag, slot)
if slotType == "bag" then
if button == "RightButton" and IsShiftKeyDown() then
-- Destroy (permanent)
Send("GEAR_DESTROY_ITEM", tostring(botGuid), tostring(bag), tostring(slot))
Send("GEAR_DESTROY_ITEM", botName, 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))
Send("GEAR_EQUIP_ITEM", botName, tostring(bag), tostring(slot))
C_Timer.After(0.3, RefreshAll)
elseif button == "LeftButton" then
-- Move: pick up / place
@@ -152,7 +145,7 @@ local function CreateSlotButton(parent, x, y, slotType, bag, slot)
selected = nil
RefreshBags()
else
Send("GEAR_MOVE_ITEM", tostring(botGuid),
Send("GEAR_MOVE_ITEM", botName,
tostring(selected.bag), tostring(selected.slot),
tostring(bag), tostring(slot))
selected = nil
@@ -160,7 +153,7 @@ local function CreateSlotButton(parent, x, y, slotType, bag, slot)
end
end
elseif slotType == "equip" and button == "RightButton" then
Send("GEAR_UNEQUIP_ITEM", tostring(botGuid), tostring(slot))
Send("GEAR_UNEQUIP_ITEM", botName, tostring(slot))
C_Timer.After(0.3, RefreshAll)
end
end)
@@ -242,7 +235,7 @@ 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
if guidStr ~= botName then return end
bagData = {}
bagLayout = {}
gold = 0
@@ -276,7 +269,7 @@ local function ParseBagsResp(guidStr, body)
end
local function ParseEquipResp(guidStr, body)
if not guidStr or tonumber(guidStr) ~= botGuid then return end
if guidStr ~= botName then return end
equipData = {}
if body then
for part in body:gmatch("[^|]+") do
@@ -294,12 +287,11 @@ end
-- UI ----------------------------------------------------------------
local function RefreshTarget()
local g, n = ResolveTarget()
if g then
botGuid = g
local n = ResolveTarget()
if n then
botName = n
if frame and frame.title then
frame.title:SetText("Bot - " .. (n or "?"))
frame.title:SetText("Bot - " .. n)
end
if modelActor then
modelActor:SetUnit("target")
@@ -406,5 +398,9 @@ SlashCmdList["BOTGEAR"] = function()
else
frame:Show()
RefreshTarget()
-- The target/unit can be transiently unavailable the instant the
-- command runs (freshly targeted unit not fully resolved yet) —
-- retry a beat later so the first open still populates.
C_Timer.After(0.25, RefreshTarget)
end
end
@@ -928,21 +928,34 @@ void HandleSelfToggle(uint32 client_seq, WorldSession* sess,
SendFields(sess, client_seq, "SELF_RESP", resp);
}
// Resolve the bot targeted by a GEAR request. <ident> is a numeric guid-low
// (legacy) or a character name (current addon — the client's UnitGUID format
// varies, the name does not).
Player* ResolveGearBot(std::string const& ident)
{
if (ident.empty()) return nullptr;
char* end = nullptr;
unsigned long const low = std::strtoul(ident.c_str(), &end, 10);
if (end && *end == '\0' && low != 0)
return ObjectAccessor::FindConnectedPlayer(
ObjectGuid::Create<HighGuid::Player>(static_cast<uint64>(low)));
return ObjectAccessor::FindConnectedPlayerByName(ident);
}
// ----------------------------------------------------------------------------
// 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>
// GEAR_BAGS_REQ <ident> — return gold, bag containers, and every item in the
// backpack + the four equipped bags. <ident> is a numeric guid-low (legacy) or
// a character name (addon sends the name — robust across client guid formats).
// Payload: <ident>|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)
{
if (fields.empty()) return;
uint32 const guidLow = uint32(std::strtoul(fields[0].c_str(), nullptr, 10));
ObjectGuid const botGuid = ObjectGuid::Create<HighGuid::Player>(guidLow);
Player* bot = ObjectAccessor::FindConnectedPlayer(botGuid);
Player* bot = ResolveGearBot(fields[0]);
if (!bot) return;
if (!Services::Owners().IsOwner(guidLow, sess->GetAccountId(),
if (!Services::Owners().IsOwner(bot->GetGUID().GetCounter(), sess->GetAccountId(),
sess->GetPlayer() ? sess->GetPlayer()->GetGUID().GetCounter() : 0u))
return;
@@ -1011,11 +1024,9 @@ void HandleGearEquipReq(uint32 client_seq, WorldSession* sess,
std::vector<std::string> const& fields)
{
if (fields.empty()) return;
uint32 const guidLow = uint32(std::strtoul(fields[0].c_str(), nullptr, 10));
ObjectGuid const botGuid = ObjectGuid::Create<HighGuid::Player>(guidLow);
Player* bot = ObjectAccessor::FindConnectedPlayer(botGuid);
Player* bot = ResolveGearBot(fields[0]);
if (!bot) return;
if (!Services::Owners().IsOwner(guidLow, sess->GetAccountId(),
if (!Services::Owners().IsOwner(bot->GetGUID().GetCounter(), sess->GetAccountId(),
sess->GetPlayer() ? sess->GetPlayer()->GetGUID().GetCounter() : 0u))
return;
@@ -1041,7 +1052,6 @@ 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 srcBag = 0;
uint8 srcSlot = 0;
if (fields.size() >= 3)
@@ -1052,10 +1062,9 @@ void HandleGearEquipItem(uint32 /*client_seq*/, WorldSession* sess,
else
srcSlot = uint8(std::strtoul(fields[1].c_str(), nullptr, 10));
ObjectGuid const botGuid = ObjectGuid::Create<HighGuid::Player>(guidLow);
Player* bot = ObjectAccessor::FindConnectedPlayer(botGuid);
Player* bot = ResolveGearBot(fields[0]);
if (!bot) return;
if (!Services::Owners().IsOwner(guidLow, sess->GetAccountId(),
if (!Services::Owners().IsOwner(bot->GetGUID().GetCounter(), sess->GetAccountId(),
sess->GetPlayer() ? sess->GetPlayer()->GetGUID().GetCounter() : 0u))
return;
@@ -1112,12 +1121,10 @@ void HandleGearUnequipItem(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 equipSlot = uint8(std::strtoul(fields[1].c_str(), nullptr, 10));
ObjectGuid const botGuid = ObjectGuid::Create<HighGuid::Player>(guidLow);
Player* bot = ObjectAccessor::FindConnectedPlayer(botGuid);
Player* bot = ResolveGearBot(fields[0]);
if (!bot) return;
if (!Services::Owners().IsOwner(guidLow, sess->GetAccountId(),
if (!Services::Owners().IsOwner(bot->GetGUID().GetCounter(), sess->GetAccountId(),
sess->GetPlayer() ? sess->GetPlayer()->GetGUID().GetCounter() : 0u))
return;
@@ -1144,13 +1151,11 @@ 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);
Player* bot = ResolveGearBot(fields[0]);
if (!bot) return;
if (!Services::Owners().IsOwner(guidLow, sess->GetAccountId(),
if (!Services::Owners().IsOwner(bot->GetGUID().GetCounter(), sess->GetAccountId(),
sess->GetPlayer() ? sess->GetPlayer()->GetGUID().GetCounter() : 0u))
return;
@@ -1168,15 +1173,13 @@ 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);
Player* bot = ResolveGearBot(fields[0]);
if (!bot) return;
if (!Services::Owners().IsOwner(guidLow, sess->GetAccountId(),
if (!Services::Owners().IsOwner(bot->GetGUID().GetCounter(), sess->GetAccountId(),
sess->GetPlayer() ? sess->GetPlayer()->GetGUID().GetCounter() : 0u))
return;