/* * Copyright (C) 2016+ AzerothCore , 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 "ListSpellsAction.h" #include "Event.h" #include "Playerbots.h" #include "PlayerbotSpellCache.h" using SpellListEntry = std::pair; // CHANGE: Simplified and cheap comparator used in MapUpdater worker thread. // It now avoids scanning the entire SkillLineAbilityStore for each comparison // and only relies on spell school and spell name to keep sorting fast and bounded. // lhs = the left element, rhs = the right element. static bool CompareSpells(SpellListEntry const& lhSpell, SpellListEntry const& rhSpell) { SpellInfo const* lhSpellInfo = sSpellMgr->GetSpellInfo(lhSpell.first); SpellInfo const* rhSpellInfo = sSpellMgr->GetSpellInfo(rhSpell.first); if (!lhSpellInfo || !rhSpellInfo) { LOG_ERROR("playerbots", "SpellInfo missing for spell {} or {}", lhSpell.first, rhSpell.first); // Fallback: order by spell id to keep comparator strict and deterministic. return lhSpell.first < rhSpell.first; } uint32 lhsKey = lhSpellInfo->SchoolMask; uint32 rhsKey = rhSpellInfo->SchoolMask; if (lhsKey == rhsKey) { // Defensive check: if DBC data is broken and spell names are nullptr, // fall back to id ordering instead of risking a crash in std::strcmp. if (!lhSpellInfo->SpellName[0] || !rhSpellInfo->SpellName[0]) return lhSpell.first < rhSpell.first; return std::strcmp(lhSpellInfo->SpellName[0], rhSpellInfo->SpellName[0]) > 0; } return lhsKey > rhsKey; } std::vector> ListSpellsAction::GetSpellList(std::string filter) { uint32 skill = 0; std::vector ss = split(filter, ' '); if (!ss.empty()) { skill = chat->parseSkill(ss[0]); if (skill != SKILL_NONE) { filter = ss.size() > 1 ? ss[1] : ""; } // Guard access to ss[1]/ss[2] to avoid out-of-bounds // when the player only types "first" without "aid". if (ss[0] == "first" && ss.size() > 1 && ss[1] == "aid") { skill = SKILL_FIRST_AID; filter = ss.size() > 2 ? ss[2] : ""; } } std::string const ignoreList = ",Opening,Closing,Stuck,Remove Insignia,Opening - No Text,Grovel,Duel,Honorless Target,"; std::string alreadySeenList = ","; uint32 minLevel = 0; uint32 maxLevel = 0; if (filter.find('-') != std::string::npos) { std::vector ff = split(filter, '-'); if (ff.size() >= 2) { minLevel = std::atoi(ff[0].c_str()); maxLevel = std::atoi(ff[1].c_str()); if (minLevel > maxLevel) std::swap(minLevel, maxLevel); } filter.clear(); } bool canCraftNow = false; if (filter.find('+') != std::string::npos) { canCraftNow = true; // Support "+" syntax (e.g. "spells +tailoring" or "spells tailoring+"). // If no explicit skill was detected yet, try to parse the filter (without '+') // as a profession/skill name so that craftable-only filters still work with skills. if (skill == SKILL_NONE) { std::string skillFilter = filter; // Remove '+' before trying to interpret the first token as a skill name. skillFilter.erase(remove(skillFilter.begin(), skillFilter.end(), '+'), skillFilter.end()); std::vector skillTokens = split(skillFilter, ' '); if (!skillTokens.empty()) { uint32 parsedSkill = chat->parseSkill(skillTokens[0]); if (parsedSkill != SKILL_NONE) { skill = parsedSkill; // Any remaining text after the skill token becomes the "name" filter // (e.g. "spells +tailoring cloth" -> skill = tailoring, filter = "cloth"). filter = skillTokens.size() > 1 ? skillTokens[1] : ""; } } } // Finally remove '+' from the filter that will be used for name/range parsing. filter.erase(remove(filter.begin(), filter.end(), '+'), filter.end()); } uint32 slot = chat->parseSlot(filter); if (slot != EQUIPMENT_SLOT_END) filter.clear(); std::vector spells; for (PlayerSpellMap::iterator itr = bot->GetSpellMap().begin(); itr != bot->GetSpellMap().end(); ++itr) { if (itr->second->State == PLAYERSPELL_REMOVED || !itr->second->Active) continue; if (!(itr->second->specMask & bot->GetActiveSpecMask())) continue; SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(itr->first); if (!spellInfo) continue; if (spellInfo->IsPassive()) continue; SkillLineAbilityEntry const* skillLine = sPlayerbotSpellCache->GetSkillLine(itr->first); if (skill != SKILL_NONE && (!skillLine || skillLine->SkillLine != skill)) continue; std::string const comp = spellInfo->SpellName[0]; if (!(ignoreList.find(comp) == std::string::npos && alreadySeenList.find(comp) == std::string::npos)) continue; if (!filter.empty() && !strstri(spellInfo->SpellName[0], filter.c_str())) continue; bool first = true; int32 craftsPossible = -1; std::ostringstream materials; for (uint32 x = 0; x < MAX_SPELL_REAGENTS; ++x) { if (spellInfo->Reagent[x] <= 0) { continue; } uint32 itemid = spellInfo->Reagent[x]; uint32 reagentsRequired = spellInfo->ReagentCount[x]; if (itemid) { if (ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemid)) { if (first) { materials << ": "; first = false; } else materials << ", "; materials << chat->FormatItem(proto, reagentsRequired); FindItemByIdVisitor visitor(itemid); uint32 reagentsInInventory = InventoryAction::GetItemCount(&visitor); bool buyable = sPlayerbotSpellCache->IsItemBuyable(itemid); if (!buyable) { uint32 craftable = reagentsInInventory / reagentsRequired; if (craftsPossible < 0 || craftsPossible > static_cast(craftable)) craftsPossible = static_cast(craftable); } if (reagentsInInventory) materials << "|cffffff00(x" << reagentsInInventory << ")|r "; else if (buyable) materials << "|cffffff00(buy)|r "; } } } if (craftsPossible < 0) craftsPossible = 0; std::ostringstream out; bool filtered = false; if (skillLine) { for (uint8 i = 0; i < 3; ++i) { if (spellInfo->Effects[i].Effect == SPELL_EFFECT_CREATE_ITEM) { if (ItemTemplate const* proto = sObjectMgr->GetItemTemplate(spellInfo->Effects[i].ItemType)) { if (craftsPossible) out << "|cffffff00(x" << craftsPossible << ")|r "; out << chat->FormatItem(proto); if ((minLevel || maxLevel) && (!proto->RequiredLevel || proto->RequiredLevel < minLevel || proto->RequiredLevel > maxLevel)) { filtered = true; break; } if (slot != EQUIPMENT_SLOT_END && bot->FindEquipSlot(proto, slot, true) != slot) { filtered = true; break; } } } } } if (out.str().empty()) out << chat->FormatSpell(spellInfo); if (filtered) continue; if (canCraftNow && !craftsPossible) continue; out << materials.str(); if (skillLine && skillLine->SkillLine) { uint32 GrayLevel = skillLine->TrivialSkillLineRankHigh; uint32 GreenLevel = (skillLine->TrivialSkillLineRankHigh + skillLine->MinSkillLineRank) / 2; uint32 YellowLevel = skillLine->MinSkillLineRank; uint32 SkillValue = bot->GetSkillValue(skillLine->SkillLine); out << " - "; if (SkillValue >= GrayLevel) out << " |cff808080gray"; else if (SkillValue >= GreenLevel) out << " |cff80be80green"; else if (SkillValue >= YellowLevel) out << " |cffffff00yellow"; else out << " |cffff8040orange"; out << "|r"; } if (out.str().empty()) continue; if (itr->first == 0) LOG_ERROR("playerbots", "?! {}", itr->first); spells.emplace_back(itr->first, out.str()); alreadySeenList += spellInfo->SpellName[0]; alreadySeenList += ","; } return spells; } bool ListSpellsAction::Execute(Event event) { Player* master = GetMaster(); if (!master) return false; std::string const filter = event.getParam(); std::vector spells = GetSpellList(filter); if (spells.empty()) { // CHANGE: Give early feedback when no spells match the filter. botAI->TellMaster("No spells found."); return true; } botAI->TellMaster("=== Spells ==="); std::sort(spells.begin(), spells.end(), CompareSpells); // CHANGE: Send the full spell list again so client-side addons // (e.g. Multibot / Unbot) can reconstruct the // complete spellbook for configuration. The heavy part that caused // freezes before was the old CompareSpells implementation scanning // the entire SkillLineAbility DBC on every comparison. With the new // cheap comparator above, sending all lines here is safe and keeps // behaviour compatible with existing addons. for (std::vector::const_iterator i = spells.begin(); i != spells.end(); ++i) botAI->TellMasterNoFacing(i->second); return true; }