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:
@@ -19,7 +19,7 @@ local BACKPACK_SLOTS = 16
|
|||||||
local MAX_BAGS = 4
|
local MAX_BAGS = 4
|
||||||
|
|
||||||
-- State -------------------------------------------------------------
|
-- State -------------------------------------------------------------
|
||||||
local frame, botGuid, botName
|
local frame, botName
|
||||||
local equipButtons = {} -- [equipSlot] = button
|
local equipButtons = {} -- [equipSlot] = button
|
||||||
local bagButtons = {} -- [bagNum] = { [slot] = button }
|
local bagButtons = {} -- [bagNum] = { [slot] = button }
|
||||||
local equipData = {} -- [equipSlot] = {entry, quality}
|
local equipData = {} -- [equipSlot] = {entry, quality}
|
||||||
@@ -52,25 +52,18 @@ local function Send(mtype, ...)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function ResolveTarget()
|
local function ResolveTarget()
|
||||||
if not UnitExists("target") then return nil, nil end
|
if not UnitExists("target") then return nil end
|
||||||
local guid = UnitGUID("target")
|
return UnitName("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")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function RequestBags()
|
local function RequestBags()
|
||||||
if not botGuid then return end
|
if not botName then return end
|
||||||
Send("GEAR_BAGS_REQ", tostring(botGuid))
|
Send("GEAR_BAGS_REQ", botName)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function RequestEquip()
|
local function RequestEquip()
|
||||||
if not botGuid then return end
|
if not botName then return end
|
||||||
Send("GEAR_EQUIP_REQ", tostring(botGuid))
|
Send("GEAR_EQUIP_REQ", botName)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function RefreshAll()
|
local function RefreshAll()
|
||||||
@@ -135,12 +128,12 @@ local function CreateSlotButton(parent, x, y, slotType, bag, slot)
|
|||||||
if slotType == "bag" then
|
if slotType == "bag" then
|
||||||
if button == "RightButton" and IsShiftKeyDown() then
|
if button == "RightButton" and IsShiftKeyDown() then
|
||||||
-- Destroy (permanent)
|
-- Destroy (permanent)
|
||||||
Send("GEAR_DESTROY_ITEM", tostring(botGuid), tostring(bag), tostring(slot))
|
Send("GEAR_DESTROY_ITEM", botName, tostring(bag), tostring(slot))
|
||||||
selected = nil
|
selected = nil
|
||||||
C_Timer.After(0.3, RefreshAll)
|
C_Timer.After(0.3, RefreshAll)
|
||||||
elseif button == "RightButton" then
|
elseif button == "RightButton" then
|
||||||
-- Equip
|
-- 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)
|
C_Timer.After(0.3, RefreshAll)
|
||||||
elseif button == "LeftButton" then
|
elseif button == "LeftButton" then
|
||||||
-- Move: pick up / place
|
-- Move: pick up / place
|
||||||
@@ -152,7 +145,7 @@ local function CreateSlotButton(parent, x, y, slotType, bag, slot)
|
|||||||
selected = nil
|
selected = nil
|
||||||
RefreshBags()
|
RefreshBags()
|
||||||
else
|
else
|
||||||
Send("GEAR_MOVE_ITEM", tostring(botGuid),
|
Send("GEAR_MOVE_ITEM", botName,
|
||||||
tostring(selected.bag), tostring(selected.slot),
|
tostring(selected.bag), tostring(selected.slot),
|
||||||
tostring(bag), tostring(slot))
|
tostring(bag), tostring(slot))
|
||||||
selected = nil
|
selected = nil
|
||||||
@@ -160,7 +153,7 @@ local function CreateSlotButton(parent, x, y, slotType, bag, slot)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
elseif slotType == "equip" and button == "RightButton" then
|
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)
|
C_Timer.After(0.3, RefreshAll)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
@@ -242,7 +235,7 @@ end
|
|||||||
|
|
||||||
-- Wire protocol: <guid>|G|<gold>|B|<bag>|<entry>|<size>...|I|<bag>|<slot>|<entry>|<count>|<quality>...
|
-- Wire protocol: <guid>|G|<gold>|B|<bag>|<entry>|<size>...|I|<bag>|<slot>|<entry>|<count>|<quality>...
|
||||||
local function ParseBagsResp(guidStr, body)
|
local function ParseBagsResp(guidStr, body)
|
||||||
if not guidStr or tonumber(guidStr) ~= botGuid then return end
|
if guidStr ~= botName then return end
|
||||||
bagData = {}
|
bagData = {}
|
||||||
bagLayout = {}
|
bagLayout = {}
|
||||||
gold = 0
|
gold = 0
|
||||||
@@ -276,7 +269,7 @@ local function ParseBagsResp(guidStr, body)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function ParseEquipResp(guidStr, body)
|
local function ParseEquipResp(guidStr, body)
|
||||||
if not guidStr or tonumber(guidStr) ~= botGuid then return end
|
if guidStr ~= botName then return end
|
||||||
equipData = {}
|
equipData = {}
|
||||||
if body then
|
if body then
|
||||||
for part in body:gmatch("[^|]+") do
|
for part in body:gmatch("[^|]+") do
|
||||||
@@ -294,12 +287,11 @@ end
|
|||||||
|
|
||||||
-- UI ----------------------------------------------------------------
|
-- UI ----------------------------------------------------------------
|
||||||
local function RefreshTarget()
|
local function RefreshTarget()
|
||||||
local g, n = ResolveTarget()
|
local n = ResolveTarget()
|
||||||
if g then
|
if n then
|
||||||
botGuid = g
|
|
||||||
botName = n
|
botName = n
|
||||||
if frame and frame.title then
|
if frame and frame.title then
|
||||||
frame.title:SetText("Bot - " .. (n or "?"))
|
frame.title:SetText("Bot - " .. n)
|
||||||
end
|
end
|
||||||
if modelActor then
|
if modelActor then
|
||||||
modelActor:SetUnit("target")
|
modelActor:SetUnit("target")
|
||||||
@@ -406,5 +398,9 @@ SlashCmdList["BOTGEAR"] = function()
|
|||||||
else
|
else
|
||||||
frame:Show()
|
frame:Show()
|
||||||
RefreshTarget()
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -928,21 +928,34 @@ void HandleSelfToggle(uint32 client_seq, WorldSession* sess,
|
|||||||
SendFields(sess, client_seq, "SELF_RESP", resp);
|
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 — return the bot's backpack contents.
|
||||||
// GEAR_BAGS_REQ <guid> — return gold, bag containers, and every item in the
|
// GEAR_BAGS_REQ <ident> — return gold, bag containers, and every item in the
|
||||||
// backpack + the four equipped bags.
|
// backpack + the four equipped bags. <ident> is a numeric guid-low (legacy) or
|
||||||
// Payload: <guid>|G|<goldCopper>|B|<bagNum>|<bagEntry>|<bagSize>|I|<bagNum>|<slot>|<entry>|<count>|<quality>
|
// 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).
|
// bagNum 0 = backpack, 1..4 = equipped bags (in bag-slot order).
|
||||||
void HandleGearBagsReq(uint32 client_seq, WorldSession* sess,
|
void HandleGearBagsReq(uint32 client_seq, WorldSession* sess,
|
||||||
std::vector<std::string> const& fields)
|
std::vector<std::string> const& fields)
|
||||||
{
|
{
|
||||||
if (fields.empty()) return;
|
if (fields.empty()) return;
|
||||||
uint32 const guidLow = uint32(std::strtoul(fields[0].c_str(), nullptr, 10));
|
Player* bot = ResolveGearBot(fields[0]);
|
||||||
ObjectGuid const botGuid = ObjectGuid::Create<HighGuid::Player>(guidLow);
|
|
||||||
Player* bot = ObjectAccessor::FindConnectedPlayer(botGuid);
|
|
||||||
if (!bot) return;
|
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))
|
sess->GetPlayer() ? sess->GetPlayer()->GetGUID().GetCounter() : 0u))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -1011,11 +1024,9 @@ void HandleGearEquipReq(uint32 client_seq, WorldSession* sess,
|
|||||||
std::vector<std::string> const& fields)
|
std::vector<std::string> const& fields)
|
||||||
{
|
{
|
||||||
if (fields.empty()) return;
|
if (fields.empty()) return;
|
||||||
uint32 const guidLow = uint32(std::strtoul(fields[0].c_str(), nullptr, 10));
|
Player* bot = ResolveGearBot(fields[0]);
|
||||||
ObjectGuid const botGuid = ObjectGuid::Create<HighGuid::Player>(guidLow);
|
|
||||||
Player* bot = ObjectAccessor::FindConnectedPlayer(botGuid);
|
|
||||||
if (!bot) return;
|
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))
|
sess->GetPlayer() ? sess->GetPlayer()->GetGUID().GetCounter() : 0u))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -1041,7 +1052,6 @@ void HandleGearEquipItem(uint32 /*client_seq*/, WorldSession* sess,
|
|||||||
std::vector<std::string> const& fields)
|
std::vector<std::string> const& fields)
|
||||||
{
|
{
|
||||||
if (fields.size() < 2) return;
|
if (fields.size() < 2) return;
|
||||||
uint32 const guidLow = uint32(std::strtoul(fields[0].c_str(), nullptr, 10));
|
|
||||||
uint8 srcBag = 0;
|
uint8 srcBag = 0;
|
||||||
uint8 srcSlot = 0;
|
uint8 srcSlot = 0;
|
||||||
if (fields.size() >= 3)
|
if (fields.size() >= 3)
|
||||||
@@ -1052,10 +1062,9 @@ void HandleGearEquipItem(uint32 /*client_seq*/, WorldSession* sess,
|
|||||||
else
|
else
|
||||||
srcSlot = uint8(std::strtoul(fields[1].c_str(), nullptr, 10));
|
srcSlot = uint8(std::strtoul(fields[1].c_str(), nullptr, 10));
|
||||||
|
|
||||||
ObjectGuid const botGuid = ObjectGuid::Create<HighGuid::Player>(guidLow);
|
Player* bot = ResolveGearBot(fields[0]);
|
||||||
Player* bot = ObjectAccessor::FindConnectedPlayer(botGuid);
|
|
||||||
if (!bot) return;
|
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))
|
sess->GetPlayer() ? sess->GetPlayer()->GetGUID().GetCounter() : 0u))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -1112,12 +1121,10 @@ void HandleGearUnequipItem(uint32 /*client_seq*/, WorldSession* sess,
|
|||||||
std::vector<std::string> const& fields)
|
std::vector<std::string> const& fields)
|
||||||
{
|
{
|
||||||
if (fields.size() < 2) return;
|
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));
|
uint8 const equipSlot = uint8(std::strtoul(fields[1].c_str(), nullptr, 10));
|
||||||
ObjectGuid const botGuid = ObjectGuid::Create<HighGuid::Player>(guidLow);
|
Player* bot = ResolveGearBot(fields[0]);
|
||||||
Player* bot = ObjectAccessor::FindConnectedPlayer(botGuid);
|
|
||||||
if (!bot) return;
|
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))
|
sess->GetPlayer() ? sess->GetPlayer()->GetGUID().GetCounter() : 0u))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -1144,13 +1151,11 @@ void HandleGearDestroyItem(uint32 /*client_seq*/, WorldSession* sess,
|
|||||||
std::vector<std::string> const& fields)
|
std::vector<std::string> const& fields)
|
||||||
{
|
{
|
||||||
if (fields.size() < 3) return;
|
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 bagNum = uint8(std::strtoul(fields[1].c_str(), nullptr, 10));
|
||||||
uint8 const slot = uint8(std::strtoul(fields[2].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 = ResolveGearBot(fields[0]);
|
||||||
Player* bot = ObjectAccessor::FindConnectedPlayer(botGuid);
|
|
||||||
if (!bot) return;
|
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))
|
sess->GetPlayer() ? sess->GetPlayer()->GetGUID().GetCounter() : 0u))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -1168,15 +1173,13 @@ void HandleGearMoveItem(uint32 /*client_seq*/, WorldSession* sess,
|
|||||||
std::vector<std::string> const& fields)
|
std::vector<std::string> const& fields)
|
||||||
{
|
{
|
||||||
if (fields.size() < 5) return;
|
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 srcBag = uint8(std::strtoul(fields[1].c_str(), nullptr, 10));
|
||||||
uint8 const srcSlot = uint8(std::strtoul(fields[2].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 dstBag = uint8(std::strtoul(fields[3].c_str(), nullptr, 10));
|
||||||
uint8 const dstSlot = uint8(std::strtoul(fields[4].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 = ResolveGearBot(fields[0]);
|
||||||
Player* bot = ObjectAccessor::FindConnectedPlayer(botGuid);
|
|
||||||
if (!bot) return;
|
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))
|
sess->GetPlayer() ? sess->GetPlayer()->GetGUID().GetCounter() : 0u))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user