You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
447 lines
16 KiB
447 lines
16 KiB
-- ------------------------------------------------------------------------------ --
|
|
-- TradeSkillMaster --
|
|
-- https://tradeskillmaster.com --
|
|
-- All Rights Reserved - Detailed license information included with addon. --
|
|
-- ------------------------------------------------------------------------------ --
|
|
|
|
local TSM = select(2, ...) ---@type TSM
|
|
local CustomPrice = TSM.Init("Service.CustomPrice") ---@class Service.CustomPrice
|
|
local L = TSM.Include("Locale").GetTable()
|
|
local TempTable = TSM.Include("Util.TempTable")
|
|
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 SmartMap = TSM.Include("Util.SmartMap")
|
|
local CustomString = TSM.Include("Util.CustomString")
|
|
local Settings = TSM.Include("Service.Settings")
|
|
local Conversions = TSM.Include("Service.Conversions")
|
|
local private = {
|
|
sanitizeMap = nil,
|
|
sanitizeMapReader = nil,
|
|
customStrings = {}, ---@type table<string,CustomStringObject>
|
|
priceSourceKeys = {},
|
|
priceSourceInfo = {},
|
|
settings = nil,
|
|
sanitizeCache = {},
|
|
customSourceCallbacks = {},
|
|
}
|
|
|
|
|
|
|
|
-- ============================================================================
|
|
-- Module Loading
|
|
-- ============================================================================
|
|
|
|
CustomPrice:OnModuleLoad(function()
|
|
private.sanitizeMap = SmartMap.New("string", "string", function(str) return strlower(strtrim(str)) end)
|
|
private.sanitizeMapReader = private.sanitizeMap:CreateReader()
|
|
CustomString.SetPriceFunc(private.PriceFunc)
|
|
end)
|
|
|
|
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
|
|
else
|
|
Log.PrintfUser(L["Removed custom price source (%s) which has an invalid name."], name)
|
|
CustomPrice.DeleteCustomPriceSource(name)
|
|
end
|
|
end
|
|
end)
|
|
|
|
|
|
|
|
-- ============================================================================
|
|
-- Module Functions
|
|
-- ============================================================================
|
|
|
|
---Register a built-in price source.
|
|
---@param moduleName string The name of the module which provides this source
|
|
---@param key string The key for this price source (i.e. DBMarket)
|
|
---@param label string The label which describes this price source for display to the user
|
|
---@param callback function The price source callback
|
|
---@param isVolatile? booolean Should be set if the price source may change without CustomPrice.OnSourceChange being called
|
|
function CustomPrice.RegisterSource(moduleName, key, label, callback, isVolatile)
|
|
tinsert(private.priceSourceKeys, strlower(key))
|
|
private.priceSourceInfo[strlower(key)] = {
|
|
moduleName = moduleName,
|
|
key = key,
|
|
label = label,
|
|
callback = callback,
|
|
isVolatile = isVolatile,
|
|
cache = {},
|
|
}
|
|
end
|
|
|
|
---Register a callback when custom sources change.
|
|
---@param callback function The callback function
|
|
function CustomPrice.RegisterCustomSourceCallback(callback)
|
|
tinsert(private.customSourceCallbacks, callback)
|
|
end
|
|
|
|
---Create a new custom price source.
|
|
---@param name string The name of the custom price source
|
|
---@param value string 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
|
|
private.CallCustomSourceCallbacks()
|
|
end
|
|
|
|
---Rename a custom price source.
|
|
---@param oldName string The old name of the custom price source
|
|
---@param newName string 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
|
|
CustomPrice.OnSourceChange(oldName)
|
|
CustomPrice.OnSourceChange(newName)
|
|
private.CallCustomSourceCallbacks()
|
|
end
|
|
|
|
---Delete a custom price source.
|
|
---@param name string The name of the custom price source
|
|
function CustomPrice.DeleteCustomPriceSource(name)
|
|
assert(private.settings.customPriceSources[name])
|
|
private.settings.customPriceSources[name] = nil
|
|
CustomPrice.OnSourceChange(name)
|
|
private.CallCustomSourceCallbacks()
|
|
end
|
|
|
|
---Sets the value of a custom price source.
|
|
---@param name string The name of the custom price source
|
|
---@param value string 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
|
|
CustomPrice.OnSourceChange(name)
|
|
end
|
|
|
|
---Bulk creates custom price sources from a group import.
|
|
---@param customSources table<string, string> The custom sources to impor
|
|
---@param replaceExisting boolean Whether or not existing sources should be replaced
|
|
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
|
|
|
|
---Gets the description of a price source.
|
|
---@param key string The custom price source
|
|
---@return string?
|
|
function CustomPrice.GetDescription(key)
|
|
local info = private.priceSourceInfo[key]
|
|
return info and info.label or nil
|
|
end
|
|
|
|
---Validate a custom price name.
|
|
---@param customPriceName string The custom price name
|
|
---@param ignoreExistingCustomPriceSources boolean Whether or not to ignore existing custom price sources
|
|
---@return boolean @Whether or not the custom price name is valid
|
|
function CustomPrice.ValidateName(customPriceName, ignoreExistingCustomPriceSources)
|
|
if gsub(customPriceName, "([a-z]+)", "") ~= "" or 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
|
|
-- Reserved words
|
|
if private.priceSourceInfo[customPriceName] or CustomString.IsReservedWord(customPriceName) then
|
|
return false, format(L["Custom price name %s is a reserved word which cannot be used."], Theme.GetColor("INDICATOR"):ColorText(customPriceName))
|
|
end
|
|
return true
|
|
end
|
|
|
|
---Validate a custom price string.
|
|
---@param str string The custom price string
|
|
---@param badPriceSources? table A table of price sources (as keys) which aren't allowed to be used
|
|
---@return boolean @Whether or not the custom price string is valid
|
|
---@return string? @The error message if the custom price string was invalid
|
|
function CustomPrice.Validate(str, badPriceSources)
|
|
local obj, errMsg = private.GetObject(str)
|
|
if not obj then
|
|
return nil, errMsg
|
|
end
|
|
if badPriceSources then
|
|
for source in pairs(badPriceSources) do
|
|
if obj:IsDependantOnSource(source) then
|
|
return false, format(L["You cannot use %s as part of this custom price."], source)
|
|
end
|
|
end
|
|
end
|
|
return true, nil
|
|
end
|
|
|
|
---Evaulates a custom price source for an item.
|
|
---@param str string The custom price string
|
|
---@param itemString string The item to evaluate the custom price string for
|
|
---@param allowZero boolean If true, allows the result to be 0
|
|
---@return number? @The resulting value or nil if the custom price string is invalid
|
|
---@return string? @The error message if the custom price string was invalid
|
|
function CustomPrice.GetValue(str, itemString, allowZero)
|
|
local obj, errMsg = private.GetObject(str)
|
|
if not obj then
|
|
return nil, errMsg
|
|
end
|
|
local value = obj:Evaluate(itemString)
|
|
if not value 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.
|
|
---@param itemString string The item to evaluate the price source for
|
|
---@param key string The key of the price source
|
|
---@return number? @The resulting value or nil if no price was found for the item
|
|
function CustomPrice.GetSourcePrice(itemString, key)
|
|
key = strlower(key)
|
|
local info = private.priceSourceInfo[key]
|
|
if not itemString or not info then
|
|
return nil
|
|
end
|
|
if info.isVolatile then
|
|
local currentFrame = GetTime()
|
|
if (info.cache.frame or currentFrame) ~= currentFrame then
|
|
wipe(info.cache)
|
|
end
|
|
info.cache.frame = currentFrame
|
|
end
|
|
local cachedValue = info.cache[itemString]
|
|
if cachedValue ~= nil then
|
|
return cachedValue or nil
|
|
end
|
|
local value = info.callback(itemString)
|
|
value = type(value) == "number" and value or nil
|
|
info.cache[itemString] = value or false
|
|
return value
|
|
end
|
|
|
|
---Iterate over the price sources.
|
|
---@return any @An iterator which provides the following fields: `index, key, moduleName, label`
|
|
function CustomPrice.Iterator()
|
|
return private.IteratorHelper, nil, 0
|
|
end
|
|
|
|
---Returns whether or not a key is a math function.
|
|
---@param key string The key to check
|
|
---@return boolean
|
|
function CustomPrice.IsMathFunction(key)
|
|
key = strlower(key)
|
|
return CustomString.IsReservedWord(key) and key ~= "convert"
|
|
end
|
|
|
|
---Returns whether or not a key is a source.
|
|
---@param key string The key to check
|
|
---@return boolean
|
|
function CustomPrice.IsSource(key)
|
|
key = strlower(key)
|
|
return (private.priceSourceInfo[key] or key == "convert") and true or false
|
|
end
|
|
|
|
---Returns whether or not a key is a custom source.
|
|
---@param key string The key to check
|
|
---@return boolean
|
|
function CustomPrice.IsCustomSource(key)
|
|
return private.settings.customPriceSources[strlower(key)] and true or false
|
|
end
|
|
|
|
---Should be called when the value of a registered source changes.
|
|
---@param key string The key of the price source
|
|
---@param itemString? string The item which the source changed for or nil if it changed for all items
|
|
function CustomPrice.OnSourceChange(key, itemString)
|
|
key = strlower(key)
|
|
local info = private.priceSourceInfo[key]
|
|
if not info then
|
|
return
|
|
end
|
|
assert(not info.isVolatile)
|
|
if itemString then
|
|
info.cache[itemString] = nil
|
|
else
|
|
wipe(info.cache)
|
|
end
|
|
end
|
|
|
|
---Iterates over the custom sources which a string depends on.
|
|
---@param str string The custom string
|
|
---@return fun():number, string, string @An iterator with fields: `index`, `name`, `customSourceStr`
|
|
function CustomPrice.DependantCustomSourceIterator(str)
|
|
local result = TempTable.Acquire()
|
|
local obj = private.GetObject(str)
|
|
if obj then
|
|
for name, customSourceStr in pairs(private.settings.customPriceSources) do
|
|
if obj:IsDependantOnSource(name) then
|
|
tinsert(result, name)
|
|
tinsert(result, customSourceStr)
|
|
end
|
|
end
|
|
end
|
|
return TempTable.Iterator(result, 2)
|
|
end
|
|
|
|
|
|
|
|
-- ============================================================================
|
|
-- Helper Functions
|
|
-- ============================================================================
|
|
|
|
function private.PriceFunc(itemString, key, extraArg)
|
|
local value = nil
|
|
if key == "convert" then
|
|
local conversions = Conversions.GetSourceItems(itemString)
|
|
if not conversions then
|
|
return nil
|
|
end
|
|
local minPrice = nil
|
|
for sourceItemString, rate in pairs(conversions) do
|
|
local price = CustomPrice.GetSourcePrice(sourceItemString, extraArg)
|
|
if price then
|
|
price = price / rate
|
|
minPrice = min(minPrice or price, price)
|
|
end
|
|
end
|
|
value = minPrice
|
|
else
|
|
local customPriceSourceStr = private.settings.customPriceSources[key]
|
|
if customPriceSourceStr then
|
|
value = CustomPrice.GetValue(customPriceSourceStr, itemString)
|
|
else
|
|
value = CustomPrice.GetSourcePrice(itemString, key)
|
|
end
|
|
end
|
|
if not value or value < 0 then
|
|
return nil
|
|
end
|
|
return value
|
|
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
|
|
|
|
function private.IteratorHelper(_, index)
|
|
index = index + 1
|
|
local key = private.priceSourceKeys[index]
|
|
if not key then
|
|
return
|
|
end
|
|
local info = private.priceSourceInfo[key]
|
|
return index, info.key, info.moduleName, info.label
|
|
end
|
|
|
|
function private.GetObject(str)
|
|
str = private.sanitizeMapReader[tostring(str)]
|
|
if str == "" then
|
|
return nil, L["Empty price string."]
|
|
end
|
|
|
|
private.customStrings[str] = private.customStrings[str] or CustomString.Parse(str)
|
|
local obj = private.customStrings[str]
|
|
|
|
local isValid, errType, _, errTokenStr = obj:Validate()
|
|
if not isValid then
|
|
if errType == CustomString.ERROR_TYPE.INVALID_TOKEN then
|
|
assert(errTokenStr)
|
|
return nil, format(L["Unexpected word ('%s') in custom string."], errTokenStr)
|
|
elseif errType == CustomString.ERROR_TYPE.UNBALANCED_PARENS then
|
|
return nil, L["There are unbalanced parentheses in this custom string."]
|
|
elseif errType == CustomString.ERROR_TYPE.INVALID_NUM_ARGS then
|
|
assert(errTokenStr)
|
|
return nil, format(L["The '%s' function has an invalid number of arguments."], errTokenStr)
|
|
elseif errType == CustomString.ERROR_TYPE.TOO_MANY_VARS then
|
|
return nil, L["This custom string is too complex for WoW to handle; use custom sources to simplify it."]
|
|
elseif errType == CustomString.ERROR_TYPE.INVALID_ITEM_STRING then
|
|
assert(errTokenStr)
|
|
return nil, format(L["'%s' is not a valid item argument."], errTokenStr)
|
|
elseif errType == CustomString.ERROR_TYPE.INVALID_CONVERT_ARG then
|
|
assert(errTokenStr)
|
|
return nil, format(L["'%s' is not a valid argument for convert()."], errTokenStr)
|
|
elseif errType == CustomString.ERROR_TYPE.NO_ITEM_PARAM_PARENT then
|
|
assert(errTokenStr)
|
|
return nil, format(L["The '%s' item parameter was used outside of a custom source."], errTokenStr)
|
|
else
|
|
error("Invalid error type: "..tostring(errType))
|
|
end
|
|
end
|
|
for source in obj:DependantSourceIterator() do
|
|
if source ~= "convert" and not private.priceSourceInfo[source] and not private.settings.customPriceSources[source] then
|
|
return nil, format(L["%s is not a valid source."], source)
|
|
end
|
|
end
|
|
return obj, nil
|
|
end
|
|
|