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.
1587 lines
54 KiB
1587 lines
54 KiB
-- ------------------------------------------------------------------------------ --
|
|
-- TradeSkillMaster --
|
|
-- https://tradeskillmaster.com --
|
|
-- All Rights Reserved - Detailed license information included with addon. --
|
|
-- ------------------------------------------------------------------------------ --
|
|
|
|
local TSM = select(2, ...) ---@type TSM
|
|
local ItemInfo = TSM.Init("Service.ItemInfo") ---@class Service.ItemInfo
|
|
local Environment = TSM.Include("Environment")
|
|
local L = TSM.Include("Locale").GetTable()
|
|
local ItemClass = TSM.Include("Data.ItemClass")
|
|
local VendorSell = TSM.Include("Data.VendorSell")
|
|
local ItemString = TSM.Include("Util.ItemString")
|
|
local Analytics = TSM.Include("Util.Analytics")
|
|
local Database = TSM.Include("Util.Database")
|
|
local Event = TSM.Include("Util.Event")
|
|
local Delay = TSM.Include("Util.Delay")
|
|
local TempTable = TSM.Include("Util.TempTable")
|
|
local Math = TSM.Include("Util.Math")
|
|
local String = TSM.Include("Util.String")
|
|
local Log = TSM.Include("Util.Log")
|
|
local Table = TSM.Include("Util.Table")
|
|
local Reactive = TSM.Include("Util.Reactive")
|
|
local DefaultUI = TSM.Include("Service.DefaultUI")
|
|
local Settings = TSM.Include("Service.Settings")
|
|
local private = {
|
|
db = nil,
|
|
pendingItems = {},
|
|
priorityPendingItems = {},
|
|
priorityPendingTime = 0,
|
|
numRequests = {},
|
|
availableItems = {},
|
|
rebuildStage = nil,
|
|
settings = nil,
|
|
hasChanged = false,
|
|
lastDebugLog = 0,
|
|
stream = nil,
|
|
processInfoTimer = nil,
|
|
processAvailableTimer = nil,
|
|
merchantTimer = nil,
|
|
deferredSetSingleField = {},
|
|
deferredSetSingleFieldTimer = nil,
|
|
}
|
|
local ITEM_MAX_ID = 999999
|
|
local SEP_CHAR = "\002"
|
|
local ITEM_INFO_INTERVAL = 0.05
|
|
local MAX_REQUESTED_ITEM_INFO = 50
|
|
local MAX_REQUESTS_PER_ITEM = 5
|
|
local UNKNOWN_ITEM_NAME = L["Unknown Item"]
|
|
local PLACEHOLDER_ITEM_NAME = L["Example Item"]
|
|
local UNKNOWN_ITEM_TEXTURE = 136254
|
|
local DB_VERSION = 13
|
|
local ENCODING_NUM_BITS = 6
|
|
local ENCODING_NUM_VALUES = 2 ^ ENCODING_NUM_BITS
|
|
local ENCODING_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
|
assert(#ENCODING_ALPHABET == ENCODING_NUM_VALUES)
|
|
local ENCODING_TABLE = {}
|
|
local ENCODING_TABLE_2 = {}
|
|
local DECODING_TABLE = {}
|
|
local DECODING_TABLE_1 = {}
|
|
local DECODED_NIL_VALUE = ENCODING_NUM_VALUES - 1
|
|
for i = 0, ENCODING_NUM_VALUES - 1 do
|
|
local encodedValue = strbyte(ENCODING_ALPHABET, i + 1, i + 1)
|
|
ENCODING_TABLE[i] = encodedValue
|
|
DECODING_TABLE[encodedValue] = i
|
|
if i == DECODED_NIL_VALUE then
|
|
DECODING_TABLE_1[encodedValue] = -1
|
|
else
|
|
DECODING_TABLE_1[encodedValue] = i
|
|
end
|
|
end
|
|
for i = 0, ENCODING_NUM_VALUES ^ 2 - 1 do
|
|
local value = i
|
|
local charValue0 = value % 2 ^ ENCODING_NUM_BITS
|
|
value = (value - charValue0) / 2 ^ ENCODING_NUM_BITS
|
|
local charValue1 = value % 2 ^ ENCODING_NUM_BITS
|
|
value = (value - charValue1) / 2 ^ ENCODING_NUM_BITS
|
|
ENCODING_TABLE_2[i] = { ENCODING_TABLE[charValue0], ENCODING_TABLE[charValue1] }
|
|
assert(value == 0)
|
|
end
|
|
local ENCODED_NIL_CHAR = ENCODING_TABLE[DECODED_NIL_VALUE]
|
|
local RECORD_DATA_LENGTH_CHARS = 24
|
|
local FIELD_INFO = {
|
|
itemLevel = { numBits = 12 },
|
|
minLevel = { numBits = 12 },
|
|
vendorSell = { numBits = 30 },
|
|
maxStack = { numBits = 12 },
|
|
invSlotId = { numBits = 6 },
|
|
texture = { numBits = 30 },
|
|
classId = { numBits = 6 },
|
|
subClassId = { numBits = 6 },
|
|
quality = { numBits = 6 },
|
|
isBOP = { numBits = 6 },
|
|
isCraftingReagent = { numBits = 6 },
|
|
expansionId = { numBits = 6 },
|
|
craftedQuality = { numBits = 6 },
|
|
}
|
|
do
|
|
local totalLengthChars = 0
|
|
for _, info in pairs(FIELD_INFO) do
|
|
assert(info.numBits % ENCODING_NUM_BITS == 0)
|
|
info.numChars = info.numBits / ENCODING_NUM_BITS
|
|
totalLengthChars = totalLengthChars + info.numChars
|
|
info.nilValue = 2 ^ info.numBits - 1
|
|
info.maxValue = 2 ^ info.numBits - 2
|
|
end
|
|
assert(totalLengthChars == RECORD_DATA_LENGTH_CHARS)
|
|
end
|
|
local PENDING_STATE_NEW = newproxy()
|
|
local PENDING_STATE_CREATED = newproxy()
|
|
local ITEM_QUALITY_BY_HEX_LOOKUP = {}
|
|
for quality, info in pairs(ITEM_QUALITY_COLORS) do
|
|
ITEM_QUALITY_BY_HEX_LOOKUP[info.hex] = quality
|
|
end
|
|
-- URLs for non-disenchantable items:
|
|
-- http://www.wowhead.com/items=2?filter=qu=2%3A3%3A4%3Bcr=8%3A2%3Bcrs=2%3A2%3Bcrv=0%3A0
|
|
-- http://www.wowhead.com/items=4?filter=qu=2%3A3%3A4%3Bcr=8%3A2%3Bcrs=2%3A2%3Bcrv=0%3A0
|
|
local NON_DISENCHANTABLE_ITEMS = {
|
|
["i:11287"] = true,
|
|
["i:11288"] = true,
|
|
["i:11289"] = true,
|
|
["i:11290"] = true,
|
|
["i:20406"] = true,
|
|
["i:20407"] = true,
|
|
["i:20408"] = true,
|
|
["i:21766"] = true,
|
|
["i:52252"] = true,
|
|
["i:52485"] = true,
|
|
["i:52486"] = true,
|
|
["i:52487"] = true,
|
|
["i:52488"] = true,
|
|
["i:60223"] = true,
|
|
["i:75274"] = true,
|
|
["i:84661"] = true,
|
|
["i:89586"] = true,
|
|
["i:97826"] = true,
|
|
["i:97827"] = true,
|
|
["i:97828"] = true,
|
|
["i:97829"] = true,
|
|
["i:97830"] = true,
|
|
["i:97831"] = true,
|
|
["i:97832"] = true,
|
|
["i:109262"] = true,
|
|
["i:186056"] = true,
|
|
["i:186058"] = true,
|
|
["i:186163"] = true,
|
|
}
|
|
local REBUILD_MSG_THRESHOLD = 5000
|
|
local REBUILD_STAGE = {
|
|
IDLE = 1,
|
|
TRIGGERED = 2,
|
|
NOTIFIED = 3,
|
|
}
|
|
|
|
|
|
|
|
-- ============================================================================
|
|
-- Module Loading
|
|
-- ============================================================================
|
|
|
|
ItemInfo:OnModuleLoad(function()
|
|
private.rebuildStage = REBUILD_STAGE.IDLE
|
|
private.stream = Reactive.CreateStream()
|
|
private.processAvailableTimer = Delay.CreateTimer("ITEM_INFO_PROCESS_AVAILABLE", private.ProcessAvailableItems)
|
|
private.processInfoTimer = Delay.CreateTimer("ITEM_INFO_PROCESS_INFO", private.ProcessItemInfo)
|
|
private.merchantTimer = Delay.CreateTimer("ITEM_INFO_SCAN_MERCHANT", private.ScanMerchant)
|
|
private.deferredSetSingleFieldTimer = Delay.CreateTimer("ITEM_INFO_DEFERRED_SET_SINGLE_FIELD", private.HandleDeferredSetSingleField)
|
|
end)
|
|
|
|
ItemInfo:OnSettingsLoad(function()
|
|
private.settings = Settings.NewView()
|
|
:AddKey("global", "internalData", "vendorItems")
|
|
|
|
Event.Register("GET_ITEM_INFO_RECEIVED", function(_, itemId, success)
|
|
if not success or itemId <= 0 or itemId > ITEM_MAX_ID or private.numRequests[itemId] == math.huge then
|
|
return
|
|
end
|
|
private.availableItems[itemId] = true
|
|
private.processAvailableTimer:RunForTime(0)
|
|
end)
|
|
|
|
-- load the item info database
|
|
local names, itemStrings = nil, nil
|
|
local build, revision = GetBuildInfo()
|
|
local isValid = true
|
|
if not TSMItemInfoDB or TSMItemInfoDB.version ~= DB_VERSION or TSMItemInfoDB.locale ~= GetLocale() or TSMItemInfoDB.build ~= build or TSMItemInfoDB.revision ~= revision then
|
|
isValid = false
|
|
elseif #TSMItemInfoDB.data % RECORD_DATA_LENGTH_CHARS ~= 0 then
|
|
Analytics.Action("CORRUPTED_ITEM_INFO", "DATA", #TSMItemInfoDB.data)
|
|
isValid = false
|
|
else
|
|
names = private.LoadLongString(TSMItemInfoDB.names)
|
|
itemStrings = private.LoadLongString(TSMItemInfoDB.itemStrings)
|
|
if #names ~= #itemStrings then
|
|
Analytics.Action("CORRUPTED_ITEM_INFO", "NAMES_ITEM_STRINGS", #names, #itemStrings)
|
|
isValid = false
|
|
names = nil
|
|
itemStrings = nil
|
|
end
|
|
end
|
|
if not isValid then
|
|
TSMItemInfoDB = {
|
|
names = nil,
|
|
itemStrings = nil,
|
|
data = "",
|
|
}
|
|
private.hasChanged = true
|
|
wipe(private.settings.vendorItems)
|
|
end
|
|
|
|
-- load hard-coded vendor costs
|
|
for itemString, cost in VendorSell.Iterator() do
|
|
private.settings.vendorItems[itemString] = private.settings.vendorItems[itemString] or cost
|
|
end
|
|
|
|
names = names or {}
|
|
itemStrings = itemStrings or {}
|
|
assert(#names == #itemStrings)
|
|
local numItemsLoaded = #names
|
|
Log.Info("Imported %d items worth of data", numItemsLoaded)
|
|
if not Table.IsSorted(names, private.NameSortHelper) then
|
|
-- we'll sort our data on logout to make ItemInfo.MatchItemFilter a bit more efficient
|
|
private.hasChanged = true
|
|
end
|
|
|
|
-- The following code for populating our database is highly optimized as we're processing an excessive amount of data here
|
|
private.db = Database.NewSchema("ITEM_INFO")
|
|
:AddUniqueStringField("itemString")
|
|
:AddStringField("name")
|
|
:AddNumberField("itemLevel")
|
|
:AddNumberField("minLevel")
|
|
:AddNumberField("maxStack")
|
|
:AddNumberField("vendorSell")
|
|
:AddNumberField("invSlotId")
|
|
:AddNumberField("texture")
|
|
:AddNumberField("classId")
|
|
:AddNumberField("subClassId")
|
|
:AddNumberField("quality")
|
|
:AddNumberField("isBOP")
|
|
:AddNumberField("isCraftingReagent")
|
|
:AddNumberField("expansionId")
|
|
:AddNumberField("craftedQuality")
|
|
:AddTrigramIndex("name")
|
|
:Commit()
|
|
private.db:BulkInsertStart()
|
|
for i = 1, numItemsLoaded do
|
|
local itemString = itemStrings[i]
|
|
-- check the itemString
|
|
if ItemString.Get(itemString) == itemString then
|
|
-- load all the fields from the string
|
|
local dataOffset = (i - 1) * RECORD_DATA_LENGTH_CHARS + 1
|
|
local b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15, b16, b17, b18, b19, b20, b21, b22, b23, bExtra = strbyte(TSMItemInfoDB.data, dataOffset, dataOffset + RECORD_DATA_LENGTH_CHARS - 1)
|
|
assert(b23 and not bExtra)
|
|
|
|
-- load the fields
|
|
local itemLevel = (b0 == ENCODED_NIL_CHAR and b1 == ENCODED_NIL_CHAR) and -1 or (DECODING_TABLE[b0] + DECODING_TABLE[b1] * 2 ^ ENCODING_NUM_BITS)
|
|
local minLevel = (b2 == ENCODED_NIL_CHAR and b3 == ENCODED_NIL_CHAR) and -1 or (DECODING_TABLE[b2] + DECODING_TABLE[b3] * 2 ^ ENCODING_NUM_BITS)
|
|
local vendorSell = nil
|
|
if b4 == ENCODED_NIL_CHAR and b5 == ENCODED_NIL_CHAR and b6 == ENCODED_NIL_CHAR and b7 == ENCODED_NIL_CHAR and b8 == ENCODED_NIL_CHAR then
|
|
vendorSell = -1
|
|
else
|
|
vendorSell = DECODING_TABLE[b4] + DECODING_TABLE[b5] * 2 ^ ENCODING_NUM_BITS + DECODING_TABLE[b6] * 2 ^ (ENCODING_NUM_BITS * 2) + DECODING_TABLE[b7] * 2 ^ (ENCODING_NUM_BITS * 3) + DECODING_TABLE[b8] * 2 ^ (ENCODING_NUM_BITS * 4)
|
|
end
|
|
local maxStack = (b9 == ENCODED_NIL_CHAR and b10 == ENCODED_NIL_CHAR) and -1 or (DECODING_TABLE[b9] + DECODING_TABLE[b10] * 2 ^ ENCODING_NUM_BITS)
|
|
local invSlotId = DECODING_TABLE_1[b11]
|
|
local texture = nil
|
|
if b12 == ENCODED_NIL_CHAR and b13 == ENCODED_NIL_CHAR and b14 == ENCODED_NIL_CHAR and b15 == ENCODED_NIL_CHAR and b16 == ENCODED_NIL_CHAR then
|
|
texture = -1
|
|
else
|
|
texture = DECODING_TABLE[b12] + DECODING_TABLE[b13] * 2 ^ ENCODING_NUM_BITS + DECODING_TABLE[b14] * 2 ^ (ENCODING_NUM_BITS * 2) + DECODING_TABLE[b15] * 2 ^ (ENCODING_NUM_BITS * 3) + DECODING_TABLE[b16] * 2 ^ (ENCODING_NUM_BITS * 4)
|
|
end
|
|
local classId = DECODING_TABLE_1[b17]
|
|
local subClassId = DECODING_TABLE_1[b18]
|
|
local quality = DECODING_TABLE_1[b19]
|
|
local isBOP = DECODING_TABLE_1[b20]
|
|
local isCraftingReagent = DECODING_TABLE_1[b21]
|
|
local expansionId = DECODING_TABLE_1[b22]
|
|
local craftedQuality = DECODING_TABLE_1[b23]
|
|
|
|
-- store in the DB
|
|
local name = names[i]
|
|
private.db:BulkInsertNewRowFast15(itemString, name, itemLevel, minLevel, maxStack, vendorSell, invSlotId, texture, classId, subClassId, quality, isBOP, isCraftingReagent, expansionId, craftedQuality)
|
|
end
|
|
end
|
|
private.db:BulkInsertEnd()
|
|
private.stream:Send(nil)
|
|
|
|
-- process pending item info every 0.05 seconds
|
|
private.processInfoTimer:RunForTime(0)
|
|
-- scan the merchant when the goods are shown
|
|
DefaultUI.RegisterMerchantVisibleCallback(private.ScanMerchant, true)
|
|
Event.Register("MERCHANT_UPDATE", function()
|
|
private.merchantTimer:RunForTime(0.1)
|
|
end)
|
|
end)
|
|
|
|
ItemInfo:OnModuleUnload(function()
|
|
-- save the DB
|
|
if not TSMItemInfoDB or not private.hasChanged then
|
|
-- bailing if TSMItemInfoDB doesn't exist gives us an easy way to wipe the DB via "/run TSMItemInfoDB = nil"
|
|
return
|
|
end
|
|
local names = {}
|
|
local itemStrings = {}
|
|
local dataParts = {}
|
|
local rawData = private.db:GetRawData()
|
|
local numFields = private.db:GetNumStoredFields()
|
|
for i = 1, private.db:GetNumRows() do
|
|
local startOffset = (i - 1) * numFields + 1
|
|
local itemString, name, itemLevel, minLevel, maxStack, vendorSell, invSlotId, texture, classId, subClassId, quality, isBOP, isCraftingReagent, expansionId, craftedQuality = unpack(rawData, startOffset, startOffset + numFields - 1)
|
|
local b0, b1, b2, b3, b4, b5, b6, b7, b8, b9 = ENCODED_NIL_CHAR, ENCODED_NIL_CHAR, ENCODED_NIL_CHAR, ENCODED_NIL_CHAR, ENCODED_NIL_CHAR, ENCODED_NIL_CHAR, ENCODED_NIL_CHAR, ENCODED_NIL_CHAR, ENCODED_NIL_CHAR, ENCODED_NIL_CHAR
|
|
local b10, b11, b12, b13, b14, b15, b16, b17, b18, b19 = ENCODED_NIL_CHAR, ENCODED_NIL_CHAR, ENCODED_NIL_CHAR, ENCODED_NIL_CHAR, ENCODED_NIL_CHAR, ENCODED_NIL_CHAR, ENCODED_NIL_CHAR, ENCODED_NIL_CHAR, ENCODED_NIL_CHAR, ENCODED_NIL_CHAR
|
|
local b20, b21, b22, b23 = ENCODED_NIL_CHAR, ENCODED_NIL_CHAR, ENCODED_NIL_CHAR, ENCODED_NIL_CHAR
|
|
if itemLevel ~= -1 then
|
|
local chars = ENCODING_TABLE_2[itemLevel]
|
|
b0 = chars[1]
|
|
b1 = chars[2]
|
|
end
|
|
if minLevel ~= -1 then
|
|
local chars = ENCODING_TABLE_2[minLevel]
|
|
b2 = chars[1]
|
|
b3 = chars[2]
|
|
end
|
|
if vendorSell ~= -1 then
|
|
local charValue0 = vendorSell % 2 ^ ENCODING_NUM_BITS
|
|
vendorSell = (vendorSell - charValue0) / 2 ^ ENCODING_NUM_BITS
|
|
local charValue1 = vendorSell % 2 ^ ENCODING_NUM_BITS
|
|
vendorSell = (vendorSell - charValue1) / 2 ^ ENCODING_NUM_BITS
|
|
local charValue2 = vendorSell % 2 ^ ENCODING_NUM_BITS
|
|
vendorSell = (vendorSell - charValue2) / 2 ^ ENCODING_NUM_BITS
|
|
local charValue3 = vendorSell % 2 ^ ENCODING_NUM_BITS
|
|
vendorSell = (vendorSell - charValue3) / 2 ^ ENCODING_NUM_BITS
|
|
local charValue4 = vendorSell % 2 ^ ENCODING_NUM_BITS
|
|
vendorSell = (vendorSell - charValue4) / 2 ^ ENCODING_NUM_BITS
|
|
if vendorSell ~= 0 then
|
|
error("Invalid remainder")
|
|
end
|
|
b4 = ENCODING_TABLE[charValue0]
|
|
b5 = ENCODING_TABLE[charValue1]
|
|
b6 = ENCODING_TABLE[charValue2]
|
|
b7 = ENCODING_TABLE[charValue3]
|
|
b8 = ENCODING_TABLE[charValue4]
|
|
end
|
|
if maxStack ~= -1 then
|
|
local chars = ENCODING_TABLE_2[maxStack]
|
|
b9 = chars[1]
|
|
b10 = chars[2]
|
|
end
|
|
if invSlotId ~= -1 then
|
|
b11 = ENCODING_TABLE[invSlotId]
|
|
end
|
|
if texture ~= -1 then
|
|
local charValue0 = texture % 2 ^ ENCODING_NUM_BITS
|
|
texture = (texture - charValue0) / 2 ^ ENCODING_NUM_BITS
|
|
local charValue1 = texture % 2 ^ ENCODING_NUM_BITS
|
|
texture = (texture - charValue1) / 2 ^ ENCODING_NUM_BITS
|
|
local charValue2 = texture % 2 ^ ENCODING_NUM_BITS
|
|
texture = (texture - charValue2) / 2 ^ ENCODING_NUM_BITS
|
|
local charValue3 = texture % 2 ^ ENCODING_NUM_BITS
|
|
texture = (texture - charValue3) / 2 ^ ENCODING_NUM_BITS
|
|
local charValue4 = texture % 2 ^ ENCODING_NUM_BITS
|
|
texture = (texture - charValue4) / 2 ^ ENCODING_NUM_BITS
|
|
if texture ~= 0 then
|
|
error("Invalid remainder")
|
|
end
|
|
b12 = ENCODING_TABLE[charValue0]
|
|
b13 = ENCODING_TABLE[charValue1]
|
|
b14 = ENCODING_TABLE[charValue2]
|
|
b15 = ENCODING_TABLE[charValue3]
|
|
b16 = ENCODING_TABLE[charValue4]
|
|
end
|
|
if classId ~= -1 then
|
|
b17 = ENCODING_TABLE[classId]
|
|
end
|
|
if subClassId ~= -1 then
|
|
b18 = ENCODING_TABLE[subClassId]
|
|
end
|
|
if quality ~= -1 then
|
|
b19 = ENCODING_TABLE[quality]
|
|
end
|
|
if isBOP ~= -1 then
|
|
b20 = ENCODING_TABLE[isBOP]
|
|
end
|
|
if isCraftingReagent ~= -1 then
|
|
b21 = ENCODING_TABLE[isCraftingReagent]
|
|
end
|
|
if expansionId ~= -1 then
|
|
b22 = ENCODING_TABLE[expansionId]
|
|
end
|
|
if craftedQuality ~= -1 then
|
|
b23 = ENCODING_TABLE[craftedQuality]
|
|
end
|
|
|
|
names[i] = name
|
|
itemStrings[i] = itemString
|
|
dataParts[i] = strchar(b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15, b16, b17, b18, b19, b20, b21, b22, b23)
|
|
|
|
if #dataParts[i] ~= RECORD_DATA_LENGTH_CHARS then
|
|
names[i] = nil
|
|
itemStrings[i] = nil
|
|
dataParts[i] = nil
|
|
end
|
|
end
|
|
if not Table.IsSorted(names, private.NameSortHelper) then
|
|
-- sort all the data by the name to make ItemInfo.MatchItemFilter a bit more efficient in the future
|
|
local lowerNames = {}
|
|
local sortedIndexes = {}
|
|
for i = 1, #names do
|
|
if names[i] ~= nil then
|
|
tinsert(sortedIndexes, i)
|
|
lowerNames[i] = strlower(names[i])
|
|
end
|
|
end
|
|
Table.SortWithValueLookup(sortedIndexes, lowerNames)
|
|
local newNames = {}
|
|
local newItemStrings = {}
|
|
local newDataParts = {}
|
|
for i, oldIndex in ipairs(sortedIndexes) do
|
|
newNames[i] = names[oldIndex]
|
|
newItemStrings[i] = itemStrings[oldIndex]
|
|
newDataParts[i] = dataParts[oldIndex]
|
|
end
|
|
names = newNames
|
|
itemStrings = newItemStrings
|
|
dataParts = newDataParts
|
|
end
|
|
TSMItemInfoDB.names = private.StoreLongString(names)
|
|
TSMItemInfoDB.itemStrings = private.StoreLongString(itemStrings)
|
|
TSMItemInfoDB.data = table.concat(dataParts)
|
|
|
|
if #TSMItemInfoDB.data % RECORD_DATA_LENGTH_CHARS ~= 0 then
|
|
TSMItemInfoDB = nil
|
|
return
|
|
end
|
|
|
|
local build, revision = GetBuildInfo()
|
|
TSMItemInfoDB.version = DB_VERSION
|
|
TSMItemInfoDB.locale = GetLocale()
|
|
TSMItemInfoDB.build = build
|
|
TSMItemInfoDB.revision = revision
|
|
end)
|
|
|
|
|
|
|
|
-- ============================================================================
|
|
-- Module Functions
|
|
-- ============================================================================
|
|
|
|
function ItemInfo.ClearDB()
|
|
TSMItemInfoDB = nil
|
|
ReloadUI()
|
|
end
|
|
|
|
---Gets a publisher for item info changes.
|
|
---@return ReactivePublisher
|
|
function ItemInfo.GetPublisher()
|
|
return private.stream:Publisher()
|
|
end
|
|
|
|
---Sets whether or not query updates are paused on the item info DB
|
|
---@param paused boolean Whether or not query updates are paused
|
|
function ItemInfo.SetQueryUpdatesPaused(paused)
|
|
private.db:SetQueryUpdatesPaused(paused)
|
|
end
|
|
|
|
---Store the name of an item.
|
|
-- This function is used to opportunistically populate the item cache with item names.
|
|
---@param itemString string The itemString
|
|
---@param name string The item name
|
|
function ItemInfo.StoreItemName(itemString, name)
|
|
assert(not ItemString.ParseLevel(itemString))
|
|
private.DeferSetSingleField(itemString, "name", name)
|
|
end
|
|
|
|
---Store information about an item from its link.
|
|
---This function is used to opportunistically populate the item cache with item info.
|
|
---@param itemLink string The item link
|
|
function ItemInfo.StoreItemInfoByLink(itemLink)
|
|
-- see if we can extract the quality and name from the link
|
|
local colorHex, name = strmatch(itemLink, "^(\124cff[0-9a-z]+)\124[Hh].+\124h%[(.+)%]\124h\124r$")
|
|
if name then
|
|
name = gsub(name, " \124A:.+\124a", "")
|
|
end
|
|
if name == "" or name == UNKNOWN_ITEM_NAME or name == PLACEHOLDER_ITEM_NAME then
|
|
name = nil
|
|
end
|
|
local quality = ITEM_QUALITY_BY_HEX_LOOKUP[colorHex]
|
|
local itemString = ItemString.Get(itemLink)
|
|
if not itemString then
|
|
return nil
|
|
end
|
|
assert(not ItemString.ParseLevel(itemString))
|
|
if name then
|
|
private.DeferSetSingleField(itemString, "name", name)
|
|
end
|
|
if quality then
|
|
private.DeferSetSingleField(itemString, "quality", quality)
|
|
end
|
|
end
|
|
|
|
---Get the itemString from an item name.
|
|
---This API will return the base itemString when there are multiple variants with the same name and will return nil if
|
|
---there are multiple distinct items with the same name.
|
|
---@param name string The item name
|
|
---@return string?
|
|
function ItemInfo.ItemNameToItemString(name)
|
|
local result = nil
|
|
local query = private.db:NewQuery()
|
|
:Select("itemString")
|
|
:Equal("name", name)
|
|
for _, itemString in query:Iterator() do
|
|
if not result then
|
|
result = itemString
|
|
elseif result ~= ItemString.GetUnknown() then
|
|
-- multiple matching items
|
|
if ItemString.GetBase(itemString) == ItemString.GetBase(result) then
|
|
result = ItemString.GetBase(itemString)
|
|
else
|
|
result = ItemString.GetUnknown()
|
|
end
|
|
end
|
|
end
|
|
query:Release()
|
|
return result
|
|
end
|
|
|
|
---Get the name.
|
|
---@param item string The item
|
|
---@return string?
|
|
function ItemInfo.GetName(item)
|
|
local itemString = ItemString.Get(item)
|
|
if not itemString then
|
|
return nil
|
|
elseif itemString == ItemString.GetUnknown() then
|
|
return UNKNOWN_ITEM_NAME
|
|
elseif itemString == ItemString.GetPlaceholder() then
|
|
return PLACEHOLDER_ITEM_NAME
|
|
end
|
|
if ItemString.ParseLevel(itemString) then
|
|
itemString = ItemString.GetBaseFast(itemString)
|
|
end
|
|
local name = private.GetField(itemString, "name")
|
|
if not name then
|
|
-- we can fetch info instantly for pets, so try again afterwards
|
|
ItemInfo.FetchInfo(itemString)
|
|
name = private.GetField(itemString, "name")
|
|
end
|
|
if not name then
|
|
-- if we got passed an item link, we can maybe extract the name from it
|
|
name = strmatch(item, "^\124cff[0-9a-z]+\124[Hh].+\124h%[(.+)%]\124h\124r$")
|
|
if name then
|
|
name = gsub(name, " \124A:.+\124a", "")
|
|
end
|
|
if name == "" or name == UNKNOWN_ITEM_NAME or name == PLACEHOLDER_ITEM_NAME then
|
|
name = nil
|
|
end
|
|
if name then
|
|
private.DeferSetSingleField(itemString, "name", name)
|
|
end
|
|
end
|
|
return name
|
|
end
|
|
|
|
---Get the link (or an "Unknown Item" link).
|
|
---@param item string The item
|
|
---@return string?
|
|
function ItemInfo.GetLink(item)
|
|
local itemString = ItemString.Get(item)
|
|
if not itemString then
|
|
return nil
|
|
end
|
|
local link = nil
|
|
local itemStringType, speciesId, level, quality, health, power, speed, petId = strsplit(":", itemString)
|
|
local name = ItemInfo.GetName(item) or UNKNOWN_ITEM_NAME
|
|
local wowItemString = nil
|
|
if itemStringType == "p" then
|
|
quality = tonumber(quality) or 0
|
|
wowItemString = strjoin(":", "battlepet", speciesId, level or "", quality or "", health or "", power or "", speed or "", petId or "")
|
|
else
|
|
quality = ItemInfo.GetQuality(item)
|
|
wowItemString = ItemString.ToWow(itemString)
|
|
end
|
|
local qualityColor = ITEM_QUALITY_COLORS[quality] and ITEM_QUALITY_COLORS[quality].hex or "|cffff0000"
|
|
link = qualityColor.."|H"..wowItemString.."|h["..name.."]|h|r"
|
|
return link
|
|
end
|
|
|
|
---Get the expansion id.
|
|
---@param item string The item
|
|
---@return number?
|
|
function ItemInfo.GetExpansion(item)
|
|
local itemString = ItemString.Get(item)
|
|
if not itemString then
|
|
return nil
|
|
elseif ItemString.ParseLevel(itemString) then
|
|
itemString = ItemString.GetBaseFast(itemString)
|
|
end
|
|
local expansionId = private.GetFieldValueHelper(itemString, "expansionId", true, true, 0)
|
|
return expansionId
|
|
end
|
|
|
|
---Gets the crafted quality.
|
|
---@param item string The item
|
|
---@return number?
|
|
function ItemInfo.GetCraftedQuality(item)
|
|
if not Environment.HasFeature(Environment.FEATURES.CRAFTING_QUALITY) then
|
|
return nil
|
|
end
|
|
local itemString = ItemString.Get(item)
|
|
if not itemString then
|
|
return nil
|
|
elseif itemString == ItemString.GetUnknown() or itemString == ItemString.GetPlaceholder() then
|
|
return nil
|
|
elseif ItemString.ParseLevel(itemString) then
|
|
itemString = ItemString.GetBaseFast(itemString)
|
|
end
|
|
local craftedQuality = private.GetFieldValueHelper(itemString, "craftedQuality", false, false, 0)
|
|
return (craftedQuality or 0) > 0 and craftedQuality or nil
|
|
end
|
|
|
|
---Get the quality.
|
|
---@param item string The item
|
|
---@return number?
|
|
function ItemInfo.GetQuality(item)
|
|
local itemString = ItemString.Get(item)
|
|
if not itemString then
|
|
return nil
|
|
elseif ItemString.ParseLevel(itemString) then
|
|
itemString = ItemString.GetBaseFast(itemString)
|
|
end
|
|
local itemType, _, randOrLevel, bonusOrQuality = strsplit(":", itemString)
|
|
randOrLevel = tonumber(randOrLevel)
|
|
bonusOrQuality = tonumber(bonusOrQuality)
|
|
local petDefault = itemType == "p" and (bonusOrQuality or 0) or nil
|
|
local quality = private.GetFieldValueHelper(itemString, "quality", false, false, petDefault)
|
|
if quality then
|
|
return quality
|
|
end
|
|
if itemType == "i" and randOrLevel and not bonusOrQuality then
|
|
-- there is a random enchant, but no bonusIds, so the quality is the same as the base item
|
|
quality = ItemInfo.GetQuality(ItemString.GetBase(itemString))
|
|
elseif itemType == "i" and bonusOrQuality then
|
|
-- this item has bonusIds
|
|
local classId = ItemInfo.GetClassId(itemString)
|
|
if classId and classId ~= Enum.ItemClass.Weapon and classId ~= Enum.ItemClass.Armor then
|
|
-- the bonusId does not affect the quality of this item
|
|
quality = ItemInfo.GetQuality(ItemString.GetBase(itemString))
|
|
end
|
|
end
|
|
if quality then
|
|
private.DeferSetSingleField(itemString, "quality", quality)
|
|
else
|
|
ItemInfo.FetchInfo(itemString)
|
|
end
|
|
return quality
|
|
end
|
|
|
|
---Get the quality color.
|
|
---@param item string The item
|
|
---@return string?
|
|
function ItemInfo.GetQualityColor(item)
|
|
local itemString = ItemString.Get(item)
|
|
if itemString == ItemString.GetUnknown() then
|
|
return "|cffff0000"
|
|
elseif itemString == ItemString.GetPlaceholder() then
|
|
return "|cffffffff"
|
|
end
|
|
local quality = ItemInfo.GetQuality(itemString)
|
|
return quality and ITEM_QUALITY_COLORS[quality] and ITEM_QUALITY_COLORS[quality].hex or nil
|
|
end
|
|
|
|
---Get the item level.
|
|
---@param item string The item
|
|
---@return number?
|
|
function ItemInfo.GetItemLevel(item)
|
|
local itemString = ItemString.Get(item)
|
|
if not itemString then
|
|
return nil
|
|
end
|
|
local itemStringLevel, itemStringLevelIsAbs = ItemString.ParseLevel(itemString)
|
|
if itemStringLevel then
|
|
if itemStringLevelIsAbs then
|
|
return itemStringLevel
|
|
else
|
|
-- level is relative to the base item
|
|
local baseItemLevel = ItemInfo.GetItemLevel(ItemString.GetBaseFast(itemString))
|
|
if not baseItemLevel then
|
|
return nil
|
|
end
|
|
return baseItemLevel + itemStringLevel
|
|
end
|
|
end
|
|
local itemLevel = private.GetField(itemString, "itemLevel")
|
|
if itemLevel then
|
|
return itemLevel
|
|
end
|
|
local itemType, _, randOrLevel, bonusOrQuality = strsplit(":", itemString)
|
|
randOrLevel = tonumber(randOrLevel)
|
|
bonusOrQuality = tonumber(bonusOrQuality)
|
|
if itemType == "p" then
|
|
-- we can fetch info instantly for pets so try again
|
|
ItemInfo.FetchInfo(itemString)
|
|
itemLevel = private.GetField(itemString, "itemLevel")
|
|
if not itemLevel then
|
|
-- just get the level from the item string
|
|
itemLevel = randOrLevel or 0
|
|
private.DeferSetSingleField(itemString, "itemLevel", itemLevel)
|
|
end
|
|
elseif itemType == "i" then
|
|
if randOrLevel and not bonusOrQuality then
|
|
-- there is a random enchant, but no bonusIds, so the itemLevel is the same as the base item
|
|
itemLevel = ItemInfo.GetItemLevel(ItemString.GetBaseFast(itemString))
|
|
end
|
|
if itemLevel then
|
|
private.DeferSetSingleField(itemString, "itemLevel", itemLevel)
|
|
end
|
|
ItemInfo.FetchInfo(itemString)
|
|
else
|
|
error("Invalid item: "..tostring(itemString))
|
|
end
|
|
return itemLevel
|
|
end
|
|
|
|
---Get the min level.
|
|
---@param item string The item
|
|
---@return number?
|
|
function ItemInfo.GetMinLevel(item)
|
|
local itemString = ItemString.Get(item)
|
|
if not itemString then
|
|
return nil
|
|
elseif ItemString.ParseLevel(itemString) then
|
|
-- Create a fake itemString with the same itemLevel and look up that.
|
|
itemString = ItemString.Get(ItemString.ToWow(itemString))
|
|
assert(itemString)
|
|
end
|
|
-- if there is a random enchant, but no bonusIds, so the itemLevel is the same as the base item
|
|
local baseIsSame = strmatch(itemString, "^i:[0-9]+:[%-0-9]+$") and true or false
|
|
local minLevel = private.GetFieldValueHelper(itemString, "minLevel", baseIsSame, true, 0)
|
|
if not minLevel and ItemString.IsItem(itemString) then
|
|
local baseItemString = ItemString.GetBase(itemString)
|
|
local canHaveVariations = ItemInfo.CanHaveVariations(itemString)
|
|
if itemString ~= baseItemString and canHaveVariations == false then
|
|
-- the bonusId does not affect the minLevel of this item
|
|
minLevel = ItemInfo.GetMinLevel(baseItemString)
|
|
if minLevel then
|
|
private.DeferSetSingleField(itemString, "minLevel", minLevel)
|
|
end
|
|
end
|
|
end
|
|
return minLevel
|
|
end
|
|
|
|
---Get the max stack size.
|
|
---@param item string The item
|
|
---@return number?
|
|
function ItemInfo.GetMaxStack(item)
|
|
local itemString = ItemString.Get(item)
|
|
if not itemString then
|
|
return nil
|
|
elseif ItemString.ParseLevel(itemString) then
|
|
itemString = ItemString.GetBaseFast(itemString)
|
|
end
|
|
local maxStack = private.GetFieldValueHelper(itemString, "maxStack", true, true, 1)
|
|
if not maxStack and ItemString.IsItem(itemString) then
|
|
-- we might be able to deduce the maxStack based on the classId and subClassId
|
|
local classId = ItemInfo.GetClassId(item)
|
|
local subClassId = ItemInfo.GetSubClassId(item)
|
|
if classId and subClassId then
|
|
if classId == 1 then
|
|
maxStack = 1
|
|
elseif classId == 2 then
|
|
maxStack = 1
|
|
elseif classId == 4 then
|
|
if subClassId > 0 then
|
|
maxStack = 1
|
|
end
|
|
elseif classId == 15 then
|
|
if subClassId == 5 then
|
|
maxStack = 1
|
|
end
|
|
elseif classId == 16 then
|
|
maxStack = 20
|
|
elseif classId == 17 then
|
|
maxStack = 1
|
|
elseif classId == 18 then
|
|
maxStack = 1
|
|
end
|
|
end
|
|
if maxStack then
|
|
private.DeferSetSingleField(itemString, "maxStack", maxStack)
|
|
end
|
|
end
|
|
return maxStack
|
|
end
|
|
|
|
---Get the inventory slot id.
|
|
---@param item string The item
|
|
---@return number?
|
|
function ItemInfo.GetInvSlotId(item)
|
|
local itemString = ItemString.Get(item)
|
|
if not itemString then
|
|
return nil
|
|
elseif ItemString.ParseLevel(itemString) then
|
|
itemString = ItemString.GetBaseFast(itemString)
|
|
end
|
|
local invSlotId = private.GetFieldValueHelper(itemString, "invSlotId", true, true, 0)
|
|
return invSlotId
|
|
end
|
|
|
|
---Get the texture.
|
|
---@param item string The item
|
|
---@return number?
|
|
function ItemInfo.GetTexture(item)
|
|
local itemString = ItemString.Get(item)
|
|
if not itemString then
|
|
return nil
|
|
elseif itemString == ItemString.GetUnknown() then
|
|
return UNKNOWN_ITEM_TEXTURE
|
|
elseif ItemString.ParseLevel(itemString) then
|
|
itemString = ItemString.GetBaseFast(itemString)
|
|
end
|
|
local texture = private.GetFieldValueHelper(itemString, "texture", true, false, nil)
|
|
if texture then
|
|
return texture
|
|
end
|
|
private.StoreGetItemInfoInstant(itemString)
|
|
return private.GetField(itemString, "texture")
|
|
end
|
|
|
|
---Get the vendor sell price.
|
|
---@param item string The item
|
|
---@return number?
|
|
function ItemInfo.GetVendorSell(item)
|
|
local itemString = ItemString.Get(item)
|
|
if not itemString then
|
|
return nil
|
|
elseif ItemString.ParseLevel(itemString) then
|
|
-- The vendorSell price does seem to scale linearly with item level, but at a different
|
|
-- rate for different items, so there's no easy way to figure this out directly. Instead,
|
|
-- we create a fake itemString with the same itemLevel and look up that.
|
|
itemString = ItemString.Get(ItemString.ToWow(itemString))
|
|
assert(itemString)
|
|
end
|
|
local vendorSell = private.GetFieldValueHelper(itemString, "vendorSell", false, false, 0)
|
|
return (vendorSell or 0) > 0 and vendorSell or nil
|
|
end
|
|
|
|
---Get the class id.
|
|
---@param item string The item
|
|
---@return number?
|
|
function ItemInfo.GetClassId(item)
|
|
local itemString = ItemString.Get(item)
|
|
if not itemString then
|
|
return nil
|
|
elseif ItemString.ParseLevel(itemString) then
|
|
itemString = ItemString.GetBaseFast(itemString)
|
|
end
|
|
local classId = private.GetFieldValueHelper(itemString, "classId", true, true, Enum.ItemClass.Battlepet)
|
|
if classId then
|
|
return classId
|
|
end
|
|
private.StoreGetItemInfoInstant(itemString)
|
|
return private.GetField(itemString, "classId")
|
|
end
|
|
|
|
---Get the sub-class id.
|
|
---@param item string The item
|
|
---@return number?
|
|
function ItemInfo.GetSubClassId(item)
|
|
local itemString = ItemString.Get(item)
|
|
if not itemString then
|
|
return nil
|
|
elseif ItemString.ParseLevel(itemString) then
|
|
itemString = ItemString.GetBaseFast(itemString)
|
|
end
|
|
local subClassId = private.GetFieldValueHelper(itemString, "subClassId", true, true, nil)
|
|
if subClassId then
|
|
return subClassId
|
|
end
|
|
private.StoreGetItemInfoInstant(itemString)
|
|
return private.GetField(itemString, "subClassId")
|
|
end
|
|
|
|
|
|
---Get whether or not the item is bind on pickup.
|
|
---@param item string The item
|
|
---@return boolean?
|
|
function ItemInfo.IsSoulbound(item)
|
|
local itemString = ItemString.Get(item)
|
|
if not itemString then
|
|
return nil
|
|
elseif ItemString.ParseLevel(itemString) then
|
|
itemString = ItemString.GetBaseFast(itemString)
|
|
end
|
|
local isBOP = private.GetFieldValueHelper(itemString, "isBOP", true, true, false)
|
|
if type(isBOP) == "number" then
|
|
isBOP = isBOP == 1
|
|
end
|
|
return isBOP
|
|
end
|
|
|
|
---Get whether or not the item is a crafting reagent.
|
|
---@param item string The item
|
|
---@return boolean?
|
|
function ItemInfo.IsCraftingReagent(item)
|
|
local itemString = ItemString.Get(item)
|
|
if not itemString then
|
|
return nil
|
|
elseif ItemString.ParseLevel(itemString) then
|
|
itemString = ItemString.GetBaseFast(itemString)
|
|
end
|
|
local isCraftingReagent = private.GetFieldValueHelper(itemString, "isCraftingReagent", true, true, false)
|
|
if type(isCraftingReagent) == "number" then
|
|
isCraftingReagent = isCraftingReagent == 1
|
|
end
|
|
return isCraftingReagent
|
|
end
|
|
|
|
---Get the vendor buy price.
|
|
---@param item string The item
|
|
---@return number?
|
|
function ItemInfo.GetVendorBuy(item)
|
|
local itemString = ItemString.Get(item)
|
|
if not itemString or ItemString.ParseLevel(itemString) then
|
|
return nil
|
|
end
|
|
return private.settings.vendorItems[itemString]
|
|
end
|
|
|
|
---Get whether or not the item is disenchantable.
|
|
---@param item string The item
|
|
---@return boolean?
|
|
function ItemInfo.IsDisenchantable(item)
|
|
local itemString = ItemString.Get(item)
|
|
if not itemString then
|
|
return nil
|
|
elseif ItemString.ParseLevel(itemString) then
|
|
itemString = ItemString.GetBaseFast(itemString)
|
|
end
|
|
local invSlotId = ItemInfo.GetInvSlotId(itemString)
|
|
if invSlotId == Enum.InventoryType.IndexBodyType or invSlotId == Enum.InventoryType.IndexTabardType or NON_DISENCHANTABLE_ITEMS[itemString] then
|
|
return nil
|
|
end
|
|
local quality = ItemInfo.GetQuality(itemString)
|
|
local classId = ItemInfo.GetClassId(itemString)
|
|
if not quality or not classId then
|
|
return nil
|
|
end
|
|
return quality >= (Enum.ItemQuality.Good or Enum.ItemQuality.Uncommon) and quality < Enum.ItemQuality.Legendary and (classId == Enum.ItemClass.Armor or classId == Enum.ItemClass.Weapon or classId == Enum.ItemClass.Profession)
|
|
end
|
|
|
|
---Get whether or not the item is a commodity in WoW 8.3 (and above).
|
|
---@param item string The item
|
|
---@return number?
|
|
function ItemInfo.IsCommodity(item)
|
|
if not Environment.HasFeature(Environment.FEATURES.COMMODITY_ITEMS) then
|
|
return false
|
|
end
|
|
local stackSize = ItemInfo.GetMaxStack(item)
|
|
if not stackSize then
|
|
return nil
|
|
end
|
|
return stackSize > 1
|
|
end
|
|
|
|
---Get whether or not the item can have variations.
|
|
---@param item string The item
|
|
---@return boolean?
|
|
function ItemInfo.CanHaveVariations(item)
|
|
local classId = ItemInfo.GetClassId(item)
|
|
if not classId then
|
|
return nil
|
|
end
|
|
if classId == Enum.ItemClass.Armor or classId == Enum.ItemClass.Weapon or classId == Enum.ItemClass.Battlepet then
|
|
return true
|
|
elseif classId == Enum.ItemClass.Gem then
|
|
local subClassId = ItemInfo.GetSubClassId(item)
|
|
if not subClassId then
|
|
return nil
|
|
end
|
|
return subClassId == Enum.ItemGemSubclass.Artifactrelic
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
|
|
---Fetch info for the item.
|
|
---This function can be called ahead of time for items which we know we need to have info cached for.
|
|
---@param item? string The item
|
|
function ItemInfo.FetchInfo(item)
|
|
if item == ItemString.GetUnknown() or item == ItemString.GetPlaceholder() or ItemString.ParseLevel(item) then
|
|
return
|
|
end
|
|
local itemString = ItemString.Get(item)
|
|
if not itemString then return end
|
|
if ItemString.IsPet(itemString) then
|
|
if not private.GetField(itemString, "name") then
|
|
private.StoreGetItemInfoInstant(itemString)
|
|
end
|
|
return
|
|
end
|
|
private.pendingItems[itemString] = private.pendingItems[itemString] or PENDING_STATE_NEW
|
|
if private.priorityPendingTime ~= time() then
|
|
wipe(private.priorityPendingItems)
|
|
private.priorityPendingTime = time()
|
|
end
|
|
private.priorityPendingItems[itemString] = true
|
|
|
|
private.processInfoTimer:RunForTime(0)
|
|
end
|
|
|
|
---Generalize an item link.
|
|
---@param itemLink string The item link
|
|
---@return string?
|
|
function ItemInfo.GeneralizeLink(itemLink)
|
|
local itemString = ItemString.Get(itemLink)
|
|
if not itemString then return end
|
|
if ItemString.IsItem(itemString) and not strmatch(itemString, "i:[0-9]+:[0-9%-]*:[0-9]*") then
|
|
-- swap out the itemString part of the link
|
|
local leader, quality, _, name, trailer, trailer2, extra = strsplit("\124", itemLink)
|
|
if trailer2 and not extra then
|
|
return strjoin("\124", leader, quality, "H"..ItemString.ToWow(itemString), name, trailer, trailer2)
|
|
end
|
|
end
|
|
return ItemInfo.GetLink(itemString)
|
|
end
|
|
|
|
---Creates a query which matches the specified item filter.
|
|
---@param itemFilter ItemFilter The item filter
|
|
---@param query? DatabaseQuery Optionally, an existing query to reset and reuse
|
|
---@return DatabaseQuery
|
|
function ItemInfo.MatchItemFilterQuery(itemFilter, query)
|
|
if query then
|
|
query:Reset()
|
|
else
|
|
query = private.db:NewQuery()
|
|
end
|
|
|
|
local str = itemFilter:GetStr()
|
|
if str then
|
|
if itemFilter:GetExactOnly() then
|
|
query:Equal("name", str)
|
|
else
|
|
query:Contains("name", str)
|
|
end
|
|
end
|
|
local minQuality = itemFilter:GetMinQuality()
|
|
if minQuality then
|
|
query:GreaterThanOrEqual("quality", minQuality)
|
|
end
|
|
local maxQuality = itemFilter:GetMaxQuality()
|
|
if maxQuality then
|
|
query:LessThanOrEqual("quality", maxQuality)
|
|
end
|
|
local minLevel = itemFilter:GetMinLevel()
|
|
if minLevel then
|
|
query:GreaterThanOrEqual("minLevel", minLevel)
|
|
end
|
|
local maxLevel = itemFilter:GetMaxLevel()
|
|
if maxLevel then
|
|
query:LessThanOrEqual("minLevel", maxLevel)
|
|
end
|
|
local minItemLevel = itemFilter:GetMinItemLevel()
|
|
if minItemLevel then
|
|
query:GreaterThanOrEqual("itemLevel", minItemLevel)
|
|
end
|
|
local maxItemLevel = itemFilter:GetMaxItemLevel()
|
|
if maxItemLevel then
|
|
query:LessThanOrEqual("itemLevel", maxItemLevel)
|
|
end
|
|
local classId = itemFilter:GetClass()
|
|
if classId then
|
|
query:Equal("classId", classId)
|
|
end
|
|
local subClassId = itemFilter:GetSubClass()
|
|
if subClassId then
|
|
query:Equal("subClassId", subClassId)
|
|
end
|
|
local invSlotId = itemFilter:GetInvSlotId()
|
|
if invSlotId then
|
|
query:Equal("invSlotId", invSlotId)
|
|
end
|
|
|
|
return query
|
|
end
|
|
|
|
|
|
|
|
-- ============================================================================
|
|
-- Helper Functions
|
|
-- ============================================================================
|
|
|
|
function private.GetFieldValueHelper(itemString, field, baseIsSame, storeBaseValue, petDefaultValue)
|
|
local value = private.GetField(itemString, field)
|
|
if value ~= nil then
|
|
return value
|
|
end
|
|
ItemInfo.FetchInfo(itemString)
|
|
if ItemString.IsPet(itemString) then
|
|
-- We can fetch info instantly for pets so try again
|
|
value = private.GetField(itemString, field)
|
|
if value == nil and petDefaultValue ~= nil then
|
|
value = petDefaultValue
|
|
private.DeferSetSingleField(itemString, field, value)
|
|
end
|
|
end
|
|
if value == nil and baseIsSame then
|
|
-- The value is the same for the base item
|
|
local baseItemString = ItemString.GetBase(itemString)
|
|
if baseItemString ~= itemString then
|
|
value = private.GetFieldValueHelper(baseItemString, field)
|
|
if value ~= nil and storeBaseValue then
|
|
private.DeferSetSingleField(itemString, field, value)
|
|
end
|
|
end
|
|
end
|
|
return value
|
|
end
|
|
|
|
function private.ProcessPendingItemInfo(itemString)
|
|
local name = private.GetField(itemString, "name")
|
|
local quality = private.GetField(itemString, "quality")
|
|
local itemLevel = private.GetField(itemString, "itemLevel")
|
|
if (private.numRequests[itemString] or 0) > MAX_REQUESTS_PER_ITEM then
|
|
-- give up on this item
|
|
if private.numRequests[itemString] ~= math.huge then
|
|
private.numRequests[itemString] = math.huge
|
|
local itemId = ItemString.IsItem(itemString) and ItemString.ToId(itemString) or nil
|
|
if Environment.IsRetail() then
|
|
Log.Err("Giving up on item info for %s", itemString)
|
|
end
|
|
if itemId and itemString == ItemString.GetBaseFast(itemString) then
|
|
private.numRequests[itemId] = math.huge
|
|
end
|
|
end
|
|
private.pendingItems[itemString] = nil
|
|
private.priorityPendingItems[itemString] = nil
|
|
elseif name and name ~= "" and quality and quality >= 0 and itemLevel and itemLevel >= 0 then
|
|
-- we have info for this item
|
|
private.pendingItems[itemString] = nil
|
|
private.priorityPendingItems[itemString] = nil
|
|
private.numRequests[itemString] = nil
|
|
else
|
|
-- request info for this item
|
|
if not private.StoreGetItemInfo(itemString) then
|
|
private.numRequests[itemString] = (private.numRequests[itemString] or 0) + 1
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function private.ProcessItemInfo()
|
|
private.processInfoTimer:RunForTime(ITEM_INFO_INTERVAL)
|
|
if InCombatLockdown() then
|
|
return
|
|
end
|
|
local startTime = GetTimePreciseSec()
|
|
private.db:SetQueryUpdatesPaused(true)
|
|
|
|
-- create rows for items which don't exist at all in the DB in bulk
|
|
private.db:BulkInsertStart()
|
|
local priorityPendingItems = TempTable.Acquire()
|
|
local pendingItems = TempTable.Acquire()
|
|
for itemString, state in pairs(private.pendingItems) do
|
|
if state == PENDING_STATE_NEW then
|
|
local baseItemString = ItemString.GetBase(itemString)
|
|
private.CreateDBRowIfNotExists(itemString, true)
|
|
if baseItemString ~= itemString then
|
|
private.CreateDBRowIfNotExists(baseItemString, true)
|
|
end
|
|
private.pendingItems[itemString] = PENDING_STATE_CREATED
|
|
end
|
|
if private.priorityPendingItems[itemString] then
|
|
tinsert(priorityPendingItems, itemString)
|
|
else
|
|
tinsert(pendingItems, itemString)
|
|
end
|
|
end
|
|
private.db:BulkInsertEnd()
|
|
|
|
-- throttle the max number of item info requests based on the frame rate
|
|
local framerate = GetFramerate()
|
|
local maxRequests = nil
|
|
if framerate < 30 then
|
|
maxRequests = MAX_REQUESTED_ITEM_INFO / 5
|
|
elseif framerate < 60 then
|
|
maxRequests = MAX_REQUESTED_ITEM_INFO / 3
|
|
elseif framerate < 100 then
|
|
maxRequests = MAX_REQUESTED_ITEM_INFO / 2
|
|
else
|
|
maxRequests = MAX_REQUESTED_ITEM_INFO
|
|
end
|
|
|
|
local shouldStop = false
|
|
local numRequested = 0
|
|
-- do the priority items first
|
|
for i = 1, #priorityPendingItems do
|
|
if private.ProcessPendingItemInfo(priorityPendingItems[i]) then
|
|
numRequested = numRequested + 1
|
|
if numRequested >= maxRequests then
|
|
shouldStop = true
|
|
break
|
|
end
|
|
end
|
|
if (GetTimePreciseSec() - startTime) > ITEM_INFO_INTERVAL / 5 and numRequested >= maxRequests / 2 then
|
|
-- bail early since we've already used a good number of CPU cycles this frame
|
|
shouldStop = true
|
|
break
|
|
end
|
|
end
|
|
if not shouldStop then
|
|
for i = 1, #pendingItems do
|
|
if private.ProcessPendingItemInfo(pendingItems[i]) then
|
|
numRequested = numRequested + 1
|
|
if numRequested >= maxRequests then
|
|
shouldStop = true
|
|
break
|
|
end
|
|
end
|
|
if (GetTimePreciseSec() - startTime) > ITEM_INFO_INTERVAL / 5 and numRequested >= maxRequests / 2 then
|
|
-- bail early since we've already used a good number of CPU cycles this frame
|
|
shouldStop = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if #pendingItems > 0 and GetTime() - private.lastDebugLog > 5 then
|
|
private.lastDebugLog = GetTime()
|
|
Log.Info("%d/%d pending items (just requested %d)", #pendingItems, #priorityPendingItems, numRequested)
|
|
end
|
|
TempTable.Release(pendingItems)
|
|
TempTable.Release(priorityPendingItems)
|
|
|
|
if private.rebuildStage == REBUILD_STAGE.IDLE and numRequested >= maxRequests / 2 and Table.Count(private.pendingItems) >= REBUILD_MSG_THRESHOLD then
|
|
private.rebuildStage = REBUILD_STAGE.TRIGGERED
|
|
-- delay this message to make it more likely to be seen and make sure we're actually rebuilding
|
|
local timer = Delay.CreateTimer("ITEM_INFO_REBUILD_MESSAGE", private.ShowRebuildMessage)
|
|
timer:RunForTime(1)
|
|
end
|
|
if not next(private.pendingItems) then
|
|
if private.rebuildStage == REBUILD_STAGE.NOTIFIED then
|
|
Log.PrintUser(L["Done rebuilding item cache."])
|
|
private.rebuildStage = REBUILD_STAGE.IDLE
|
|
end
|
|
private.processInfoTimer:Cancel()
|
|
end
|
|
|
|
private.db:SetQueryUpdatesPaused(false)
|
|
end
|
|
|
|
function private.ShowRebuildMessage()
|
|
if Table.Count(private.pendingItems) < REBUILD_MSG_THRESHOLD then
|
|
-- no longer rebuilding
|
|
private.rebuildStage = REBUILD_STAGE.IDLE
|
|
return
|
|
end
|
|
Log.PrintUser(L["TSM is currently rebuilding its item cache which may cause FPS drops and result in TSM not being fully functional until this process is complete. This is normal and typically takes a few minutes."])
|
|
private.rebuildStage = REBUILD_STAGE.NOTIFIED
|
|
end
|
|
|
|
function private.ScanMerchant()
|
|
for i = 1, GetMerchantNumItems() do
|
|
local itemString = ItemString.Get(GetMerchantItemLink(i))
|
|
if itemString then
|
|
local currentValue = private.settings.vendorItems[itemString]
|
|
local newValue = nil
|
|
local _, _, price, quantity, numAvailable, _, _, extendedCost = GetMerchantItemInfo(i)
|
|
-- only store vendor prices for unlimited quantity items
|
|
if numAvailable == -1 then
|
|
-- bug with big keech vendor returning extendedCost = true for gold only items so need to check GetMerchantItemCostInfo
|
|
if price > 0 and (not extendedCost or GetMerchantItemCostInfo(i) == 0) then
|
|
newValue = Math.Round(price / quantity)
|
|
end
|
|
if newValue ~= currentValue then
|
|
private.settings.vendorItems[itemString] = newValue
|
|
private.stream:Send(itemString)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function private.CheckFieldValue(key, value)
|
|
if value == -1 then
|
|
return
|
|
end
|
|
assert(value >= 0 and value <= FIELD_INFO[key].maxValue)
|
|
end
|
|
|
|
function private.GetField(itemString, key)
|
|
local value = private.db:GetUniqueRowField("itemString", itemString, key)
|
|
if value == -1 or value == "" then
|
|
return nil
|
|
end
|
|
return value
|
|
end
|
|
|
|
function private.CreateDBRowIfNotExists(itemString, isBulkInsert)
|
|
if private.db:HasUniqueRow("itemString", itemString) then
|
|
return
|
|
end
|
|
if isBulkInsert then
|
|
private.db:BulkInsertNewRow(itemString, "", -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1)
|
|
else
|
|
private.db:NewRow()
|
|
:SetField("itemString", itemString)
|
|
:SetField("name", "")
|
|
:SetField("minLevel", -1)
|
|
:SetField("itemLevel", -1)
|
|
:SetField("maxStack", -1)
|
|
:SetField("vendorSell", -1)
|
|
:SetField("quality", -1)
|
|
:SetField("isBOP", -1)
|
|
:SetField("isCraftingReagent", -1)
|
|
:SetField("texture", -1)
|
|
:SetField("classId", -1)
|
|
:SetField("subClassId", -1)
|
|
:SetField("invSlotId", -1)
|
|
:SetField("expansionId", -1)
|
|
:SetField("craftedQuality", -1)
|
|
:Create()
|
|
end
|
|
private.hasChanged = true
|
|
end
|
|
|
|
function private.DeferSetSingleField(itemString, key, value)
|
|
if type(value) == "boolean" then
|
|
value = value and 1 or 0
|
|
end
|
|
if key ~= "name" then
|
|
private.CheckFieldValue(key, value)
|
|
end
|
|
Table.InsertMultiple(private.deferredSetSingleField, itemString, key, value)
|
|
private.deferredSetSingleFieldTimer:RunForFrames(0)
|
|
end
|
|
|
|
function private.HandleDeferredSetSingleField()
|
|
for _, itemString, key, value in Table.StrideIterator(private.deferredSetSingleField, 3) do
|
|
-- Make sure it actually changed
|
|
if private.db:GetUniqueRowField("itemString", itemString, key) ~= value then
|
|
private.CreateDBRowIfNotExists(itemString)
|
|
private.db:SetUniqueRowField("itemString", itemString, key, value)
|
|
private.hasChanged = true
|
|
private.stream:Send(itemString)
|
|
end
|
|
end
|
|
end
|
|
|
|
function private.SetItemInfoInstantFields(itemString, texture, classId, subClassId, invSlotId)
|
|
private.CheckFieldValue("texture", texture)
|
|
private.CheckFieldValue("classId", classId)
|
|
private.CheckFieldValue("subClassId", subClassId)
|
|
private.CheckFieldValue("invSlotId", invSlotId)
|
|
private.CreateDBRowIfNotExists(itemString)
|
|
private.db:SetUniqueRowField("itemString", itemString, "texture", texture)
|
|
private.db:SetUniqueRowField("itemString", itemString, "classId", classId)
|
|
private.db:SetUniqueRowField("itemString", itemString, "subClassId", subClassId)
|
|
private.db:SetUniqueRowField("itemString", itemString, "invSlotId", invSlotId)
|
|
private.hasChanged = true
|
|
private.stream:Send(itemString)
|
|
end
|
|
|
|
function private.StoreGetItemInfoInstant(itemString)
|
|
local itemStringType, id, extra1, extra2 = strmatch(itemString, "^([pi]):([0-9]+):?([0-9]*):?([0-9]*)")
|
|
id = tonumber(id)
|
|
if private.GetField(itemString, "texture") and private.GetField(itemString, "invSlotId") then
|
|
-- we already have info cached for this item
|
|
return
|
|
end
|
|
extra1 = tonumber(extra1)
|
|
extra2 = tonumber(extra2)
|
|
|
|
if itemStringType == "i" then
|
|
local _, classStr, subClassStr, equipSlot, texture, classId, subClassId = GetItemInfoInstant(id)
|
|
equipSlot = equipSlot and equipSlot ~= "" and _G[equipSlot] or nil
|
|
if not texture then
|
|
return
|
|
end
|
|
-- some items (such as i:37445) give a classId of -1 for some reason in which case we can look up the classId
|
|
if classId < 0 then
|
|
classId = ItemClass.GetClassIdFromClassString(classStr)
|
|
if not classId and not Environment.IsRetail() then
|
|
-- this can happen for items which don't yet exist in classic (i.e. WoW Tokens)
|
|
return
|
|
end
|
|
assert(subClassStr == "")
|
|
subClassId = 0
|
|
end
|
|
local invSlotId = equipSlot and ItemClass.GetInventorySlotIdFromInventorySlotString(equipSlot) or 0
|
|
private.SetItemInfoInstantFields(itemString, texture, classId, subClassId, invSlotId)
|
|
local baseItemString = ItemString.GetBase(itemString)
|
|
if baseItemString ~= itemString then
|
|
private.SetItemInfoInstantFields(baseItemString, texture, classId, subClassId, invSlotId)
|
|
end
|
|
elseif itemStringType == "p" then
|
|
if not Environment.HasFeature(Environment.FEATURES.BATTLE_PETS) then
|
|
return
|
|
end
|
|
local name, texture, petTypeId = C_PetJournal.GetPetInfoBySpeciesID(id)
|
|
if not texture or not petTypeId then
|
|
return
|
|
end
|
|
-- we can now store all the info for this pet
|
|
local classId = Enum.ItemClass.Battlepet
|
|
local subClassId = petTypeId - 1
|
|
local invSlotId = 0
|
|
local minLevel = extra1 or 0
|
|
local itemLevel = extra1 or 0
|
|
local quality = extra2 or 0
|
|
local maxStack = 1
|
|
local vendorSell = 0
|
|
local isBOP = 0
|
|
local isCraftingReagent = 0
|
|
local expansionId = -1
|
|
local craftedQuality = -1
|
|
private.SetItemInfoInstantFields(itemString, texture, classId, subClassId, invSlotId)
|
|
private.SetGetItemInfoFields(itemString, name, minLevel, itemLevel, maxStack, vendorSell, quality, isBOP, isCraftingReagent, expansionId, craftedQuality)
|
|
local baseItemString = ItemString.GetBase(itemString)
|
|
if baseItemString ~= itemString then
|
|
minLevel = 0
|
|
itemLevel = 0
|
|
quality = 0
|
|
private.SetItemInfoInstantFields(baseItemString, texture, classId, subClassId, invSlotId)
|
|
private.SetGetItemInfoFields(baseItemString, name, minLevel, itemLevel, maxStack, vendorSell, quality, isBOP, isCraftingReagent, expansionId, craftedQuality)
|
|
end
|
|
else
|
|
assert("Invalid itemString: "..itemString)
|
|
end
|
|
end
|
|
|
|
function private.SetGetItemInfoFields(itemString, name, minLevel, itemLevel, maxStack, vendorSell, quality, isBOP, isCraftingReagent, expansionId, craftedQuality)
|
|
private.CheckFieldValue("minLevel", minLevel)
|
|
private.CheckFieldValue("itemLevel", itemLevel)
|
|
private.CheckFieldValue("maxStack", maxStack)
|
|
private.CheckFieldValue("vendorSell", vendorSell)
|
|
private.CheckFieldValue("quality", quality)
|
|
private.CheckFieldValue("isBOP", isBOP)
|
|
private.CheckFieldValue("isCraftingReagent", isCraftingReagent)
|
|
private.CheckFieldValue("expansionId", expansionId)
|
|
private.CheckFieldValue("craftedQuality", craftedQuality)
|
|
private.CreateDBRowIfNotExists(itemString)
|
|
private.db:SetUniqueRowField("itemString", itemString, "name", name)
|
|
private.db:SetUniqueRowField("itemString", itemString, "minLevel", minLevel)
|
|
private.db:SetUniqueRowField("itemString", itemString, "itemLevel", itemLevel)
|
|
private.db:SetUniqueRowField("itemString", itemString, "maxStack", maxStack)
|
|
private.db:SetUniqueRowField("itemString", itemString, "vendorSell", vendorSell)
|
|
private.db:SetUniqueRowField("itemString", itemString, "quality", quality)
|
|
private.db:SetUniqueRowField("itemString", itemString, "isBOP", isBOP)
|
|
private.db:SetUniqueRowField("itemString", itemString, "isCraftingReagent", isCraftingReagent)
|
|
private.db:SetUniqueRowField("itemString", itemString, "expansionId", expansionId)
|
|
private.db:SetUniqueRowField("itemString", itemString, "craftedQuality", craftedQuality)
|
|
private.hasChanged = true
|
|
private.stream:Send(itemString)
|
|
end
|
|
|
|
function private.StoreGetItemInfo(itemString)
|
|
private.StoreGetItemInfoInstant(itemString)
|
|
assert(ItemString.IsItem(itemString))
|
|
local wowItemString = ItemString.ToWow(itemString)
|
|
local baseItemString = ItemString.GetBase(itemString)
|
|
local baseWowItemString = ItemString.ToWow(baseItemString)
|
|
|
|
local name, link, quality, itemLevel, minLevel, _, _, maxStack, _, _, vendorSell, _, _, bindType, expansionId, _, isCraftingReagent = GetItemInfo(baseWowItemString)
|
|
local craftedQuality = nil
|
|
if not Environment.HasFeature(Environment.FEATURES.CRAFTING_QUALITY) then
|
|
expansionId = -1
|
|
craftedQuality = -1
|
|
elseif link then
|
|
craftedQuality = strmatch(link, "\124A:Professions%-ChatIcon%-Quality%-Tier([0-9]+)")
|
|
craftedQuality = tonumber(craftedQuality) or -1
|
|
end
|
|
local isBOP = (bindType == LE_ITEM_BIND_ON_ACQUIRE or bindType == LE_ITEM_BIND_QUEST) and 1 or 0
|
|
isCraftingReagent = isCraftingReagent and 1 or 0
|
|
-- some items (i.e. "i:40752" produce a very high max stack, so cap it)
|
|
maxStack = maxStack and min(maxStack, FIELD_INFO.maxStack.maxValue) or nil
|
|
-- some items (i.e. "i:117356::1:573") produce an negative min level
|
|
minLevel = minLevel and max(minLevel, 0) or nil
|
|
|
|
-- store info for the base item
|
|
if name and quality and craftedQuality then
|
|
private.SetGetItemInfoFields(baseItemString, name, minLevel, itemLevel, maxStack, vendorSell, quality, isBOP, isCraftingReagent, expansionId, craftedQuality)
|
|
end
|
|
local gotInfo = true
|
|
if not name or name == "" or not quality or quality < 0 or not itemLevel or itemLevel < 0 or not craftedQuality then
|
|
gotInfo = false
|
|
end
|
|
|
|
-- store info for the specific item if it's different
|
|
if itemString ~= baseItemString then
|
|
-- get new values of the fields which can change from the base item
|
|
local baseVendorSell = vendorSell
|
|
name, _, quality, _, minLevel, _, _, _, _, _, vendorSell = GetItemInfo(wowItemString)
|
|
-- some items (i.e. "i:130064::2:196:1812") produce a negative vendor sell, so just use the base one
|
|
if vendorSell and vendorSell < 0 then
|
|
vendorSell = baseVendorSell
|
|
end
|
|
-- some items (i.e. "i:117356::1:573") produce an negative min level
|
|
minLevel = minLevel and max(minLevel, 0) or nil
|
|
itemLevel = GetDetailedItemLevelInfo(wowItemString)
|
|
if name or quality or itemLevel or maxStack then
|
|
if not name then
|
|
name = ""
|
|
minLevel = -1
|
|
end
|
|
quality = quality or -1
|
|
itemLevel = itemLevel or -1
|
|
expansionId = expansionId or -1
|
|
if not maxStack then
|
|
maxStack = -1
|
|
vendorSell = -1
|
|
isBOP = -1
|
|
isCraftingReagent = -1
|
|
end
|
|
craftedQuality = -1
|
|
private.SetGetItemInfoFields(itemString, name, minLevel, itemLevel, maxStack, vendorSell, quality, isBOP, isCraftingReagent, expansionId, craftedQuality)
|
|
end
|
|
if not name or name == "" or not quality or quality < 0 or not itemLevel or itemLevel < 0 then
|
|
gotInfo = false
|
|
end
|
|
end
|
|
|
|
return gotInfo
|
|
end
|
|
|
|
function private.ProcessAvailableItems()
|
|
private.db:SetQueryUpdatesPaused(true)
|
|
|
|
-- bulk insert items we didn't previously know about
|
|
private.db:BulkInsertStart()
|
|
for itemId in pairs(private.availableItems) do
|
|
local itemString = "i:"..itemId
|
|
private.CreateDBRowIfNotExists(itemString, true)
|
|
end
|
|
private.db:BulkInsertEnd()
|
|
|
|
-- remove the items we process after processing them all because GET_ITEM_INFO_RECEIVED events may fire as we do this
|
|
local processedItems = TempTable.Acquire()
|
|
for itemId in pairs(private.availableItems) do
|
|
processedItems[itemId] = true
|
|
local itemString = "i:"..itemId
|
|
if private.StoreGetItemInfo(itemString) then
|
|
private.pendingItems[itemString] = nil
|
|
private.priorityPendingItems[itemString] = nil
|
|
end
|
|
end
|
|
for itemId in pairs(processedItems) do
|
|
private.availableItems[itemId] = nil
|
|
end
|
|
TempTable.Release(processedItems)
|
|
|
|
private.db:SetQueryUpdatesPaused(false)
|
|
end
|
|
|
|
function private.NameSortHelper(a, b)
|
|
return strlower(a) < strlower(b)
|
|
end
|
|
|
|
function private.LoadLongString(value)
|
|
local result = {}
|
|
if type(value) == "string" then
|
|
String.SafeSplit(value, SEP_CHAR, result)
|
|
elseif type(value) == "table" then
|
|
for _, part in ipairs(value) do
|
|
String.SafeSplit(part, SEP_CHAR, result)
|
|
end
|
|
else
|
|
assert(value == nil)
|
|
end
|
|
return result
|
|
end
|
|
|
|
function private.StoreLongString(values)
|
|
assert(type(values) == "table")
|
|
-- store no more than 1000 values per string
|
|
if #values == 0 then
|
|
return nil
|
|
elseif #values <= 1000 then
|
|
return table.concat(values, SEP_CHAR)
|
|
else
|
|
local result = {}
|
|
for i = 1, #values, 1000 do
|
|
tinsert(result, table.concat(values, SEP_CHAR, i, min(i + 1000 - 1, #values)))
|
|
end
|
|
return result
|
|
end
|
|
end
|
|
|