458 lines
16 KiB
Lua
458 lines
16 KiB
Lua
-- ------------------------------------------------------------------------------ --
|
|
-- 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 "<priceSource> ~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 "<priceSource>" 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 "<customPriceSource> ~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 "<customPriceSource>" 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 |