mirror of
https://github.com/liyunfan1223/mod-playerbots.git
synced 2026-06-20 15:39:25 +02:00
## Pull Request Description
This change fixes two cases of broken bot text with respect to inventory
items.
1. Reserved inventory qualifiers such as "mount," "food," "drink," etc.
no longer also trigger generic item name matching. I first noticed this
problem when my resto Shaman who had the "Mounting Vengeance" weapon in
her inventory would repeatedly give error messages of failing to use it
while mounting (because mounting also causes bots to use items that fit
the reserved "mount," which due to this bug, also caused bots to try to
use any item with "mount" in its name).
2. Custom cast output text no longer reports an inferred bag item as the
spell target for normal unit-targeted casts such as "cast chain heal on
Keleborn." There was a bug where the action would first parse the actual
target and then parse the spell text and then try to match the last word
of the string to a bag item (so the bot would say it was casting chain
heal on a healing potion, even though the heal was in fact cast
correctly on a player).
## Feature Evaluation
- Describe the **minimum logic** required to achieve the intended
behavior.
- Add a reserved-qualifier check in InventoryAction::parseItems() so
reserved selectors do not also run through FindNamedItemVisitor.
- In custom cast output text, choose the displayed target based on the
actual target type already resolved for the cast.
- It does not change mount selection behavior itself.
- It does not add new spell-target parsing rules.
- Describe the **processing cost** when this logic executes across many
bots.
- None.
## How to Test the Changes
Reserved qualifiers:
1. Give a bot in your party the "Mounting Vengeance" weapon.
2. Mount up, and the bot should mount too without saying anything
(before the fix, the bot would say it is using the weapon and that the
item was not found).
Spell cast text:
1. Give a bot an inventory item whose name overlaps with part of a spell
name, such as a healing potion.
2. Command a bot to cast some heal on a player.
3. The bot should cast the spell on the intended player (as was the case
previously), and the status text names the player instead of the
inventory item.
## Impact Assessment
- Does this change increase per-bot/per-tick processing or risk scaling
poorly with thousands of bots?
- [x] No, not at all
- [ ] Minimal impact (**explain below**)
- Does this change modify default bot behavior?
- [x] No
- [ ] Yes (**explain why**)
- Does this change add new decision branches or increase maintenance
complexity?
- [ ] No
- [x] Yes (**explain below**)
Very minor changes. InventoryAction gets one explicit reserved-qualifier
guard. Custom cast text selection becomes more explicit about which
target type should be displayed.
## AI Assistance
Was AI assistance used while working on this change?
- [ ] No
- [x] Yes (**explain below**)
GPT-5.4 was used to trace the relevant code paths for the errors and
propose the changes.
## Final Checklist
- [x] Stability is not compromised.
- [x] Performance impact is understood, tested, and acceptable.
- [x] Added logic complexity is justified and explained.
- [x] Any new bot dialogue lines are translated.
- [x] Documentation updated if needed (Conf comments, WiKi commands).
## Notes for Reviewers
---------
Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com>
Co-authored-by: bash <hermensb@gmail.com>
Co-authored-by: Revision <tkn963@gmail.com>
Co-authored-by: kadeshar <kadeshar@gmail.com>
384 lines
12 KiB
C++
384 lines
12 KiB
C++
/*
|
|
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
|
|
* and/or modify it under version 3 of the License, or (at your option), any later version.
|
|
*/
|
|
|
|
#include "CastCustomSpellAction.h"
|
|
|
|
#include "ChatHelper.h"
|
|
#include "Event.h"
|
|
#include "ItemUsageValue.h"
|
|
#include "Playerbots.h"
|
|
#include "ServerFacade.h"
|
|
|
|
size_t FindLastSeparator(std::string const text, std::string const sep)
|
|
{
|
|
size_t pos = text.rfind(sep);
|
|
if (pos == std::string::npos)
|
|
return pos;
|
|
|
|
size_t lastLinkBegin = text.rfind("|H");
|
|
size_t lastLinkEnd = text.find("|h|r", lastLinkBegin + 1);
|
|
if (pos >= lastLinkBegin && pos <= lastLinkEnd)
|
|
pos = text.find_last_of(sep, lastLinkBegin);
|
|
|
|
return pos;
|
|
}
|
|
|
|
static inline void ltrim(std::string& s)
|
|
{
|
|
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { return !std::isspace(ch); }));
|
|
}
|
|
|
|
bool CastCustomSpellAction::Execute(Event event)
|
|
{
|
|
// only allow proper vehicle seats
|
|
if (botAI->IsInVehicle() && !botAI->IsInVehicle(false, false, true))
|
|
return false;
|
|
|
|
Player* master = GetMaster();
|
|
Unit* target = nullptr;
|
|
std::string text = event.getParam();
|
|
|
|
GuidVector gos = chat->parseGameobjects(text);
|
|
if (!gos.empty())
|
|
{
|
|
for (auto go : gos)
|
|
{
|
|
if (!target)
|
|
target = botAI->GetUnit(go);
|
|
|
|
if (!botAI->GetUnit(go) || !botAI->GetUnit(go)->IsInWorld())
|
|
continue;
|
|
|
|
chat->eraseAllSubStr(text, chat->FormatWorldobject(botAI->GetUnit(go)));
|
|
}
|
|
|
|
ltrim(text);
|
|
}
|
|
|
|
uint32 spell = 0;
|
|
if (!target)
|
|
{
|
|
size_t onPos = FindLastSeparator(text, " on ");
|
|
if (onPos != std::string::npos)
|
|
{
|
|
std::string targetName = text.substr(onPos + 4);
|
|
ltrim(targetName);
|
|
if (!targetName.empty())
|
|
{
|
|
// check if spell still exists after we remove " on PlayerName" part
|
|
std::string truncatedText = text.substr(0, onPos);
|
|
ltrim(truncatedText);
|
|
spell = AI_VALUE2(uint32, "spell id", truncatedText);
|
|
|
|
if (spell)
|
|
{
|
|
if (Player* targetPlayer = ObjectAccessor::FindPlayerByName(targetName))
|
|
{
|
|
target = targetPlayer;
|
|
text = truncatedText;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!target)
|
|
if (master && master->GetTarget())
|
|
target = botAI->GetUnit(master->GetTarget());
|
|
|
|
if (!target)
|
|
target = bot;
|
|
|
|
if (!master) // Use self as master for permissions.
|
|
master = bot;
|
|
|
|
Item* itemTarget = nullptr;
|
|
|
|
size_t pos = FindLastSeparator(text, " ");
|
|
uint32 castCount = 1;
|
|
if (pos != std::string::npos)
|
|
{
|
|
std::string const param = text.substr(pos + 1);
|
|
std::vector<Item*> items = InventoryAction::parseItems(param, ITERATE_ITEMS_IN_BAGS);
|
|
if (!items.empty())
|
|
itemTarget = *items.begin();
|
|
else
|
|
{
|
|
if (atoi(param.c_str()) > 0)
|
|
text = text.substr(0, pos);
|
|
}
|
|
}
|
|
|
|
if (!spell)
|
|
spell = AI_VALUE2(uint32, "spell id", text);
|
|
|
|
std::ostringstream msg;
|
|
if (!spell)
|
|
{
|
|
msg << "Unknown spell " << text;
|
|
botAI->TellError(msg.str());
|
|
return false;
|
|
}
|
|
|
|
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spell);
|
|
if (!spellInfo)
|
|
{
|
|
msg << "Unknown spell " << text;
|
|
botAI->TellError(msg.str());
|
|
return false;
|
|
}
|
|
|
|
if (target != bot && !bot->HasInArc(CAST_ANGLE_IN_FRONT, target, sPlayerbotAIConfig.sightDistance))
|
|
{
|
|
ServerFacade::instance().SetFacingTo(bot, target);
|
|
botAI->SetNextCheckDelay(sPlayerbotAIConfig.reactDelay);
|
|
|
|
msg << "cast " << text;
|
|
botAI->HandleCommand(CHAT_MSG_WHISPER, msg.str(), master);
|
|
return true;
|
|
}
|
|
|
|
std::ostringstream spellName;
|
|
spellName << ChatHelper::FormatSpell(spellInfo) << " on ";
|
|
|
|
bool const hasItemTarget = itemTarget &&
|
|
(spellInfo->Targets & TARGET_FLAG_ITEM || spellInfo->Targets & TARGET_FLAG_GAMEOBJECT_ITEM);
|
|
|
|
if (bot->GetTrader())
|
|
spellName << "trade item";
|
|
else if (hasItemTarget)
|
|
spellName << chat->FormatItem(itemTarget->GetTemplate());
|
|
else if (target != bot)
|
|
spellName << target->GetName();
|
|
else
|
|
spellName << "self";
|
|
|
|
if (!bot->GetTrader() && !botAI->CanCastSpell(spell, target, true, itemTarget))
|
|
{
|
|
msg << "Cannot cast " << spellName.str();
|
|
botAI->TellError(msg.str());
|
|
return false;
|
|
}
|
|
|
|
bool result = spell ? botAI->CastSpell(spell, target, itemTarget) : botAI->CastSpell(text, target, itemTarget);
|
|
if (result)
|
|
{
|
|
msg << "Casting " << spellName.str();
|
|
|
|
if (castCount > 1)
|
|
{
|
|
std::ostringstream cmd;
|
|
cmd << castString(target) << " " << text << " " << (castCount - 1);
|
|
botAI->HandleCommand(CHAT_MSG_WHISPER, cmd.str(), master);
|
|
msg << "|cffffff00(x" << (castCount - 1) << " left)|r";
|
|
}
|
|
|
|
botAI->TellMasterNoFacing(msg.str());
|
|
}
|
|
else
|
|
{
|
|
msg << "Cast " << spellName.str() << " is failed";
|
|
botAI->TellError(msg.str());
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
bool CastCustomNcSpellAction::isUseful() { return !bot->IsInCombat(); }
|
|
|
|
std::string const CastCustomNcSpellAction::castString(WorldObject* target)
|
|
{
|
|
return "castnc " + chat->FormatWorldobject(target);
|
|
}
|
|
|
|
bool CastRandomSpellAction::AcceptSpell(SpellInfo const* spellInfo)
|
|
{
|
|
bool isTradeSkill = spellInfo->Effects[EFFECT_0].Effect == SPELL_EFFECT_CREATE_ITEM &&
|
|
spellInfo->ReagentCount[EFFECT_0] > 0 && spellInfo->SchoolMask == 1;
|
|
return !isTradeSkill && spellInfo->GetRecoveryTime() < MINUTE * IN_MILLISECONDS;
|
|
}
|
|
|
|
bool CastRandomSpellAction::Execute(Event event)
|
|
{
|
|
std::vector<std::pair<uint32, std::string>> spellMap = GetSpellList();
|
|
Player* master = GetMaster();
|
|
|
|
Unit* target = nullptr;
|
|
GameObject* got = nullptr;
|
|
|
|
std::string name = event.getParam();
|
|
if (name.empty())
|
|
name = getName();
|
|
|
|
GuidVector wos = chat->parseGameobjects(name);
|
|
for (auto wo : wos)
|
|
{
|
|
target = botAI->GetUnit(wo);
|
|
got = botAI->GetGameObject(wo);
|
|
|
|
if (got || target)
|
|
break;
|
|
}
|
|
|
|
if (!got && !target && bot->GetTarget())
|
|
{
|
|
target = botAI->GetUnit(bot->GetTarget());
|
|
got = botAI->GetGameObject(bot->GetTarget());
|
|
}
|
|
|
|
if (!got && !target)
|
|
if (master && master->GetTarget())
|
|
target = botAI->GetUnit(master->GetTarget());
|
|
|
|
if (!got && !target)
|
|
target = bot;
|
|
|
|
std::vector<std::pair<uint32, std::pair<uint32, WorldObject*>>> spellList;
|
|
|
|
for (auto& spell : spellMap)
|
|
{
|
|
uint32 spellId = spell.first;
|
|
|
|
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId);
|
|
if (!spellInfo)
|
|
continue;
|
|
|
|
if (!AcceptSpell(spellInfo))
|
|
continue;
|
|
|
|
if (bot->HasSpell(spellId))
|
|
{
|
|
uint32 spellPriority = GetSpellPriority(spellInfo);
|
|
|
|
if (target && botAI->CanCastSpell(spellId, target, true))
|
|
spellList.push_back(std::make_pair(spellId, std::make_pair(spellPriority, target)));
|
|
if (got &&
|
|
botAI->CanCastSpell(spellId, got->GetPositionX(), got->GetPositionY(), got->GetPositionZ(), true))
|
|
spellList.push_back(std::make_pair(spellId, std::make_pair(spellPriority, got)));
|
|
if (botAI->CanCastSpell(spellId, bot, true))
|
|
spellList.push_back(std::make_pair(spellId, std::make_pair(spellPriority, bot)));
|
|
}
|
|
}
|
|
|
|
if (spellList.empty())
|
|
return false;
|
|
|
|
// bool isCast = false; //not used, line marked for removal.
|
|
|
|
std::sort(spellList.begin(), spellList.end(),
|
|
[](std::pair<uint32, std::pair<uint32, WorldObject*>> i,
|
|
std::pair<uint32, std::pair<uint32, WorldObject*>> j) { return i.first > j.first; });
|
|
|
|
uint32 rndBound = spellList.size() / 4;
|
|
|
|
rndBound = std::min(rndBound, (uint32)10);
|
|
rndBound = std::max(rndBound, (uint32)0);
|
|
|
|
for (uint32 i = 0; i < 5; i++)
|
|
{
|
|
uint32 rnd = urand(0, rndBound);
|
|
|
|
auto spell = spellList[rnd];
|
|
|
|
uint32 spellId = spell.first;
|
|
WorldObject* wo = spell.second.second;
|
|
|
|
bool isCast = castSpell(spellId, wo);
|
|
|
|
if (isCast)
|
|
{
|
|
if (MultiCast && ((wo && bot->HasInArc(CAST_ANGLE_IN_FRONT, wo, sPlayerbotAIConfig.sightDistance))))
|
|
{
|
|
std::ostringstream cmd;
|
|
cmd << "castnc " << chat->FormatWorldobject(wo) + " " << spellId << " " << 19;
|
|
botAI->HandleCommand(CHAT_MSG_WHISPER, cmd.str(), bot);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool CraftRandomItemAction::AcceptSpell(SpellInfo const* spellInfo)
|
|
{
|
|
return spellInfo->Effects[EFFECT_0].Effect == SPELL_EFFECT_CREATE_ITEM && spellInfo->ReagentCount[EFFECT_0] > 0 &&
|
|
spellInfo->SchoolMask == 0;
|
|
}
|
|
|
|
uint32 CraftRandomItemAction::GetSpellPriority(SpellInfo const* spellInfo)
|
|
{
|
|
if (spellInfo->Effects[EFFECT_0].Effect != SPELL_EFFECT_CREATE_ITEM)
|
|
{
|
|
uint32 newItemId = spellInfo->Effects[EFFECT_0].ItemType;
|
|
if (newItemId)
|
|
{
|
|
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", newItemId);
|
|
|
|
if (usage == ITEM_USAGE_REPLACE || usage == ITEM_USAGE_EQUIP || usage == ITEM_USAGE_AMMO ||
|
|
usage == ITEM_USAGE_QUEST || usage == ITEM_USAGE_SKILL || usage == ITEM_USAGE_USE)
|
|
return 10;
|
|
}
|
|
|
|
if (ItemUsageValue::SpellGivesSkillUp(spellInfo->Id, bot))
|
|
return 8;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
bool CastRandomSpellAction::castSpell(uint32 spellId, WorldObject* wo)
|
|
{
|
|
if (wo->GetGUID().IsUnit())
|
|
return botAI->CastSpell(spellId, (Unit*)(wo));
|
|
else
|
|
return botAI->CastSpell(spellId, wo->GetPositionX(), wo->GetPositionY(), wo->GetPositionZ());
|
|
}
|
|
|
|
bool DisEnchantRandomItemAction::Execute(Event /*event*/)
|
|
{
|
|
std::vector<Item*> items =
|
|
AI_VALUE2(std::vector<Item*>, "inventory items", "usage " + std::to_string(ITEM_USAGE_DISENCHANT));
|
|
std::reverse(items.begin(), items.end());
|
|
|
|
for (auto& item : items)
|
|
{
|
|
// don't touch rare+ items if with real player/guild
|
|
if ((botAI->HasRealPlayerMaster() || botAI->IsInRealGuild()) &&
|
|
item->GetTemplate()->Quality > ITEM_QUALITY_UNCOMMON)
|
|
return false;
|
|
|
|
if (CastCustomSpellAction::Execute(
|
|
Event("disenchant random item", "13262 " + chat->FormatQItem(item->GetEntry()))))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool DisEnchantRandomItemAction::isUseful()
|
|
{
|
|
return botAI->HasSkill(SKILL_ENCHANTING) && !bot->IsInCombat() &&
|
|
AI_VALUE2(uint32, "item count", "usage " + std::to_string(ITEM_USAGE_DISENCHANT)) > 0;
|
|
}
|
|
|
|
bool EnchantRandomItemAction::isUseful() { return botAI->HasSkill(SKILL_ENCHANTING) && !bot->IsInCombat(); }
|
|
|
|
bool EnchantRandomItemAction::AcceptSpell(SpellInfo const* spellInfo)
|
|
{
|
|
return spellInfo->Effects[EFFECT_0].Effect == SPELL_EFFECT_ENCHANT_ITEM && spellInfo->ReagentCount[EFFECT_0] > 0;
|
|
}
|
|
|
|
uint32 EnchantRandomItemAction::GetSpellPriority(SpellInfo const* spellInfo)
|
|
{
|
|
if (spellInfo->Effects[EFFECT_0].Effect == SPELL_EFFECT_ENCHANT_ITEM)
|
|
{
|
|
if (AI_VALUE2(Item*, "item for spell", spellInfo->Id) && ItemUsageValue::SpellGivesSkillUp(spellInfo->Id, bot))
|
|
return 10;
|
|
}
|
|
|
|
return 1;
|
|
}
|