-- Bagshui Rule Functions -- Exposes: Bagshui.config.RuleFunctions -- -- Defines the built-in rule functions available for use in Category rule expressions. Bagshui:AddComponent(function() -- Internal constant for BagType() rule function. local ALL_PROFESSION_BAGS = "~AllProfessionBags~" -- Internal constant for BagType() rule function. local PROFESSION_BAGS = "~ProfessionBags~" -- Property names/values of tables in this array should match `Rules:AddFunction()` -- parameters, however the `templates` property should *not* be used in this file. -- Instead, there are two options for adding built-in rule templates: -- 1. Localization (see `Rules:AddRuleExamplesFromLocalization() for details). -- 2. Generated in Config\RuleFunctionTemplates.lua or elsewhere and added to `Bagshui.config.RuleFunctionTemplates`. ---@type table[] Bagshui.config.RuleFunctions = { -- Item belongs to the list of active quest items. -- Only works by name due to client limitations. { functionNames = { "ActiveQuest", "aq", "ActiveQuestItem", }, ruleFunction = function(rules, ruleArguments) return Bagshui.activeQuestItems[rules.item.name] ~= nil end, -- Templates come from localization. }, -- Item is in the given bag number. { functionNames = { "Bag", "BagNum", }, ruleFunction = function(rules, ruleArguments) return rules:TestItemAttribute("bagNum", ruleArguments, "number") end, -- Templates are autogenerated in Components\Rules.lua (can't be done in -- RuleFunctionTemplates.lua because it's too early to read each inventory type). }, -- Item is in a bag of the given type. { functionNames = { "BagType", "bt", "ContainerType", }, ruleFunction = function(rules, ruleArguments) for _, arg in ipairs(ruleArguments) do -- Profession bags, triggered by "*ProfessionBags*" or "*AllProfessionBags*" if arg == PROFESSION_BAGS or arg == ALL_PROFESSION_BAGS then for profession, bagType in pairs(BsGameInfo.professionsToBags) do if (arg == PROFESSION_BAGS and rules.character.skills.professions[profession]) or arg == ALL_PROFESSION_BAGS then table.insert(ruleArguments, bagType) end end end end return rules:TestItemAttribute("bagType", ruleArguments) end, environmentVariables = { AllProfessionBags = ALL_PROFESSION_BAGS, AllTradeskillBags = ALL_PROFESSION_BAGS, ProfessionBag = PROFESSION_BAGS, ProfessionBags = PROFESSION_BAGS, TradeskillBag = PROFESSION_BAGS, TradeskillBags = PROFESSION_BAGS, }, -- Templates are autogenerated in RuleFunctionTemplates.lua }, -- Item binds on equip. { functionNames = { "BindsOnEquip", "BindsWhenEquipped", "boe", }, ruleFunction = function(rules, ruleArguments) -- Reusable table parameter to avoid activating garbage collection. if not rules._bindsOnEquip_Tooltip then rules._bindsOnEquip_Tooltip = { _G.ITEM_BIND_ON_EQUIP } end return rules:Rule_Tooltip(rules._bindsOnEquip_Tooltip) end, -- Templates come from localization. }, -- CharacterLevelRange() -- Exactly the current character's level. -- CharacterLevelRange(, ) -- Within the range provided. { functionNames = { "CharacterLevelRange", "clr", }, ruleFunction = function(rules, ruleArguments) -- Avoid an error about missing arguments vy setting 0 as the only argument -- When adjusted by `betweenAdjustmentAmount` it will become the -- character's current level. if table.getn(ruleArguments) == 0 then table.insert(ruleArguments, 0) end -- Passing the currently targeted character's level for `betweenAdjustmentAmount` -- so that upper and lower bound are relative to the current character's level return rules:TestItemAttribute("minLevel", ruleArguments, nil, "between", rules.character.level) end, -- Templates come from localization. }, -- Count(#): There are at least this many of the item. -- Count(min, max): There are between and of the item. { functionNames = { "Count", "Num", }, ruleFunction = function(rules, ruleArguments) -- Passing true for the last parameter so that Count(5) will match >= 5 -- (infinite upper bound if table.getn(ruleArguments) == 1). return rules:TestItemAttribute("count", ruleArguments, nil, "between", nil, true) end, -- Templates come from localization. }, -- Empty slots. { functionNames = { "EmptySlot", "empty", }, ruleFunction = function(rules, ruleArguments) return rules.item.emptySlot == 1 end, -- Templates come from localization. }, -- Item can be equipped at the specified location. { functionNames = { "EquipLocation", "eq", "Equip", "EquipLoc", }, ruleFunction = function(rules, ruleArguments) if table.getn(ruleArguments) == 0 then return string.len(rules.item.equipLocation) > 0 else return rules:TestItemAttribute("equipLocation", ruleArguments) or rules:TestItemAttribute("equipLocationLocalized", ruleArguments) end end, -- Template are autogenerated in RuleFunctionTemplates.lua. }, -- Item has been equipped at least once. { functionNames = { "Equipped", "Worn", }, ruleFunction = function(rules, ruleArguments) return BsCharacter:TestItem(rules.item.itemString, rules.character.equippedHistory, ruleArguments) end, -- Templates come from localization. }, -- Item's ID is in the given list. { functionNames = { "Id", }, ruleFunction = function(rules, ruleArguments) return rules:TestItemAttribute("id", ruleArguments, "string,number") end, -- Templates come from localization. }, -- Vanilla doesn't have ilvls. { functionNames = { "ItemLevelStat", "ilvl", "itemlevel", "statlevel", }, ruleFunction = function(rules, ruleArguments) rules.errorMessage = L.Error_Rule_ItemLevelStat return false end, hideFromUi = true, }, -- Item string matches the given list. { functionNames = { "ItemString", "is", }, ruleFunction = function(rules, ruleArguments) rules:RequireArguments(ruleArguments) -- Ensure all item strings are properly formatted to increase the likelihood of matching. -- The goal is to come out with something like: -- - "^item:itemId:enchantId:suffixId:uniqueId" -- - "^item:itemId:" for i = 1, table.getn(ruleArguments) do local arg = ruleArguments[i] if string.find(tostring(arg), "^/.-/$") then -- Already a pattern; nothing to do. elseif type(arg) == "number" or string.find(tostring(arg), "^%d+$") then -- It's just a number: 12345 and "12345" -> "^item:12345:". arg = "^item:" .. tostring(arg) .. ":" elseif type(arg) == "string" then -- Other strings require more work. -- Clean up any stray colons from both ends before starting arg = BsUtil.Trim(arg, ":") -- Everything other than a string like "item:1000" needs to be tweaked if not string.find(arg, "^%^?%a+:%d+") then arg = "item:" .. arg end -- Expand any empty colons (Wowhead-style) to zeros (this needs to run twice to catch them all). arg = string.gsub(string.gsub(arg, "::", ":0:"), "::", ":0:") -- More stuff for standard item strings if string.find(arg, "^item") then local _, colonCount = string.gsub(arg, ":", "") -- Item strings in Vanilla only have 4 parts, so remove any extraneous things from the end -- (again, getting a string from Wowhead is the issue here). if colonCount > 4 then arg = BsItemInfo:ParseItemLink(arg) -- Standard item strings have 4 colons, so if there are fewer than 4 and -- the argument doesn't already end in a colon, add one to ensure correct -- partial matches (item:123 shouldn't match item:1234:0:0:0). elseif colonCount < 4 and not string.find(arg, ":$") then arg = arg .. ":" end end -- Anchor the pattern to the start of the string if it's not already anchored. if not string.find(arg, "^%^") then arg = "^" .. arg end end -- Activate pattern matching. ruleArguments[i] = "/" .. arg .. "/" end return rules:TestItemAttribute("itemString", ruleArguments, "string,number", "contains") end, -- Templates come from localization. }, -- We're not parsing tooltips to get item stats. { functionNames = { "ItemStat", "ItemStatActive" }, ruleFunction = function(rules, ruleArguments) rules.errorMessage = L.Error_Rule_ItemStat return false end, hideFromUi = true, }, -- Item is usable by the current character based on the item's minimum level requirement. { functionNames = { "MinLevel", "ireq", "ItemLevelUse", "lvl", "ReqLevel", "UseLevel", }, ruleFunction = function(rules, ruleArguments) -- Passing true for the last parameter so that MinLevel(5) will match >= 5 (infinite upper bound if table.getn(ruleArguments) == 1) return rules:TestItemAttribute("minLevel", ruleArguments, nil, "between", nil, true) end, -- Templates come from localization. }, -- Item is in the specified inventory location. { functionNames = { "Location", "InventoryType", "loc", }, ruleFunction = function(rules, ruleArguments) rules:RequireArguments(ruleArguments) for i, arg in ipairs(ruleArguments) do arg = string.lower(arg) if arg == string.lower(L[BS_INVENTORY_TYPE.BAGS]) or arg == "bags" or arg == "bag" then ruleArguments[i] = BS_INVENTORY_TYPE.BAGS elseif arg == string.lower(L[BS_INVENTORY_TYPE.BANK]) or arg == "bank" then ruleArguments[i] = BS_INVENTORY_TYPE.BANK elseif arg == string.lower(L[BS_INVENTORY_TYPE.KEYRING]) or arg == "keyring" or arg == "keychain" or arg == "keys" then ruleArguments[i] = BS_INVENTORY_TYPE.KEYRING end end return rules:TestItemAttribute("bagshuiInventoryType", ruleArguments) end, -- Template are autogenerated in RuleFunctionTemplates.lua. }, -- Item name match (partial allowed). { functionNames = { "Name", "n", }, ruleFunction = function(rules, ruleArguments) return rules:TestItemAttribute("name", ruleArguments, nil, "contains") end, -- Templates come from localization. }, -- Item name exact match. { functionNames = { "NameExact", "ne", }, ruleFunction = function(rules, ruleArguments) return rules:TestItemAttribute("name", ruleArguments) end, -- Templates come from localization. }, -- Outfit() stub -- needs to be replaced by another function. -- Stubs current { functionNames = { "Outfit", }, ruleFunction = function(rules, ruleArguments) rules.errorMessage = string.format(L.Error_AddonDependency_Generic_FunctionName, "Outfit()") return false end, replaceable = true, ruleTemplates = { { code = GRAY_FONT_COLOR_CODE .. "Outfit()" .. FONT_COLOR_CODE_CLOSE, description = RED_FONT_COLOR_CODE .. L.Error_AddonDependency_Generic .. FONT_COLOR_CODE_CLOSE } }, -- Templates are defined in localization. -- Replacement function should supply `ruleFunctionTemplateFormatStrings`; -- (see ItemRack just below for an example). }, -- ItemRack set. { functionNames = { "Outfit", "ItemRack", "Outfit_ItemRack", }, conditional = function() return _G.IsAddOnLoaded("ItemRack") end, ruleFunction = function(rules, ruleArguments) if not _G.IsAddOnLoaded("ItemRack") then rules.errorMessage = string.format(L.Error_AddonDependency, "ItemRack") return false end if not rules.item.itemLink then return false end -- Here's a sample of the relevant parts of the data structure we're expecting to process: -- ``` -- Rack_User = { -- [" of "] = { -- ["Sets"] = { -- ["Test"] = { -- [0] = { -- ["id"] = "2516:0:0", -- ["name"] = "Light Shot", -- }, -- [1] = { -- ["id"] = "14566:0:0", -- ["name"] = "Prospector's Pads", -- }, -- ["icon"] = "Interface\\Icons\\Spell_Nature_NullWard", -- }, -- ["Rack-CombatQueue"] = { -- [1] = { -- }, -- [0] = { -- }, -- }, -- }, -- }, -- } -- ``` local itemRackUser = _G.Rack_User[rules.character.name .. " of " .. rules.character.realm] if type(itemRackUser) ~= "table" then return false end local itemRackOutfits = itemRackUser.Sets if type(itemRackOutfits) ~= "table" then return false end -- Borrowing some code from ItemRack because it doesn't provide an -- isolated function to parse an itemLink into their format. local _ ,_, id = string.find(rules.item.itemLink or "", "(item:%d+:%d+:%d+:%d+)") local _ ,_, itemRackItemId = string.find(id or "", "item:(%d+:%d+:%d+):%d+") if not itemRackItemId or (type(itemRackItemId) == "string" and string.len(itemRackItemId) == 0) then return false end local matchAny = (table.getn(ruleArguments) == 0) for outfitName, outfitItems in pairs(itemRackOutfits) do local outfitName = string.lower(BsUtil.Trim(outfitName)) for _, item in pairs(outfitItems) do -- Type check is needed here because ItemRack stores the outfit icon as a string at the same level. if type(item) == "table" and item.id == itemRackItemId then if matchAny then return true else for _, outfitToMatch in ipairs(ruleArguments) do if string.lower(BsUtil.Trim(outfitToMatch)) == outfitName then return true end end end end end end return false end, -- Templates come from localization and need a substitution string. ruleFunctionTemplateFormatStrings = { "ItemRack" } }, -- Outfitter set. { functionNames = { "Outfit", "Outfitter", "Outfit_Outfitter", }, conditional = function() return _G.IsAddOnLoaded("Outfitter") end, ruleFunction = function(rules, ruleArguments) if not _G.IsAddOnLoaded("Outfitter") then rules.errorMessage = string.format(L.Error_AddonDependency, "Outfitter") return false end -- Extra check to make sure Outfitter is ready to go. if not _G.Outfitter_IsInitialized and _G.Outfitter_IsInitialized() then return false end if not rules.item.itemLink then return false end -- Here's a sample of the relevant parts of the data structure we're expecting to process: -- gOutfitter_Settings = { -- ["Outfits"] = { -- ["Partial"] = { -- }, -- ["Accessory"] = { -- }, -- ["Special"] = { -- [1] = { -- ["Items"] = {}, -- Snipped -- ["StatID"] = "ArgentDawn", -- ["Name"] = "Argent Dawn", -- ["CategoryID"] = "Special", -- ["SpecialID"] = "ArgentDawn", -- }, -- }, -- ["Complete"] = { -- [1] = { -- ["Items"] = {}, -- ["CategoryID"] = "Complete", -- ["Name"] = "Birthday Suit", -- }, -- [2] = { -- ["Items"] = {}, -- ["CategoryID"] = "Complete", -- ["Name"] = "Normal", -- }, -- [3] = { -- ["Items"] = { -- ["ShoulderSlot"] = { -- ["Name"] = "Prospector's Pads", -- ["Code"] = 14566, -- ["SubCode"] = 0, -- ["EnchantCode"] = 0, -- }, -- ["AmmoSlot"] = { -- ["Name"] = "Light Shot", -- ["Code"] = 2516, -- ["SubCode"] = 0, -- ["EnchantCode"] = 0, -- }, -- }, -- ["CategoryID"] = "Complete", -- ["Name"] = "Test", -- }, -- }, -- }, -- ["HideHelm"] = { -- }, -- ["HideCloak"] = { -- }, -- } -- gOutfitter_Settings is NOT a typo! local outfitterOutfits = _G.gOutfitter_Settings.Outfits if type(outfitterOutfits) ~= "table" then return false end if not _G.Outfitter_GetItemInfoFromLink then return false end local outfitterItemInfo = _G.Outfitter_GetItemInfoFromLink(rules.item.itemLink) if not outfitterItemInfo then return false end local matchAny = (table.getn(ruleArguments) == 0) for _, outfits in pairs(outfitterOutfits) do for _, outfit in pairs(outfits) do local outfitName = string.lower(BsUtil.Trim(outfit.Name)) for _, item in pairs(outfit.Items) do if item.Code == outfitterItemInfo.Code and item.SubCode == outfitterItemInfo.SubCode and item.EnchantCode == outfitterItemInfo.EnchantCode then if matchAny then return true else for _, outfitToMatch in ipairs(ruleArguments) do if string.lower(BsUtil.Trim(outfitToMatch)) == outfitName then return true end end end end end end end return false end, -- Templates come from localization and need a substitution string. ruleFunctionTemplateFormatStrings = { "Outfitter" } }, -- Item belongs to the PeriodicTable set(s). { functionNames = { "PeriodicTable", "pt", "Taxonomy", "tx", }, ruleFunction = function(rules, ruleArguments) if not rules.item.itemLink then return false end rules:RequireArguments(ruleArguments) for _, ptSet in ipairs(ruleArguments) do if Bagshui.libs.PT:ItemInSet(rules.item.itemLink, BsUtil.Trim(ptSet)) then return true end end end, -- Template are autogenerated in RuleFunctionTemplates.lua. }, -- Item is crafted by a learned profession recipe. { functionNames = { "ProfessionCraft", "pc", "TradeskillCraft", "tsc", }, ruleFunction = function(rules, ruleArguments) return BsCharacter:TestItem(rules.item.id, rules.character.professionCrafts, ruleArguments) end, -- Templates come from localization. }, -- Item is a reagent for a learned profession recipe. { functionNames = { "ProfessionReagent", "pr", "TradeskillReagent", "tsr", }, ruleFunction = function(rules, ruleArguments) return BsCharacter:TestItem(rules.item.id, rules.character.professionReagents, ruleArguments) end, -- Templates come from localization. }, -- Item is of the specified quality. { functionNames = { "Quality", "q", "Rarity", }, ruleFunction = function(rules, ruleArguments) return rules:TestItemAttribute("quality", ruleArguments, "string,number") or rules:TestItemAttribute("qualityLocalized", ruleArguments, "string,number") end, -- Template are autogenerated in RuleFunctionTemplates.lua. }, -- Does the tooltip contain "Class: "? { functionNames = { "RequiresClass", "Class", "AllowedClass", "rq", }, ruleFunction = function(rules, ruleArguments) -- Reusable table parameter to avoid activating garbage collection. if not rules._requiresClass_Tooltip then -- Normal and localized class names. rules._requiresClass_Tooltip = { "", "" } -- Need slashes to active pattern matching. rules._requiresClass_BasePattern = "/" .. _G.ITEM_CLASSES_ALLOWED .. "/" end rules:RequireArguments(ruleArguments) for _, arg in ipairs(ruleArguments) do -- Wrapping the class name in a frontier pattern for word boundary matching. -- This allows for safely matching things like "Classes: Hunter, Rogue, Mage". rules._requiresClass_Tooltip[1] = string.format(rules._requiresClass_BasePattern, ".*%f[%a]" .. arg .. "%f[%A].*") rules._requiresClass_Tooltip[2] = string.format(rules._requiresClass_BasePattern, ".*%f[%a]" .. (BsGameInfo.lowercaseToNormalCaseLocalizedCharacterClasses[string.lower(arg)] or arg) .. "%f[%A].*") if rules:Rule_Tooltip(rules._requiresClass_Tooltip) then return true end end return false end, -- Template are autogenerated in RuleFunctionTemplates.lua. }, -- Item is soulbound. { functionNames = { "Soulbound", "sb", }, ruleFunction = function(rules, ruleArguments) -- Reusable table parameter to avoid activating garbage collection. if not rules._soulbound_Tooltip then rules._soulbound_Tooltip = { _G.ITEM_SOULBOUND } end return rules:Rule_Tooltip(rules._soulbound_Tooltip) end, -- Templates come from localization. }, -- Item is allowed to stack. { functionNames = { "Stacks", "st", }, ruleFunction = function(rules, ruleArguments) -- Reusable table parameter to avoid activating garbage collection. if not rules._stacks_Minimum then rules._stacks_Minimum = { 2 } end -- Passing true for the last parameter so that Stacks() will match >= 2 -- (infinite upper bound if table.getn(ruleArguments) == 1). return rules:TestItemAttribute("maxStackCount", rules._stacks_Minimum, nil, "between", nil, true) end, -- Templates come from localization. }, -- Item subtype match. { functionNames = { "Subtype", "sty", "stype", }, ruleFunction = function(rules, ruleArguments) -- Final parameter is true to allow matches via localization of arguments. return rules:TestItemAttribute("subtype", ruleArguments, nil, nil, nil, nil, true) end, -- Template are autogenerated in RuleFunctionTemplates.lua. }, -- Tooltip contains the given string. { functionNames = { "Tooltip", "tt", }, ruleFunction = function(rules, ruleArguments) return rules:TestItemAttribute("tooltip", ruleArguments, "string", "contains") end, -- Templates come from localization. }, -- Transmog() stub. { functionNames = { "Transmog", }, ruleFunction = function(rules, ruleArguments) rules.errorMessage = string.format(L.Error_AddonDependency_Generic_FunctionName, "Transmog()") return false end, ruleTemplates = { { code = GRAY_FONT_COLOR_CODE .. "Transmog()" .. FONT_COLOR_CODE_CLOSE, description = RED_FONT_COLOR_CODE .. L.Error_AddonDependency_Generic .. FONT_COLOR_CODE_CLOSE } }, replaceable = true, }, -- Item type match. { functionNames = { "Type", "ty", }, ruleFunction = function(rules, ruleArguments) -- Final parameter is true to allow matches via localization of arguments return rules:TestItemAttribute("type", ruleArguments, nil, nil, nil, nil, true) end, -- Template are autogenerated in RuleFunctionTemplates.lua. }, -- Item is usable by the current character. { functionNames = { "Usable", "u", "Useable", }, ruleFunction = function(rules, ruleArguments) return BsItemInfo:IsUsable(rules.item, rules.character) end, -- Templates come from localization. }, -- Gray items. -- It would be possible to extend this with more intelligent junk identification -- in the future by tapping into data from other addons. -- To turn this into a stub, alias support for stubs would need to be added. { functionNames = { "Trash", "Junk", }, hideFromUi = true, ruleFunction = function(rules, ruleArguments) if not rules._trashQuality then rules._trashQuality = {0} end return rules:Rule_Quality(rules._trashQuality) end, -- Templates come from localization. }, -- Wishlist() stub. { functionNames = { "Wishlist", }, ruleFunction = function(rules, ruleArguments) rules.errorMessage = string.format(L.Error_AddonDependency_Generic_FunctionName, "Wishlist()") return false end, ruleTemplates = { { code = GRAY_FONT_COLOR_CODE .. "Wishlist()" .. FONT_COLOR_CODE_CLOSE, description = RED_FONT_COLOR_CODE .. L.Error_AddonDependency_Generic .. FONT_COLOR_CODE_CLOSE } }, replaceable = true, -- Templates are defined in localization. -- Replacement function should supply `ruleFunctionTemplateFormatStrings`; -- (see ItemRack just below for an example). }, -- AtlasLoot wishlist -- replaces Wishlist() when AtlasLoot is available. { functionNames = { "Wishlist", "AtlasLootWish", "Wishlist_AtlasLoot", "Wish", }, conditional = function() return _G.IsAddOnLoaded("AtlasLoot") end, ruleFunction = function(rules, ruleArguments) if not _G.IsAddOnLoaded("AtlasLoot") then rules.errorMessage = string.format(L.Error_AddonDependency, "AtlasLoot") return false end if not rules.item.itemLink then return false end -- Here's a sample of the relevant parts of the data structure we're expecting to process: -- ``` -- _G.AtlasLootCharDB.WishList = { -- [1] = { -- [1] = 19346, -- [2] = "INV_Weapon_Shortblade_29", -- [3] = "=q4=Dragonfang Blade", -- [4] = "=ds=#h1#, #w4#", -- [5] = "BWLVaelastrasz|AtlasLootItems", -- }, -- [2] = { -- [1] = 19395, -- [2] = "INV_Misc_Gem_Topaz_02", -- [3] = "=q4=Rejuvenating Gem", -- [4] = "=ds=#s14#", -- [5] = "BWLEbonroc|AtlasLootItems", -- }, -- }, -- ``` if type(_G.AtlasLootCharDB) ~= "table" then return false end if type(_G.AtlasLootCharDB.WishList) ~= "table" then return false end for _, itemInfo in ipairs(_G.AtlasLootCharDB.WishList) do if type(itemInfo) == "table" and itemInfo[1] == rules.item.id then return true end end return false end, -- Templates come from localization and need a substitution string. ruleFunctionTemplateFormatStrings = { "AtlasLoot" } }, } end)