Files
coa-atlasloot/AtlasLoot/Core/Search.lua
T

638 lines
21 KiB
Lua

local GREY = "|cff999999";
local RED = "|cffff0000";
local WHITE = "|cffFFFFFF";
local GREEN = "|cff1eff00";
local PURPLE = "|cff9F3FFF";
local BLUE = "|cff0070dd";
local ORANGE = "|cffFF8400";
local AL = LibStub("AceLocale-3.0"):GetLocale("AtlasLoot");
local modules = { "AtlasLoot_BurningCrusade", "AtlasLoot_Crafting", "AtlasLoot_OriginalWoW", "AtlasLoot_WorldEvents", "AtlasLoot_WrathoftheLichKing" };
local currentPage = 1;
local SearchResult = nil;
-- Supported Operators
local OP_AND = "&"
local BINARYOPERATORS = { OP_AND };
-- multi-character patterns must come before single-character patterns
local RELATIONAL_OPERATORS = { ">=", "<=", "<>", "<", ">", "=", };
-- Supported Stat Filters
local STAT_FILTERS = {
-- Base Stats
["stamina"] = "ITEM_MOD_STAMINA_SHORT",
["stam"] = "ITEM_MOD_STAMINA_SHORT",
["sta"] = "ITEM_MOD_STAMINA_SHORT",
["strength"] = "ITEM_MOD_STRENGTH_SHORT",
["str"] = "ITEM_MOD_STRENGTH_SHORT",
["agility"] = "ITEM_MOD_AGILITY_SHORT",
["agi"] = "ITEM_MOD_AGILITY_SHORT",
["intellect"] = "ITEM_MOD_INTELLECT_SHORT",
["int"] = "ITEM_MOD_INTELLECT_SHORT",
["spirit"] = "ITEM_MOD_SPIRIT_SHORT",
["spir"] = "ITEM_MOD_SPIRIT_SHORT",
["spi"] = "ITEM_MOD_SPIRIT_SHORT",
["health"] = "ITEM_MOD_HEALTH_SHORT",
["mana"] = "ITEM_MOD_MANA_SHORT",
["mp5"] = "ITEM_MOD_POWER_REGEN0_SHORT",
["mpr"] = "ITEM_MOD_POWER_REGEN0_SHORT",
["hp5"] = "ITEM_MOD_HEALTH_REGEN_SHORT",
["hpr"] = "ITEM_MOD_HEALTH_REGEN_SHORT",
--Sockets
["socketblue"] = "EMPTY_SOCKET_BLUE",
["socketred"] = "EMPTY_SOCKET_RED",
["socketyellow"] = "EMPTY_SOCKET_YELLOW",
["socketnocolor"] = "EMPTY_SOCKET_NO_COLOR",
["socketwhite"] = "EMPTY_SOCKET_NO_COLOR",
["socketmeta"] = "EMPTY_SOCKET_META",
["meta"] = "EMPTY_SOCKET_META",
--Secondary Stats
["attackpowerferal"] = "ITEM_MOD_FERAL_ATTACK_POWER_SHORT",
["attackpowferal"] = "ITEM_MOD_FERAL_ATTACK_POWER_SHORT",
["apferal"] = "ITEM_MOD_FERAL_ATTACK_POWER_SHORT",
["attackpower"] = "ITEM_MOD_ATTACK_POWER_SHORT",
["attackpow"] = "ITEM_MOD_ATTACK_POWER_SHORT",
["ap"] = "ITEM_MOD_ATTACK_POWER_SHORT",
["spellpower"] = "ITEM_MOD_SPELL_POWER_SHORT",
["spellpow"] = "ITEM_MOD_SPELL_POWER_SHORT",
["sp"] = "ITEM_MOD_SPELL_POWER_SHORT",
["spellpenetration"] = "ITEM_MOD_SPELL_PENETRATION_SHORT",
["spellpen"] = "ITEM_MOD_SPELL_PENETRATION_SHORT",
["spp"] = "ITEM_MOD_SPELL_PENETRATION_SHORT",
["crit"] = "ITEM_MOD_CRIT_RATING_SHORT",
["haste"] = "ITEM_MOD_HASTE_RATING_SHORT",
["hit"] = "ITEM_MOD_HIT_RATING_SHORT",
["armorpenetration"] = "ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT",
["armourpenetration"] = "ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT",
["armorpen"] = "ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT",
["armourpen"] = "ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT",
["arp"] = "ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT",
["expertise"] = "ITEM_MOD_EXPERTISE_RATING_SHORT",
["exp"] = "ITEM_MOD_EXPERTISE_RATING_SHORT",
["dps"] = "ITEM_MOD_DAMAGE_PER_SECOND_SHORT",
["resilience"] = "ITEM_MOD_RESILIENCE_RATING",
["resil"] = "ITEM_MOD_RESILIENCE_RATING",
["res"] = "ITEM_MOD_RESILIENCE_RATING",
["defense"] = "ITEM_MOD_DEFENSE_SKILL_RATING_SHORT",
["def"] = "ITEM_MOD_DEFENSE_SKILL_RATING_SHORT",
["dodge"] = "ITEM_MOD_DODGE_RATING_SHORT",
["dod"] = "ITEM_MOD_DODGE_RATING_SHORT",
["block"] = "ITEM_MOD_BLOCK_RATING_SHORT",
["blockvalue"] = "ITEM_MOD_BLOCK_VALUE_SHORT",
["blockval"] = "ITEM_MOD_BLOCK_VALUE_SHORT",
["bv"] = "ITEM_MOD_BLOCK_VALUE_SHORT",
["parry"] = "ITEM_MOD_PARRY_RATING_SHORT",
--Resistances
["armor"] = "RESISTANCE0_NAME",
["armour"] = "RESISTANCE0_NAME",
["arm"] = "RESISTANCE0_NAME",
["resistancephysical"] = "RESISTANCE0_NAME",
["resistancephys"] = "RESISTANCE0_NAME",
["resphys"] = "RESISTANCE0_NAME",
["resistanceholy"] = "RESISTANCE1_NAME",
["resholy"] = "RESISTANCE1_NAME",
["resistancefire"] = "RESISTANCE2_NAME",
["resfire"] = "RESISTANCE2_NAME",
["resistancenature"] = "RESISTANCE3_NAME",
["resnature"] = "RESISTANCE3_NAME",
["resnat"] = "RESISTANCE3_NAME",
["resistanceforst"] = "RESISTANCE4_NAME",
["resfrost"] = "RESISTANCE4_NAME",
["resistanceshadow"] = "RESISTANCE5_NAME",
["resshadow"] = "RESISTANCE5_NAME",
["resshad"] = "RESISTANCE5_NAME",
["resistancearcane"] = "RESISTANCE6_NAME",
["resarcane"] = "RESISTANCE6_NAME",
["resarc"] = "RESISTANCE6_NAME"
};
local SOCKET_FILTERS = {
["socket"] = true,
["sockets"] = true,
["gem"] = true,
["gems"] = true,
};
local SOCKET_TYPES = {
"EMPTY_SOCKET_BLUE",
"EMPTY_SOCKET_RED",
"EMPTY_SOCKET_YELLOW",
"EMPTY_SOCKET_META",
"EMPTY_SOCKET_NO_COLOR"
};
local INFO_FILTERS = {
["ilvl"] = true,
["minlvl"] = true,
--["type"] = true,
--["subtype"] = true,
};
local QUALITY_FILTERS = {
["poor"] = 0,
["common"] = 1,
["uncommon"] = 2,
["rare"] = 3,
["epic"] = 4,
["legendary"] = 5,
["artifact"] = 6,
["heirloom"] = 7
};
local SLOT_FILTERS = {
["none"] = "INVTYPE_NON_EQUIP",
["head"] = "INVTYPE_HEAD",
["neck"] = "INVTYPE_NECK",
["shoulder"] = "INVTYPE_SHOULDER",
["body"] = "INVTYPE_BODY",
["chest"] = "INVTYPE_CHEST",
["waist"] = "INVTYPE_WAIST",
["legs"] = "INVTYPE_LEGS",
["feet"] = "INVTYPE_FEET",
["wrist"] = "INVTYPE_WRIST",
["hand"] = "INVTYPE_HAND",
["finger"] = "INVTYPE_FINGER",
["trinket"] = "INVTYPE_TRINKET",
["weapon"] = "INVTYPE_WEAPON",
["shield"] = "INVTYPE_SHIELD",
["ranged"] = "INVTYPE_RANGED",
["cloak"] = "INVTYPE_CLOAK",
["2hweapon"] = "INVTYPE_2HWEAPON",
["bag"] = "INVTYPE_BAG",
["tabard"] = "INVTYPE_TABARD",
["robe"] = "INVTYPE_ROBE",
["weaponmainhand"] = "INVTYPE_WEAPONMAINHAND",
["mainhand"] = "INVTYPE_WEAPONMAINHAND",
["weaponoffhand"] = "INVTYPE_WEAPONOFFHAND",
["offhand"] = "INVTYPE_WEAPONOFFHAND",
["holdable"] = "INVTYPE_HOLDABLE",
["ammo"] = "INVTYPE_AMMO",
["thrown"] = "INVTYPE_THROWN",
["rangedright"] = "INVTYPE_RANGEDRIGHT",
["quiver"] = "INVTYPE_QUIVER",
["relic"] = "INVTYPE_RELIC"
};
local NON_EQUIPABLE_SLOTS = {
["INVTYPE_NON_EQUIP"] = true,
["INVTYPE_BODY"] = true,
["INVTYPE_BAG"] = true,
["INVTYPE_AMMO"] = true,
["INVTYPE_QUIVER"] = true,
};
SLASH_ATLASLOOTHELP1 = "/atlasloothelp";
SlashCmdList["ATLASLOOTHELP"] = function(msg, editBox)
local function show(caption, t)
local keys = {}
for key in pairs(t) do table.insert(keys, key) end
table.sort(keys, function(a,b) return a < b end)
print(caption..": ".. table.concat(keys, ", "))
end
show("stat", STAT_FILTERS)
show("socket", SOCKET_FILTERS)
show("slot", SLOT_FILTERS)
show("quality", QUALITY_FILTERS)
show("info", INFO_FILTERS)
print("\nExamples:")
print("gladiator")
print("str>40")
print("gladiator&str>40")
print("str>40&ilvl>=120&ilvl<140&int>0&socket>2");
print("sp>20&quality>=rare&quality<legendary&slot=finger");
print("stam>20&minlvl<=50")
end
SLASH_ATLASLOOTSEARCH1 = "/atlaslootsearch";
SLASH_ATLASLOOTSEARCH2 = "/als";
SlashCmdList["ATLASLOOTSEARCH"] = function(search, editBox)
AtlasLootDefaultFrame:Show()
if search and search ~= '' then
AtlasLootDefaultFrameSearchBox:SetText(search)
AtlasLootDefaultFrameSearchButton:Click()
end
end
-- split one string on another (delimiter can be more than one character)
local function SplitString(str, delimiter)
local result = {}
local start = 1
local len = #str
while start <= len do
local delimPos = str:find(delimiter, start, true)
if delimPos then
result[#result+1] = str:sub(start, delimPos-1) -- grab up to the delimeter
start = delimPos + #delimiter -- advance past the delimiter
else
result[#result+1] = str:sub(start) -- grab the rest of the string
break
end
end
return result
end
local function CompareNumbersByOperator(operator, left, right)
return left and right and (
(operator == "<>" and left ~= right)
or (operator == "<=" and left <= right)
or (operator == ">=" and left >= right)
or (operator == "=" and left == right)
or (operator == "<" and left < right)
or (operator == ">" and left > right)
)
end
local function ThrowQueryError(...)
error("Error: "..string.format(...))
end
local function IsItemStatMatch(term, stats)
local filterKey = STAT_FILTERS[term.left]
if not filterKey then
return
end
local statValue = tonumber(stats[filterKey])
if not statValue then
return
end
local searchedValue = tonumber(term.right)
if not searchedValue then
ThrowQueryError("'%s' requires a numeric argument", term.left)
end
return CompareNumbersByOperator(term.relational, statValue, searchedValue)
end
local function IsItemLevelFilterMatch(term, itemLvl, itemEquipLoc)
if term.left ~= "ilvl" then
return
end
local searchedValue = tonumber(term.right);
if not searchedValue then
ThrowQueryError("ilvl search requires a numeric argument")
end
return itemLvl ~= nil and itemLvl > 0 and CompareNumbersByOperator(term.relational, itemLvl, searchedValue)
end
local function IsItemQualityMatch(term, itemQuality)
if term.left ~= "quality" then
return
end
local searchedValue = QUALITY_FILTERS[term.right]
if not searchedValue then
ThrowQueryError("unrecognized quality value \"%s\"", term.right)
end
return CompareNumbersByOperator(term.relational, itemQuality, searchedValue)
end
local function IsItemSocketMatch(term, stats)
if not SOCKET_FILTERS[term.left] then
return
end
local searchedValue = tonumber(term.right)
if not searchedValue then
ThrowQueryError("'%s' requires a numeric argument", term.left)
end
local socketCount = 0;
for _, socketType in pairs(SOCKET_TYPES) do
local statValue = tonumber(stats[socketType]);
if statValue then
socketCount = socketCount + statValue;
end
end
return CompareNumbersByOperator(term.relational, socketCount, searchedValue)
end
local function IsMinLevelFilterMatch(term, minLvl)
if term.left ~= "minlvl" then
return
end
local searchedValue = tonumber(term.right);
if not searchedValue then
ThrowQueryError("minlvl search requires a numeric argument")
end
return minLvl ~= nil and minLvl > 0 and CompareNumbersByOperator(term.relational, minLvl, searchedValue)
end
local function IsItemSlotMatch(term, itemEquipLoc)
if term.left ~= "slot" then
return
end
if term.relational ~= "=" then
ThrowQueryError("slot searches should be in the form \"slot=[slotname]\"")
end
local slot = SLOT_FILTERS[term.right]
if not slot then
ThrowQueryError("unrecognized slot name: \"%s\"", term.right)
end
return slot == itemEquipLoc
end
local function nameMatches(name, searchText)
if AtlasLoot.db.profile.PartialMatching then
return string.find(string.lower(name), searchText);
else
return string.lower(name) == searchText;
end
end
local function ItemMatchesTerm(term, itemName, stats, itemLvl, minLvl, itemQuality, itemEquipLoc)
if term.relational then
return IsItemStatMatch(term, stats)
or IsItemSocketMatch(term, stats)
or IsItemLevelFilterMatch(term, itemLvl, itemEquipLoc)
or IsMinLevelFilterMatch(term, minLvl)
or IsItemSlotMatch(term, itemEquipLoc)
or IsItemQualityMatch(term, itemQuality)
else
return nameMatches(itemName, term.name)
end
end
local function ItemMatchesAllTerms(searchTerms, itemName, stats, itemLvl, minLvl, itemQuality, itemEquipLoc)
for _, term in ipairs(searchTerms) do
if not ItemMatchesTerm(term, itemName, stats, itemLvl, minLvl, itemQuality, itemEquipLoc) then
return false
end
end
return true
end
local function ParseTerm(termText)
for _, relational in ipairs(RELATIONAL_OPERATORS) do
local operands = SplitString(termText, relational)
if #operands == 2 then
return {
left = operands[1],
right = operands[2],
relational = relational
}
end
end
return { name = termText } -- simple name search
end
-- Parse search text into '&'-delimited search terms,
-- then parse each term on its relational operator, if present.
local function ParseQuery(searchText)
local terms = {}
for i, term in pairs(SplitString(searchText, OP_AND)) do
table.insert(terms, ParseTerm(term))
end
return terms
end
local function GetItemDetails(itemId, atlasName)
-- Name, Link, Quality(num), iLvl(num), minLvl(num), itemType(localized string), itemSubType(localized string), stackCount(num), itemEquipLoc(enum), texture(link to a local file), displayId(num)
local itemName, _, itemQuality, itemLvl, minLvl, _, _, _, itemEquipLoc = GetItemInfo(itemId);
if not itemName then itemName = gsub(atlasName, "=q%d=", "") end
return itemName, itemQuality, itemLvl, minLvl, itemEquipLoc, GetItemStats("item:"..itemId)
end
local function GetSpellName(itemId, atlasName)
local spellName = GetSpellInfo(string.sub(itemId, 2));
if spellName then
return spellName
elseif (string.sub(atlasName, 1, 2) == "=d") then
return gsub(atlasName, "=ds=", "");
else
return gsub(atlasName, "=q%d=", "");
end
end
local function DoSearch(searchText)
AtlasLootCharDB["SearchResult"] = {};
AtlasLootCharDB.LastSearchedText = Text;
local function AddItemToSearchResult(itemId, itemType, itemName, dataID)
local lootPage = AtlasLoot_TableNames[dataID] and AtlasLoot_TableNames[dataID][1] or "Argh!";
table.insert(AtlasLootCharDB["SearchResult"], { 0, itemId, itemType, itemName, lootPage, "", "", dataID.."|".."\"\"", [AtlasLoot_Difficulty.DIF_SEARCH] = 2});
end
local searchTerms = ParseQuery(searchText)
local equipableFilterOn = AtlasLoot.db.profile.EquipableFilter
local function IsItemEquipableMatch(itemEquipLoc)
return not equipableFilterOn or (itemEquipLoc and itemEquipLoc ~= '' and not NON_EQUIPABLE_SLOTS[itemEquipLoc])
end
for dataID, data in pairs(AtlasLoot_Data) do
for _, v in ipairs(data) do
local _, itemId, itemType, atlasName = unpack(v)
if type(itemId) == "number" and itemId > 0 then
local itemName, itemQuality, itemLvl, minLvl, itemEquipLoc, stats = GetItemDetails(itemId, atlasName);
if IsItemEquipableMatch(itemEquipLoc) and ItemMatchesAllTerms(searchTerms, itemName, stats, itemLvl, minLvl, itemQuality, itemEquipLoc) then
AddItemToSearchResult(itemId, itemType, itemName, dataID)
end
elseif not equipableFilterOn and itemId and itemId ~= "" and string.sub(itemId, 1, 1) == "s" then
local spellName = GetSpellName(itemId, atlasName)
if nameMatches(spellName, searchText) then
spellName = string.sub(atlasName, 1, 4)..spellName;
AddItemToSearchResult(itemId, itemType, spellName, dataID)
end
end
end
end
end
function AtlasLoot:ShowSearchResult()
AtlasLoot_ShowItemsFrame("SearchResult", "SearchResultPage"..currentPage, (AL["Search Result: %s"]):format(AtlasLootCharDB.LastSearchedText or ""), pFrame);
end
function AtlasLoot:Search(Text)
if not Text then return end
Text = strtrim(Text);
if Text == "" then return end
-- Decide if we need load all modules or just specified ones
local allDisabled = not self.db.profile.SearchOn.All;
if allDisabled then
for _, module in ipairs(modules) do
if self.db.profile.SearchOn[module] == true then
allDisabled = false;
break;
end
end
end
if allDisabled then
DEFAULT_CHAT_FRAME:AddMessage(RED..AL["AtlasLoot"]..": "..WHITE..AL["You don't have any module selected to search on."]);
return;
end
if self.db.profile.SearchOn.All then
AtlasLoot_LoadAllModules();
else
for k, v in pairs(self.db.profile.SearchOn) do
if k ~= "All" and v == true and not IsAddOnLoaded(k) and LoadAddOn(k) and self.db.profile.LoDNotify then
DEFAULT_CHAT_FRAME:AddMessage(GREEN..AL["AtlasLoot"]..": "..ORANGE..k..WHITE.." "..AL["sucessfully loaded."]);
end
end
end
local success, message = pcall(DoSearch, Text)
if not success then
message = message:match("[^:]+: (.*)") or message -- strip stack location
DEFAULT_CHAT_FRAME:AddMessage(RED..AL["AtlasLoot"]..": "..WHITE..message);
elseif #AtlasLootCharDB["SearchResult"] == 0 then
local itemFilterErrorMessage = "";
if operator then
itemFilterErrorMessage = [[
Please check if you have a typo in the filter.
For help, type "/atlasloothelp".
You might also have to query the server for item informations to load them into your client's Cache.]];
end
DEFAULT_CHAT_FRAME:AddMessage(RED..AL["AtlasLoot"]..": "..WHITE..AL["No match found for"].." \""..Text.."\"."..itemFilterErrorMessage);
else
currentPage = 1;
SearchResult = AtlasLoot_CategorizeWishList(AtlasLootCharDB["SearchResult"]);
AtlasLoot_ShowItemsFrame("SearchResult", "SearchResultPage1", (AL["Search Result: %s"]):format(AtlasLootCharDB.LastSearchedText or ""), pFrame);
end
end
function AtlasLoot:ShowSearchOptions(button)
local dewdrop = AceLibrary("Dewdrop-2.0");
if dewdrop:IsOpen(button) then
dewdrop:Close(1);
else
local setOptions = function()
dewdrop:AddLine(
"text", AL["Search on"],
"isTitle", true,
"notCheckable", true
);
dewdrop:AddLine(
"text", AL["All modules"],
"checked", self.db.profile.SearchOn.All,
"tooltipTitle", AL["All modules"],
"tooltipText", AL["If checked, AtlasLoot will load and search across all the modules."],
"func", function()
self.db.profile.SearchOn.All = not self.db.profile.SearchOn.All;
end
);
for _, module in ipairs(modules) do
if IsAddOnLoadOnDemand(module) then
local title = GetAddOnMetadata(module, "title");
local notes = GetAddOnMetadata(module, "notes");
dewdrop:AddLine(
"text", title,
"checked", self.db.profile.SearchOn.All or self.db.profile.SearchOn[module],
"disabled", self.db.profile.SearchOn.All,
"tooltipTitle", title,
"tooltipText", notes,
"func", function()
if self.db.profile.SearchOn[module] == nil then
self.db.profile.SearchOn[module] = true;
else
self.db.profile.SearchOn[module] = nil;
end
end
);
end
end
dewdrop:AddLine(
"text", AL["Search options"],
"isTitle", true,
"notCheckable", true
);
dewdrop:AddLine(
"text", AL["Partial matching"],
"checked", self.db.profile.PartialMatching,
"tooltipTitle", AL["Partial matching"],
"tooltipText", AL["If checked, AtlasLoot search item names for a partial match."],
"func", function() self.db.profile.PartialMatching = not self.db.profile.PartialMatching end
);
dewdrop:AddLine(
"text", "Only search equipable", -- TODO: put in AL
"checked", self.db.profile.EquipableFilter,
"tooltipTitle", "Equipable filter", -- TODO: put in AL
"tooltipText", "If checked, AtlasLoot only includes items that are euipable.", -- TODO: put in AL
"func", function() self.db.profile.EquipableFilter = not self.db.profile.EquipableFilter end
);
end;
dewdrop:Open(button,
'point', function(parent)
return "BOTTOMLEFT", "BOTTOMRIGHT";
end,
"children", setOptions
);
end
end
function AtlasLoot:GetOriginalDataFromSearchResult(itemID)
for i, v in ipairs(AtlasLootCharDB["SearchResult"]) do
if v[2] == itemID then
AtlasLoot_ShowWishListDropDown(v[2], v[3], v[4], v[5], v[8], this, nil, v[AtlasLoot_Difficulty.DIF_SEARCH]);
end
end
end
-- Copied and modified from AtlasLoot_GetWishListPage
function AtlasLoot:GetSearchResultPage(page)
if not SearchResult then SearchResult = AtlasLoot_CategorizeWishList(AtlasLootCharDB["SearchResult"]) end
-- Calc for maximal pages
local pageMax = math.ceil(#SearchResult / 30);
if page < 1 then page = 1 end
if page > pageMax then page = pageMax end
currentPage = page;
-- Table copy
local k=1;
local result = {};
local start = (page - 1) * 30 + 1;
for i = start, start + 29 do
if not SearchResult[i] then break end
SearchResult[i][1] = k;
table.insert(result, SearchResult[i]);
k=k+1;
end
return result, pageMax;
end