-- ------------------------------------------------------------------------------ -- -- TradeSkillMaster -- -- http://www.curse.com/addons/wow/tradeskill-master -- -- -- -- A TradeSkillMaster Addon (http://tradeskillmaster.com) -- -- All Rights Reserved* - Detailed license information included with addon. -- -- ------------------------------------------------------------------------------ -- -- This file contains price related TSMAPI functions. local TSM = select(2, ...) local moduleObjects = TSM.moduleObjects local L = LibStub("AceLocale-3.0"):GetLocale("TradeSkillMaster") -- loads the localization table TSM_PRICE_TEMP = {loopError=function(str) TSM:Printf(L["Loop detected in the following custom price:"].." "..TSMAPI.Design:GetInlineColor("link")..str.."|r") end} local MONEY_PATTERNS = { "([0-9]+g[ ]*[0-9]+s[ ]*[0-9]+c)", -- g/s/c "([0-9]+g[ ]*[0-9]+s)", -- g/s "([0-9]+g[ ]*[0-9]+c)", -- g/c "([0-9]+s[ ]*[0-9]+c)", -- s/c "([0-9]+g)", -- g "([0-9]+s)", -- s "([0-9]+c)", -- c } local MATH_FUNCTIONS = { ["avg"] = "_avg", ["min"] = "_min", ["max"] = "_max", ["first"] = "_first", ["check"] = "_check", } function TSMAPI:GetPriceSources() local sources = {} for _, obj in pairs(moduleObjects) do if obj.priceSources then for _, info in ipairs(obj.priceSources) do sources[info.key] = info.label end end end return sources end local itemValueKeyCache = {} function TSMAPI:GetItemValue(link, key) local itemLink = select(2, TSMAPI:GetSafeItemInfo(link)) or link if not itemLink then return end -- look in module objects for this key if itemValueKeyCache[key] then local info = itemValueKeyCache[key] return info.callback(itemLink, info.arg) end for _, obj in pairs(moduleObjects) do if obj.priceSources then for _, info in ipairs(obj.priceSources) do if info.key == key then itemValueKeyCache[key] = info local value = info.callback(itemLink, info.arg) return (type(value) == "number" and value > 0) and value or nil end end end end end -- validates a price string that was passed into TSMAPI:ParseCustomPrice local supportedOperators = { "+", "-", "*", "/" } local function ParsePriceString(str, badPriceSource) if tonumber(str) then return function() return tonumber(str) end end local origStr = str -- make everything lower case str = strlower(str) -- remove any colors around gold/silver/copper str = gsub(str, "|cff([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])g|r", "g") str = gsub(str, "|cff([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])s|r", "s") str = gsub(str, "|cff([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])c|r", "c") -- replace all formatted gold amount with their copper value local start = 1 local goldAmountContinue = true while goldAmountContinue do goldAmountContinue = false local minFind = {} for _, pattern in ipairs(MONEY_PATTERNS) do local s, e, sub = strfind(str, pattern, start) if s and (not minFind.s or minFind.s > s) then minFind.s = s minFind.e = e minFind.sub = sub end end if minFind.s then local value = TSMAPI:UnformatTextMoney(minFind.sub) if not value then return end -- sanity check local preStr = strsub(str, 1, minFind.s-1) local postStr = strsub(str, minFind.e+1) str = preStr .. value .. postStr start = #str - #postStr + 1 goldAmountContinue = true end end -- remove up to 1 occurance of convert(priceSource[, item]) local convertPriceSource, convertItem local _, _, convertParams = strfind(str, "convert%(([^%)]+)%)") if convertParams then local source local s = strfind(convertParams, "|c") if s then local _, e = strfind(convertParams, "|r") local itemString = e and TSMAPI:GetItemString(strsub(convertParams, s, e)) if not itemString then return nil, L["Invalid item link."] end -- there's an invalid item link in the convertParams convertItem = itemString source = strsub(convertParams, 1, s - 1) elseif strfind(convertParams, "item:") then local s, e = strfind(convertParams, "item:([0-9]+):?([0-9]*):?([0-9]*):?([0-9]*):?([0-9]*):?([0-9]*):?([0-9]*)") convertItem = strsub(convertParams, s, e) source = strsub(convertParams, 1, s - 1) elseif strfind(convertParams, "battlepet:") then local s, e = strfind(convertParams, "item:([0-9]+):?([0-9]*):?([0-9]*):?([0-9]*):?([0-9]*):?([0-9]*):?([0-9]*)") convertItem = strsub(convertParams, s, e) source = strsub(convertParams, 1, s - 1) else source = convertParams end source = gsub(source:trim(), ",$", ""):trim() for key in pairs(TSMAPI:GetPriceSources()) do if strlower(key) == source then convertPriceSource = key break end end if not convertPriceSource then return nil, L["Invalid price source in convert."] end local num = 0 str, num = gsub(str, "convert%(([^%)]+)%)", "~convert~") if num > 1 then return nil, L["A maximum of 1 convert() function is allowed."] end end -- replace all item links with "~item~" local items = {} while true do local s = strfind(str, "|c") if not s then break end -- no more item links local _, e = strfind(str, "|r") local itemString = e and TSMAPI:GetItemString(strsub(str, s, e)) if not itemString then return nil, L["Invalid item link."] end -- there's an invalid item link in the str tinsert(items, itemString) str = strsub(str, 1, s - 1) .. "~item~" .. strsub(str, e + 1) end -- replace all itemStrings with "~item~" while true do local s, e if strfind(str, "item:") then s, e = strfind(str, "item:([0-9]+):?([0-9]*):?([0-9]*):?([0-9]*):?([0-9]*):?([0-9]*):?([0-9]*)") elseif strfind(str, "battlepet:") then s, e = strfind(str, "battlepet:([0-9]+):?([0-9]*):?([0-9]*):?([0-9]*):?([0-9]*):?([0-9]*):?([0-9]*)") else break end local itemString = strsub(str, s, e) tinsert(items, itemString) str = strsub(str, 1, s - 1) .. "~item~" .. strsub(str, e + 1) end -- make sure there's spaces on either side of operators for _, operator in ipairs(supportedOperators) do str = gsub(str, TSMAPI:StrEscape(operator), " " .. operator .. " ") end -- add space to start of string for percentage matching str = " "..str -- fix any whitespace issues around item links and remove parenthesis str = gsub(str, "%(([ ]*)~item~([ ]*)%)", " ~item~") -- ensure a space on either side of parenthesis and commas str = gsub(str, "%(", " ( ") str = gsub(str, "%)", " ) ") str = gsub(str, ",", " , ") -- remove any occurances of more than one consecutive space str = gsub(str, "([ ]+)", " ") -- ensure equal number of left/right parenthesis if select(2, gsub(str, "%(", "")) ~= select(2, gsub(str, "%)", "")) then return nil, L["Unbalanced parentheses."] end -- convert percentages to decimal numbers for leading, pctValue in gmatch(str, "([^0-9%.])([0-9%.]+)%%") do if tonumber(pctValue) then local number = tonumber(pctValue) / 100 str = gsub(str, leading..pctValue.."%%", leading .. number .. " *") end end -- create array of valid price sources local priceSourceKeys = {} for key in pairs(TSMAPI:GetPriceSources()) do tinsert(priceSourceKeys, strlower(key)) end for key in pairs(TSM.db.global.customPriceSources) do tinsert(priceSourceKeys, strlower(key)) end -- validate all words in the string local parts = TSMAPI:SafeStrSplit(str:trim(), " ") for i, word in ipairs(parts) do if tContains(supportedOperators, word) then if i == #parts then return nil, L["Invalid operator at end of custom price."] end -- valid operand elseif badPriceSource == word then -- price source that's explicitly invalid return nil, format(L["You cannot use %s as part of this custom price."], word) elseif tContains(priceSourceKeys, word) then -- valid price source elseif tonumber(word) then -- make sure it's not an itemID (incorrect) if i > 2 and parts[i-1] == "(" and tContains(priceSourceKeys, parts[i-2]) then return nil, L["Invalid parameter to price source."] end -- valid number elseif word == "~item~" then -- make sure previous word was a price source if i > 1 and tContains(priceSourceKeys, parts[i-1]) then -- valid item parameter else return nil, L["Item links may only be used as parameters to price sources."] end elseif word == "(" or word == ")" then -- valid parenthesis elseif word == "," then if not parts[i+1] or parts[i+1] == ")" then return nil, L["Misplaced comma"] else -- we're hoping this is a valid comma within a function, will be caught by loadstring otherwise end elseif MATH_FUNCTIONS[word] then if not parts[i+1] or parts[i+1] ~= "(" then return nil, format(L["Invalid word: '%s'"], word) end -- valid math function elseif word == "~convert~" then -- valid convert statement elseif word:trim() == "" then -- harmless extra spaces else return nil, format(L["Invalid word: '%s'"], word) end end for key in pairs(TSMAPI:GetPriceSources()) do -- replace all " ~item~" occurances with the parameters to TSMAPI:GetItemValue (with "~item~" left in for the item) for match in gmatch(" "..str.." ", " "..strlower(key).." ~item~") do match = match:trim() str = gsub(str, match, "(\"~item~\",\"" .. key .. "\",\"reg\")") end -- replace all "" occurances with the parameters to TSMAPI:GetItemValue (with _item for the item) for match in gmatch(" "..str.." ", " "..strlower(key).." ") do match = match:trim() str = gsub(str, match, "(\"_item\",\"" .. key .. "\",\"reg\")") end end for key in pairs(TSM.db.global.customPriceSources) do -- price sources need to have at least 1 capital letter for this algorithm to work, so temporarily give it one key = strupper(strsub(key, 1, 1))..strsub(key, 2) -- replace all " ~item~" occurances with the parameters to TSMAPI:GetCustomPriceSourceValue (with "~item~" left in for the item) for match in gmatch(" "..str.." ", " " .. strlower(key) .. " ~item~") do match = match:trim() str = gsub(str, match, "(\"~item~\",\"" .. key .. "\",\"custom\")") end -- replace all "" occurances with the parameters to TSMAPI:GetCustomPriceSourceValue (with _item for the item) for match in gmatch(" "..str.." ", " " .. strlower(key) .. " ") do match = match:trim() str = gsub(str, match, "(\"_item\",\"" .. key .. "\",\"custom\")") end -- change custom price sources back to lower case str = gsub(str, TSMAPI:StrEscape("(\"~item~\",\"" .. key .. "\",\"custom\")"), strlower("(\"~item~\",\"" .. key .. "\",\"custom\")")) str = gsub(str, TSMAPI:StrEscape("(\"_item\",\"" .. key .. "\",\"custom\")"), strlower("(\"_item\",\"" .. key .. "\",\"custom\")")) end -- replace all occurances of "~item~" with the item link for match in gmatch(str, "~item~") do local itemString = tremove(items, 1) if not itemString then return nil, L["Wrong number of item links."] end str = gsub(str, match, itemString, 1) end -- replace any itemValue API calls with a lookup in the 'values' array local itemValues = {} for match in gmatch(str, "%(\"([^%)]+)%)") do local index = #itemValues + 1 itemValues[index] = "{\"" .. match .. "}" str = gsub(str, TSMAPI:StrEscape("(\"" .. match .. ")"), "values[" .. index .. "]") end -- replace "~convert~" appropriately if convertPriceSource then tinsert(itemValues, "{\"" .. (convertItem or "_item") .. "\",\"convert\",\"" .. convertPriceSource .. "\"}") str = gsub(str, "~convert~", "values[" .. #itemValues .. "]") end -- replace math functions special custom function names for word, funcName in pairs(MATH_FUNCTIONS) do str = gsub(str, word, funcName) end -- remove any unused values for i in ipairs(itemValues) do if not strfind(" "..str.." ", " values%["..i.."%] ") then itemValues[i] = "{}" end end -- finally, create and return the function local funcTemplate = [[ return function(_item) local NAN = math.huge*0 local origStr = %s local isTop if not TSM_PRICE_TEMP.num then TSM_PRICE_TEMP.num = 0 isTop = true end TSM_PRICE_TEMP.num = TSM_PRICE_TEMP.num + 1 if TSM_PRICE_TEMP.num > 100 then if (TSM_PRICE_TEMP.lastPrint or 0) + 1 < time() then TSM_PRICE_TEMP.lastPrint = time() TSM_PRICE_TEMP.loopError(origStr) end return end local function isNAN(num) return tostring(num) == tostring(NAN) end local function _min(...) local nums = {...} for i=#nums, 1, -1 do if isNAN(nums[i]) then tremove(nums, i) end end if #nums == 0 then return NAN end return min(unpack(nums)) end local function _max(...) local nums = {...} for i=#nums, 1, -1 do if isNAN(nums[i]) then tremove(nums, i) end end if #nums == 0 then return NAN end return max(unpack(nums)) end local function _first(...) local nums = {...} for i=1, #nums do if type(nums[i]) == "number" and not isNAN(nums[i]) then return nums[i] end end return NAN end local function _avg(...) local nums = {...} local total, count = 0, 0 for i=#nums, 1, -1 do if type(nums[i]) == "number" and not isNAN(nums[i]) then total = total + nums[i] count = count + 1 end end if count == 0 then return NAN end return floor(total / count + 0.5) end local function _check(...) if select('#', ...) > 3 then return end local check, ifValue, elseValue = ... check = check or NAN ifValue = ifValue or NAN elseValue = elseValue or NAN return check > 0 and ifValue or elseValue end local values = {} for i, params in ipairs({%s}) do local itemString, key, extraParam = unpack(params) if itemString then itemString = (itemString == "_item") and _item or itemString if key == "convert" then values[i] = TSMAPI:GetConvertCost(itemString, extraParam) elseif extraParam == "custom" then values[i] = TSMAPI:GetCustomPriceSourceValue(itemString, key) else values[i] = TSMAPI:GetItemValue(itemString, key) end values[i] = values[i] or NAN end end local result = floor((%s) + 0.5) if TSM_PRICE_TEMP.num then TSM_PRICE_TEMP.num = TSM_PRICE_TEMP.num - 1 end if isTop then TSM_PRICE_TEMP.num = nil end return not isNAN(result) and result or nil end ]] local func, loadErr = loadstring(format(funcTemplate, "\""..origStr.."\"", table.concat(itemValues, ","), str)) if loadErr then loadErr = gsub(loadErr:trim(), "([^:]+):.", "") return nil, L["Invalid function."].." Details: "..loadErr end local success, func = pcall(func) if not success then return nil, L["Invalid function."] end return func end local customPriceCache = {} local badCustomPriceCache = {} function TSMAPI:ParseCustomPrice(priceString, badPriceSource) priceString = strlower(tostring(priceString):trim()) if priceString == "" then return nil, L["Empty price string."] end if badCustomPriceCache[priceString] then return nil, badCustomPriceCache[priceString] end if customPriceCache[priceString] then return customPriceCache[priceString] end local func, err = ParsePriceString(priceString, badPriceSource) if err then badCustomPriceCache[priceString] = err return nil, err end customPriceCache[priceString] = func return func end function TSMAPI:GetCustomPriceSourceValue(itemString, key) local source = TSM.db.global.customPriceSources[key] if not source then return end local func = TSMAPI:ParseCustomPrice(source) if not func then return end return func(itemString) end