Playerbot: single quest-item propagation path + bot spec control
- OnLootUnit's corpse-open quest-item scan double-granted every creature quest item (owner opens corpse -> +1, owner takes item -> +1 via OnLootItem). Remove the redundant scan; OnLootItem is now the single propagation path (covers creature AND GO loot). Fixes double loot. - BotGear: add specialization control. New GEAR_SPECS_REQ lists the bot's specs; GEAR_SET_SPEC switches via Player::ActivateTalentGroup. Addon renders a spec button row on the panel; active spec is highlighted.
This commit is contained in:
@@ -29,6 +29,9 @@ local gold = 0
|
|||||||
local selected = nil -- {bag=, slot=} item picked up for move
|
local selected = nil -- {bag=, slot=} item picked up for move
|
||||||
local modelActor = nil
|
local modelActor = nil
|
||||||
local bagScroll, bagContent, goldText
|
local bagScroll, bagContent, goldText
|
||||||
|
local specList = {} -- { {id, order, name} }
|
||||||
|
local currentSpec = 0
|
||||||
|
local specButtons = {} -- [order] = button
|
||||||
|
|
||||||
local EQUIP_SLOT_NAMES = {
|
local EQUIP_SLOT_NAMES = {
|
||||||
[0]="Head",[1]="Neck",[2]="Shoulder",[3]="Shirt",[4]="Chest",
|
[0]="Head",[1]="Neck",[2]="Shoulder",[3]="Shirt",[4]="Chest",
|
||||||
@@ -68,9 +71,22 @@ local function RequestEquip()
|
|||||||
Send("GEAR_EQUIP_REQ", botName)
|
Send("GEAR_EQUIP_REQ", botName)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function RequestSpecs()
|
||||||
|
if not botName then return end
|
||||||
|
Send("GEAR_SPECS_REQ", botName)
|
||||||
|
end
|
||||||
|
|
||||||
local function RefreshAll()
|
local function RefreshAll()
|
||||||
RequestBags()
|
RequestBags()
|
||||||
C_Timer.After(0.15, RequestEquip)
|
C_Timer.After(0.15, RequestEquip)
|
||||||
|
RequestSpecs()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function Unescape(s)
|
||||||
|
if not s then return s end
|
||||||
|
s = s:gsub("\\p", "|")
|
||||||
|
s = s:gsub("\\\\", "\\")
|
||||||
|
return s
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Rendering ---------------------------------------------------------
|
-- Rendering ---------------------------------------------------------
|
||||||
@@ -287,6 +303,47 @@ local function ParseEquipResp(guidStr, body)
|
|||||||
RefreshEquip()
|
RefreshEquip()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function RefreshSpecButtons()
|
||||||
|
for _, b in pairs(specButtons) do
|
||||||
|
b:Hide()
|
||||||
|
b:SetParent(nil)
|
||||||
|
end
|
||||||
|
specButtons = {}
|
||||||
|
|
||||||
|
local x = 205
|
||||||
|
for _, s in ipairs(specList) do
|
||||||
|
local b = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
|
||||||
|
b:SetSize(74, 22)
|
||||||
|
b:SetPoint("TOPLEFT", frame, "TOPLEFT", x, -66)
|
||||||
|
b:SetText(s.name)
|
||||||
|
b.specID = s.id
|
||||||
|
if s.id == currentSpec then
|
||||||
|
b:GetFontString():SetTextColor(1, 0.82, 0.2)
|
||||||
|
end
|
||||||
|
b:SetScript("OnClick", function(self)
|
||||||
|
Send("GEAR_SET_SPEC", botName, tostring(self.specID))
|
||||||
|
end)
|
||||||
|
specButtons[s.order] = b
|
||||||
|
x = x + 78
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ParseSpecsResp(guidStr, rest)
|
||||||
|
if guidStr ~= botName then return end
|
||||||
|
local cur, tupleStr = rest:match("^(%d+)|(.*)$")
|
||||||
|
currentSpec = tonumber(cur) or 0
|
||||||
|
specList = {}
|
||||||
|
if tupleStr then
|
||||||
|
for tuple in tupleStr:gmatch("[^|]+") do
|
||||||
|
local id, order, name = (Unescape(tuple)):match("^(%d+)|(-?%d+)|(.+)$")
|
||||||
|
if id then
|
||||||
|
tinsert(specList, { id = tonumber(id), order = tonumber(order), name = name })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if frame then RefreshSpecButtons() end
|
||||||
|
end
|
||||||
|
|
||||||
-- UI ----------------------------------------------------------------
|
-- UI ----------------------------------------------------------------
|
||||||
local function RefreshTarget()
|
local function RefreshTarget()
|
||||||
local n = ResolveTarget()
|
local n = ResolveTarget()
|
||||||
@@ -359,8 +416,12 @@ local function BuildFrame()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Bags (scroll)
|
-- Bags (scroll)
|
||||||
|
local specLbl = frame:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
|
||||||
|
specLbl:SetPoint("TOPLEFT", frame, "TOPLEFT", 205, -44)
|
||||||
|
specLbl:SetText("Specialization:")
|
||||||
|
|
||||||
bagScroll = CreateFrame("ScrollFrame", nil, frame, "BackdropTemplate")
|
bagScroll = CreateFrame("ScrollFrame", nil, frame, "BackdropTemplate")
|
||||||
bagScroll:SetPoint("TOPLEFT", frame, "TOPLEFT", 205, -40)
|
bagScroll:SetPoint("TOPLEFT", frame, "TOPLEFT", 205, -92)
|
||||||
bagScroll:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -12, 34)
|
bagScroll:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -12, 34)
|
||||||
bagScroll:SetBackdrop({ bgFile="Interface\\DialogFrame\\UI-DialogBox-Background" })
|
bagScroll:SetBackdrop({ bgFile="Interface\\DialogFrame\\UI-DialogBox-Background" })
|
||||||
bagContent = CreateFrame("Frame", nil, bagScroll)
|
bagContent = CreateFrame("Frame", nil, bagScroll)
|
||||||
@@ -402,6 +463,9 @@ f:SetScript("OnEvent", function(_, _, prefix, msg, channel, sender)
|
|||||||
elseif mtype == "GEAR_EQUIP_RESP" then
|
elseif mtype == "GEAR_EQUIP_RESP" then
|
||||||
local guidStr, rest = body:match("^([^|]+)|(.*)$")
|
local guidStr, rest = body:match("^([^|]+)|(.*)$")
|
||||||
ParseEquipResp(guidStr, rest)
|
ParseEquipResp(guidStr, rest)
|
||||||
|
elseif mtype == "GEAR_SPECS_RESP" then
|
||||||
|
local guidStr, rest = body:match("^([^|]+)|(.*)$")
|
||||||
|
ParseSpecsResp(guidStr, rest)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|||||||
@@ -3111,35 +3111,12 @@ void Module::OnLootUnit(Player* player, ObjectGuid creature_guid)
|
|||||||
|
|
||||||
auto& reg = ::Playerbot::Services::Registry();
|
auto& reg = ::Playerbot::Services::Registry();
|
||||||
|
|
||||||
// QUEST-ITEM SCAN: copy needed quest items to online alts
|
// NOTE: The old "QUEST-ITEM SCAN" here granted copies of every quest item
|
||||||
{
|
// on the corpse when the owner opened loot. OnLootItem (hooked in
|
||||||
Creature* c = ObjectAccessor::GetCreature(*player, creature_guid);
|
// Player::StoreLootItem) already grants per-item copies when the owner
|
||||||
if (c && !c->IsAlive())
|
// actually takes the item, so the corpse-open scan double-granted every
|
||||||
{
|
// creature quest item. The scan is removed — OnLootItem is the single
|
||||||
Loot* loot = c->GetLootForPlayer(player);
|
// quest-item propagation path (covers both creature and GO loot).
|
||||||
if (loot)
|
|
||||||
{
|
|
||||||
for (uint8 i = 0; i < loot->items.size(); ++i)
|
|
||||||
{
|
|
||||||
LootItem* li = loot->LootItemInSlot(i, player);
|
|
||||||
if (!li || li->is_blocked)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
for (BotId id : bots)
|
|
||||||
{
|
|
||||||
Player* bot = ObjectAccessor::FindConnectedPlayer(
|
|
||||||
ObjectGuid::Create<HighGuid::Player>(id));
|
|
||||||
if (!bot || !bot->IsInWorld())
|
|
||||||
continue;
|
|
||||||
if (bot->GetMapId() != player->GetMapId())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
GiveQuestItem(bot, li->itemid, li->count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AI corpse walk: only bots with AI registry
|
// AI corpse walk: only bots with AI registry
|
||||||
for (BotId id : bots)
|
for (BotId id : bots)
|
||||||
|
|||||||
@@ -29,6 +29,8 @@
|
|||||||
#include "ObjectAccessor.h"
|
#include "ObjectAccessor.h"
|
||||||
#include "ObjectGuid.h"
|
#include "ObjectGuid.h"
|
||||||
#include "ObjectMgr.h"
|
#include "ObjectMgr.h"
|
||||||
|
#include "DB2Stores.h"
|
||||||
|
#include "DB2Structure.h"
|
||||||
#include "Player.h"
|
#include "Player.h"
|
||||||
#include "World.h"
|
#include "World.h"
|
||||||
#include "WorldSession.h"
|
#include "WorldSession.h"
|
||||||
@@ -1201,6 +1203,63 @@ void HandleGearMoveItem(uint32 /*client_seq*/, WorldSession* sess,
|
|||||||
uint16(dstBagPos << 8) | dstSlot);
|
uint16(dstBagPos << 8) | dstSlot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// SPECS_REQ — list a bot's available specializations for the controller UI.
|
||||||
|
//
|
||||||
|
// Wire: GEAR_SPECS_REQ|<botName>
|
||||||
|
// Reply: GEAR_SPECS_RESP|<botName>|<currentSpecID>|<specID>|<name>|... (3-tuples)
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
void HandleGearSpecsReq(uint32 client_seq, WorldSession* sess,
|
||||||
|
std::vector<std::string> const& fields)
|
||||||
|
{
|
||||||
|
if (fields.empty()) return;
|
||||||
|
Player* bot = ResolveGearBot(fields[0]);
|
||||||
|
if (!bot) return;
|
||||||
|
if (!Services::Owners().IsOwner(bot->GetGUID().GetCounter(), sess->GetAccountId(),
|
||||||
|
sess->GetPlayer() ? sess->GetPlayer()->GetGUID().GetCounter() : 0u))
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::vector<std::string> out;
|
||||||
|
out.push_back(Escape(fields[0]));
|
||||||
|
out.push_back(std::to_string(AsUnderlyingType(bot->GetPrimarySpecialization())));
|
||||||
|
for (uint32 i = 0; i < MAX_SPECIALIZATIONS; ++i)
|
||||||
|
{
|
||||||
|
ChrSpecializationEntry const* spec = sDB2Manager.GetChrSpecializationByIndex(bot->GetClass(), i);
|
||||||
|
if (!spec)
|
||||||
|
continue;
|
||||||
|
char const* name = spec->Name[sWorld->GetDefaultDbcLocale()];
|
||||||
|
out.push_back(Escape(std::to_string(spec->ID) + "|" +
|
||||||
|
std::to_string(int32(spec->OrderIndex)) + "|" +
|
||||||
|
(name ? name : "?")));
|
||||||
|
}
|
||||||
|
SendFields(sess, client_seq, "GEAR_SPECS_RESP", out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// SET_SPEC — switch a bot to the given specialization.
|
||||||
|
//
|
||||||
|
// Wire: GEAR_SET_SPEC|<botName>|<specID>
|
||||||
|
// Reply: GEAR_SPECS_RESP|<botName>|... (fresh spec list so the UI updates)
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
void HandleGearSetSpec(uint32 client_seq, WorldSession* sess,
|
||||||
|
std::vector<std::string> const& fields)
|
||||||
|
{
|
||||||
|
if (fields.size() < 2) return;
|
||||||
|
Player* bot = ResolveGearBot(fields[0]);
|
||||||
|
if (!bot) return;
|
||||||
|
if (!Services::Owners().IsOwner(bot->GetGUID().GetCounter(), sess->GetAccountId(),
|
||||||
|
sess->GetPlayer() ? sess->GetPlayer()->GetGUID().GetCounter() : 0u))
|
||||||
|
return;
|
||||||
|
|
||||||
|
uint32 const specID = uint32(std::strtoul(fields[1].c_str(), nullptr, 10));
|
||||||
|
ChrSpecializationEntry const* spec = sChrSpecializationStore.LookupEntry(specID);
|
||||||
|
if (!spec || spec->ClassID != bot->GetClass() || spec->IsPetSpecialization())
|
||||||
|
return;
|
||||||
|
|
||||||
|
bot->ActivateTalentGroup(spec);
|
||||||
|
HandleGearSpecsReq(client_seq, sess, { fields[0] });
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// SUMMON — addon counterpart to `.playerbot summon <name>`. Enforces the
|
// SUMMON — addon counterpart to `.playerbot summon <name>`. Enforces the
|
||||||
// SAME guards as the chat command (account match, cap, not-online refusal,
|
// SAME guards as the chat command (account match, cap, not-online refusal,
|
||||||
@@ -1611,6 +1670,14 @@ void DispatchAssembled(WorldSession* sess, uint32 seq,
|
|||||||
{
|
{
|
||||||
HandleGearMoveItem(seq, sess, fields);
|
HandleGearMoveItem(seq, sess, fields);
|
||||||
}
|
}
|
||||||
|
else if (mtype == "GEAR_SPECS_REQ")
|
||||||
|
{
|
||||||
|
HandleGearSpecsReq(seq, sess, fields);
|
||||||
|
}
|
||||||
|
else if (mtype == "GEAR_SET_SPEC")
|
||||||
|
{
|
||||||
|
HandleGearSetSpec(seq, sess, fields);
|
||||||
|
}
|
||||||
else if (mtype == "ACK")
|
else if (mtype == "ACK")
|
||||||
{
|
{
|
||||||
// Retry spool not implemented in v1 — silent drop.
|
// Retry spool not implemented in v1 — silent drop.
|
||||||
|
|||||||
Reference in New Issue
Block a user