1039 lines
36 KiB
Lua
1039 lines
36 KiB
Lua
-- ------------------------------------------------------------------------------ --
|
|
-- TradeSkillMaster --
|
|
-- https://tradeskillmaster.com --
|
|
-- All Rights Reserved - Detailed license information included with addon. --
|
|
-- ------------------------------------------------------------------------------ --
|
|
|
|
--- Custom Price Functions
|
|
-- @module CustomPrice
|
|
|
|
local _, TSM = ...
|
|
local CustomPrice = TSM.Init("Service.CustomPrice")
|
|
local L = TSM.Include("Locale").GetTable()
|
|
local DisenchantInfo = TSM.Include("Data.DisenchantInfo")
|
|
local TempTable = TSM.Include("Util.TempTable")
|
|
local Table = TSM.Include("Util.Table")
|
|
local Math = TSM.Include("Util.Math")
|
|
local Money = TSM.Include("Util.Money")
|
|
local String = TSM.Include("Util.String")
|
|
local Log = TSM.Include("Util.Log")
|
|
local Theme = TSM.Include("Util.Theme")
|
|
local ItemString = TSM.Include("Util.ItemString")
|
|
local SmartMap = TSM.Include("Util.SmartMap")
|
|
local ItemInfo = TSM.Include("Service.ItemInfo")
|
|
local Settings = TSM.Include("Service.Settings")
|
|
local Conversions = TSM.Include("Service.Conversions")
|
|
local private = {
|
|
context = {},
|
|
priceSourceKeys = {},
|
|
priceSourceInfo = {},
|
|
customPriceCache = {},
|
|
proxyData = {},
|
|
settings = nil,
|
|
sanitizeCache = {},
|
|
customSourceCallbacks = {},
|
|
}
|
|
local ITEM_STRING_PATTERN = "[ip]:[0-9:%-]+"
|
|
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"] = "self._avg",
|
|
["min"] = "self._min",
|
|
["max"] = "self._max",
|
|
["first"] = "self._first",
|
|
["check"] = "self._check",
|
|
["ifgt"] = "self._ifgt",
|
|
["ifgte"] = "self._ifgte",
|
|
["iflt"] = "self._iflt",
|
|
["iflte"] = "self._iflte",
|
|
["ifeq"] = "self._ifeq",
|
|
["round"] = "self._round",
|
|
["roundup"] = "self._roundup",
|
|
["rounddown"] = "self._rounddown",
|
|
}
|
|
local CUSTOM_PRICE_FUNC_TEMPLATE = [[
|
|
return function(self, _item, _baseitem)
|
|
local isTop
|
|
local context = self.globalContext
|
|
if not context.num then
|
|
context.num = 0
|
|
isTop = true
|
|
end
|
|
context.num = context.num + 1
|
|
if context.num > 100 then
|
|
if (context.lastPrint or 0) + 1 < time() then
|
|
context.lastPrint = time()
|
|
self.loopError(self.origStr)
|
|
end
|
|
return
|
|
end
|
|
|
|
local result = floor((%s) + 0.5)
|
|
if context.num then
|
|
context.num = context.num - 1
|
|
end
|
|
if isTop then
|
|
context.num = nil
|
|
end
|
|
if not result or self.IsInvalid(result) or result <= 0 then return end
|
|
return result
|
|
end
|
|
]]
|
|
local function IsInvalid(num)
|
|
-- We want to treat math.huge/-math.huge/NAN as invalid.
|
|
return num == math.huge or num == -math.huge or Math.IsNan(num)
|
|
end
|
|
-- Make sure our IsInvalid function continues to work as expected
|
|
assert(IsInvalid(Math.GetNan()) and IsInvalid(math.huge) and IsInvalid(math.huge) and not IsInvalid(0) and not IsInvalid(1000))
|
|
local COMPARISONS = {
|
|
["gt"] = 1,
|
|
["gte"] = 2,
|
|
["lt"] = 3,
|
|
["lte"] = 4,
|
|
["eq"] = 5,
|
|
}
|
|
|
|
|
|
|
|
-- ============================================================================
|
|
-- Module Loading
|
|
-- ============================================================================
|
|
|
|
CustomPrice:OnSettingsLoad(function()
|
|
private.settings = Settings.NewView()
|
|
:AddKey("global", "userData", "customPriceSources")
|
|
|
|
for name, str in pairs(private.settings.customPriceSources) do
|
|
if CustomPrice.ValidateName(name, true) then
|
|
str = private.SanitizeCustomPriceString(str)
|
|
private.settings.customPriceSources[name] = str
|
|
for _, data in pairs(private.proxyData) do
|
|
if data.origStr == str then
|
|
data.customPriceSourceNames[name] = true
|
|
end
|
|
end
|
|
else
|
|
Log.PrintfUser(L["Removed custom price source (%s) which has an invalid name."], name)
|
|
CustomPrice.DeleteCustomPriceSource(name)
|
|
end
|
|
end
|
|
end)
|
|
|
|
|
|
|
|
-- ============================================================================
|
|
-- Module Functions
|
|
-- ============================================================================
|
|
|
|
function CustomPrice.OnEnable()
|
|
for name in pairs(TSM.db.global.userData.customPriceSources) do
|
|
if not CustomPrice.ValidateName(name, true) then
|
|
Log.PrintfUser(L["Removed custom price source (%s) which has an invalid name."], name)
|
|
CustomPrice.DeleteCustomPriceSource(name)
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Register a built-in price source.
|
|
-- @tparam string moduleName The name of the module which provides this source
|
|
-- @tparam string key The key for this price source (i.e. DBMarket)
|
|
-- @tparam string label The label which describes this price source for display to the user
|
|
-- @tparam function callback The price source callback
|
|
-- @tparam[opt=false] boolean fullLink Whether or not the full itemLink is required instead of just the itemString
|
|
-- @param[opt] arg An additional argument which is passed to the callback
|
|
-- @tparam[opt=false] booolean isVolatile Should be set if the price source may change without CustomPrice.OnSourceChange being called
|
|
function CustomPrice.RegisterSource(moduleName, key, label, callback, fullLink, arg, isVolatile)
|
|
tinsert(private.priceSourceKeys, strlower(key))
|
|
private.priceSourceInfo[strlower(key)] = {
|
|
moduleName = moduleName,
|
|
key = key,
|
|
label = label,
|
|
callback = callback,
|
|
takeItemString = not fullLink,
|
|
arg = arg,
|
|
isVolatile = isVolatile,
|
|
cache = {},
|
|
}
|
|
end
|
|
|
|
function CustomPrice.RegisterCustomSourceCallback(callback)
|
|
tinsert(private.customSourceCallbacks, callback)
|
|
end
|
|
|
|
--- Create a new custom price source.
|
|
-- @tparam string name The name of the custom price source
|
|
-- @tparam string value The value of the custom price source
|
|
function CustomPrice.CreateCustomPriceSource(name, value)
|
|
assert(name ~= "")
|
|
assert(gsub(name, "([a-z]+)", "") == "")
|
|
assert(not private.settings.customPriceSources[name])
|
|
value = private.SanitizeCustomPriceString(value)
|
|
private.settings.customPriceSources[name] = value
|
|
for _, data in pairs(private.proxyData) do
|
|
if data.origStr == value then
|
|
data.customPriceSourceNames[name] = true
|
|
end
|
|
end
|
|
wipe(private.customPriceCache)
|
|
private.CallCustomSourceCallbacks()
|
|
end
|
|
|
|
--- Rename a custom price source.
|
|
-- @tparam string oldName The old name of the custom price source
|
|
-- @tparam string newName The new name of the custom price source
|
|
function CustomPrice.RenameCustomPriceSource(oldName, newName)
|
|
if oldName == newName then
|
|
return
|
|
end
|
|
local value = private.settings.customPriceSources[oldName]
|
|
assert(value)
|
|
private.settings.customPriceSources[newName] = value
|
|
private.settings.customPriceSources[oldName] = nil
|
|
for _, data in pairs(private.proxyData) do
|
|
data.customPriceSourceNames[oldName] = nil
|
|
if data.origStr == value then
|
|
data.customPriceSourceNames[newName] = true
|
|
end
|
|
end
|
|
wipe(private.customPriceCache)
|
|
CustomPrice.OnSourceChange(oldName)
|
|
CustomPrice.OnSourceChange(newName)
|
|
private.CallCustomSourceCallbacks()
|
|
end
|
|
|
|
--- Delete a custom price source.
|
|
-- @tparam string name The name of the custom price source
|
|
function CustomPrice.DeleteCustomPriceSource(name)
|
|
assert(private.settings.customPriceSources[name])
|
|
private.settings.customPriceSources[name] = nil
|
|
for _, data in pairs(private.proxyData) do
|
|
data.customPriceSourceNames[name] = nil
|
|
end
|
|
wipe(private.customPriceCache)
|
|
CustomPrice.OnSourceChange(name)
|
|
private.CallCustomSourceCallbacks()
|
|
end
|
|
|
|
--- Sets the value of a custom price source.
|
|
-- @tparam string name The name of the custom price source
|
|
-- @tparam string value The value of the custom price source
|
|
function CustomPrice.SetCustomPriceSource(name, value)
|
|
assert(private.settings.customPriceSources[name])
|
|
value = private.SanitizeCustomPriceString(value)
|
|
private.settings.customPriceSources[name] = value
|
|
for _, data in pairs(private.proxyData) do
|
|
data.customPriceSourceNames[name] = data.origStr == value or nil
|
|
end
|
|
wipe(private.customPriceCache)
|
|
CustomPrice.OnSourceChange(name)
|
|
end
|
|
|
|
function CustomPrice.BulkCreateCustomPriceSourcesFromImport(customSources, replaceExisting)
|
|
for name, value in pairs(customSources) do
|
|
value = private.SanitizeCustomPriceString(value)
|
|
assert(not private.settings.customPriceSources[name] or replaceExisting)
|
|
if private.settings.customPriceSources[name] then
|
|
CustomPrice.SetCustomPriceSource(name, value)
|
|
else
|
|
CustomPrice.CreateCustomPriceSource(name, value)
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Print built-in price sources to chat.
|
|
function CustomPrice.PrintSources()
|
|
Log.PrintUser(L["Below is a list of all available price sources, along with a brief description of what they represent."])
|
|
local moduleList = TempTable.Acquire()
|
|
|
|
for _, info in pairs(private.priceSourceInfo) do
|
|
if not tContains(moduleList, info.moduleName) then
|
|
tinsert(moduleList, info.moduleName)
|
|
end
|
|
end
|
|
sort(moduleList, private.ModuleSortFunc)
|
|
|
|
for _, module in ipairs(moduleList) do
|
|
Log.PrintUserRaw("|cffffff00"..module..":|r")
|
|
local lines = TempTable.Acquire()
|
|
for _, info in pairs(private.priceSourceInfo) do
|
|
if info.moduleName == module then
|
|
tinsert(lines, format(" %s (%s)", Log.ColorUserAccentText(info.key), info.label))
|
|
end
|
|
end
|
|
sort(lines)
|
|
for _, line in ipairs(lines) do
|
|
Log.PrintfUserRaw(line)
|
|
end
|
|
TempTable.Release(lines)
|
|
end
|
|
|
|
TempTable.Release(moduleList)
|
|
end
|
|
|
|
function CustomPrice.GetDescription(key)
|
|
local info = private.priceSourceInfo[key]
|
|
return info and info.label or nil
|
|
end
|
|
|
|
--- Validate a custom price name.
|
|
-- @tparam string customPriceName The custom price name
|
|
-- @tparam[opt=false] boolean ignoreExistingCustomPriceSources Whether or not to ignore existing custom price sources
|
|
-- @treturn boolean Whether or not the custom price name is valid
|
|
function CustomPrice.ValidateName(customPriceName, ignoreExistingCustomPriceSources)
|
|
-- custom price names must be lowercase
|
|
if strlower(customPriceName) ~= customPriceName then
|
|
return false, L["Custom price names can only contain lowercase letters."]
|
|
end
|
|
-- User defined price sources
|
|
if not ignoreExistingCustomPriceSources and private.settings.customPriceSources[customPriceName] then
|
|
return false, format(L["Custom price name %s already exists."], Theme.GetColor("INDICATOR"):ColorText(customPriceName))
|
|
end
|
|
-- TSM defined price sources
|
|
for source in CustomPrice.Iterator() do
|
|
if strlower(source) == strlower(customPriceName) then
|
|
return false, format(L["Custom price name %s is a reserved word which cannot be used."], Theme.GetColor("INDICATOR"):ColorText(customPriceName))
|
|
end
|
|
end
|
|
-- Math Functions
|
|
for mathFunction in pairs(MATH_FUNCTIONS) do
|
|
if strlower(mathFunction) == strlower(customPriceName) then
|
|
return false, format(L["Custom price name %s is a reserved word which cannot be used."], Theme.GetColor("INDICATOR"):ColorText(customPriceName))
|
|
end
|
|
end
|
|
-- Comparisons
|
|
for comparison in pairs(COMPARISONS) do
|
|
if strlower(comparison) == strlower(customPriceName) then
|
|
return false, format(L["Custom price name %s is a reserved word which cannot be used."], Theme.GetColor("INDICATOR"):ColorText(customPriceName))
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
--- Validate a custom price string.
|
|
-- @tparam string customPriceStr The custom price string
|
|
-- @tparam ?table badPriceSources A table of price sources (as keys) which aren't allowed to be used
|
|
-- @treturn boolean Whether or not the custom price string is valid
|
|
-- @treturn ?string The error message if the custom price string was invalid
|
|
function CustomPrice.Validate(customPriceStr, badPriceSources)
|
|
local proxy, err = private.ParseCustomPrice(customPriceStr, badPriceSources)
|
|
return proxy and true or false, err
|
|
end
|
|
|
|
--- Evaulates a custom price source for an item.
|
|
-- @tparam string customPriceStr The custom price string
|
|
-- @tparam string itemString The item to evalulate the custom price string for
|
|
-- @tparam[opt=false] boolean allowZero If true, allows the result to be 0
|
|
-- @treturn ?number The resulting value or nil if the custom price string is invalid
|
|
-- @treturn ?string The error message if the custom price string was invalid
|
|
function CustomPrice.GetValue(customPriceStr, itemString, allowZero)
|
|
local proxy, err = private.ParseCustomPrice(customPriceStr)
|
|
if not proxy then
|
|
return nil, err
|
|
end
|
|
local value = nil
|
|
local mapReader = private.proxyData[proxy].mapReader
|
|
if mapReader then
|
|
value = mapReader[itemString]
|
|
else
|
|
value = proxy(itemString)
|
|
end
|
|
if not value or value < 0 or (not allowZero and value == 0) then
|
|
return nil, L["No value was returned by the custom price for the specified item."]
|
|
end
|
|
return value
|
|
end
|
|
|
|
--- Gets a built-in price source's value for an item.
|
|
-- @tparam string itemString The item to evalulate the price source for
|
|
-- @tparam string key The key of the price source
|
|
-- @treturn ?number The resulting value or nil if no price was found for the item
|
|
function CustomPrice.GetItemPrice(itemString, key)
|
|
itemString = ItemString.Get(itemString)
|
|
if not itemString then
|
|
return
|
|
end
|
|
|
|
local info = private.priceSourceInfo[strlower(key)]
|
|
if not info then
|
|
return
|
|
end
|
|
local cachedValue = info.cache[itemString]
|
|
if cachedValue ~= nil then
|
|
assert(not info.isVolatile)
|
|
return cachedValue or nil
|
|
end
|
|
if not info.takeItemString then
|
|
-- this price source does not take an itemString, so pass it an itemLink instead
|
|
itemString = ItemInfo.GetLink(itemString)
|
|
if not itemString then
|
|
return
|
|
end
|
|
end
|
|
local value = info.callback(itemString, info.arg)
|
|
value = type(value) == "number" and value or nil
|
|
if not info.isVolatile then
|
|
info.cache[itemString] = value or false
|
|
end
|
|
return value
|
|
end
|
|
|
|
function CustomPrice.GetConversionsValue(sourceItemString, customPrice, method)
|
|
if not customPrice then
|
|
return
|
|
end
|
|
|
|
-- calculate disenchant value first
|
|
if (not method or method == Conversions.METHOD.DISENCHANT) and ItemInfo.IsDisenchantable(sourceItemString) then
|
|
local quality = ItemInfo.GetQuality(sourceItemString)
|
|
local ilvl = ItemInfo.GetItemLevel(ItemString.GetBase(sourceItemString)) or 0
|
|
local classId = ItemInfo.GetClassId(sourceItemString)
|
|
local value = 0
|
|
if quality and ilvl and classId then
|
|
for targetItemString in DisenchantInfo.TargetItemIterator() do
|
|
local amountOfMats = DisenchantInfo.GetTargetItemSourceInfo(targetItemString, classId, quality, ilvl)
|
|
if amountOfMats then
|
|
local matValue = CustomPrice.GetValue(customPrice, targetItemString)
|
|
if not matValue or matValue == 0 then
|
|
return
|
|
end
|
|
value = value + matValue * amountOfMats
|
|
end
|
|
end
|
|
end
|
|
|
|
value = floor(value)
|
|
if value > 0 then
|
|
return value
|
|
end
|
|
end
|
|
|
|
-- calculate other conversion values
|
|
local value = 0
|
|
for targetItemString, rate in Conversions.TargetItemsByMethodIterator(sourceItemString, method) do
|
|
local matValue = CustomPrice.GetValue(customPrice, targetItemString)
|
|
value = value + (matValue or 0) * rate
|
|
end
|
|
|
|
value = Math.Round(value)
|
|
return value > 0 and value or nil
|
|
end
|
|
|
|
local function CustomPriceIteratorHelper(_, key)
|
|
local info = private.priceSourceInfo[key]
|
|
return info.key, info.moduleName, info.label
|
|
end
|
|
--- Iterate over the price sources.
|
|
-- @return An iterator which provides the following fields: `key, moduleName, label`
|
|
function CustomPrice.Iterator()
|
|
return Table.Iterator(private.priceSourceKeys, CustomPriceIteratorHelper)
|
|
end
|
|
|
|
--- Should be called when the value of a registered source changes.
|
|
-- @tparam string key The key of the price source
|
|
-- @tparam[opt=nil] string itemString The item which the source changed for or nil if it changed for all items
|
|
function CustomPrice.OnSourceChange(key, itemString)
|
|
key = strlower(key)
|
|
if private.priceSourceInfo[key] then
|
|
if itemString then
|
|
private.priceSourceInfo[key].cache[itemString] = nil
|
|
else
|
|
wipe(private.priceSourceInfo[key].cache)
|
|
end
|
|
end
|
|
local isSpecificItem = itemString ~= ItemString.GetBase(itemString) or not ItemString.HasNonBase(itemString)
|
|
for _, data in pairs(private.proxyData) do
|
|
if data.map then
|
|
local clearAll, clearItem, clearBaseItem = false, false, false
|
|
if data.dependantPriceSources[key] then
|
|
if not itemString then
|
|
-- clear all items
|
|
clearAll = true
|
|
else
|
|
for dependantItemString in pairs(data.dependantPriceSources[key]) do
|
|
if dependantItemString == "_item" then
|
|
if isSpecificItem then
|
|
-- just clear the specific item
|
|
clearItem = true
|
|
else
|
|
-- clear all items which have a matching baseItemString
|
|
clearBaseItem = true
|
|
end
|
|
elseif dependantItemString == "_baseitem" then
|
|
if not isSpecificItem then
|
|
-- just clear the item which was passed (and was a base item)
|
|
clearItem = true
|
|
end
|
|
else
|
|
if dependantItemString == (isSpecificItem and itemString or ItemString.GetBase(dependantItemString)) then
|
|
-- clear all items
|
|
clearAll = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if data.dependantPriceSources._convertPriceSource == key then
|
|
-- TODO: could optimize this to only clear the items which have the specified item as a source item, but this should be pretty rare
|
|
clearAll = true
|
|
end
|
|
|
|
data.map:SetCallbacksPaused(true)
|
|
if clearAll then
|
|
for mapItemString in data.map:Iterator() do
|
|
data.map:ValueChanged(mapItemString)
|
|
end
|
|
for name in pairs(data.customPriceSourceNames) do
|
|
CustomPrice.OnSourceChange(name)
|
|
end
|
|
elseif clearBaseItem then
|
|
for mapItemString in data.map:Iterator() do
|
|
if ItemString.GetBase(mapItemString) == itemString then
|
|
data.map:ValueChanged(mapItemString)
|
|
for name in pairs(data.customPriceSourceNames) do
|
|
CustomPrice.OnSourceChange(name, mapItemString)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if not clearAll and clearItem then
|
|
data.map:ValueChanged(itemString)
|
|
for name in pairs(data.customPriceSourceNames) do
|
|
CustomPrice.OnSourceChange(name, itemString)
|
|
end
|
|
end
|
|
data.map:SetCallbacksPaused(false)
|
|
end
|
|
end
|
|
end
|
|
|
|
function CustomPrice.DependantCustomSourceIterator(str)
|
|
local result = TempTable.Acquire()
|
|
local proxy = private.ParseCustomPrice(str)
|
|
if proxy then
|
|
local data = private.proxyData[proxy]
|
|
for name, customSourceStr in pairs(TSM.db.global.userData.customPriceSources) do
|
|
if data.dependantPriceSources[name] then
|
|
tinsert(result, name)
|
|
tinsert(result, customSourceStr)
|
|
end
|
|
end
|
|
end
|
|
return TempTable.Iterator(result, 2)
|
|
end
|
|
|
|
|
|
|
|
-- ============================================================================
|
|
-- Helper Functions
|
|
-- ============================================================================
|
|
|
|
private.customPriceFunctions = {
|
|
IsInvalid = IsInvalid,
|
|
loopError = function(str)
|
|
Log.PrintUser(L["Loop detected in the following custom price:"].." "..Log.ColorUserAccentText(str))
|
|
end,
|
|
_avg = function(...)
|
|
local total, count = 0, 0
|
|
for i = 1, select('#', ...) do
|
|
local num = select(i, ...)
|
|
if type(num) == "number" and not IsInvalid(num) then
|
|
total = total + num
|
|
count = count + 1
|
|
end
|
|
end
|
|
return count == 0 and Math.GetNan() or (total / count)
|
|
end,
|
|
_min = function(...)
|
|
local minVal = nil
|
|
for i = 1, select('#', ...) do
|
|
local num = select(i, ...)
|
|
if type(num) == "number" and not IsInvalid(num) and (not minVal or num < minVal) then
|
|
minVal = num
|
|
end
|
|
end
|
|
return minVal or Math.GetNan()
|
|
end,
|
|
_max = function(...)
|
|
local maxVal = nil
|
|
for i = 1, select('#', ...) do
|
|
local num = select(i, ...)
|
|
if type(num) == "number" and not IsInvalid(num) and (not maxVal or num > maxVal) then
|
|
maxVal = num
|
|
end
|
|
end
|
|
return maxVal or Math.GetNan()
|
|
end,
|
|
_first = function(...)
|
|
for i = 1, select('#', ...) do
|
|
local num = select(i, ...)
|
|
if type(num) == "number" and not IsInvalid(num) then
|
|
return num
|
|
end
|
|
end
|
|
return Math.GetNan()
|
|
end,
|
|
_check = function(check, ...)
|
|
return private.RunComparison(COMPARISONS.gt, check, 0, ...)
|
|
end,
|
|
_ifgt = function(...)
|
|
return private.RunComparison(COMPARISONS.gt, ...)
|
|
end,
|
|
_ifgte = function(...)
|
|
return private.RunComparison(COMPARISONS.gte, ...)
|
|
end,
|
|
_iflt = function(...)
|
|
return private.RunComparison(COMPARISONS.lt, ...)
|
|
end,
|
|
_iflte = function(...)
|
|
return private.RunComparison(COMPARISONS.lte, ...)
|
|
end,
|
|
_ifeq = function(...)
|
|
return private.RunComparison(COMPARISONS.eq, ...)
|
|
end,
|
|
_round = function(...)
|
|
if select('#', ...) < 1 or select('#', ...) > 2 then return Math.GetNan() end
|
|
return Math.Round(...)
|
|
end,
|
|
_roundup = function(...)
|
|
if select('#', ...) < 1 or select('#', ...) > 2 then return Math.GetNan() end
|
|
return Math.Ceil(...)
|
|
end,
|
|
_rounddown = function(...)
|
|
if select('#', ...) < 1 or select('#', ...) > 2 then return Math.GetNan() end
|
|
return Math.Floor(...)
|
|
end,
|
|
_priceHelper = function(itemString, key, extraParam)
|
|
itemString = ItemString.Get(itemString)
|
|
if not itemString then
|
|
return Math.GetNan()
|
|
end
|
|
if key == "convert" then
|
|
local conversions = Conversions.GetSourceItems(itemString)
|
|
if not conversions then
|
|
return Math.GetNan()
|
|
end
|
|
local minPrice = nil
|
|
for sourceItemString, rate in pairs(conversions) do
|
|
local price = CustomPrice.GetItemPrice(sourceItemString, extraParam)
|
|
if price then
|
|
price = price / rate
|
|
minPrice = min(minPrice or price, price)
|
|
end
|
|
end
|
|
return minPrice or Math.GetNan()
|
|
elseif extraParam == "custom" then
|
|
local customPriceSourceStr = private.settings.customPriceSources[key]
|
|
if not customPriceSourceStr then
|
|
-- custom price source has since been deleted
|
|
return Math.GetNan()
|
|
end
|
|
return CustomPrice.GetValue(customPriceSourceStr, itemString) or Math.GetNan()
|
|
else
|
|
return CustomPrice.GetItemPrice(itemString, key) or Math.GetNan()
|
|
end
|
|
end,
|
|
}
|
|
local PROXY_MT = {
|
|
__index = function(self, index)
|
|
local data = private.proxyData[self]
|
|
if private.customPriceFunctions[index] then
|
|
return private.customPriceFunctions[index]
|
|
elseif index == "globalContext" or index == "origStr" then
|
|
-- these keys can always be accessed
|
|
return data[index]
|
|
end
|
|
if not data.isUnlocked then
|
|
error("Attempt to access a hidden table", 2)
|
|
end
|
|
return data[index]
|
|
end,
|
|
__newindex = function(self, index, value)
|
|
local data = private.proxyData[self]
|
|
if not data.isUnlocked then
|
|
error("Attempt to modify a hidden table", 2)
|
|
end
|
|
data[index] = value
|
|
end,
|
|
__call = function(self, item)
|
|
local data = private.proxyData[self]
|
|
data.isUnlocked = true
|
|
local result = data.func(self, item, ItemString.GetBase(item)) or 0
|
|
data.isUnlocked = false
|
|
return result
|
|
end,
|
|
__metatable = false,
|
|
}
|
|
|
|
function private.RunComparison(comparison, ...)
|
|
if select('#', ...) > 4 then return Math.GetNan() end
|
|
|
|
local leftCheck, rightCheck, ifValue, elseValue = ...
|
|
leftCheck = leftCheck or Math.GetNan()
|
|
rightCheck = rightCheck or Math.GetNan()
|
|
ifValue = ifValue or Math.GetNan()
|
|
elseValue = elseValue or Math.GetNan()
|
|
|
|
if IsInvalid(leftCheck) or IsInvalid(rightCheck) then
|
|
return Math.GetNan()
|
|
elseif comparison == COMPARISONS.gt then
|
|
return leftCheck > rightCheck and ifValue or elseValue
|
|
elseif comparison == COMPARISONS.gte then
|
|
return leftCheck >= rightCheck and ifValue or elseValue
|
|
elseif comparison == COMPARISONS.lt then
|
|
return leftCheck < rightCheck and ifValue or elseValue
|
|
elseif comparison == COMPARISONS.lte then
|
|
return leftCheck <= rightCheck and ifValue or elseValue
|
|
elseif comparison == COMPARISONS.eq then
|
|
return leftCheck == rightCheck and ifValue or elseValue
|
|
else
|
|
error("Error in custom price comparison")
|
|
end
|
|
end
|
|
|
|
function private.CreateCustomPriceObj(func, origStr, dependantPriceSources, canCache)
|
|
local customPriceSourceNames = {}
|
|
for name, str in pairs(private.settings.customPriceSources) do
|
|
if str == origStr then
|
|
customPriceSourceNames[name] = true
|
|
end
|
|
end
|
|
local proxy = newproxy(true)
|
|
private.proxyData[proxy] = {
|
|
isUnlocked = false,
|
|
globalContext = private.context,
|
|
origStr = origStr,
|
|
customPriceSourceNames = customPriceSourceNames,
|
|
func = func,
|
|
dependantPriceSources = dependantPriceSources,
|
|
map = nil,
|
|
mapReader = nil,
|
|
}
|
|
local mt = getmetatable(proxy)
|
|
for key, value in pairs(PROXY_MT) do
|
|
mt[key] = value
|
|
end
|
|
if canCache then
|
|
local map = SmartMap.New("string", "number", proxy)
|
|
private.proxyData[proxy].map = map
|
|
private.proxyData[proxy].mapReader = map:CreateReader()
|
|
end
|
|
return proxy
|
|
end
|
|
|
|
function private.ParsePriceString(str, badPriceSources)
|
|
if tonumber(str) then
|
|
return private.CreateCustomPriceObj(function() return Math.Round(tonumber(str)) end, str, {}, true)
|
|
end
|
|
|
|
local origStr = str
|
|
-- put a space at the start and end
|
|
str = " "..str.." "
|
|
-- remove any colors around gold/silver/copper
|
|
while true do
|
|
local num1, num2, num3
|
|
str, num1 = gsub(str, "\124cff[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\124r", "g")
|
|
str, num2 = gsub(str, "\124cff[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\124r", "s")
|
|
str, num3 = gsub(str, "\124cff[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\124r", "c")
|
|
if num1 + num2 + num3 == 0 then break end
|
|
end
|
|
-- replace old itemStrings with the new format
|
|
str = gsub(str, "([^h]i)tem:([0-9:%-]+)", "%1:%2")
|
|
|
|
-- replace all formatted gold amount with their copper value
|
|
local start = 1
|
|
local goldAmountContinue = true
|
|
while goldAmountContinue do
|
|
goldAmountContinue = false
|
|
local minFindStart, minFindEnd, minFindSub = nil, nil, nil
|
|
for _, pattern in ipairs(MONEY_PATTERNS) do
|
|
local s, e, sub = strfind(str, pattern, start)
|
|
if s and (not minFindStart or minFindStart > s + 1) then
|
|
minFindStart = s + 1
|
|
minFindEnd = e
|
|
minFindSub = sub
|
|
end
|
|
end
|
|
if minFindStart then
|
|
if strmatch(strsub(str, minFindStart-1, minFindStart-1), "[0-9a-zA-Z]") or strmatch(strsub(str, minFindEnd+1, minFindEnd+1), "[0-9a-zA-Z]") then
|
|
return nil, L["Invalid gold value."]
|
|
end
|
|
local value = Money.FromString(minFindSub)
|
|
if not value then
|
|
-- sanity check
|
|
return nil, L["Invalid function."]
|
|
end
|
|
local preStr = strsub(str, 1, minFindStart - 1)
|
|
local postStr = strsub(str, minFindEnd + 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 = strmatch(str, "[^a-z]convert%((.-)%)")
|
|
if convertParams then
|
|
local convertItemLink = strmatch(convertParams, "\124c.-\124r")
|
|
local convertItemString = strmatch(convertParams, ITEM_STRING_PATTERN)
|
|
if convertItemLink then -- check for itemLink in convert params
|
|
convertItem = ItemString.Get(convertItemLink)
|
|
if not convertItem then
|
|
return nil, L["Invalid item link."] -- there's an invalid item link in the convertParams
|
|
end
|
|
convertPriceSource = strmatch(convertParams, "^ *(.-) *,")
|
|
elseif convertItemString then -- check for itemString in convert params
|
|
convertItem = convertItemString
|
|
convertPriceSource = strmatch(convertParams, "^ *(.-) *,")
|
|
else
|
|
convertPriceSource = gsub(convertParams, ", *$", "")
|
|
convertPriceSource = strtrim(convertPriceSource)
|
|
end
|
|
if convertPriceSource and ((badPriceSources and badPriceSources[convertPriceSource]) or convertPriceSource == "matprice") then
|
|
return nil, format(L["You cannot use %s within convert() as part of this custom price."], convertPriceSource)
|
|
end
|
|
|
|
-- can't allow custom price sources in convert, so just check regular ones
|
|
if not private.priceSourceInfo[convertPriceSource] then
|
|
return nil, L["Invalid price source in convert."]
|
|
end
|
|
local num = 0
|
|
str, num = gsub(str, "([^a-z])convert%(.-%)", "%1~convert~")
|
|
if num > 1 then
|
|
return nil, L["A maximum of 1 convert() function is allowed."]
|
|
end
|
|
end
|
|
|
|
while true do
|
|
local itemLink = strmatch(str, "\124c.*\124r")
|
|
if not itemLink then break end
|
|
local _, endIndex = strfind(itemLink, "\124r")
|
|
itemLink = strsub(itemLink, 1, endIndex)
|
|
local itemString = ItemString.Get(itemLink)
|
|
if not itemString then
|
|
-- there's an invalid item link in the str
|
|
return nil, L["Invalid item link."]
|
|
end
|
|
str = gsub(str, String.Escape(itemLink), itemString)
|
|
end
|
|
|
|
-- make sure there's spaces on either side of math operators
|
|
str = gsub(str, "[%-%+%/%*%^]", " %1 ")
|
|
-- make sure there's a space to the right of % signs
|
|
str = gsub(str, "[%%]", "%1 ")
|
|
-- convert percentages to decimal numbers
|
|
str = gsub(str, "([0-9%.]+)%%", "( %1 / 100 ) *")
|
|
-- ensure a space on either side of item strings and remove parentheses around them
|
|
str = gsub(str, "%([ ]*("..ITEM_STRING_PATTERN..")[ ]*%)", " %1 ")
|
|
-- ensure a space on either side of baseitem arguments and remove parentheses around them
|
|
str = gsub(str, "%([ ]*(baseitem)[ ]*%)", " ~baseitem~ ")
|
|
-- ensure a space on either side of parentheses and commas
|
|
str = gsub(str, "[%(%),]", " %1 ")
|
|
-- 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
|
|
|
|
-- validate all words in the string
|
|
local parts = String.SafeSplit(strtrim(str), " ")
|
|
local i = 1
|
|
while i <= #parts do
|
|
local word = parts[i]
|
|
if strmatch(word, "^[%-%+%/%*%^]$") then
|
|
if i == #parts then
|
|
return nil, L["Invalid operator at end of custom price."]
|
|
end
|
|
-- valid math operator
|
|
elseif badPriceSources and badPriceSources[word] then
|
|
-- price source that's explicitly invalid
|
|
return nil, format(L["You cannot use %s as part of this custom price."], word)
|
|
elseif private.priceSourceInfo[word] or private.settings.customPriceSources[word] then
|
|
-- make sure we're not trying to take the price source of a number
|
|
if parts[i+1] == "(" and type(parts[i+2]) == "string" and not strfind(parts[i+2], "^[ip].*:") then
|
|
return nil, L["Invalid parameter to price source."]
|
|
end
|
|
-- valid price source
|
|
elseif tonumber(word) then
|
|
-- make sure it's not an itemID (incorrect)
|
|
if i > 2 and parts[i-1] == "(" and (private.priceSourceInfo[parts[i-2]] or private.settings.customPriceSources[parts[i-2]]) then
|
|
return nil, L["Invalid parameter to price source."]
|
|
end
|
|
-- valid number
|
|
elseif strmatch(word, "^"..ITEM_STRING_PATTERN.."$") or word == "~baseitem~" then
|
|
-- make sure previous word was a price source
|
|
if i > 1 and (private.priceSourceInfo[parts[i-1]] or private.settings.customPriceSources[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 == "(" then
|
|
-- empty parenthesis are not allowed
|
|
if not parts[i+1] or parts[i+1] == ")" then
|
|
return nil, L["Empty parentheses are not allowed"]
|
|
end
|
|
-- should never have ") ("
|
|
if i > 1 and parts[i-1] == ")" then
|
|
return nil, L["Missing operator between sets of parenthesis"]
|
|
end
|
|
elseif 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 strtrim(word) == "" then
|
|
-- harmless extra spaces
|
|
else
|
|
if strfind(word, "^%^1%^t%^") then
|
|
-- this is an operation export that they tried to use as a custom price
|
|
return nil, L["This looks like an exported operation and not a custom price."]
|
|
elseif strfind(word, "global") then
|
|
-- this is an old global price
|
|
return nil, L["It looks like you're trying to reference an old global price source which no longer exists."]
|
|
end
|
|
return nil, format(L["Invalid word: '%s'"], word)
|
|
end
|
|
i = i + 1
|
|
end
|
|
|
|
local canCache = true
|
|
local dependantPriceSources = {}
|
|
for key, value in pairs(private.settings.customPriceSources) do
|
|
key = strlower(key)
|
|
local usedKey = nil
|
|
str, usedKey = private.PriceSourceParsingHelper(str, key, "custom", dependantPriceSources)
|
|
if usedKey then
|
|
local customPriceProxy, errMsg = private.ParseCustomPrice(value, badPriceSources)
|
|
if not customPriceProxy then
|
|
return nil, format(L["The '%s' custom price source is invalid."], key).." "..errMsg
|
|
end
|
|
canCache = canCache and private.proxyData[customPriceProxy].map
|
|
end
|
|
end
|
|
|
|
for key, info in pairs(private.priceSourceInfo) do
|
|
local usedKey = nil
|
|
str, usedKey = private.PriceSourceParsingHelper(str, key, nil, dependantPriceSources)
|
|
canCache = canCache and (not usedKey or not info.isVolatile)
|
|
end
|
|
|
|
-- replace "~convert~" appropriately
|
|
if convertPriceSource then
|
|
canCache = canCache and not private.priceSourceInfo[convertPriceSource].isVolatile
|
|
dependantPriceSources._convertPriceSource = convertPriceSource
|
|
convertItem = convertItem and ('"'..convertItem..'"') or "_item"
|
|
str = gsub(str, "~convert~", format("self._priceHelper(%s, \"convert\", \"%s\")", convertItem, convertPriceSource))
|
|
end
|
|
|
|
-- replace math functions with special custom function names
|
|
for word, funcName in pairs(MATH_FUNCTIONS) do
|
|
str = gsub(str, " "..word.." ", " "..funcName.." ")
|
|
end
|
|
|
|
-- finally, create and return the function
|
|
local func, loadErr = loadstring(format(CUSTOM_PRICE_FUNC_TEMPLATE, str), "TSMCustomPrice: "..origStr)
|
|
if loadErr then
|
|
loadErr = gsub(strtrim(loadErr), "([^:]+):.", "")
|
|
return nil, L["Invalid function."].." "..L["Details"]..": "..loadErr
|
|
end
|
|
local success = nil
|
|
success, func = pcall(func)
|
|
if not success then
|
|
return nil, L["Invalid function."]
|
|
end
|
|
return private.CreateCustomPriceObj(func, origStr, dependantPriceSources, canCache)
|
|
end
|
|
|
|
function private.PriceSourceParsingHelper(str, key, extraArg, dependantPriceSources)
|
|
extraArg = extraArg and (",\""..extraArg.."\"") or ""
|
|
local numReplacements, usedKey = nil, false
|
|
-- replace all "<priceSource> <itemString>" occurances with the proper parameters (with the itemString)
|
|
str, numReplacements = gsub(str, format(" %s (%s) ", key, ITEM_STRING_PATTERN), format(" self._priceHelper(\"%%1\",\"%s\"%s) ", key, extraArg))
|
|
if numReplacements > 0 then
|
|
for itemString in gmatch(str, " self%._priceHelper%(\"("..ITEM_STRING_PATTERN..")\",\""..key.."\""..String.Escape(extraArg).."%) ") do
|
|
-- add all the items used for this key
|
|
dependantPriceSources[key] = dependantPriceSources[key] or {}
|
|
dependantPriceSources[key][itemString] = true
|
|
end
|
|
usedKey = true
|
|
end
|
|
-- replace all "<priceSource> baseitem" occurances with the proper parameters (with _baseitem for the item)
|
|
str, numReplacements = gsub(str, format(" %s ~baseitem~ ", key), format(" self._priceHelper(_baseitem,\"%s\"%s) ", key, extraArg))
|
|
if numReplacements > 0 then
|
|
dependantPriceSources[key] = dependantPriceSources[key] or {}
|
|
dependantPriceSources[key]._baseitem = true
|
|
usedKey = true
|
|
end
|
|
-- replace all "<priceSource>" occurances with the proper parameters (with _item for the item)
|
|
str, numReplacements = gsub(str, format(" %s ", key), format(" self._priceHelper(_item,\"%s\"%s) ", key, extraArg))
|
|
if numReplacements > 0 then
|
|
dependantPriceSources[key] = dependantPriceSources[key] or {}
|
|
dependantPriceSources[key]._item = true
|
|
usedKey = true
|
|
end
|
|
return str, usedKey
|
|
end
|
|
|
|
function private.ParseCustomPrice(customPriceStr, badPriceSources)
|
|
customPriceStr = private.SanitizeCustomPriceString(customPriceStr)
|
|
if not customPriceStr or customPriceStr == "" then
|
|
return nil, L["Empty price string."]
|
|
end
|
|
|
|
if badPriceSources then
|
|
return private.ParsePriceString(customPriceStr, badPriceSources)
|
|
end
|
|
|
|
if not private.customPriceCache[customPriceStr] then
|
|
private.customPriceCache[customPriceStr] = {private.ParsePriceString(customPriceStr)}
|
|
end
|
|
return unpack(private.customPriceCache[customPriceStr])
|
|
end
|
|
|
|
function private.ModuleSortFunc(a, b)
|
|
if a == "TSM" then
|
|
return true
|
|
elseif b == "TSM" then
|
|
return false
|
|
else
|
|
return a < b
|
|
end
|
|
end
|
|
|
|
function private.SanitizeCustomPriceString(customPriceStr)
|
|
assert(customPriceStr)
|
|
local result = private.sanitizeCache[customPriceStr]
|
|
if not result then
|
|
result = strlower(strtrim(tostring(customPriceStr)))
|
|
result = Money.FromString(result) and gsub(result, String.Escape(LARGE_NUMBER_SEPERATOR), "") or result
|
|
private.sanitizeCache[customPriceStr] = result
|
|
end
|
|
return result
|
|
end
|
|
|
|
function private.CallCustomSourceCallbacks()
|
|
for _, callback in ipairs(private.customSourceCallbacks) do
|
|
callback()
|
|
end
|
|
end
|