diff --git a/README.md b/README.md
index 4ee3b2d..5161eb4 100644
--- a/README.md
+++ b/README.md
@@ -378,6 +378,14 @@ The exact payloads are consumed internally by the MultiBot addon.
GET~BOT_SKILLS
Refresh character info skills, professions, secondary skills, weapon skills and armor skills.
+
+ GET~INVENTORY_BULK
+ Refresh inventory data for multiple bots in a single request (bag entries and item locations).
+
+
+ GET~BOT_SKILLS_BULK
+ Refresh skills data for multiple bots in a single request.
+
GET~BOT_REPUTATIONS
Refresh visible bot reputation standings for the Character Info frame.
@@ -414,6 +422,34 @@ The exact payloads are consumed internally by the MultiBot addon.
RUN~CRAFT_RECIPE
Ask a bot to craft one known profession recipe and return detailed cast failure reasons.
+
+ RUN~QUEST_ABANDON
+ Abandon a quest from the bot's log.
+
+
+ RUN~QUEST_SHARE
+ Share a quest to the bot's group (empty target = group-wide share).
+
+
+ RUN~ITEM_EQUIP
+ Equip an item with slot hint (AUTO, BAG, MAIN_HAND, OFF_HAND, RANGED).
+
+
+ RUN~ITEM_TRADE
+ Trade an item to another player/bot with optional count.
+
+
+ RUN~CAST_SPELL
+ Cast a spell with optional target and detailed failure reasons mapped from SpellCastResult.
+
+
+ RUN~TALENT_APPLY
+ Apply a talent build (including reset via 0-0-0).
+
+
+ RUN~CRAFT_RECIPE_TARGET
+ Craft a profession recipe targeting a specific item (bag, equipped, or trade slot).
+
RUN~ITEM_ACTION
Run whitelisted inventory item actions such as bank deposit, bank withdraw, guild bank deposit, guild bank withdraw and vendor buy.
@@ -426,6 +462,18 @@ The exact payloads are consumed internally by the MultiBot addon.
RUN~TRAINER_LEARN
Ask a bot to learn one trainer spell or all available trainer spells after revalidating the selected trainer.
+
+ INV_BAG
+ Send bag entries with type, slots, and item ID for inventory view.
+
+
+ INV_ITEM_LOC
+ Send exact bag/slot locations for each inventory item.
+
+
+ INV_EQUIP_LOC
+ Send equipped items with slot index and count.
+
RUN~RTI
Run whitelist-only RTI icon and RTI target commands.
diff --git a/src/MultiBotBridge.cpp b/src/MultiBotBridge.cpp
index 0c19d93..afacae5 100644
--- a/src/MultiBotBridge.cpp
+++ b/src/MultiBotBridge.cpp
@@ -10,9 +10,12 @@
#include "GuildMgr.h"
#include "Item.h"
#include "ItemPackets.h"
+#include "ObjectAccessor.h"
#include "ObjectMgr.h"
#include "Player.h"
#include "PlayerbotAI.h"
+#include "PlayerbotAIConfig.h"
+#include "PlayerbotFactory.h"
#include "PlayerbotMgr.h"
#include "Playerbots.h"
#include "RandomPlayerbotMgr.h"
@@ -23,10 +26,12 @@
#include "SharedDefines.h"
#include "Spell.h"
#include "SpellMgr.h"
+#include "Talentspec.h"
#include "Trainer.h"
#include "Unit.h"
#include "World.h"
#include "WorldPacket.h"
+#include "QuestPackets.h"
#include
#include
@@ -59,10 +64,20 @@ std::vector GetBridgeVisibleBots(Player* player);
void SendAddonPacket(Player* player, ChatMsg chatType, std::string const& opcode, std::string const& payload = "");
void SendOutfitPackets(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken);
void SendTrainerPackets(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken);
+void SendBagEntryPackets(Player* requester, ChatMsg replyType, Player* bot, std::string const& requestToken);
void RunOutfitCommand(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken, std::string const& encodedSuffix, std::string const& persistToken);
void RunTrainerLearnCommand(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken, std::string const& trainerEntryValue, std::string const& spellIdValue);
void RunProfessionRecipeCraftCommand(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken, std::string const& skillIdValue, std::string const& spellIdValue, std::string const& itemIdValue);
void RunInventoryItemActionCommand(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken, std::string const& actionValue, std::string const& itemIdValue, std::string const& countValue);
+void RunQuestAbandonCommand(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken, std::string const& questIdValue);
+void RunQuestShareCommand(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken, std::string const& questIdValue, std::string const& encodedTargetName);
+void RunItemEquipCommand(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken, std::string const& itemIdValue, std::string const& slotHintValue, std::string const& bagValue, std::string const& slotValue);
+void RunItemTradeCommand(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken, std::string const& itemIdValue, std::string const& encodedTargetName, std::string const& countValue, std::string const& bagValue, std::string const& slotValue);
+void RunSpellCastCommand(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken, std::string const& spellIdValue, std::string const& encodedTargetName);
+void RunTalentApplyCommand(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken, std::string const& encodedBuildString, std::string const& dryRunFlag);
+void RunProfessionRecipeCraftTargetCommand(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken, std::string const& skillIdValue, std::string const& spellIdValue, std::string const& targetItemIdValue, std::string const& targetBagValue, std::string const& targetSlotValue, std::string const& targetModeValue);
+void SendInventoryBulkPackets(Player* requester, ChatMsg replyType, std::string const& requestToken);
+void SendBotSkillsBulkPackets(Player* requester, ChatMsg replyType, std::string const& requestToken);
void SendBotReputationPackets(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken);
void SendBotEmblemPackets(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken);
uint32 GetPct(uint32 current, uint32 max);
@@ -1801,6 +1816,33 @@ void SendInventorySnapshot(Player* requester, ChatMsg replyType, std::string con
return;
}
+ SendBagEntryPackets(requester, replyType, bot, requestToken);
+
+ for (uint8 slot = EQUIPMENT_SLOT_START; slot < EQUIPMENT_SLOT_END; ++slot)
+ {
+ Item* const item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot);
+ if (!item)
+ continue;
+
+ ItemTemplate const* const proto = item->GetTemplate();
+ if (!proto)
+ continue;
+
+ std::string line = ChatHelper::FormatItem(proto, item->GetCount());
+ if (item->IsSoulBound())
+ line += " (soulbound)";
+
+ std::ostringstream equipPayload;
+ equipPayload << UrlEncodeField(bot->GetName())
+ << kFieldSeparator << requestToken
+ << kFieldSeparator << static_cast(slot)
+ << kFieldSeparator << proto->ItemId
+ << kFieldSeparator << item->GetCount()
+ << kFieldSeparator << UrlEncodeField(line)
+ << kFieldSeparator << (item->IsSoulBound() ? 1 : 0);
+ SendAddonPacket(requester, replyType, "INV_EQUIP_LOC", equipPayload.str());
+ }
+
InventorySummaryData const summary = BuildInventorySummary(bot);
std::ostringstream summaryPayload;
summaryPayload << bot->GetName() << kFieldSeparator << requestToken << kFieldSeparator << summary.gold << kFieldSeparator
@@ -1826,6 +1868,22 @@ void SendInventorySnapshot(Player* requester, ChatMsg replyType, std::string con
continue;
uint32 const itemId = proto->ItemId;
+
+ std::string locationLine = ChatHelper::FormatItem(proto, item->GetCount());
+ if (item->IsSoulBound())
+ locationLine += " (soulbound)";
+
+ std::ostringstream locationPayload;
+ locationPayload << UrlEncodeField(bot->GetName())
+ << kFieldSeparator << requestToken
+ << kFieldSeparator << static_cast(item->GetBagSlot())
+ << kFieldSeparator << static_cast(item->GetSlot())
+ << kFieldSeparator << itemId
+ << kFieldSeparator << item->GetCount()
+ << kFieldSeparator << UrlEncodeField(locationLine)
+ << kFieldSeparator << (item->IsSoulBound() ? 1 : 0);
+ SendAddonPacket(requester, replyType, "INV_ITEM_LOC", locationPayload.str());
+
itemCounts[itemId] += item->GetCount();
itemTemplates[itemId] = proto;
if (item->IsSoulBound())
@@ -3060,371 +3118,1818 @@ bool ExecuteSilentBotCommand(Player* requester, Player* bot, std::string const&
return true;
}
-uint32 MoveMatchingBagItemsToBank(Player* bot, uint32 itemId, uint32 requestedCount, std::string& reason)
+void SendRunResult(Player* requester, ChatMsg replyType, char const* opcode, std::string const& botName, std::string const& token, bool ok, std::string const& reason, std::vector const& extraFields = std::vector())
{
- if (!bot || !itemId)
- {
- reason = "BAD_REQUEST";
- return 0;
- }
+ std::ostringstream payload;
+ payload << UrlEncodeField(botName)
+ << kFieldSeparator << token
+ << kFieldSeparator << (ok ? "OK" : "ERR")
+ << kFieldSeparator << UrlEncodeField(reason);
- if (!FindNearbyNpcWithFlag(bot, UNIT_NPC_FLAG_BANKER))
- {
- reason = "BANKER_NOT_FOUND";
- return 0;
- }
+ for (std::string const& field : extraFields)
+ payload << kFieldSeparator << UrlEncodeField(field);
- uint32 moved = 0;
- while (Item* const item = FindBagItemByEntry(bot, itemId))
- {
- uint32 const stackCount = item->GetCount();
- ItemPosCountVec dest;
- InventoryResult const msg = bot->CanBankItem(NULL_BAG, NULL_SLOT, dest, item, false);
- if (msg != EQUIP_ERR_OK)
- {
- reason = "BANK_FULL";
- return moved;
- }
+ SendAddonPacket(requester, replyType, opcode, payload.str());
+}
- bot->RemoveItem(item->GetBagSlot(), item->GetSlot(), true);
- bot->BankItem(dest, item, true);
- moved += stackCount;
+bool ParseUint32Field(std::string const& value, uint32& out)
+{
+ std::string const trimmed = Trim(value);
+ if (trimmed.empty())
+ return false;
- if (requestedCount > 0 && moved >= requestedCount)
- break;
- }
+ char* end = nullptr;
+ unsigned long const parsed = std::strtoul(trimmed.c_str(), &end, 10);
+ if (!end || *end != '\0' || parsed > std::numeric_limits::max())
+ return false;
- if (!moved)
- reason = "ITEM_NOT_FOUND";
+ out = static_cast(parsed);
+ return true;
+}
- return moved;
+bool ParseUint8Field(std::string const& value, uint8& out)
+{
+ uint32 parsed = 0;
+ if (!ParseUint32Field(value, parsed) || parsed > std::numeric_limits::max())
+ return false;
+
+ out = static_cast(parsed);
+ return true;
}
-uint32 MoveMatchingBankItemsToBags(Player* bot, uint32 itemId, uint32 requestedCount, std::string& reason)
+bool SameName(std::string const& left, std::string const& right)
{
- if (!bot || !itemId)
- {
- reason = "BAD_REQUEST";
- return 0;
- }
+ return ToUpper(Trim(left)) == ToUpper(Trim(right));
+}
- if (!FindNearbyNpcWithFlag(bot, UNIT_NPC_FLAG_BANKER))
- {
- reason = "BANKER_NOT_FOUND";
- return 0;
- }
+bool FindQuestLogSlot(Player* bot, uint32 questId, uint8& outSlot)
+{
+ if (!bot || !questId)
+ return false;
- uint32 moved = 0;
- while (Item* const item = FindBankItemByEntry(bot, itemId))
+ for (uint8 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot)
{
- uint32 const stackCount = item->GetCount();
- ItemPosCountVec dest;
- InventoryResult const msg = bot->CanStoreItem(NULL_BAG, NULL_SLOT, dest, item, false);
- if (msg != EQUIP_ERR_OK)
+ if (bot->GetQuestSlotQuestId(slot) == questId)
{
- reason = "BAGS_FULL";
- return moved;
+ outSlot = slot;
+ return true;
}
+ }
- bot->RemoveItem(item->GetBagSlot(), item->GetSlot(), true);
- bot->StoreItem(dest, item, true);
- moved += stackCount;
+ return false;
+}
- if (requestedCount > 0 && moved >= requestedCount)
- break;
- }
+Player* FindAllowedPlayerTarget(Player* requester, std::string const& targetName)
+{
+ std::string normalizedName = Trim(UrlDecodeField(targetName));
+ if (normalizedName.empty())
+ return nullptr;
- if (!moved)
- reason = "ITEM_NOT_FOUND";
+ if (requester && SameName(requester->GetName(), normalizedName))
+ return requester;
- return moved;
+ if (Player* const bot = FindBotByName(requester, normalizedName))
+ return bot;
+
+ if (!normalizePlayerName(normalizedName))
+ return nullptr;
+
+ Player* const target = ObjectAccessor::FindPlayerByName(normalizedName, false);
+ if (!target)
+ return nullptr;
+
+ if (requester && requester->GetGroup() && target->GetGroup() == requester->GetGroup())
+ return target;
+
+ return nullptr;
}
-uint32 MoveMatchingBagItemsToGuildBank(Player* requester, Player* bot, uint32 itemId, uint32 requestedCount, std::string& reason)
+Unit* ResolveSpellTarget(Player* requester, Player* bot, PlayerbotAI* botAI, std::string const& encodedTargetName)
{
- if (!bot || !itemId)
- {
- reason = "BAD_REQUEST";
- return 0;
- }
+ std::string const targetName = Trim(UrlDecodeField(encodedTargetName));
+ if (!bot)
+ return nullptr;
- if (!bot->GetGuildId())
+ if (targetName.empty())
{
- reason = "BOT_NOT_IN_GUILD";
- return 0;
+ if (Unit* const selected = bot->GetSelectedUnit())
+ return selected;
+
+ return bot;
}
- Guild* const guild = sGuildMgr->GetGuildById(bot->GetGuildId());
- if (!guild)
+ if (Player* const targetPlayer = FindAllowedPlayerTarget(requester, targetName))
+ return targetPlayer;
+
+ if (!botAI || !botAI->GetAiObjectContext())
+ return nullptr;
+
+ static char const* const valueNames[] = { "possible targets", "all targets", "nearest npcs" };
+ for (char const* const valueName : valueNames)
{
- reason = "BOT_NOT_IN_GUILD";
- return 0;
+ GuidVector const units = *botAI->GetAiObjectContext()->GetValue(valueName);
+ for (ObjectGuid const guid : units)
+ {
+ Unit* const unit = botAI->GetUnit(guid);
+ if (unit && SameName(unit->GetName(), targetName))
+ return unit;
+ }
}
- if (!FindNearbyGuildBank(bot))
+ return nullptr;
+}
+
+std::string GetBridgeSpellFailureReason(SpellCastResult result)
+{
+ switch (result)
{
- reason = "GUILD_BANK_NOT_FOUND";
- return 0;
+ case SPELL_CAST_OK:
+ case SPELL_FAILED_SUCCESS:
+ return "OK";
+ case SPELL_FAILED_NOT_KNOWN:
+ return "MISSING_SPELL";
+ case SPELL_FAILED_BAD_TARGETS:
+ case SPELL_FAILED_BAD_IMPLICIT_TARGETS:
+ case SPELL_FAILED_NO_VALID_TARGETS:
+ return "INVALID_TARGET";
+ case SPELL_FAILED_OUT_OF_RANGE:
+ case SPELL_FAILED_TOO_CLOSE:
+ return "OUT_OF_RANGE";
+ case SPELL_FAILED_NO_POWER:
+ return "NO_MANA";
+ case SPELL_FAILED_NOT_READY:
+ case SPELL_FAILED_ITEM_NOT_READY:
+ return "COOLDOWN";
+ case SPELL_FAILED_REAGENTS:
+ return "REAGENTS";
+ case SPELL_FAILED_NEED_MORE_ITEMS:
+ return "NO_MATERIALS";
+ case SPELL_FAILED_MOVING:
+ return "MOVING";
+ case SPELL_FAILED_NOT_IN_CONTROL:
+ case SPELL_FAILED_STUNNED:
+ case SPELL_FAILED_CONFUSED:
+ case SPELL_FAILED_FLEEING:
+ return "LOST_CONTROL";
+ case SPELL_FAILED_TRY_AGAIN:
+ case SPELL_FAILED_SPELL_IN_PROGRESS:
+ return "TRY_AGAIN";
+ case SPELL_FAILED_REQUIRES_SPELL_FOCUS:
+ return "REQUIRES_SPELL_FOCUS";
+ case SPELL_FAILED_EQUIPPED_ITEM:
+ case SPELL_FAILED_EQUIPPED_ITEM_CLASS:
+ case SPELL_FAILED_EQUIPPED_ITEM_CLASS_MAINHAND:
+ case SPELL_FAILED_EQUIPPED_ITEM_CLASS_OFFHAND:
+ case SPELL_FAILED_TOTEM_CATEGORY:
+ case SPELL_FAILED_TOTEMS:
+ return "MISSING_TOOLS";
+ default:
+ return "CAST_FAILED";
}
+}
- if (!guild->MemberHasTabRights(bot->GetGUID(), 0, GUILD_BANK_RIGHT_DEPOSIT_ITEM))
+struct BridgeSpellCheckData
+{
+ bool ok = false;
+ std::string reason = "CAST_FAILED";
+ SpellCastResult result = SPELL_FAILED_ERROR;
+};
+
+void BuildBridgeSpellTargets(Player* bot, SpellInfo const* spellInfo, Unit* target, Item* itemTarget, SpellCastTargets& targets)
+{
+ if (!target)
+ target = bot;
+
+ if (spellInfo->Effects[0].Effect != SPELL_EFFECT_OPEN_LOCK &&
+ (spellInfo->Targets & TARGET_FLAG_ITEM || spellInfo->Targets & TARGET_FLAG_GAMEOBJECT_ITEM))
{
- reason = "NO_GUILD_BANK_RIGHTS";
- return 0;
+ // EN: Trade enchant/enhancement spells target the other player's not-traded slot.
+ // FR: Les enchantements/améliorations en échange ciblent l'emplacement non échangé de l'autre joueur.
+ if (itemTarget && bot->GetTrader())
+ targets.SetTradeItemTarget(bot);
+ else
+ targets.SetItemTarget(itemTarget);
}
+ else if (spellInfo->Targets & TARGET_FLAG_DEST_LOCATION)
+ targets.SetDst(*target);
+ else if (spellInfo->Targets & TARGET_FLAG_SOURCE_LOCATION)
+ targets.SetDst(*bot);
+ else
+ targets.SetUnitTarget(target);
+}
- uint32 moved = 0;
- while (Item* const item = FindBagItemByEntry(bot, itemId))
- {
- uint32 const stackCount = item->GetCount();
- uint32 const playerSlot = item->GetSlot();
- uint32 const playerBag = item->GetBagSlot();
- ObjectGuid const itemGuid = item->GetGUID();
- guild->SwapItemsWithInventory(bot, false, 0, 255, playerBag, playerSlot, 0);
+SpellCastResult CastBridgeSpellDirect(Player* bot, SpellInfo const* spellInfo, Unit* target, Item* itemTarget = nullptr)
+{
+ if (!bot || !spellInfo)
+ return SPELL_FAILED_ERROR;
- if (Item* const remaining = bot->GetItemByPos(playerBag, playerSlot))
- if (remaining->GetGUID() == itemGuid)
- {
- reason = "GUILD_BANK_FULL";
- return moved;
- }
+ if (!target)
+ target = bot;
- moved += stackCount;
+ ObjectGuid const oldSelection = bot->GetSelectedUnit() ? bot->GetSelectedUnit()->GetGUID() : ObjectGuid();
+ bot->SetSelection(target->GetGUID());
- if (requestedCount > 0 && moved >= requestedCount)
- break;
- }
+ Spell* const spell = new Spell(bot, spellInfo, TRIGGERED_NONE);
+ SpellCastTargets targets;
+ BuildBridgeSpellTargets(bot, spellInfo, target, itemTarget, targets);
+ SpellCastResult const result = spell->prepare(&targets);
- if (!moved)
- reason = "ITEM_NOT_FOUND";
+ bot->SetSelection(oldSelection);
- return moved;
+ return result;
}
-uint32 MoveMatchingGuildBankItemsToBags(Player* bot, uint32 itemId, uint32 requestedCount, std::string& reason)
+BridgeSpellCheckData CheckBridgeSpellCast(Player* bot, PlayerbotAI* botAI, uint32 spellId, Unit* target, Item* itemTarget = nullptr)
{
- if (!bot || !itemId)
+ BridgeSpellCheckData data;
+
+ if (!bot || !botAI || !spellId)
{
- reason = "BAD_REQUEST";
- return 0;
+ data.reason = "BAD_REQUEST";
+ return data;
}
- if (!bot->GetGuildId())
+ if (!target)
+ target = bot;
+
+ if (!target->IsInWorld())
{
- reason = "BOT_NOT_IN_GUILD";
- return 0;
+ data.reason = "INVALID_TARGET";
+ return data;
}
- Guild* const guild = sGuildMgr->GetGuildById(bot->GetGuildId());
- if (!guild)
+ if (bot->HasUnitState(UNIT_STATE_LOST_CONTROL))
{
- reason = "BOT_NOT_IN_GUILD";
- return 0;
+ data.reason = "LOST_CONTROL";
+ return data;
}
- if (!FindNearbyGuildBank(bot))
+ SpellInfo const* const spellInfo = sSpellMgr->GetSpellInfo(spellId);
+ if (!spellInfo)
{
- reason = "GUILD_BANK_NOT_FOUND";
- return 0;
+ data.reason = "UNKNOWN_SPELL";
+ return data;
}
- if (GetGuildBankWithdrawRemaining(guild, bot) == 0)
+ Pet* const pet = bot->GetPet();
+ if (!bot->HasSpell(spellId) && !(pet && pet->HasSpell(spellId)))
{
- reason = "NO_GUILD_BANK_RIGHTS";
- return 0;
+ data.result = SPELL_FAILED_NOT_KNOWN;
+ data.reason = "MISSING_SPELL";
+ return data;
}
- QueryResult result = CharacterDatabase.Query(
- "SELECT gbi.TabId, gbi.SlotId, ii.count "
- "FROM guild_bank_item gbi "
- "INNER JOIN item_instance ii ON ii.guid = gbi.item_guid "
- "WHERE gbi.guildid = {} AND ii.itemEntry = {} "
- "ORDER BY gbi.TabId, gbi.SlotId",
- guild->GetId(), itemId);
+ if (bot->GetCurrentSpell(CURRENT_CHANNELED_SPELL) != nullptr)
+ {
+ data.reason = "TRY_AGAIN";
+ return data;
+ }
- if (!result)
+ if (bot->HasSpellCooldown(spellId))
{
- reason = "ITEM_NOT_FOUND";
- return 0;
+ data.result = SPELL_FAILED_NOT_READY;
+ data.reason = "COOLDOWN";
+ return data;
}
- bool foundAny = false;
- bool foundWithdrawable = false;
- uint32 moved = 0;
+ if (bot->IsFlying() || bot->HasUnitState(UNIT_STATE_IN_FLIGHT))
+ {
+ data.reason = "CAST_FAILED";
+ return data;
+ }
- do
+ if (!bot->IsStandState())
{
- Field* const fields = result->Fetch();
- uint8 const tabId = fields[0].Get();
- uint8 const slotId = fields[1].Get();
- uint32 const stackCount = fields[2].Get();
- foundAny = true;
+ bot->SetStandState(UNIT_STAND_STATE_STAND);
+ data.result = SPELL_FAILED_NOT_STANDING;
+ data.reason = "TRY_AGAIN";
+ return data;
+ }
- if (GetGuildBankTabWithdrawRemaining(guild, bot, tabId) == 0)
+ uint32 const castTime = !spellInfo->IsChanneled() ? spellInfo->CalcCastTime(bot) : spellInfo->GetDuration();
+ if ((castTime || spellInfo->IsAutoRepeatRangedSpell()) && bot->isMoving())
+ {
+ data.result = SPELL_FAILED_MOVING;
+ data.reason = "MOVING";
+ return data;
+ }
+
+ ObjectGuid const oldSelection = bot->GetSelectedUnit() ? bot->GetSelectedUnit()->GetGUID() : ObjectGuid();
+ bot->SetSelection(target->GetGUID());
+
+ Spell* const spell = new Spell(bot, spellInfo, TRIGGERED_NONE);
+ SpellCastTargets targets;
+ BuildBridgeSpellTargets(bot, spellInfo, target, itemTarget, targets);
+
+ // EN: CheckCast reads Spell::m_targets; validate the same target set that CastSpell will prepare.
+ // FR: CheckCast lit Spell::m_targets; valider la même cible que CastSpell préparera.
+ spell->m_targets = targets;
+ data.result = spell->CheckCast(true);
+ delete spell;
+
+ bot->SetSelection(oldSelection);
+
+ data.reason = GetBridgeSpellFailureReason(data.result);
+ data.ok = data.reason == "OK";
+ return data;
+}
+
+Item* FindInventoryItemByEntry(Player* bot, uint32 itemEntry)
+{
+ if (!bot || !itemEntry)
+ return nullptr;
+
+ for (uint8 slot = INVENTORY_SLOT_ITEM_START; slot < INVENTORY_SLOT_ITEM_END; ++slot)
+ if (Item* const item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot))
+ if (item->GetEntry() == itemEntry)
+ return item;
+
+ for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag)
+ {
+ Bag const* const pBag = (Bag*)bot->GetItemByPos(INVENTORY_SLOT_BAG_0, bag);
+ if (!pBag)
continue;
- foundWithdrawable = true;
+ for (uint8 slot = 0; slot < pBag->GetBagSize(); ++slot)
+ if (Item* const item = bot->GetItemByPos(bag, slot))
+ if (item->GetEntry() == itemEntry)
+ return item;
+ }
- uint32 splitCount = 0;
- if (requestedCount > 0)
+ return nullptr;
+}
+
+Item* FindItemByOptionalPosition(Player* bot, uint32 itemEntry, uint8 bag, uint8 slot, bool hasPosition)
+{
+ if (!bot || !itemEntry)
+ return nullptr;
+
+ if (hasPosition)
+ {
+ Item* const item = bot->GetItemByPos(bag, slot);
+ return item && item->GetEntry() == itemEntry ? item : nullptr;
+ }
+
+ return FindInventoryItemByEntry(bot, itemEntry);
+}
+
+std::string MapEquipError(InventoryResult result)
+{
+ switch (result)
+ {
+ case EQUIP_ERR_OK:
+ return "OK";
+ case EQUIP_ERR_ITEM_NOT_FOUND:
+ return "MISSING_ITEM";
+ case EQUIP_ERR_YOU_CAN_NEVER_USE_THAT_ITEM:
+ case EQUIP_ERR_YOU_CAN_NEVER_USE_THAT_ITEM2:
+ return "WRONG_CLASS";
+ case EQUIP_ERR_CANT_EQUIP_LEVEL_I:
+ case EQUIP_ERR_PURCHASE_LEVEL_TOO_LOW:
+ return "WRONG_LEVEL";
+ case EQUIP_ERR_INVENTORY_FULL:
+ case EQUIP_ERR_BAG_FULL:
+ return "SLOT_BLOCKED";
+ default:
+ return "EQUIP_FAILED";
+ }
+}
+
+uint8 ResolveEquipSlot(Player* bot, PlayerbotAI* botAI, ItemTemplate const* proto, std::string const& slotHintValue, std::string& reason)
+{
+ if (!bot || !botAI || !proto)
+ {
+ reason = "BAD_REQUEST";
+ return NULL_SLOT;
+ }
+
+ std::string const slotHint = ToUpper(Trim(slotHintValue.empty() ? "AUTO" : slotHintValue));
+ if (slotHint == "AUTO")
+ {
+ if (proto->Class == ITEM_CLASS_CONTAINER)
{
- uint32 const remainingRequest = requestedCount > moved ? requestedCount - moved : 0;
- if (!remainingRequest)
+ for (uint8 bagSlot = INVENTORY_SLOT_BAG_START; bagSlot < INVENTORY_SLOT_BAG_END; ++bagSlot)
+ if (!bot->GetItemByPos(INVENTORY_SLOT_BAG_0, bagSlot))
+ return bagSlot;
+
+ return INVENTORY_SLOT_BAG_START;
+ }
+
+ if (proto->InventoryType == INVTYPE_RANGED || proto->InventoryType == INVTYPE_THROWN || proto->InventoryType == INVTYPE_RANGEDRIGHT)
+ return EQUIPMENT_SLOT_RANGED;
+
+ return botAI->FindEquipSlot(proto, NULL_SLOT, true);
+ }
+
+ if (slotHint == "BAG")
+ {
+ if (proto->Class != ITEM_CLASS_CONTAINER)
+ {
+ reason = "NOT_EQUIPPABLE";
+ return NULL_SLOT;
+ }
+
+ for (uint8 bagSlot = INVENTORY_SLOT_BAG_START; bagSlot < INVENTORY_SLOT_BAG_END; ++bagSlot)
+ if (!bot->GetItemByPos(INVENTORY_SLOT_BAG_0, bagSlot))
+ return bagSlot;
+
+ return INVENTORY_SLOT_BAG_START;
+ }
+
+ if (slotHint == "MAIN_HAND")
+ {
+ switch (proto->InventoryType)
+ {
+ case INVTYPE_WEAPON:
+ case INVTYPE_2HWEAPON:
+ case INVTYPE_WEAPONMAINHAND:
+ return EQUIPMENT_SLOT_MAINHAND;
+ default:
+ reason = "NOT_EQUIPPABLE";
+ return NULL_SLOT;
+ }
+ }
+
+ if (slotHint == "OFF_HAND")
+ {
+ switch (proto->InventoryType)
+ {
+ case INVTYPE_SHIELD:
+ case INVTYPE_WEAPONOFFHAND:
+ case INVTYPE_HOLDABLE:
+ return EQUIPMENT_SLOT_OFFHAND;
+ case INVTYPE_WEAPON:
+ if (bot->CanDualWield())
+ return EQUIPMENT_SLOT_OFFHAND;
+ break;
+ case INVTYPE_2HWEAPON:
+ if (bot->CanDualWield() && bot->CanTitanGrip() && proto->SubClass != ITEM_SUBCLASS_WEAPON_POLEARM && proto->SubClass != ITEM_SUBCLASS_WEAPON_STAFF && proto->SubClass != ITEM_SUBCLASS_WEAPON_FISHING_POLE)
+ return EQUIPMENT_SLOT_OFFHAND;
+ break;
+ default:
break;
+ }
- splitCount = std::min(stackCount, remainingRequest);
- if (splitCount >= stackCount)
- splitCount = 0;
+ reason = "NOT_EQUIPPABLE";
+ return NULL_SLOT;
+ }
+
+ if (slotHint == "RANGED")
+ {
+ if (proto->InventoryType == INVTYPE_RANGED || proto->InventoryType == INVTYPE_RANGEDRIGHT || proto->InventoryType == INVTYPE_THROWN || proto->InventoryType == INVTYPE_RELIC)
+ return EQUIPMENT_SLOT_RANGED;
+
+ reason = "NOT_EQUIPPABLE";
+ return NULL_SLOT;
+ }
+
+ reason = "BAD_SLOT_HINT";
+ return NULL_SLOT;
+}
+
+bool EquipItemToSlot(Player* bot, Item* item, uint8 dstSlot, std::string& reason)
+{
+ if (!bot || !bot->GetSession() || !item || dstSlot == NULL_SLOT)
+ {
+ reason = "BAD_REQUEST";
+ return false;
+ }
+
+ uint16 dest = 0;
+ InventoryResult const canEquip = bot->CanEquipItem(dstSlot, dest, item, true);
+ if (canEquip != EQUIP_ERR_OK)
+ {
+ reason = MapEquipError(canEquip);
+ return false;
+ }
+
+ if (item->GetBagSlot() == INVENTORY_SLOT_BAG_0 && item->GetSlot() == dstSlot)
+ {
+ reason = "OK";
+ return true;
+ }
+
+ WorldPacket packet(CMSG_AUTOEQUIP_ITEM_SLOT, 2);
+ ObjectGuid itemGuid = item->GetGUID();
+ packet << itemGuid << dstSlot;
+
+ WorldPackets::Item::AutoEquipItemSlot nicePacket(std::move(packet));
+ nicePacket.Read();
+ bot->GetSession()->HandleAutoEquipItemSlotOpcode(nicePacket);
+ reason = "OK";
+ return true;
+}
+
+bool IsStackableItem(Item* item)
+{
+ if (!item || !item->GetTemplate())
+ return false;
+
+ return item->GetTemplate()->GetMaxStackSize() > 1;
+}
+
+std::vector- FindInventoryStacksByEntry(Player* bot, uint32 itemEntry)
+{
+ std::vector
- items;
+ if (!bot || !itemEntry)
+ return items;
+
+ for (uint8 slot = INVENTORY_SLOT_ITEM_START; slot < INVENTORY_SLOT_ITEM_END; ++slot)
+ if (Item* const item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot))
+ if (item->GetEntry() == itemEntry)
+ items.push_back(item);
+
+ for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag)
+ {
+ Bag const* const pBag = (Bag*)bot->GetItemByPos(INVENTORY_SLOT_BAG_0, bag);
+ if (!pBag)
+ continue;
+
+ for (uint8 slot = 0; slot < pBag->GetBagSize(); ++slot)
+ if (Item* const item = bot->GetItemByPos(bag, slot))
+ if (item->GetEntry() == itemEntry)
+ items.push_back(item);
+ }
+
+ return items;
+}
+
+Item* FindBestTradeSourceStack(Player* bot, uint32 itemId, uint32 count, bool oneStackMode, uint8 preferredBag, uint8 preferredSlot, bool hasPreferredPosition, std::string& reason)
+{
+ if (!bot || !itemId)
+ {
+ reason = "BAD_REQUEST";
+ return nullptr;
+ }
+
+ if (hasPreferredPosition)
+ {
+ Item* const item = bot->GetItemByPos(preferredBag, preferredSlot);
+ if (!item || item->GetEntry() != itemId)
+ {
+ reason = "MISSING_ITEM";
+ return nullptr;
}
- uint32 const before = bot->GetItemCount(itemId, false);
- guild->SwapItemsWithInventory(bot, true, tabId, slotId, NULL_BAG, NULL_SLOT, splitCount);
- uint32 const after = bot->GetItemCount(itemId, false);
+ if (oneStackMode || item->GetCount() >= count)
+ return item;
- if (after > before)
- moved += after - before;
+ reason = "COUNT_UNAVAILABLE";
+ return nullptr;
+ }
+
+ std::vector
- items = FindInventoryStacksByEntry(bot, itemId);
+ if (items.empty())
+ {
+ reason = "MISSING_ITEM";
+ return nullptr;
+ }
+
+ if (oneStackMode)
+ {
+ std::sort(items.begin(), items.end(), [](Item* left, Item* right)
+ {
+ return left->GetCount() < right->GetCount();
+ });
+ return items.front();
+ }
+
+ for (Item* const item : items)
+ if (item->GetCount() == count)
+ return item;
+
+ Item* best = nullptr;
+ for (Item* const item : items)
+ {
+ if (item->GetCount() > count && (!best || item->GetCount() < best->GetCount()))
+ best = item;
+ }
+
+ if (best)
+ return best;
+
+ reason = "COUNT_UNAVAILABLE";
+ return nullptr;
+}
+
+bool SelectExactTradeStacksRecursive(std::vector
- const& items, std::size_t index, uint32 remaining, std::vector
- & selected)
+{
+ if (!remaining)
+ return true;
+
+ if (index >= items.size())
+ return false;
+
+ for (std::size_t i = index; i < items.size(); ++i)
+ {
+ Item* const item = items[i];
+ if (!item || item->GetCount() > remaining)
+ continue;
+
+ selected.push_back(item);
+ if (SelectExactTradeStacksRecursive(items, i + 1, remaining - item->GetCount(), selected))
+ return true;
+ selected.pop_back();
+ }
+
+ return false;
+}
+
+std::vector
- SelectExactTradeStacks(Player* bot, uint32 itemId, uint32 count)
+{
+ std::vector
- selected;
+ std::vector
- items = FindInventoryStacksByEntry(bot, itemId);
+ std::sort(items.begin(), items.end(), [](Item* left, Item* right)
+ {
+ return left->GetCount() > right->GetCount();
+ });
+
+ SelectExactTradeStacksRecursive(items, 0, count, selected);
+ return selected;
+}
+
+Item* SplitStackForExactTrade(Player* bot, Item* source, uint32 count, std::string& reason)
+{
+ if (!bot || !source || !count || source->GetCount() <= count)
+ return source;
+
+ ItemPosCountVec dest;
+ InventoryResult const canStore = bot->CanStoreItem(NULL_BAG, NULL_SLOT, dest, source->GetEntry(), count, nullptr, false);
+ if (canStore != EQUIP_ERR_OK || dest.empty())
+ {
+ reason = "COUNT_UNAVAILABLE";
+ return nullptr;
+ }
+
+ uint16 const dst = dest.front().pos;
+ uint16 const src = (static_cast
(source->GetBagSlot()) << 8) | source->GetSlot();
+ bot->SplitItem(src, dst, count);
+
+ Item* const splitItem = bot->GetItemByPos(dst >> 8, dst & 255);
+ if (!splitItem || splitItem->GetEntry() != source->GetEntry() || splitItem->GetCount() != count)
+ {
+ reason = "COUNT_UNAVAILABLE";
+ return nullptr;
+ }
+
+ return splitItem;
+}
+
+uint8 GetFreeTradeSlotCount(Player* bot)
+{
+ if (!bot || !bot->GetTradeData())
+ return 0;
+
+ uint8 freeSlots = 0;
+ TradeData* const trade = bot->GetTradeData();
+ for (uint8 i = 0; i < TRADE_SLOT_TRADED_COUNT; ++i)
+ if (!trade->GetItem(TradeSlots(i)))
+ ++freeSlots;
+
+ return freeSlots;
+}
+
+bool AddItemToTradeSlot(Player* bot, Item* item, std::string& reason)
+{
+ if (!bot || !bot->GetSession() || !bot->GetTradeData() || !item)
+ {
+ reason = "TRADE_FAILED";
+ return false;
+ }
+
+ if (!item->CanBeTraded(false, true))
+ {
+ reason = "ITEM_NOT_TRADABLE";
+ return false;
+ }
+
+ int8 tradeSlot = -1;
+ TradeData* const trade = bot->GetTradeData();
+ for (uint8 i = 0; i < TRADE_SLOT_TRADED_COUNT; ++i)
+ {
+ if (!trade->GetItem(TradeSlots(i)))
+ {
+ tradeSlot = i;
+ break;
+ }
+ }
+
+ if (tradeSlot == -1)
+ {
+ reason = "TRADE_SLOT_FULL";
+ return false;
+ }
+
+ WorldPacket packet(CMSG_SET_TRADE_ITEM, 3);
+ packet << static_cast(tradeSlot);
+ packet << static_cast(item->GetBagSlot());
+ packet << static_cast(item->GetSlot());
+ bot->GetSession()->HandleSetTradeItemOpcode(packet);
+
+ reason = "OK";
+ return true;
+}
+
+bool IsResetTalentBuild(std::string const& build)
+{
+ std::string const trimmed = Trim(build);
+ if (trimmed.empty())
+ return true;
+
+ for (char const c : trimmed)
+ if (c != '0' && c != '-' && c != ' ' && c != '\t')
+ return false;
+
+ return true;
+}
+
+struct BagEntryData
+{
+ uint8 bagIndex = 0;
+ uint32 bagItemId = 0;
+ std::string bagLink;
+ uint32 numSlots = 0;
+ std::string bagType;
+};
+
+std::string GetBagTypeString(ItemTemplate const* proto)
+{
+ if (!proto)
+ return "BACKPACK";
+
+ if (proto->BagFamily & BAG_FAMILY_MASK_ARROWS)
+ return "QUIVER";
+ if (proto->BagFamily & BAG_FAMILY_MASK_BULLETS)
+ return "AMMO_POUCH";
+ if (proto->BagFamily & BAG_FAMILY_MASK_SOUL_SHARDS)
+ return "SOUL_SHARD";
+ if (proto->BagFamily & BAG_FAMILY_MASK_HERBS)
+ return "HERB";
+ if (proto->BagFamily & BAG_FAMILY_MASK_ENCHANTING_SUPP)
+ return "ENCHANTING";
+ if (proto->BagFamily & BAG_FAMILY_MASK_MINING_SUPP)
+ return "MINING";
+ if (proto->BagFamily & BAG_FAMILY_MASK_ENGINEERING_SUPP)
+ return "ENGINEERING";
+
+ if (proto->Class == ITEM_CLASS_CONTAINER && proto->SubClass == ITEM_SUBCLASS_CONTAINER)
+ return "NORMAL";
+
+ return "UNKNOWN";
+}
+
+std::vector BuildBagEntries(Player* bot)
+{
+ std::vector entries;
+
+ BagEntryData backpack;
+ backpack.bagIndex = 0;
+ backpack.numSlots = 16;
+ backpack.bagType = "BACKPACK";
+ entries.push_back(backpack);
+
+ if (!bot)
+ return entries;
+
+ for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag)
+ {
+ Bag* const pBag = (Bag*)bot->GetItemByPos(INVENTORY_SLOT_BAG_0, bag);
+ if (!pBag)
+ continue;
+
+ ItemTemplate const* const proto = pBag->GetTemplate();
+ BagEntryData entry;
+ entry.bagIndex = static_cast((bag - INVENTORY_SLOT_BAG_START) + 1);
+ entry.bagItemId = proto ? proto->ItemId : 0;
+ entry.bagLink = proto ? ChatHelper::FormatItem(proto, 1) : "";
+ entry.numSlots = pBag->GetBagSize();
+ entry.bagType = GetBagTypeString(proto);
+ entries.push_back(entry);
+ }
+
+ return entries;
+}
+
+void SendBagEntryPackets(Player* requester, ChatMsg replyType, Player* bot, std::string const& requestToken)
+{
+ if (!requester || !bot)
+ return;
+
+ for (BagEntryData const& entry : BuildBagEntries(bot))
+ {
+ std::ostringstream payload;
+ payload << UrlEncodeField(bot->GetName())
+ << kFieldSeparator << requestToken
+ << kFieldSeparator << static_cast(entry.bagIndex)
+ << kFieldSeparator << entry.bagItemId
+ << kFieldSeparator << UrlEncodeField(entry.bagLink)
+ << kFieldSeparator << entry.numSlots
+ << kFieldSeparator << UrlEncodeField(entry.bagType);
+ SendAddonPacket(requester, replyType, "INV_BAG", payload.str());
+ }
+}
+
+uint32 MoveMatchingBagItemsToBank(Player* bot, uint32 itemId, uint32 requestedCount, std::string& reason)
+{
+ if (!bot || !itemId)
+ {
+ reason = "BAD_REQUEST";
+ return 0;
+ }
+
+ if (!FindNearbyNpcWithFlag(bot, UNIT_NPC_FLAG_BANKER))
+ {
+ reason = "BANKER_NOT_FOUND";
+ return 0;
+ }
+
+ uint32 moved = 0;
+ while (Item* const item = FindBagItemByEntry(bot, itemId))
+ {
+ uint32 const stackCount = item->GetCount();
+ ItemPosCountVec dest;
+ InventoryResult const msg = bot->CanBankItem(NULL_BAG, NULL_SLOT, dest, item, false);
+ if (msg != EQUIP_ERR_OK)
+ {
+ reason = "BANK_FULL";
+ return moved;
+ }
+
+ uint32 const remainingRequest = requestedCount > 0 ? requestedCount - moved : 0;
+ uint32 const toMove = remainingRequest > 0 && stackCount > remainingRequest ? remainingRequest : stackCount;
+
+ if (toMove == stackCount)
+ {
+ bot->RemoveItem(item->GetBagSlot(), item->GetSlot(), true);
+ bot->BankItem(dest, item, true);
+ }
+ else
+ {
+ Item* const splitItem = SplitStackForExactTrade(bot, item, toMove, reason);
+ if (!splitItem)
+ return moved;
+ bot->BankItem(dest, splitItem, true);
+ }
+ moved += toMove;
+
+ if (requestedCount > 0 && moved >= requestedCount)
+ break;
+ }
+
+ if (!moved)
+ reason = "ITEM_NOT_FOUND";
+
+ return moved;
+}
+
+uint32 MoveMatchingBankItemsToBags(Player* bot, uint32 itemId, uint32 requestedCount, std::string& reason)
+{
+ if (!bot || !itemId)
+ {
+ reason = "BAD_REQUEST";
+ return 0;
+ }
+
+ if (!FindNearbyNpcWithFlag(bot, UNIT_NPC_FLAG_BANKER))
+ {
+ reason = "BANKER_NOT_FOUND";
+ return 0;
+ }
+
+ uint32 moved = 0;
+ while (Item* const item = FindBankItemByEntry(bot, itemId))
+ {
+ uint32 const stackCount = item->GetCount();
+ ItemPosCountVec dest;
+ InventoryResult const msg = bot->CanStoreItem(NULL_BAG, NULL_SLOT, dest, item, false);
+ if (msg != EQUIP_ERR_OK)
+ {
+ reason = "BAGS_FULL";
+ return moved;
+ }
+
+ uint32 const remainingRequest = requestedCount > 0 ? requestedCount - moved : 0;
+ uint32 const toMove = remainingRequest > 0 && stackCount > remainingRequest ? remainingRequest : stackCount;
+
+ if (toMove == stackCount)
+ {
+ bot->RemoveItem(item->GetBagSlot(), item->GetSlot(), true);
+ bot->StoreItem(dest, item, true);
+ }
+ else
+ {
+ Item* const splitItem = SplitStackForExactTrade(bot, item, toMove, reason);
+ if (!splitItem)
+ return moved;
+ bot->StoreItem(dest, splitItem, true);
+ }
+ moved += toMove;
+
+ if (requestedCount > 0 && moved >= requestedCount)
+ break;
+ }
+
+ if (!moved)
+ reason = "ITEM_NOT_FOUND";
+
+ return moved;
+}
+
+uint32 MoveMatchingBagItemsToGuildBank(Player* requester, Player* bot, uint32 itemId, uint32 requestedCount, std::string& reason)
+{
+ if (!bot || !itemId)
+ {
+ reason = "BAD_REQUEST";
+ return 0;
+ }
+
+ if (!bot->GetGuildId())
+ {
+ reason = "BOT_NOT_IN_GUILD";
+ return 0;
+ }
+
+ Guild* const guild = sGuildMgr->GetGuildById(bot->GetGuildId());
+ if (!guild)
+ {
+ reason = "BOT_NOT_IN_GUILD";
+ return 0;
+ }
+
+ if (!FindNearbyGuildBank(bot))
+ {
+ reason = "GUILD_BANK_NOT_FOUND";
+ return 0;
+ }
+
+ if (!guild->MemberHasTabRights(bot->GetGUID(), 0, GUILD_BANK_RIGHT_DEPOSIT_ITEM))
+ {
+ reason = "NO_GUILD_BANK_RIGHTS";
+ return 0;
+ }
+
+ uint32 moved = 0;
+ while (Item* const item = FindBagItemByEntry(bot, itemId))
+ {
+ uint32 const stackCount = item->GetCount();
+ uint32 const playerSlot = item->GetSlot();
+ uint32 const playerBag = item->GetBagSlot();
+
+ uint32 const remainingRequest = requestedCount > 0 ? requestedCount - moved : 0;
+ uint32 const toMove = remainingRequest > 0 && stackCount > remainingRequest ? remainingRequest : stackCount;
+
+ guild->SwapItemsWithInventory(bot, false, 0, 255, playerBag, playerSlot, toMove);
+
+ if (toMove < stackCount)
+ {
+ Item* const newItem = bot->GetItemByPos(playerBag, playerSlot);
+ if (!newItem || newItem->GetEntry() != itemId)
+ {
+ reason = "GUILD_BANK_FULL";
+ return moved;
+ }
+ }
+
+ moved += toMove;
+
+ if (requestedCount > 0 && moved >= requestedCount)
+ break;
+ }
+
+ if (!moved)
+ reason = "ITEM_NOT_FOUND";
+
+ return moved;
+}
+
+uint32 MoveMatchingGuildBankItemsToBags(Player* bot, uint32 itemId, uint32 requestedCount, std::string& reason)
+{
+ if (!bot || !itemId)
+ {
+ reason = "BAD_REQUEST";
+ return 0;
+ }
+
+ if (!bot->GetGuildId())
+ {
+ reason = "BOT_NOT_IN_GUILD";
+ return 0;
+ }
+
+ Guild* const guild = sGuildMgr->GetGuildById(bot->GetGuildId());
+ if (!guild)
+ {
+ reason = "BOT_NOT_IN_GUILD";
+ return 0;
+ }
+
+ if (!FindNearbyGuildBank(bot))
+ {
+ reason = "GUILD_BANK_NOT_FOUND";
+ return 0;
+ }
+
+ if (GetGuildBankWithdrawRemaining(guild, bot) == 0)
+ {
+ reason = "NO_GUILD_BANK_RIGHTS";
+ return 0;
+ }
+
+ QueryResult result = CharacterDatabase.Query(
+ "SELECT gbi.TabId, gbi.SlotId, ii.count "
+ "FROM guild_bank_item gbi "
+ "INNER JOIN item_instance ii ON ii.guid = gbi.item_guid "
+ "WHERE gbi.guildid = {} AND ii.itemEntry = {} "
+ "ORDER BY gbi.TabId, gbi.SlotId",
+ guild->GetId(), itemId);
+
+ if (!result)
+ {
+ reason = "ITEM_NOT_FOUND";
+ return 0;
+ }
+
+ bool foundAny = false;
+ bool foundWithdrawable = false;
+ uint32 moved = 0;
+
+ do
+ {
+ Field* const fields = result->Fetch();
+ uint8 const tabId = fields[0].Get();
+ uint8 const slotId = fields[1].Get();
+ uint32 const stackCount = fields[2].Get();
+ foundAny = true;
+
+ if (GetGuildBankTabWithdrawRemaining(guild, bot, tabId) == 0)
+ continue;
+
+ foundWithdrawable = true;
+
+ uint32 splitCount = 0;
+ if (requestedCount > 0)
+ {
+ uint32 const remainingRequest = requestedCount > moved ? requestedCount - moved : 0;
+ if (!remainingRequest)
+ break;
+
+ splitCount = std::min(stackCount, remainingRequest);
+ if (splitCount >= stackCount)
+ splitCount = 0;
+ }
+
+ uint32 const before = bot->GetItemCount(itemId, false);
+ guild->SwapItemsWithInventory(bot, true, tabId, slotId, NULL_BAG, NULL_SLOT, splitCount);
+ uint32 const after = bot->GetItemCount(itemId, false);
+
+ if (after > before)
+ moved += after - before;
+
+ if (requestedCount > 0 && moved >= requestedCount)
+ break;
+ }
+ while (result->NextRow());
+
+ if (!moved)
+ {
+ if (!foundAny)
+ reason = "ITEM_NOT_FOUND";
+ else if (!foundWithdrawable)
+ reason = "NO_GUILD_BANK_RIGHTS";
+ else
+ reason = "BAGS_FULL";
+ }
+
+ return moved;
+}
+
+uint32 BuyMatchingVendorItem(Player* bot, uint32 itemId, uint32 requestedCount, std::string& reason)
+{
+ if (!bot || !itemId)
+ {
+ reason = "BAD_REQUEST";
+ return 0;
+ }
+
+ ItemTemplate const* const proto = sObjectMgr->GetItemTemplate(itemId);
+ if (!proto)
+ {
+ reason = "ITEM_NOT_FOUND";
+ return 0;
+ }
+
+ uint32 vendorSlot = 0;
+ uint32 vendorExtendedCost = 0;
+ bool sawVendor = false;
+ Creature* const vendor = FindNearbyVendorSellingItem(bot, itemId, vendorSlot, vendorExtendedCost, sawVendor);
+ if (!vendor)
+ {
+ reason = sawVendor ? "VENDOR_DOES_NOT_SELL_ITEM" : "VENDOR_NOT_FOUND";
+ return 0;
+ }
+
+ uint32 const desired = requestedCount > 0 ? requestedCount : 1;
+ uint32 bought = 0;
+ for (uint32 i = 0; i < desired; ++i)
+ {
+ if (requestedCount > 0 && bought >= requestedCount)
+ break;
+
+ uint32 const price = uint32(std::floor(proto->BuyPrice * bot->GetReputationPriceDiscount(vendor)));
+ if (price > 0 && bot->GetMoney() < price)
+ {
+ reason = "NOT_ENOUGH_MONEY";
+ break;
+ }
+
+ uint32 const oldCount = bot->GetItemCount(itemId, false);
+ bot->BuyItemFromVendorSlot(vendor->GetGUID(), vendorSlot, itemId, 1, NULL_BAG, NULL_SLOT);
+ uint32 const newCount = bot->GetItemCount(itemId, false);
+ if (newCount <= oldCount)
+ {
+ reason = vendorExtendedCost > 0 ? "VENDOR_REQUIRES_SPECIAL_CURRENCY" : "BUY_FAILED";
+ break;
+ }
+
+ bought += newCount - oldCount;
+ }
+
+ return bought;
+}
+
+void RunInventoryItemActionCommand(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken, std::string const& actionValue, std::string const& itemIdValue, std::string const& countValue)
+{
+ std::string const trimmedBotName = Trim(botName);
+ std::string const token = Trim(requestToken);
+ std::string const action = ToUpper(Trim(actionValue));
+ uint32 const itemId = static_cast(std::strtoul(Trim(itemIdValue).c_str(), nullptr, 10));
+ uint32 const requestedCount = static_cast(std::strtoul(Trim(countValue).c_str(), nullptr, 10));
+
+ Player* const bot = FindBotByName(requester, trimmedBotName);
+ std::string const effectiveBotName = bot ? bot->GetName() : trimmedBotName;
+
+ std::string reason;
+ uint32 moved = 0;
+ if (!bot)
+ reason = "NO_BOT";
+ else if (action == "BANK_DEPOSIT")
+ moved = MoveMatchingBagItemsToBank(bot, itemId, requestedCount, reason);
+ else if (action == "BANK_WITHDRAW")
+ moved = MoveMatchingBankItemsToBags(bot, itemId, requestedCount, reason);
+ else if (action == "GBANK_DEPOSIT")
+ moved = MoveMatchingBagItemsToGuildBank(requester, bot, itemId, requestedCount, reason);
+ else if (action == "GBANK_WITHDRAW")
+ moved = MoveMatchingGuildBankItemsToBags(bot, itemId, requestedCount, reason);
+ else if (action == "BUY_ITEM")
+ moved = BuyMatchingVendorItem(bot, itemId, requestedCount, reason);
+ else
+ reason = "BAD_ACTION";
+
+ bool const ok = moved > 0;
+ if (ok)
+ reason = "OK";
+ else if (reason.empty())
+ reason = "FAILED";
+
+ std::ostringstream payload;
+ payload << UrlEncodeField(effectiveBotName)
+ << kFieldSeparator << token
+ << kFieldSeparator << action
+ << kFieldSeparator << itemId
+ << kFieldSeparator << (ok ? "OK" : "ERR")
+ << kFieldSeparator << UrlEncodeField(reason)
+ << kFieldSeparator << moved;
+
+ SendAddonPacket(requester, replyType, "INVENTORY_ITEM_ACTION", payload.str());
+}
+
+void RunProfessionRecipeCraftCommand(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken, std::string const& skillIdValue, std::string const& spellIdValue, std::string const& itemIdValue)
+{
+ std::string const trimmedBotName = Trim(botName);
+ std::string const token = Trim(requestToken);
+ uint32 const skillId = static_cast(std::strtoul(Trim(skillIdValue).c_str(), nullptr, 10));
+ uint32 const spellId = static_cast(std::strtoul(Trim(spellIdValue).c_str(), nullptr, 10));
+ uint32 const expectedItemId = static_cast(std::strtoul(Trim(itemIdValue).c_str(), nullptr, 10));
+
+ Player* const bot = FindBotByName(requester, trimmedBotName);
+ std::string const effectiveBotName = bot ? bot->GetName() : trimmedBotName;
+
+ uint32 actualItemId = expectedItemId;
+ std::string result = ValidateProfessionRecipeCraft(bot, skillId, spellId, expectedItemId, actualItemId);
+ if (result == "OK")
+ result = CastProfessionRecipe(bot, spellId);
+
+ std::ostringstream payload;
+ payload << UrlEncodeField(effectiveBotName)
+ << kFieldSeparator << token
+ << kFieldSeparator << skillId
+ << kFieldSeparator << spellId
+ << kFieldSeparator << actualItemId
+ << kFieldSeparator << (result == "OK" ? "OK" : "ERR")
+ << kFieldSeparator << UrlEncodeField(result);
+
+ SendAddonPacket(requester, replyType, "PROFESSION_RECIPE_CRAFT", payload.str());
+}
+
+void RunQuestAbandonCommand(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken, std::string const& questIdValue)
+{
+ std::string const trimmedBotName = Trim(botName);
+ std::string const token = Trim(requestToken);
+ uint32 questId = 0;
+
+ Player* const bot = FindBotByName(requester, trimmedBotName);
+ std::string const effectiveBotName = bot ? bot->GetName() : trimmedBotName;
+
+ if (!bot)
+ {
+ SendRunResult(requester, replyType, "QUEST_ABANDON", effectiveBotName, token, false, "NO_BOT");
+ return;
+ }
+
+ if (!ParseUint32Field(questIdValue, questId) || !questId || !sObjectMgr->GetQuestTemplate(questId))
+ {
+ SendRunResult(requester, replyType, "QUEST_ABANDON", effectiveBotName, token, false, "INVALID_QUEST_ID");
+ return;
+ }
+
+ uint8 questSlot = 0;
+ if (!FindQuestLogSlot(bot, questId, questSlot))
+ {
+ SendRunResult(requester, replyType, "QUEST_ABANDON", effectiveBotName, token, false, "MISSING_QUEST");
+ return;
+ }
+
+ bot->SetQuestSlot(questSlot, 0);
+ bot->TakeQuestSourceItem(questId, false);
+ bot->SetQuestStatus(questId, QUEST_STATUS_NONE);
+ bot->RemoveRewardedQuest(questId);
+ bot->RemoveActiveQuest(questId, false);
+
+ SendRunResult(requester, replyType, "QUEST_ABANDON", effectiveBotName, token, true, "OK");
+}
+
+void RunQuestShareCommand(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken, std::string const& questIdValue, std::string const& encodedTargetName)
+{
+ std::string const trimmedBotName = Trim(botName);
+ std::string const token = Trim(requestToken);
+ uint32 questId = 0;
+
+ Player* const bot = FindBotByName(requester, trimmedBotName);
+ std::string const effectiveBotName = bot ? bot->GetName() : trimmedBotName;
+
+ if (!bot)
+ {
+ SendRunResult(requester, replyType, "QUEST_SHARE", effectiveBotName, token, false, "NO_BOT");
+ return;
+ }
+
+ if (!ParseUint32Field(questIdValue, questId) || !questId)
+ {
+ SendRunResult(requester, replyType, "QUEST_SHARE", effectiveBotName, token, false, "INVALID_QUEST_ID");
+ return;
+ }
+
+ Quest const* const quest = sObjectMgr->GetQuestTemplate(questId);
+ if (!quest)
+ {
+ SendRunResult(requester, replyType, "QUEST_SHARE", effectiveBotName, token, false, "INVALID_QUEST_ID");
+ return;
+ }
+
+ uint8 questSlot = 0;
+ if (!FindQuestLogSlot(bot, questId, questSlot))
+ {
+ SendRunResult(requester, replyType, "QUEST_SHARE", effectiveBotName, token, false, "MISSING_QUEST");
+ return;
+ }
+
+ if (!bot->CanShareQuest(questId))
+ {
+ SendRunResult(requester, replyType, "QUEST_SHARE", effectiveBotName, token, false, "NOT_SHAREABLE");
+ return;
+ }
+
+ if (!bot->GetGroup())
+ {
+ SendRunResult(requester, replyType, "QUEST_SHARE", effectiveBotName, token, false, "INVALID_TARGET");
+ return;
+ }
+
+ std::string const targetName = Trim(UrlDecodeField(encodedTargetName));
+ if (!targetName.empty())
+ {
+ Player* const target = FindAllowedPlayerTarget(requester, encodedTargetName);
+ if (!target || target->GetGroup() != bot->GetGroup() || target == bot)
+ {
+ SendRunResult(requester, replyType, "QUEST_SHARE", effectiveBotName, token, false, "INVALID_TARGET");
+ return;
+ }
+
+ if (!target->IsInMap(bot))
+ {
+ SendRunResult(requester, replyType, "QUEST_SHARE", effectiveBotName, token, false, "OUT_OF_RANGE");
+ return;
+ }
+
+ if (!target->SatisfyQuestStatus(quest, false))
+ {
+ SendRunResult(requester, replyType, "QUEST_SHARE", effectiveBotName, token, false, "TARGET_HAS_QUEST");
+ return;
+ }
+
+ if (target->GetQuestStatus(questId) == QUEST_STATUS_COMPLETE)
+ {
+ SendRunResult(requester, replyType, "QUEST_SHARE", effectiveBotName, token, false, "TARGET_COMPLETE");
+ return;
+ }
+
+ if (!target->CanTakeQuest(quest, false))
+ {
+ SendRunResult(requester, replyType, "QUEST_SHARE", effectiveBotName, token, false, "NOT_SHAREABLE");
+ return;
+ }
+
+ if (!target->SatisfyQuestLog(false))
+ {
+ SendRunResult(requester, replyType, "QUEST_SHARE", effectiveBotName, token, false, "QUEST_LOG_FULL");
+ return;
+ }
+
+ if (target->GetDivider())
+ {
+ SendRunResult(requester, replyType, "QUEST_SHARE", effectiveBotName, token, false, "TARGET_BUSY");
+ return;
+ }
+ }
+
+ // EN: Native AzerothCore quest share is group-wide; empty target skips single-recipient pre-validation.
+ // FR: Le partage de quête AzerothCore est global au groupe; une cible vide saute la pré-validation mono-destinataire.
+ WorldPacket packet(CMSG_PUSHQUESTTOPARTY);
+ packet << questId;
+ WorldPackets::Quest::PushQuestToParty pushQuest(std::move(packet));
+ pushQuest.Read();
+ bot->GetSession()->HandlePushQuestToParty(pushQuest);
+
+ // EN: Bot recipients do not click the client popup; feed their AI the same packet used by playerbots auto-share.
+ // FR: Les bots destinataires ne cliquent pas la fenêtre client; on injecte au bot AI le même paquet que l'auto-partage playerbots.
+ uint32 botAcceptCount = 0;
+ if (Group* const group = bot->GetGroup())
+ {
+ for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next())
+ {
+ Player* const member = itr->GetSource();
+ if (!member || member == bot || !member->IsInWorld() || !member->IsInMap(bot))
+ continue;
+
+ if (!targetName.empty() && member->GetName() != targetName)
+ continue;
+
+ PlayerbotAI* const memberAI = GET_PLAYERBOT_AI(member);
+ if (!memberAI)
+ continue;
+
+ if (!member->SatisfyQuestStatus(quest, false) || member->GetQuestStatus(questId) == QUEST_STATUS_COMPLETE ||
+ !member->CanTakeQuest(quest, false) || !member->SatisfyQuestLog(false))
+ continue;
+
+ // EN: Native share sets Divider on eligible recipients; playerbot accept action needs it.
+ // FR: Le partage natif définit Divider sur les destinataires éligibles; l'action d'acceptation playerbot en a besoin.
+ if (member->GetDivider().IsEmpty())
+ continue;
+
+ WorldPacket acceptPacket(CMSG_PUSHQUESTTOPARTY, 20);
+ acceptPacket << questId;
+ memberAI->HandleMasterIncomingPacket(acceptPacket);
+ ++botAcceptCount;
+ }
+ }
+
+ SendRunResult(requester, replyType, "QUEST_SHARE", effectiveBotName, token, true, "OK", { "BOT_ACCEPTS:" + std::to_string(botAcceptCount) });
+}
+
+void RunItemEquipCommand(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken, std::string const& itemIdValue, std::string const& slotHintValue, std::string const& bagValue, std::string const& slotValue)
+{
+ std::string const trimmedBotName = Trim(botName);
+ std::string const token = Trim(requestToken);
+ uint32 itemId = 0;
+ uint8 bag = 0;
+ uint8 slot = 0;
+ bool const hasPosition = ParseUint8Field(bagValue, bag) && ParseUint8Field(slotValue, slot);
+
+ Player* const bot = FindBotByName(requester, trimmedBotName);
+ std::string const effectiveBotName = bot ? bot->GetName() : trimmedBotName;
+
+ if (!bot)
+ {
+ SendRunResult(requester, replyType, "ITEM_EQUIP", effectiveBotName, token, false, "NO_BOT");
+ return;
+ }
+
+ PlayerbotAI* const botAI = GetBotAI(bot);
+ if (!botAI)
+ {
+ SendRunResult(requester, replyType, "ITEM_EQUIP", effectiveBotName, token, false, "NO_AI");
+ return;
+ }
+
+ if (!ParseUint32Field(itemIdValue, itemId) || !itemId)
+ {
+ SendRunResult(requester, replyType, "ITEM_EQUIP", effectiveBotName, token, false, "BAD_REQUEST");
+ return;
+ }
+
+ Item* const item = FindItemByOptionalPosition(bot, itemId, bag, slot, hasPosition);
+ if (!item)
+ {
+ SendRunResult(requester, replyType, "ITEM_EQUIP", effectiveBotName, token, false, "MISSING_ITEM");
+ return;
+ }
+
+ ItemTemplate const* const proto = item->GetTemplate();
+ if (!proto || (proto->InventoryType == INVTYPE_NON_EQUIP && proto->Class != ITEM_CLASS_CONTAINER))
+ {
+ SendRunResult(requester, replyType, "ITEM_EQUIP", effectiveBotName, token, false, "NOT_EQUIPPABLE");
+ return;
+ }
+
+ if (proto->InventoryType == INVTYPE_AMMO)
+ {
+ bot->SetAmmo(proto->ItemId);
+ SendRunResult(requester, replyType, "ITEM_EQUIP", effectiveBotName, token, true, "OK");
+ return;
+ }
+
+ std::string reason;
+ uint8 const dstSlot = ResolveEquipSlot(bot, botAI, proto, slotHintValue, reason);
+ if (dstSlot == NULL_SLOT)
+ {
+ if (reason.empty())
+ reason = "NOT_EQUIPPABLE";
+ SendRunResult(requester, replyType, "ITEM_EQUIP", effectiveBotName, token, false, reason);
+ return;
+ }
+
+ bool const ok = EquipItemToSlot(bot, item, dstSlot, reason);
+ SendRunResult(requester, replyType, "ITEM_EQUIP", effectiveBotName, token, ok, ok ? "OK" : reason);
+}
+
+void RunSpellCastCommand(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken, std::string const& spellIdValue, std::string const& encodedTargetName)
+{
+ std::string const trimmedBotName = Trim(botName);
+ std::string const token = Trim(requestToken);
+ uint32 spellId = 0;
+
+ Player* const bot = FindBotByName(requester, trimmedBotName);
+ std::string const effectiveBotName = bot ? bot->GetName() : trimmedBotName;
+
+ if (!bot)
+ {
+ SendRunResult(requester, replyType, "CAST_SPELL", effectiveBotName, token, false, "NO_BOT");
+ return;
+ }
+
+ PlayerbotAI* const botAI = GetBotAI(bot);
+ if (!botAI)
+ {
+ SendRunResult(requester, replyType, "CAST_SPELL", effectiveBotName, token, false, "NO_AI");
+ return;
+ }
+
+ if (!ParseUint32Field(spellIdValue, spellId) || !spellId)
+ {
+ SendRunResult(requester, replyType, "CAST_SPELL", effectiveBotName, token, false, "BAD_REQUEST");
+ return;
+ }
+
+ Unit* const target = ResolveSpellTarget(requester, bot, botAI, encodedTargetName);
+ if (!target)
+ {
+ SendRunResult(requester, replyType, "CAST_SPELL", effectiveBotName, token, false, "INVALID_TARGET");
+ return;
+ }
+
+ BridgeSpellCheckData const check = CheckBridgeSpellCast(bot, botAI, spellId, target);
+ if (!check.ok)
+ {
+ SendRunResult(requester, replyType, "CAST_SPELL", effectiveBotName, token, false, check.reason, { std::to_string(static_cast(check.result)) });
+ return;
+ }
+
+ SpellInfo const* const spellInfo = sSpellMgr->GetSpellInfo(spellId);
+ SpellCastResult const result = CastBridgeSpellDirect(bot, spellInfo, target);
+ bool const ok = result == SPELL_CAST_OK;
+ SendRunResult(requester, replyType, "CAST_SPELL", effectiveBotName, token, ok, ok ? "OK" : GetBridgeSpellFailureReason(result), ok ? std::vector() : std::vector{ std::to_string(static_cast(result)) });
+}
+
+void RunTalentApplyCommand(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken, std::string const& encodedBuildString, std::string const& dryRunFlag)
+{
+ std::string const trimmedBotName = Trim(botName);
+ std::string const token = Trim(requestToken);
+ std::string const buildString = Trim(UrlDecodeField(encodedBuildString));
+ bool const dryRun = Trim(dryRunFlag) == "1" || ToUpper(Trim(dryRunFlag)) == "TRUE";
+
+ Player* const bot = FindBotByName(requester, trimmedBotName);
+ std::string const effectiveBotName = bot ? bot->GetName() : trimmedBotName;
+
+ if (!bot)
+ {
+ SendRunResult(requester, replyType, "TALENT_APPLY", effectiveBotName, token, false, "NO_BOT", { "" });
+ return;
+ }
+
+ if (IsResetTalentBuild(buildString))
+ {
+ if (dryRun)
+ {
+ SendRunResult(requester, replyType, "TALENT_APPLY", effectiveBotName, token, true, "OK", { "RESET" });
+ return;
+ }
+
+ bool const ok = bot->resetTalents(true);
+ SendRunResult(requester, replyType, "TALENT_APPLY", effectiveBotName, token, ok, ok ? "OK" : "RESET_FAILED", { "RESET" });
+ return;
+ }
+
+ std::ostringstream validation;
+ TalentSpec base(bot->getClassMask());
+ if (!base.CheckTalentLink(buildString, &validation))
+ {
+ SendRunResult(requester, replyType, "TALENT_APPLY", effectiveBotName, token, false, "INVALID_BUILD", { validation.str() });
+ return;
+ }
+
+ std::vector> const parsedSpecLink = PlayerbotAIConfig::ParseTempTalentsOrder(bot->getClass(), buildString);
+ if (parsedSpecLink.empty())
+ {
+ SendRunResult(requester, replyType, "TALENT_APPLY", effectiveBotName, token, false, "INVALID_BUILD", { "Invalid link " + buildString });
+ return;
+ }
+
+ if (dryRun)
+ {
+ SendRunResult(requester, replyType, "TALENT_APPLY", effectiveBotName, token, true, "OK", { "DRY_RUN:" + buildString });
+ return;
+ }
+
+ // EN: Match legacy "talents apply ": parser + PlayerbotFactory handles order/dependencies.
+ // FR: Même logique que l'ancien "talents apply ": le parseur + PlayerbotFactory gèrent l'ordre/dépendances.
+ PlayerbotFactory::InitTalentsByParsedSpecLink(bot, parsedSpecLink, true);
+ if (PlayerbotAI* const botAI = GetBotAI(bot))
+ botAI->ResetStrategies();
+
+ SendRunResult(requester, replyType, "TALENT_APPLY", effectiveBotName, token, true, "OK", { "APPLIED:" + buildString });
+}
+
+void RunProfessionRecipeCraftTargetCommand(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken, std::string const& skillIdValue, std::string const& spellIdValue, std::string const& targetItemIdValue, std::string const& targetBagValue, std::string const& targetSlotValue, std::string const& targetModeValue)
+{
+ std::string const trimmedBotName = Trim(botName);
+ std::string const token = Trim(requestToken);
+ uint32 skillId = 0;
+ uint32 spellId = 0;
+ uint32 targetItemId = 0;
+ uint8 targetBag = 0;
+ uint8 targetSlot = 0;
+
+ Player* const bot = FindBotByName(requester, trimmedBotName);
+ std::string const effectiveBotName = bot ? bot->GetName() : trimmedBotName;
+
+ if (!bot)
+ {
+ SendRunResult(requester, replyType, "CRAFT_RECIPE_TARGET", effectiveBotName, token, false, "NO_BOT");
+ return;
+ }
+
+ PlayerbotAI* const botAI = GetBotAI(bot);
+ if (!botAI)
+ {
+ SendRunResult(requester, replyType, "CRAFT_RECIPE_TARGET", effectiveBotName, token, false, "NO_AI");
+ return;
+ }
+
+ if (!ParseUint32Field(skillIdValue, skillId) || !ParseUint32Field(spellIdValue, spellId) || !ParseUint32Field(targetItemIdValue, targetItemId) || !ParseUint8Field(targetBagValue, targetBag) || !ParseUint8Field(targetSlotValue, targetSlot))
+ {
+ SendRunResult(requester, replyType, "CRAFT_RECIPE_TARGET", effectiveBotName, token, false, "BAD_REQUEST");
+ return;
+ }
+
+ bool const useTradeTarget = SameName(UrlDecodeField(targetModeValue), "TRADE");
+ Item* targetItem = nullptr;
+ if (useTradeTarget)
+ {
+ TradeData* const trade = bot->GetTradeData();
+ if (!trade || !trade->GetTraderData())
+ {
+ SendRunResult(requester, replyType, "CRAFT_RECIPE_TARGET", effectiveBotName, token, false, "NO_TRADE");
+ return;
+ }
+
+ // EN: The caster enchants/enhances the trader's not-traded slot, matching the trade window workflow.
+ // FR: Le lanceur enchante/améliore l'emplacement non échangé du partenaire, comme dans la fenêtre d'échange.
+ targetItem = trade->GetTraderData()->GetItem(TRADE_SLOT_NONTRADED);
+ }
+ else if (SameName(UrlDecodeField(targetModeValue), "EQUIP"))
+ targetItem = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, targetSlot);
+ else
+ targetItem = bot->GetItemByPos(targetBag, targetSlot);
+
+ if (!targetItem)
+ {
+ SendRunResult(requester, replyType, "CRAFT_RECIPE_TARGET", effectiveBotName, token, false, useTradeTarget ? "MISSING_TRADE_TARGET" : "MISSING_TARGET_ITEM");
+ return;
+ }
+
+ if (targetItemId && targetItem->GetEntry() != targetItemId)
+ {
+ SendRunResult(requester, replyType, "CRAFT_RECIPE_TARGET", effectiveBotName, token, false, "TARGET_ITEM_MISMATCH");
+ return;
+ }
+
+ uint32 actualItemId = 0;
+ std::string validation = ValidateProfessionRecipeCraft(bot, skillId, spellId, 0, actualItemId);
+ if (validation == "BAD_RECIPE")
+ validation = "UNKNOWN_RECIPE";
+ if (validation != "OK")
+ {
+ SendRunResult(requester, replyType, "CRAFT_RECIPE_TARGET", effectiveBotName, token, false, validation);
+ return;
+ }
- if (requestedCount > 0 && moved >= requestedCount)
- break;
+ SpellInfo const* const spellInfo = sSpellMgr->GetSpellInfo(spellId);
+ if (!spellInfo || !(spellInfo->Targets & TARGET_FLAG_ITEM || spellInfo->Targets & TARGET_FLAG_GAMEOBJECT_ITEM))
+ {
+ SendRunResult(requester, replyType, "CRAFT_RECIPE_TARGET", effectiveBotName, token, false, "INVALID_TARGET_ITEM");
+ return;
}
- while (result->NextRow());
- if (!moved)
+ BridgeSpellCheckData const check = CheckBridgeSpellCast(bot, botAI, spellId, bot, targetItem);
+ if (!check.ok)
{
- if (!foundAny)
- reason = "ITEM_NOT_FOUND";
- else if (!foundWithdrawable)
- reason = "NO_GUILD_BANK_RIGHTS";
- else
- reason = "BAGS_FULL";
+ SendRunResult(requester, replyType, "CRAFT_RECIPE_TARGET", effectiveBotName, token, false, check.reason, { std::to_string(static_cast(check.result)) });
+ return;
}
- return moved;
+ SpellCastResult const result = CastBridgeSpellDirect(bot, spellInfo, bot, targetItem);
+ bool const ok = result == SPELL_CAST_OK;
+ SendRunResult(requester, replyType, "CRAFT_RECIPE_TARGET", effectiveBotName, token, ok, ok ? "OK" : GetBridgeSpellFailureReason(result), ok ? std::vector() : std::vector{ std::to_string(static_cast(result)) });
}
-uint32 BuyMatchingVendorItem(Player* bot, uint32 itemId, uint32 requestedCount, std::string& reason)
+void RunItemTradeCommand(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken, std::string const& itemIdValue, std::string const& encodedTargetName, std::string const& countValue, std::string const& bagValue, std::string const& slotValue)
{
- if (!bot || !itemId)
+ std::string const trimmedBotName = Trim(botName);
+ std::string const token = Trim(requestToken);
+ uint32 itemId = 0;
+ uint32 count = 0;
+ uint8 bag = 0;
+ uint8 slot = 0;
+ bool const hasPosition = ParseUint8Field(bagValue, bag) && ParseUint8Field(slotValue, slot);
+ bool const hasExplicitCount = ParseUint32Field(countValue, count) && count > 0;
+
+ Player* const bot = FindBotByName(requester, trimmedBotName);
+ std::string const effectiveBotName = bot ? bot->GetName() : trimmedBotName;
+
+ if (!bot)
{
- reason = "BAD_REQUEST";
- return 0;
+ SendRunResult(requester, replyType, "ITEM_TRADE", effectiveBotName, token, false, "NO_BOT", { "0" });
+ return;
}
- ItemTemplate const* const proto = sObjectMgr->GetItemTemplate(itemId);
- if (!proto)
+ if (!ParseUint32Field(itemIdValue, itemId) || !itemId)
{
- reason = "ITEM_NOT_FOUND";
- return 0;
+ SendRunResult(requester, replyType, "ITEM_TRADE", effectiveBotName, token, false, "BAD_REQUEST", { "0" });
+ return;
}
- uint32 vendorSlot = 0;
- uint32 vendorExtendedCost = 0;
- bool sawVendor = false;
- Creature* const vendor = FindNearbyVendorSellingItem(bot, itemId, vendorSlot, vendorExtendedCost, sawVendor);
- if (!vendor)
+ Player* const target = FindAllowedPlayerTarget(requester, encodedTargetName);
+ if (!target || target == bot)
{
- reason = sawVendor ? "VENDOR_DOES_NOT_SELL_ITEM" : "VENDOR_NOT_FOUND";
- return 0;
+ SendRunResult(requester, replyType, "ITEM_TRADE", effectiveBotName, token, false, "INVALID_TARGET", { "0" });
+ return;
}
- uint32 const desired = requestedCount > 0 ? requestedCount : 1;
- uint32 bought = 0;
- for (uint32 i = 0; i < desired; ++i)
+ if (!target->IsWithinDistInMap(bot, TRADE_DISTANCE, false))
{
- uint32 const price = uint32(std::floor(proto->BuyPrice * bot->GetReputationPriceDiscount(vendor)));
- if (price > 0 && bot->GetMoney() < price)
- {
- reason = "NOT_ENOUGH_MONEY";
- break;
- }
+ SendRunResult(requester, replyType, "ITEM_TRADE", effectiveBotName, token, false, "OUT_OF_RANGE", { "0" });
+ return;
+ }
- uint32 const oldCount = bot->GetItemCount(itemId, false);
- bot->BuyItemFromVendorSlot(vendor->GetGUID(), vendorSlot, itemId, 1, NULL_BAG, NULL_SLOT);
- uint32 const newCount = bot->GetItemCount(itemId, false);
- if (newCount <= oldCount)
+ std::string reason;
+ Item* source = nullptr;
+ bool oneStackMode = false;
+
+ if (hasPosition)
+ {
+ source = bot->GetItemByPos(bag, slot);
+ if (!source || source->GetEntry() != itemId)
{
- reason = vendorExtendedCost > 0 ? "VENDOR_REQUIRES_SPECIAL_CURRENCY" : "BUY_FAILED";
- break;
+ SendRunResult(requester, replyType, "ITEM_TRADE", effectiveBotName, token, false, "MISSING_ITEM", { "0" });
+ return;
}
+ oneStackMode = !hasExplicitCount && IsStackableItem(source);
+ }
+ else
+ {
+ ItemTemplate const* const proto = sObjectMgr->GetItemTemplate(itemId);
+ oneStackMode = !hasExplicitCount && proto && proto->GetMaxStackSize() > 1;
+ }
- bought += newCount - oldCount;
+ uint32 requestedCount = hasExplicitCount ? count : 1;
+ source = FindBestTradeSourceStack(bot, itemId, requestedCount, oneStackMode, bag, slot, hasPosition, reason);
+
+ std::vector- selectedStacks;
+ bool needsSplit = false;
+ if (source)
+ {
+ if (oneStackMode)
+ requestedCount = source->GetCount();
+ else if (source->GetCount() > requestedCount)
+ needsSplit = true;
+ selectedStacks.push_back(source);
}
+ else if (hasExplicitCount && !hasPosition)
+ selectedStacks = SelectExactTradeStacks(bot, itemId, requestedCount);
- return bought;
-}
+ if (selectedStacks.empty())
+ {
+ SendRunResult(requester, replyType, "ITEM_TRADE", effectiveBotName, token, false, reason.empty() ? "COUNT_UNAVAILABLE" : reason, { "0" });
+ return;
+ }
-void RunInventoryItemActionCommand(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken, std::string const& actionValue, std::string const& itemIdValue, std::string const& countValue)
-{
- std::string const trimmedBotName = Trim(botName);
- std::string const token = Trim(requestToken);
- std::string const action = ToUpper(Trim(actionValue));
- uint32 const itemId = static_cast
(std::strtoul(Trim(itemIdValue).c_str(), nullptr, 10));
- uint32 const requestedCount = static_cast(std::strtoul(Trim(countValue).c_str(), nullptr, 10));
+ if (!bot->GetTrader())
+ {
+ if (target->GetTrader() && target->GetTrader() != bot)
+ {
+ SendRunResult(requester, replyType, "ITEM_TRADE", effectiveBotName, token, false, "TRADE_BUSY", { "0" });
+ return;
+ }
- Player* const bot = FindBotByName(requester, trimmedBotName);
- std::string const effectiveBotName = bot ? bot->GetName() : trimmedBotName;
+ WorldPacket packet(CMSG_INITIATE_TRADE);
+ packet << target->GetGUID();
+ bot->GetSession()->HandleInitiateTradeOpcode(packet);
+ SendRunResult(requester, replyType, "ITEM_TRADE", effectiveBotName, token, true, "TRADE_STARTED", { "0" });
+ return;
+ }
- std::string reason;
- uint32 moved = 0;
- if (!bot)
- reason = "NO_BOT";
- else if (action == "BANK_DEPOSIT")
- moved = MoveMatchingBagItemsToBank(bot, itemId, requestedCount, reason);
- else if (action == "BANK_WITHDRAW")
- moved = MoveMatchingBankItemsToBags(bot, itemId, requestedCount, reason);
- else if (action == "GBANK_DEPOSIT")
- moved = MoveMatchingBagItemsToGuildBank(requester, bot, itemId, requestedCount, reason);
- else if (action == "GBANK_WITHDRAW")
- moved = MoveMatchingGuildBankItemsToBags(bot, itemId, requestedCount, reason);
- else if (action == "BUY_ITEM")
- moved = BuyMatchingVendorItem(bot, itemId, requestedCount, reason);
- else
- reason = "BAD_ACTION";
+ if (bot->GetTrader() != target)
+ {
+ SendRunResult(requester, replyType, "ITEM_TRADE", effectiveBotName, token, false, "TRADE_BUSY", { "0" });
+ return;
+ }
- bool const ok = moved > 0;
- if (ok)
- reason = "OK";
- else if (reason.empty())
- reason = "FAILED";
+ if (GetFreeTradeSlotCount(bot) < selectedStacks.size())
+ {
+ SendRunResult(requester, replyType, "ITEM_TRADE", effectiveBotName, token, false, "TRADE_SLOT_FULL", { "0" });
+ return;
+ }
- std::ostringstream payload;
- payload << UrlEncodeField(effectiveBotName)
- << kFieldSeparator << token
- << kFieldSeparator << action
- << kFieldSeparator << itemId
- << kFieldSeparator << (ok ? "OK" : "ERR")
- << kFieldSeparator << UrlEncodeField(reason)
- << kFieldSeparator << moved;
+ if (needsSplit)
+ {
+ Item* const splitItem = SplitStackForExactTrade(bot, selectedStacks.front(), requestedCount, reason);
+ if (!splitItem)
+ {
+ SendRunResult(requester, replyType, "ITEM_TRADE", effectiveBotName, token, false, reason.empty() ? "COUNT_UNAVAILABLE" : reason, { "0" });
+ return;
+ }
+ selectedStacks.clear();
+ selectedStacks.push_back(splitItem);
+ }
- SendAddonPacket(requester, replyType, "INVENTORY_ITEM_ACTION", payload.str());
-}
+ for (Item* const tradeItem : selectedStacks)
+ {
+ if (!tradeItem || !tradeItem->CanBeTraded(false, true))
+ {
+ SendRunResult(requester, replyType, "ITEM_TRADE", effectiveBotName, token, false, "ITEM_NOT_TRADABLE", { "0" });
+ return;
+ }
+ }
-void RunProfessionRecipeCraftCommand(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken, std::string const& skillIdValue, std::string const& spellIdValue, std::string const& itemIdValue)
-{
- std::string const trimmedBotName = Trim(botName);
- std::string const token = Trim(requestToken);
- uint32 const skillId = static_cast(std::strtoul(Trim(skillIdValue).c_str(), nullptr, 10));
- uint32 const spellId = static_cast(std::strtoul(Trim(spellIdValue).c_str(), nullptr, 10));
- uint32 const expectedItemId = static_cast(std::strtoul(Trim(itemIdValue).c_str(), nullptr, 10));
+ uint32 moved = 0;
+ for (Item* const tradeItem : selectedStacks)
+ {
+ if (!tradeItem)
+ continue;
- Player* const bot = FindBotByName(requester, trimmedBotName);
- std::string const effectiveBotName = bot ? bot->GetName() : trimmedBotName;
+ if (!oneStackMode && hasExplicitCount && moved + tradeItem->GetCount() > requestedCount)
+ {
+ SendRunResult(requester, replyType, "ITEM_TRADE", effectiveBotName, token, false, "COUNT_UNAVAILABLE", { "0" });
+ return;
+ }
- uint32 actualItemId = expectedItemId;
- std::string result = ValidateProfessionRecipeCraft(bot, skillId, spellId, expectedItemId, actualItemId);
- if (result == "OK")
- result = CastProfessionRecipe(bot, spellId);
+ if (!AddItemToTradeSlot(bot, tradeItem, reason))
+ {
+ SendRunResult(requester, replyType, "ITEM_TRADE", effectiveBotName, token, false, reason, { std::to_string(moved) });
+ return;
+ }
- std::ostringstream payload;
- payload << UrlEncodeField(effectiveBotName)
- << kFieldSeparator << token
- << kFieldSeparator << skillId
- << kFieldSeparator << spellId
- << kFieldSeparator << actualItemId
- << kFieldSeparator << (result == "OK" ? "OK" : "ERR")
- << kFieldSeparator << UrlEncodeField(result);
+ moved += tradeItem->GetCount();
+ }
- SendAddonPacket(requester, replyType, "PROFESSION_RECIPE_CRAFT", payload.str());
+ if (!oneStackMode && hasExplicitCount && moved != requestedCount)
+ {
+ SendRunResult(requester, replyType, "ITEM_TRADE", effectiveBotName, token, false, "COUNT_UNAVAILABLE", { std::to_string(moved) });
+ return;
+ }
+
+ SendRunResult(requester, replyType, "ITEM_TRADE", effectiveBotName, token, moved > 0, moved > 0 ? "OK" : "TRADE_FAILED", { std::to_string(moved) });
}
void RunOutfitCommand(Player* requester, ChatMsg replyType, std::string const& botName, std::string const& requestToken, std::string const& encodedSuffix, std::string const& persistToken)
@@ -4112,6 +5617,88 @@ void SendStatsPackets(Player* player, ChatMsg replyType)
}
}
+void SendInventoryBulkPackets(Player* requester, ChatMsg replyType, std::string const& requestToken)
+{
+ std::string const token = Trim(requestToken);
+ SendAddonPacket(requester, replyType, "INV_BULK_BEGIN", token);
+
+ for (Player* const bot : GetBridgeVisibleBots(requester))
+ {
+ // Send inventory bag entries
+ SendBagEntryPackets(requester, replyType, bot, token);
+
+ // Send inventory item locations
+ PlayerbotAI* const botAI = sPlayerbotsMgr.GetPlayerbotAI(bot);
+ if (botAI)
+ {
+ std::vector- const items = botAI->GetInventoryItems();
+ for (Item* const item : items)
+ {
+ if (!item)
+ continue;
+
+ ItemTemplate const* const proto = item->GetTemplate();
+ if (!proto)
+ continue;
+
+ std::string locationLine = ChatHelper::FormatItem(proto, item->GetCount());
+ if (item->IsSoulBound())
+ locationLine += " (soulbound)";
+
+ std::ostringstream locationPayload;
+ locationPayload << UrlEncodeField(bot->GetName())
+ << kFieldSeparator << token
+ << kFieldSeparator << static_cast
(item->GetBagSlot())
+ << kFieldSeparator << static_cast(item->GetSlot())
+ << kFieldSeparator << proto->ItemId
+ << kFieldSeparator << item->GetCount()
+ << kFieldSeparator << UrlEncodeField(locationLine)
+ << kFieldSeparator << (item->IsSoulBound() ? 1 : 0);
+ SendAddonPacket(requester, replyType, "INV_ITEM_LOC", locationPayload.str());
+ }
+ }
+
+ // Send inventory summary
+ InventorySummaryData const summary = BuildInventorySummary(bot);
+ std::ostringstream payload;
+ payload << UrlEncodeField(bot->GetName())
+ << kFieldSeparator << token
+ << kFieldSeparator << summary.gold
+ << kFieldSeparator << summary.silver
+ << kFieldSeparator << summary.copper
+ << kFieldSeparator << summary.bagUsed
+ << kFieldSeparator << summary.bagTotal;
+ SendAddonPacket(requester, replyType, "INV_BULK_ITEM", payload.str());
+ }
+
+ SendAddonPacket(requester, replyType, "INV_BULK_END", token);
+}
+
+void SendBotSkillsBulkPackets(Player* requester, ChatMsg replyType, std::string const& requestToken)
+{
+ std::string const token = Trim(requestToken);
+ SendAddonPacket(requester, replyType, "BOT_SKILLS_BULK_BEGIN", token);
+
+ for (Player* const bot : GetBridgeVisibleBots(requester))
+ {
+ for (BotSkillEntryData const& entry : BuildBotSkillEntries(bot))
+ {
+ std::ostringstream payload;
+ payload << UrlEncodeField(bot->GetName())
+ << kFieldSeparator << token
+ << kFieldSeparator << entry.skillId
+ << kFieldSeparator << UrlEncodeField(entry.category)
+ << kFieldSeparator << UrlEncodeField(entry.key)
+ << kFieldSeparator << UrlEncodeField(entry.name)
+ << kFieldSeparator << entry.value
+ << kFieldSeparator << entry.maxValue;
+ SendAddonPacket(requester, replyType, "BOT_SKILLS_BULK_ITEM", payload.str());
+ }
+ }
+
+ SendAddonPacket(requester, replyType, "BOT_SKILLS_BULK_END", token);
+}
+
bool HandleBridgeOpcode(Player* player, ChatMsg replyType, std::string const& opcode, std::string const& payload)
{
std::string const normalized = ToUpper(Trim(opcode));
@@ -4238,6 +5825,12 @@ bool HandleBridgeOpcode(Player* player, ChatMsg replyType, std::string const& op
return true;
}
+ if (requestType == "INVENTORY_BULK")
+ {
+ SendInventoryBulkPackets(player, replyType, Trim(request.second));
+ return true;
+ }
+
if (requestType == "BANK")
{
std::pair const bankRequest = SplitOnce(request.second, kFieldSeparator);
@@ -4266,6 +5859,12 @@ bool HandleBridgeOpcode(Player* player, ChatMsg replyType, std::string const& op
return true;
}
+ if (requestType == "BOT_SKILLS_BULK")
+ {
+ SendBotSkillsBulkPackets(player, replyType, Trim(request.second));
+ return true;
+ }
+
if (requestType == "BOT_REPUTATIONS")
{
std::pair const reputationRequest = SplitOnce(request.second, kFieldSeparator);
@@ -4348,6 +5947,77 @@ bool HandleBridgeOpcode(Player* player, ChatMsg replyType, std::string const& op
return true;
}
+ if (requestType == "QUEST_ABANDON")
+ {
+ std::pair const botRequest = SplitOnce(request.second, kFieldSeparator);
+ std::pair const tokenRequest = SplitOnce(botRequest.second, kFieldSeparator);
+ RunQuestAbandonCommand(player, replyType, botRequest.first, tokenRequest.first, tokenRequest.second);
+ return true;
+ }
+
+ if (requestType == "QUEST_SHARE")
+ {
+ std::pair const botRequest = SplitOnce(request.second, kFieldSeparator);
+ std::pair const tokenRequest = SplitOnce(botRequest.second, kFieldSeparator);
+ std::pair const questRequest = SplitOnce(tokenRequest.second, kFieldSeparator);
+ RunQuestShareCommand(player, replyType, botRequest.first, tokenRequest.first, questRequest.first, questRequest.second);
+ return true;
+ }
+
+ if (requestType == "ITEM_EQUIP")
+ {
+ std::pair const botRequest = SplitOnce(request.second, kFieldSeparator);
+ std::pair const tokenRequest = SplitOnce(botRequest.second, kFieldSeparator);
+ std::pair const itemRequest = SplitOnce(tokenRequest.second, kFieldSeparator);
+ std::pair const slotHintRequest = SplitOnce(itemRequest.second, kFieldSeparator);
+ std::pair const bagRequest = SplitOnce(slotHintRequest.second, kFieldSeparator);
+ RunItemEquipCommand(player, replyType, botRequest.first, tokenRequest.first, itemRequest.first, slotHintRequest.first, bagRequest.first, bagRequest.second);
+ return true;
+ }
+
+ if (requestType == "ITEM_TRADE")
+ {
+ std::pair const botRequest = SplitOnce(request.second, kFieldSeparator);
+ std::pair const tokenRequest = SplitOnce(botRequest.second, kFieldSeparator);
+ std::pair const itemRequest = SplitOnce(tokenRequest.second, kFieldSeparator);
+ std::pair const targetRequest = SplitOnce(itemRequest.second, kFieldSeparator);
+ std::pair const countRequest = SplitOnce(targetRequest.second, kFieldSeparator);
+ std::pair const bagRequest = SplitOnce(countRequest.second, kFieldSeparator);
+ RunItemTradeCommand(player, replyType, botRequest.first, tokenRequest.first, itemRequest.first, targetRequest.first, countRequest.first, bagRequest.first, bagRequest.second);
+ return true;
+ }
+
+ if (requestType == "CAST_SPELL")
+ {
+ std::pair const botRequest = SplitOnce(request.second, kFieldSeparator);
+ std::pair const tokenRequest = SplitOnce(botRequest.second, kFieldSeparator);
+ std::pair const spellRequest = SplitOnce(tokenRequest.second, kFieldSeparator);
+ RunSpellCastCommand(player, replyType, botRequest.first, tokenRequest.first, spellRequest.first, spellRequest.second);
+ return true;
+ }
+
+ if (requestType == "TALENT_APPLY")
+ {
+ std::pair const botRequest = SplitOnce(request.second, kFieldSeparator);
+ std::pair const tokenRequest = SplitOnce(botRequest.second, kFieldSeparator);
+ std::pair const buildRequest = SplitOnce(tokenRequest.second, kFieldSeparator);
+ RunTalentApplyCommand(player, replyType, botRequest.first, tokenRequest.first, buildRequest.first, buildRequest.second);
+ return true;
+ }
+
+ if (requestType == "CRAFT_RECIPE_TARGET")
+ {
+ std::pair const botRequest = SplitOnce(request.second, kFieldSeparator);
+ std::pair const tokenRequest = SplitOnce(botRequest.second, kFieldSeparator);
+ std::pair const skillRequest = SplitOnce(tokenRequest.second, kFieldSeparator);
+ std::pair const spellRequest = SplitOnce(skillRequest.second, kFieldSeparator);
+ std::pair const itemRequest = SplitOnce(spellRequest.second, kFieldSeparator);
+ std::pair const bagRequest = SplitOnce(itemRequest.second, kFieldSeparator);
+ std::pair const slotRequest = SplitOnce(bagRequest.second, kFieldSeparator);
+ RunProfessionRecipeCraftTargetCommand(player, replyType, botRequest.first, tokenRequest.first, skillRequest.first, spellRequest.first, itemRequest.first, bagRequest.first, slotRequest.first, slotRequest.second);
+ return true;
+ }
+
if (requestType == "COMBAT")
{
std::pair const scopeSplit = SplitOnce(request.second, kFieldSeparator);