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.

515 lines
16 KiB

-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local TSM = select(2, ...) ---@type TSM
local ItemString = TSM.Init("Util.ItemString") ---@class Util.ItemString
local Environment = TSM.Include("Environment")
local BonusIds = TSM.Include("Data.BonusIds")
local SmartMap = TSM.Include("Util.SmartMap")
local private = {
filteredItemStringCache = {},
itemStringCache = {},
baseItemStringMap = nil,
baseItemStringReader = nil,
levelItemStringMap = nil,
hasNonBaseItemStrings = {},
bonusIdsTemp = {},
modifiersTemp = {},
modifiersValueTemp = {},
extraStatModifiersTemp = {},
}
local ITEM_MAX_ID = 999999
local UNKNOWN_ITEM_STRING = "i:0"
local PLACEHOLDER_ITEM_STRING = "i:1"
local PET_CAGE_ITEM_STRING = "i:82800"
local MINIMUM_VARIANT_ITEM_ID = 187518
local IMPORTANT_MODIFIER_TYPES = {
[9] = true,
}
local EXTRA_STAT_MODIFIER_TYPES = {
[29] = true,
[30] = true,
}
-- ============================================================================
-- Module Loading
-- ============================================================================
ItemString:OnModuleLoad(function()
private.baseItemStringMap = SmartMap.New("string", "string", private.ToBaseItemString)
private.baseItemStringReader = private.baseItemStringMap:CreateReader()
private.levelItemStringMap = SmartMap.New("string", "string", ItemString.ToLevel)
end)
-- ============================================================================
-- Module Functions
-- ============================================================================
---Gets the constant unknown item string for places where the itemString is not known.
---@return string
function ItemString.GetUnknown()
return UNKNOWN_ITEM_STRING
end
---Gets the constant placeholder item string.
---@return string
function ItemString.GetPlaceholder()
return PLACEHOLDER_ITEM_STRING
end
---Gets the battlepet cage item string.
---@return string
function ItemString.GetPetCage()
return PET_CAGE_ITEM_STRING
end
---Gets the base itemString smart map.
---@return SmartMapObject
function ItemString.GetBaseMap()
return private.baseItemStringMap
end
---Gets the level itemString smart map.
---@return SmartMapObject
function ItemString.GetLevelMap()
return private.levelItemStringMap
end
---Converts the parameter into an itemString.
---@param item number|string Either an itemId, itemLink, or itemString to be converted
---@return string
function ItemString.Get(item)
if not item then
return nil
end
if not private.itemStringCache[item] then
private.itemStringCache[item] = private.ToItemString(item)
end
return private.itemStringCache[item]
end
function ItemString.Filter(itemString)
if not private.filteredItemStringCache[itemString] then
private.filteredItemStringCache[itemString] = private.FilterBonusIdsAndModifiers(itemString, true, strsplit(":", itemString))
end
return private.filteredItemStringCache[itemString]
end
---Converts the parameter into an itemId.
---@param item string An item to get the id of
---@return number
function ItemString.ToId(item)
local itemString = ItemString.Get(item)
if type(itemString) ~= "string" then
return
end
return tonumber(strmatch(itemString, "^[ip]:(%d+)"))
end
---Converts the parameter into a base itemString.
---@param itemString string An itemString to get the base itemString of
---@return string
function ItemString.GetBaseFast(itemString)
if not itemString then
return nil
end
return private.baseItemStringReader[itemString]
end
---Converts the parameter into a base itemString.
---@param item string An item to get the base itemString of
---@return string
function ItemString.GetBase(item)
-- make sure it's a valid itemString
local itemString = ItemString.Get(item)
if not itemString then return end
-- quickly return if we're certain it's already a valid baseItemString
if type(itemString) == "string" and strmatch(itemString, "^[ip]:[0-9]+$") then return itemString end
return ItemString.GetBaseFast(itemString)
end
---Converts an itemKey from WoW into a base itemString.
---@param itemKey table An itemKey to get the itemString of
---@return string
function ItemString.GetBaseFromItemKey(itemKey)
if itemKey.battlePetSpeciesID > 0 then
return "p:"..itemKey.battlePetSpeciesID
else
return "i:"..itemKey.itemID
end
end
---Returns whether or not a non-base version of an item has been seen.
---@param baseItemString string A base itemString to check
---@return boolean
function ItemString.HasNonBase(baseItemString)
return private.hasNonBaseItemStrings[baseItemString] or false
end
---Converts an itemString to a level itemString
---@param itemString string An itemString to get the level itemString of
---@return string
function ItemString.ToLevel(itemString)
if ItemString.IsLevel(itemString) then
-- Already a level itemString
return itemString
end
local baseItemString = ItemString.GetBaseFast(itemString)
if itemString == baseItemString then
-- Already a base itemString
return itemString
end
if ItemString.IsPet(itemString) then
local petLevel = strmatch(itemString, "^p:%d+:(%d+):.+$")
return petLevel and baseItemString..":i"..petLevel or baseItemString
end
local level, isAbs = BonusIds.GetItemLevel(itemString)
if not level then
return baseItemString
end
if isAbs then
return baseItemString.."::".."i"..level
else
if level >= 0 then
level = "+"..level
end
return baseItemString.."::"..level
end
end
---Parse the level modifier from a (potential) level itemString
---@param itemString string An itemString to parse
---@return number @The level modifier
---@return boolean @Whether or not it is an absolute level
function ItemString.ParseLevel(itemString)
local petLevel = strmatch(itemString, "^p:[0-9]+:i([0-9]+)$")
if petLevel then
return tonumber(petLevel), true
end
local prefix, level = strmatch(itemString, "^i:[0-9]+:[0-9%-]*:([i%+%-])([0-9]+)")
level = level and tonumber(level) or nil
if not prefix or not level then
return nil, nil
elseif prefix == "i" then
return level, true
elseif prefix == "+" then
return level, false
elseif prefix == "-" then
return level * -1, false
else
error("Invalid prefix: "..tostring(prefix))
end
end
---Attempts to determine the itemLevel by parsing the itemString
---@param itemString string An itemString to get the itemLevel of
---@return number|nil
function ItemString.GetItemLevel(itemString)
-- Check if this is a level itemString first
local itemLevel, isAbs = ItemString.ParseLevel(itemString)
if itemLevel then
return isAbs and itemLevel or nil
end
if ItemString.IsPet(itemString) then
local petLevel = strmatch(itemString, "^p:%d+:(%d+):.+$")
return petLevel and tonumber(petLevel) or nil
else
-- Try to get the level from the bonusIds
itemLevel, isAbs = BonusIds.GetItemLevel(itemString)
return isAbs and itemLevel or nil
end
end
---Gets a list of stat modifier values which are present in an itemString
---@param itemString string An itemString to get the stat modifiers of
---@param fromBonusIdsOnly boolean Only get equivalent modifiers from bonusIds
---@param resultTbl table The table to store the results in
function ItemString.GetStatModifiers(itemString, fromBonusIdsOnly, resultTbl)
if not ItemString.IsItem(itemString) then
return
end
return private.GetStatModifiersHelper(resultTbl, fromBonusIdsOnly, select(4, strsplit(":", itemString)))
end
---Converts the parameter into a WoW itemString.
---@param itemString string An itemString to get the WoW itemString of
---@return number
function ItemString.ToWow(itemString)
local itemStringLevel, isAbsItemStringLevel = ItemString.ParseLevel(itemString)
local itemId, rand, extraPart = nil, nil, nil
if itemStringLevel then
itemId, rand = select(2, strsplit(":", itemString))
extraPart = BonusIds.GetBonusStringForLevel(itemStringLevel, isAbsItemStringLevel)
else
local _, extra = nil, nil
itemId, rand, extra = select(2, strsplit(":", itemString))
extraPart = extra and strmatch(itemString, "i:[0-9]+:[0-9%-]*:(.+)") or ""
end
local level = UnitLevel("player")
local spec = Environment.IsRetail() and GetSpecialization() or nil
spec = spec and GetSpecializationInfo(spec) or ""
return "item:"..itemId.."::::::"..(rand or "").."::"..level..":"..spec..":::"..extraPart..":::"
end
---Returns whether or not the itemString is for an item
---@param itemString string The itemString to check
---@return boolean
function ItemString.IsItem(itemString)
return strmatch(itemString, "^i:[%-:0-9%+i]+$") and true or false
end
---Returns whether or not the itemString is a level itemString
---@param itemString string The itemString to check
---@return boolean
function ItemString.IsLevel(itemString)
return strmatch(itemString, "^i:[0-9]+:[0-9%-]*:[i%+%-][0-9]+$") or strmatch(itemString, "^p:[0-9]+:i[0-9]+$") and true or false
end
---Returns whether or not the itemString is for a pet
---@param itemString string The itemString to check
---@return boolean
function ItemString.IsPet(itemString)
return strmatch(itemString, "^p:[%-:0-9i]+$") and true or false
end
-- ============================================================================
-- Helper Functions
-- ============================================================================
function private.ToItemString(item)
local paramType = type(item)
if paramType == "string" then
item = strtrim(item)
local itemId = strmatch(item, "^item:(%d+)$")
if itemId then
item = "i:"..itemId
else
itemId = strmatch(item, "^[ip]:(%d+)$")
end
if itemId then
if tonumber(itemId) > ITEM_MAX_ID then
return nil
end
-- This is already an itemString
return item
end
elseif paramType == "number" or tonumber(item) then
local itemId = tonumber(item)
if itemId > ITEM_MAX_ID then
return nil
end
-- assume this is an itemId
return "i:"..item
else
error("Invalid item parameter type: "..tostring(item))
end
-- test if it's already (likely) an item string or battle pet string
if strmatch(item, "^i:([0-9%-:i%+]+)$") then
return private.FixItemString(item)
elseif strmatch(item, "^p:([i0-9:]+)$") then
return private.FixPet(item)
end
local result = strmatch(item, "^\124cff[0-9a-z]+\124[Hh](.+)\124h%[.+%]\124h\124r$")
if result then
-- it was a full item link which we've extracted the itemString from
item = result
end
-- test if it's an old style item string
result = strjoin(":", strmatch(item, "^(i)tem:([0-9%-]+):[0-9%-]+:[0-9%-]+:[0-9%-]+:[0-9%-]+:[0-9%-]+:([0-9%-]+)$"))
if result then
return private.FixItemString(result)
end
-- test if it's an old style battle pet string (or if it was a link)
result = strjoin(":", strmatch(item, "^battle(p)et:(%d+:%d+:%d+)"))
if result then
return private.FixPet(result)
end
result = strjoin(":", strmatch(item, "^battle(p)et:(%d+)[:]*$"))
if result then
return result
end
result = strjoin(":", strmatch(item, "^(p):(%d+:%d+:%d+)"))
if result then
return private.FixPet(result)
end
-- test if it's a long item string
result = strjoin(":", strmatch(item, "(i)tem:([0-9%-]+):[0-9%-]*:[0-9%-]*:[0-9%-]*:[0-9%-]*:[0-9%-]*:([0-9%-]*):[0-9%-]*:[0-9%-]*:[0-9%-]*:[0-9%-]*:[0-9%-]*:([0-9%-:]+)"))
if result and result ~= "" then
return private.FixItemString(result)
end
-- test if it's a shorter item string (without bonuses)
result = strjoin(":", strmatch(item, "(i)tem:([0-9%-]+):[0-9%-]*:[0-9%-]*:[0-9%-]*:[0-9%-]*:[0-9%-]*:([0-9%-]*)"))
if result and result ~= "" then
return result
end
end
function private.RemoveExtra(itemString)
local num = 1
while num > 0 do
itemString, num = gsub(itemString, ":0?$", "")
end
return itemString
end
function private.FixItemString(itemString)
itemString = gsub(itemString, ":0:", "::") -- remove 0s which are in the middle
itemString = private.RemoveExtra(itemString)
return private.FilterBonusIdsAndModifiers(itemString, false, strsplit(":", itemString))
end
function private.FixPet(itemString)
itemString = private.RemoveExtra(itemString)
return strmatch(itemString, "^(p:%d+:%d+:%d+)$") or strmatch(itemString, "^(p:%d+:i%d+)$") or strmatch(itemString, "^(p:%d+)")
end
function private.FilterBonusIdsAndModifiers(itemString, importantBonusIdsOnly, itemType, itemId, rand, numBonusIds, ...)
numBonusIds = tonumber(numBonusIds) or 0
local numParts = select("#", ...)
if numParts == 0 then
return itemString
end
-- grab the modifiers and filter them
local numModifiers = numParts - numBonusIds
local modifiersStr = (numModifiers > 0 and numModifiers > 1 and numModifiers % 2 == 1) and strjoin(":", select(numBonusIds + 1, ...)) or ""
if modifiersStr ~= "" then
wipe(private.modifiersTemp)
wipe(private.modifiersValueTemp)
wipe(private.extraStatModifiersTemp)
local num, modifierType = nil, nil
for modifier in gmatch(modifiersStr, "[0-9]+") do
modifier = tonumber(modifier)
if not num then
num = modifier
elseif not modifierType then
modifierType = modifier
else
if IMPORTANT_MODIFIER_TYPES[modifierType] then
tinsert(private.modifiersTemp, modifierType)
assert(not private.modifiersValueTemp[modifierType])
private.modifiersValueTemp[modifierType] = modifier
elseif not importantBonusIdsOnly and EXTRA_STAT_MODIFIER_TYPES[modifierType] then
tinsert(private.modifiersTemp, modifierType)
tinsert(private.extraStatModifiersTemp, modifier)
end
modifierType = nil
end
end
if #private.modifiersTemp > 0 then
sort(private.modifiersTemp)
sort(private.extraStatModifiersTemp)
-- insert the values into modifiersTemp
for i = #private.modifiersTemp, 1, -1 do
local tempModifierType = private.modifiersTemp[i]
local modifier = nil
if EXTRA_STAT_MODIFIER_TYPES[tempModifierType] then
assert(not importantBonusIdsOnly)
modifier = tremove(private.extraStatModifiersTemp)
else
modifier = private.modifiersValueTemp[tempModifierType]
end
assert(modifier)
tinsert(private.modifiersTemp, i + 1, modifier)
end
tinsert(private.modifiersTemp, 1, #private.modifiersTemp / 2)
modifiersStr = table.concat(private.modifiersTemp, ":")
else
modifiersStr = ""
end
end
-- filter the bonusIds
local bonusIdsStr = ""
if numBonusIds > 0 then
-- get the list of bonusIds and filter them
wipe(private.bonusIdsTemp)
for i = 1, numBonusIds do
private.bonusIdsTemp[i] = select(i, ...)
end
if importantBonusIdsOnly then
-- Only track bonusIds if the itemId is above our minimum
if tonumber(itemId) >= MINIMUM_VARIANT_ITEM_ID then
bonusIdsStr = BonusIds.FilterImportant(table.concat(private.bonusIdsTemp, ":"))
end
else
bonusIdsStr = BonusIds.FilterAll(table.concat(private.bonusIdsTemp, ":"))
end
end
-- rebuild the itemString
itemString = strjoin(":", itemType, itemId, rand, bonusIdsStr, modifiersStr)
itemString = gsub(itemString, ":0:", "::") -- remove 0s which are in the middle
return private.RemoveExtra(itemString)
end
function private.ToBaseItemString(itemString)
local baseItemString = strmatch(itemString, "[ip]:%d+")
if baseItemString ~= itemString then
private.hasNonBaseItemStrings[baseItemString] = true
end
return baseItemString
end
function private.GetStatModifiersHelper(resultTbl, fromBonusIds, numBonusIds, ...)
numBonusIds = tonumber(numBonusIds) or 0
local numParts = select("#", ...)
if numParts == 0 then
return
end
-- check the bonusIds
for i = 1, numBonusIds do
local bonusId = select(i, ...)
bonusId = tonumber(bonusId)
assert(bonusId)
local modifier = BonusIds.GetCraftingStatModifier(bonusId)
if modifier then
tinsert(resultTbl, modifier)
end
end
if fromBonusIds then
sort(resultTbl)
return
end
-- grab the modifiers and filter them
local numModifiers = numParts - numBonusIds
local modifiersStr = (numModifiers > 0 and numModifiers > 1 and numModifiers % 2 == 1) and strjoin(":", select(numBonusIds + 1, ...)) or ""
if modifiersStr == "" then
return
end
local num, modifierType = nil, nil
for modifier in gmatch(modifiersStr, "[0-9]+") do
modifier = tonumber(modifier)
if not num then
num = modifier
elseif not modifierType then
modifierType = modifier
else
if EXTRA_STAT_MODIFIER_TYPES[modifierType] then
if not tContains(resultTbl, modifier) then
tinsert(resultTbl, modifier)
end
end
modifierType = nil
end
end
sort(resultTbl)
end