initial commit
This commit is contained in:
252
LibTSM/Core.lua
Normal file
252
LibTSM/Core.lua
Normal file
@@ -0,0 +1,252 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
-- This is loaded before anything else and simply sets up the addon table
|
||||
|
||||
local ADDON_NAME, TSM = ...
|
||||
local VERSION_RAW = GetAddOnMetadata("TradeSkillMaster", "Version")
|
||||
local IS_DEV_VERSION = strmatch(VERSION_RAW, "^@tsm%-project%-version@$") and true or false
|
||||
local private = {
|
||||
context = {},
|
||||
initOrder = {},
|
||||
loadOrder = {},
|
||||
frame = nil,
|
||||
gotAddonLoaded = false,
|
||||
gotPlayerLogin = false,
|
||||
gotPlayerLogout = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Metatable
|
||||
-- ============================================================================
|
||||
|
||||
local MODULE_METHODS = {
|
||||
OnModuleLoad = function(self, func)
|
||||
local context = private.context[self]
|
||||
assert(context and not context.moduleLoadFunc and not context.moduleLoadTime and type(func) == "function")
|
||||
context.moduleLoadFunc = func
|
||||
end,
|
||||
OnSettingsLoad = function(self, func)
|
||||
local context = private.context[self]
|
||||
assert(context and not context.settingsLoadFunc and not context.settingsLoadTime and type(func) == "function")
|
||||
context.settingsLoadFunc = func
|
||||
end,
|
||||
OnGameDataLoad = function(self, func)
|
||||
local context = private.context[self]
|
||||
assert(context and not context.gameDataLoadFunc and not context.gameDataLoadTime and type(func) == "function")
|
||||
context.gameDataLoadFunc = func
|
||||
end,
|
||||
OnModuleUnload = function(self, func)
|
||||
local context = private.context[self]
|
||||
assert(context and not context.moduleUnloadFunc and not context.moduleUnloadTime and type(func) == "function")
|
||||
context.moduleUnloadFunc = func
|
||||
end,
|
||||
}
|
||||
|
||||
local MODULE_MT = {
|
||||
__index = MODULE_METHODS,
|
||||
__newindex = function(self, key, value)
|
||||
local context = private.context[self]
|
||||
assert(context and not MODULE_METHODS[key] and not context.moduleLoadTime)
|
||||
rawset(self, key, value)
|
||||
end,
|
||||
__metatable = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Addon Object Functions
|
||||
-- ============================================================================
|
||||
|
||||
function TSM.Init(path)
|
||||
assert(type(path) == "string")
|
||||
if private.context[path] then
|
||||
error("Module already exists for path: "..tostring(path))
|
||||
end
|
||||
local module = setmetatable({}, MODULE_MT)
|
||||
private.context[path] = {
|
||||
path = path,
|
||||
module = module,
|
||||
moduleLoadFunc = nil,
|
||||
moduleLoadTime = nil,
|
||||
settingsLoadFunc = nil,
|
||||
settingsLoadTime = nil,
|
||||
gameDataLoadFunc = nil,
|
||||
gameDataLoadTime = nil,
|
||||
moduleUnloadFunc = nil,
|
||||
moduleUnloadTime = nil,
|
||||
}
|
||||
-- store a reference to the context by both the module object and the path
|
||||
private.context[module] = private.context[path]
|
||||
tinsert(private.initOrder, path)
|
||||
return module
|
||||
end
|
||||
|
||||
function TSM.Include(path)
|
||||
local context = private.context[path]
|
||||
if not context then
|
||||
error("Module doesn't exist for path: "..tostring(path))
|
||||
end
|
||||
private.ProcessModuleLoad(path)
|
||||
return context.module
|
||||
end
|
||||
|
||||
function TSM.IsDevVersion()
|
||||
return IS_DEV_VERSION
|
||||
end
|
||||
|
||||
function TSM.GetVersion()
|
||||
return IS_DEV_VERSION and "Dev" or VERSION_RAW
|
||||
end
|
||||
|
||||
function TSM.ModuleInfoIterator()
|
||||
return private.ModuleInfoIterator, nil, 0
|
||||
end
|
||||
|
||||
function TSM.IsWowClassic()
|
||||
return WOW_PROJECT_ID == WOW_PROJECT_CLASSIC
|
||||
end
|
||||
|
||||
function TSM.IsShadowlands()
|
||||
return select(4, GetBuildInfo()) >= 90000
|
||||
end
|
||||
|
||||
function TSM.DebugLogout()
|
||||
private.UnloadAll()
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.ModuleInfoIterator(_, index)
|
||||
index = index + 1
|
||||
local path = private.initOrder[index]
|
||||
if not path then
|
||||
return
|
||||
end
|
||||
local context = private.context[path]
|
||||
assert(context)
|
||||
return index, path, context.moduleLoadTime, context.settingsLoadTime, context.gameDataLoadTime, context.moduleUnloadTime
|
||||
end
|
||||
|
||||
function private.ProcessModuleLoad(path)
|
||||
local context = private.context[path]
|
||||
assert(context)
|
||||
if context.moduleLoadTime then
|
||||
-- already loaded
|
||||
return
|
||||
end
|
||||
tinsert(private.loadOrder, path)
|
||||
context.moduleLoadTime = 0
|
||||
if context.moduleLoadFunc then
|
||||
local st = debugprofilestop()
|
||||
context.moduleLoadFunc()
|
||||
context.moduleLoadTime = debugprofilestop() - st
|
||||
end
|
||||
end
|
||||
|
||||
function private.ProcessSettingsLoad(path)
|
||||
local context = private.context[path]
|
||||
assert(context)
|
||||
if context.settingsLoadTime then
|
||||
-- already loaded
|
||||
return
|
||||
end
|
||||
context.settingsLoadTime = 0
|
||||
if context.settingsLoadFunc then
|
||||
local st = debugprofilestop()
|
||||
context.settingsLoadFunc()
|
||||
context.settingsLoadTime = debugprofilestop() - st
|
||||
end
|
||||
end
|
||||
|
||||
function private.ProcessGameDataLoad(path)
|
||||
local context = private.context[path]
|
||||
assert(context)
|
||||
if context.gameDataLoadTime then
|
||||
-- already loaded
|
||||
return
|
||||
end
|
||||
context.gameDataLoadTime = 0
|
||||
if context.gameDataLoadFunc then
|
||||
local st = debugprofilestop()
|
||||
context.gameDataLoadFunc()
|
||||
context.gameDataLoadTime = debugprofilestop() - st
|
||||
end
|
||||
end
|
||||
|
||||
function private.ProcessModuleUnload(path)
|
||||
local context = private.context[path]
|
||||
assert(context)
|
||||
if context.moduleUnloadTime then
|
||||
-- already unloaded
|
||||
return
|
||||
end
|
||||
context.moduleUnloadTime = 0
|
||||
if context.moduleUnloadFunc then
|
||||
local st = debugprofilestop()
|
||||
context.moduleUnloadFunc()
|
||||
context.moduleUnloadTime = debugprofilestop() - st
|
||||
end
|
||||
end
|
||||
|
||||
function private.UnloadAll()
|
||||
-- unload in the opposite order we loaded
|
||||
for i = #private.loadOrder, 1, -1 do
|
||||
private.ProcessModuleUnload(private.loadOrder[i])
|
||||
end
|
||||
end
|
||||
|
||||
function private.OnEvent(_, event, arg)
|
||||
if event == "ADDON_LOADED" and arg == ADDON_NAME and not private.gotAddonLoaded then
|
||||
assert(not private.gotAddonLoaded and not private.gotPlayerLogin and not private.gotPlayerLogout)
|
||||
private.gotAddonLoaded = true
|
||||
-- load any module which haven't already
|
||||
for _, path in ipairs(private.initOrder) do
|
||||
private.ProcessModuleLoad(path)
|
||||
end
|
||||
-- settings are now available
|
||||
for _, path in ipairs(private.loadOrder) do
|
||||
private.ProcessSettingsLoad(path)
|
||||
end
|
||||
private.frame:UnregisterEvent("ADDON_LOADED")
|
||||
elseif event == "PLAYER_LOGIN" then
|
||||
assert(private.gotAddonLoaded and not private.gotPlayerLogin and not private.gotPlayerLogout)
|
||||
private.gotPlayerLogin = true
|
||||
-- game data is now available
|
||||
for _, path in ipairs(private.loadOrder) do
|
||||
private.ProcessGameDataLoad(path)
|
||||
end
|
||||
elseif event == "PLAYER_LOGOUT" then
|
||||
assert(private.gotAddonLoaded and not private.gotPlayerLogout)
|
||||
private.gotPlayerLogout = true
|
||||
if not private.gotPlayerLogin then
|
||||
-- this can happen if the player exists the game during the loading screen, in which case we just ignore it
|
||||
return
|
||||
end
|
||||
private.UnloadAll()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Initialization Code
|
||||
-- ============================================================================
|
||||
|
||||
do
|
||||
private.frame = CreateFrame("Frame")
|
||||
private.frame:RegisterEvent("ADDON_LOADED")
|
||||
private.frame:RegisterEvent("PLAYER_LOGIN")
|
||||
private.frame:RegisterEvent("PLAYER_LOGOUT")
|
||||
private.frame:SetScript("OnEvent", private.OnEvent)
|
||||
end
|
||||
3284
LibTSM/Data/BonusIds.lua
Normal file
3284
LibTSM/Data/BonusIds.lua
Normal file
File diff suppressed because it is too large
Load Diff
115
LibTSM/Data/ClassicRealms.lua
Normal file
115
LibTSM/Data/ClassicRealms.lua
Normal file
@@ -0,0 +1,115 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local ClassicRealms = TSM.Init("Data.ClassicRealms")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Classic Realm Data
|
||||
-- ============================================================================
|
||||
|
||||
-- Generated with the following query:
|
||||
-- SELECT TRIM(TRAILING '-Alliance' FROM r.localizedName) as localizedName, g.value
|
||||
-- FROM RealmsClassic r INNER JOIN RegionsClassic g ON g.id = r.regionId
|
||||
-- WHERE r.localizedName LIKE '%-Alliance';
|
||||
local CLASSIC_REALM_INFO = {
|
||||
["Amnennar"] = { region = "EU" },
|
||||
["Ashbringer"] = { region = "EU" },
|
||||
["Auberdine"] = { region = "EU" },
|
||||
["Bloodfang"] = { region = "EU" },
|
||||
["Хроми"] = { region = "EU" },
|
||||
["Dragon's Call"] = { region = "EU" },
|
||||
["Dreadmist"] = { region = "EU" },
|
||||
["Everlook"] = { region = "EU" },
|
||||
["Finkle"] = { region = "EU" },
|
||||
["Firemaw"] = { region = "EU" },
|
||||
["Пламегор"] = { region = "EU" },
|
||||
["Flamelash"] = { region = "EU" },
|
||||
["Gandling"] = { region = "EU" },
|
||||
["Gehennas"] = { region = "EU" },
|
||||
["Golemagg"] = { region = "EU" },
|
||||
["Hydraxian Waterlords"] = { region = "EU" },
|
||||
["Judgement"] = { region = "EU" },
|
||||
["Lakeshire"] = { region = "EU" },
|
||||
["Lucifron"] = { region = "EU" },
|
||||
["Mirage Raceway"] = { region = "EU" },
|
||||
["Mograine"] = { region = "EU" },
|
||||
["Nethergarde Keep"] = { region = "EU" },
|
||||
["Noggenfogger"] = { region = "EU" },
|
||||
["Patchwerk"] = { region = "EU" },
|
||||
["Pyrewood Village"] = { region = "EU" },
|
||||
["Razorfen"] = { region = "EU" },
|
||||
["Razorgore"] = { region = "EU" },
|
||||
["Рок-Делар"] = { region = "EU" },
|
||||
["Shazzrah"] = { region = "EU" },
|
||||
["Skullflame"] = { region = "EU" },
|
||||
["Stonespine"] = { region = "EU" },
|
||||
["Sulfuron"] = { region = "EU" },
|
||||
["Ten Storms"] = { region = "EU" },
|
||||
["Transcendence"] = { region = "EU" },
|
||||
["Venoxis"] = { region = "EU" },
|
||||
["Змейталак"] = { region = "EU" },
|
||||
["Zandalar Tribe"] = { region = "EU" },
|
||||
["Dragonfang"] = { region = "EU" },
|
||||
["Earthshaker"] = { region = "EU" },
|
||||
["Heartstriker"] = { region = "EU" },
|
||||
["Вестник Рока"] = { region = "EU" },
|
||||
["Mandokir"] = { region = "EU" },
|
||||
["Anathema"] = { region = "US" },
|
||||
["Arugal"] = { region = "US" },
|
||||
["Ashkandi"] = { region = "US" },
|
||||
["Atiesh"] = { region = "US" },
|
||||
["Azuresong"] = { region = "US" },
|
||||
["Benediction"] = { region = "US" },
|
||||
["Bigglesworth"] = { region = "US" },
|
||||
["Blaumeux"] = { region = "US" },
|
||||
["Bloodsail Buccaneers"] = { region = "US" },
|
||||
["Deviate Delight"] = { region = "US" },
|
||||
["Faerlina"] = { region = "US" },
|
||||
["Fairbanks"] = { region = "US" },
|
||||
["Felstriker"] = { region = "US" },
|
||||
["Grobbulus"] = { region = "US" },
|
||||
["Herod"] = { region = "US" },
|
||||
["Incendius"] = { region = "US" },
|
||||
["Kirtonos"] = { region = "US" },
|
||||
["Kromcrush"] = { region = "US" },
|
||||
["Kurinnaxx"] = { region = "US" },
|
||||
["Mankrik"] = { region = "US" },
|
||||
["Myzrael"] = { region = "US" },
|
||||
["Netherwind"] = { region = "US" },
|
||||
["Old Blanchy"] = { region = "US" },
|
||||
["Pagle"] = { region = "US" },
|
||||
["Rattlegore"] = { region = "US" },
|
||||
["Remulos"] = { region = "US" },
|
||||
["Skeram"] = { region = "US" },
|
||||
["Smolderweb"] = { region = "US" },
|
||||
["Stalagg"] = { region = "US" },
|
||||
["Sulfuras"] = { region = "US" },
|
||||
["Thalnos"] = { region = "US" },
|
||||
["Thunderfury"] = { region = "US" },
|
||||
["Westfall"] = { region = "US" },
|
||||
["Whitemane"] = { region = "US" },
|
||||
["Windseeker"] = { region = "US" },
|
||||
["Yojamba"] = { region = "US" },
|
||||
["Arcanite Reaper"] = { region = "US" },
|
||||
["Earthfury"] = { region = "US" },
|
||||
["Heartseeker"] = { region = "US" },
|
||||
["Loatheb"] = { region = "US" },
|
||||
["Sul'thraze"] = { region = "US" },
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function ClassicRealms.GetRegion(realmName)
|
||||
local info = CLASSIC_REALM_INFO[realmName]
|
||||
return info and info.region or nil
|
||||
end
|
||||
1147
LibTSM/Data/DisenchantInfo.lua
Normal file
1147
LibTSM/Data/DisenchantInfo.lua
Normal file
File diff suppressed because it is too large
Load Diff
88
LibTSM/Data/FontPaths.lua
Normal file
88
LibTSM/Data/FontPaths.lua
Normal file
@@ -0,0 +1,88 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local FontPaths = TSM.Init("Data.FontPaths")
|
||||
local ALPHABET_LOOKUP = {
|
||||
enUS = "roman",
|
||||
esES = "roman",
|
||||
esMX = "roman",
|
||||
deDE = "roman",
|
||||
frFR = "roman",
|
||||
itIT = "roman",
|
||||
ptBR = "roman",
|
||||
koKR = "korean",
|
||||
zhCN = "chinese",
|
||||
zhTW = "chinese",
|
||||
ruRU = "russian",
|
||||
}
|
||||
local ALPHABET = ALPHABET_LOOKUP[GetLocale()]
|
||||
assert(ALPHABET)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Font Path Data
|
||||
-- ============================================================================
|
||||
|
||||
local FONT_PATHS = {
|
||||
BodyRegular = {
|
||||
roman = "Interface\\Addons\\TradeSkillMaster\\Media\\Montserrat-Regular.ttf",
|
||||
korean = "Fonts\\2002.ttf",
|
||||
chinese = "Fonts\\ARKai_C.ttf",
|
||||
russian = "Interface\\Addons\\TradeSkillMaster\\Media\\Montserrat-Regular.ttf",
|
||||
},
|
||||
BodyMedium = {
|
||||
roman = "Interface\\Addons\\TradeSkillMaster\\Media\\Montserrat-Medium.ttf",
|
||||
korean = "Fonts\\2002.ttf",
|
||||
chinese = "Fonts\\ARKai_C.ttf",
|
||||
russian = "Interface\\Addons\\TradeSkillMaster\\Media\\Montserrat-Medium.ttf",
|
||||
},
|
||||
BodyBold = {
|
||||
roman = "Interface\\Addons\\TradeSkillMaster\\Media\\Montserrat-Bold.ttf",
|
||||
korean = "Fonts\\2002.ttf",
|
||||
chinese = "Fonts\\ARKai_C.ttf",
|
||||
russian = "Interface\\Addons\\TradeSkillMaster\\Media\\Montserrat-Bold.ttf",
|
||||
},
|
||||
Item = {
|
||||
roman = "Fonts\\FRIZQT__.ttf",
|
||||
korean = "Fonts\\2002.ttf",
|
||||
chinese = "Fonts\\ARKai_C.ttf",
|
||||
russian = "Fonts\\FRIZQT___CYR.ttf",
|
||||
},
|
||||
Table = {
|
||||
roman = "Interface\\Addons\\TradeSkillMaster\\Media\\Roboto-Medium.ttf",
|
||||
korean = "Fonts\\2002.ttf",
|
||||
chinese = "Fonts\\ARKai_C.ttf",
|
||||
russian = "Interface\\Addons\\TradeSkillMaster\\Media\\Roboto-Medium.ttf",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function FontPaths.GetBodyRegular()
|
||||
return FONT_PATHS.BodyRegular[ALPHABET]
|
||||
end
|
||||
|
||||
function FontPaths.GetBodyMedium()
|
||||
return FONT_PATHS.BodyMedium[ALPHABET]
|
||||
end
|
||||
|
||||
function FontPaths.GetBodyBold()
|
||||
return FONT_PATHS.BodyBold[ALPHABET]
|
||||
end
|
||||
|
||||
function FontPaths.GetItem()
|
||||
return FONT_PATHS.Item[ALPHABET]
|
||||
end
|
||||
|
||||
function FontPaths.GetTable()
|
||||
return FONT_PATHS.Table[ALPHABET]
|
||||
end
|
||||
137
LibTSM/Data/ItemClass.lua
Normal file
137
LibTSM/Data/ItemClass.lua
Normal file
@@ -0,0 +1,137 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local ItemClass = TSM.Init("Data.ItemClass")
|
||||
local STATIC_DATA = {
|
||||
classes = {},
|
||||
subClasses = {},
|
||||
classLookup = {},
|
||||
classIdLookup = {},
|
||||
inventorySlotIdLookup = {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Population of the Static Data
|
||||
-- ============================================================================
|
||||
|
||||
do
|
||||
-- Needed because NUM_LE_ITEM_CLASSS contains an erroneous value
|
||||
local ITEM_CLASS_IDS = nil
|
||||
if not TSM.IsWowClassic() then
|
||||
ITEM_CLASS_IDS = {
|
||||
LE_ITEM_CLASS_WEAPON,
|
||||
LE_ITEM_CLASS_ARMOR,
|
||||
LE_ITEM_CLASS_CONTAINER,
|
||||
LE_ITEM_CLASS_GEM,
|
||||
LE_ITEM_CLASS_ITEM_ENHANCEMENT,
|
||||
LE_ITEM_CLASS_CONSUMABLE,
|
||||
LE_ITEM_CLASS_GLYPH,
|
||||
LE_ITEM_CLASS_TRADEGOODS,
|
||||
LE_ITEM_CLASS_RECIPE,
|
||||
LE_ITEM_CLASS_BATTLEPET,
|
||||
LE_ITEM_CLASS_QUESTITEM,
|
||||
LE_ITEM_CLASS_MISCELLANEOUS,
|
||||
}
|
||||
else
|
||||
ITEM_CLASS_IDS = {
|
||||
LE_ITEM_CLASS_WEAPON,
|
||||
LE_ITEM_CLASS_ARMOR,
|
||||
LE_ITEM_CLASS_CONTAINER,
|
||||
LE_ITEM_CLASS_CONSUMABLE,
|
||||
LE_ITEM_CLASS_TRADEGOODS,
|
||||
LE_ITEM_CLASS_PROJECTILE,
|
||||
LE_ITEM_CLASS_QUIVER,
|
||||
LE_ITEM_CLASS_RECIPE,
|
||||
LE_ITEM_CLASS_REAGENT,
|
||||
LE_ITEM_CLASS_MISCELLANEOUS,
|
||||
}
|
||||
end
|
||||
|
||||
for _, classId in ipairs(ITEM_CLASS_IDS) do
|
||||
local class = GetItemClassInfo(classId)
|
||||
if class then
|
||||
STATIC_DATA.classIdLookup[strlower(class)] = classId
|
||||
STATIC_DATA.classLookup[class] = {}
|
||||
STATIC_DATA.classLookup[class]._index = classId
|
||||
local subClasses = nil
|
||||
if TSM.IsWowClassic() then
|
||||
subClasses = {GetAuctionItemSubClasses(classId)}
|
||||
else
|
||||
subClasses = C_AuctionHouse.GetAuctionItemSubClasses(classId)
|
||||
end
|
||||
for _, subClassId in pairs(subClasses) do
|
||||
local subClassName = GetItemSubClassInfo(classId, subClassId)
|
||||
if not strfind(subClassName, "(OBSOLETE)") then
|
||||
STATIC_DATA.classLookup[class][subClassName] = subClassId
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for class, subClasses in pairs(STATIC_DATA.classLookup) do
|
||||
tinsert(STATIC_DATA.classes, class)
|
||||
STATIC_DATA.subClasses[class] = {}
|
||||
for subClass in pairs(subClasses) do
|
||||
if subClass ~= "_index" then
|
||||
tinsert(STATIC_DATA.subClasses[class], subClass)
|
||||
end
|
||||
end
|
||||
sort(STATIC_DATA.subClasses[class], function(a, b) return STATIC_DATA.classLookup[class][a] < STATIC_DATA.classLookup[class][b] end)
|
||||
end
|
||||
sort(STATIC_DATA.classes, function(a, b) return STATIC_DATA.classIdLookup[strlower(a)] < STATIC_DATA.classIdLookup[strlower(b)] end)
|
||||
|
||||
if TSM.IsShadowlands() then
|
||||
for _, id in pairs(Enum.InventoryType) do
|
||||
local invType = GetItemInventorySlotInfo(id)
|
||||
if invType then
|
||||
STATIC_DATA.inventorySlotIdLookup[strlower(invType)] = id
|
||||
end
|
||||
end
|
||||
else
|
||||
for i = 0, NUM_LE_INVENTORY_TYPES do
|
||||
local invType = GetItemInventorySlotInfo(i)
|
||||
if invType then
|
||||
STATIC_DATA.inventorySlotIdLookup[strlower(invType)] = i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function ItemClass.GetClasses()
|
||||
return STATIC_DATA.classes
|
||||
end
|
||||
|
||||
function ItemClass.GetSubClasses(class)
|
||||
return STATIC_DATA.subClasses[class]
|
||||
end
|
||||
|
||||
function ItemClass.GetClassIdFromClassString(classStr)
|
||||
return STATIC_DATA.classIdLookup[strlower(classStr)]
|
||||
end
|
||||
|
||||
function ItemClass.GetSubClassIdFromSubClassString(subClass, classId)
|
||||
if not classId then return end
|
||||
local class = GetItemClassInfo(classId)
|
||||
if not STATIC_DATA.classLookup[class] then return end
|
||||
for str, index in pairs(STATIC_DATA.classLookup[class]) do
|
||||
if strlower(str) == strlower(subClass) then
|
||||
return index
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ItemClass.GetInventorySlotIdFromInventorySlotString(slot)
|
||||
return STATIC_DATA.inventorySlotIdLookup[strlower(slot)]
|
||||
end
|
||||
255
LibTSM/Data/Mill.lua
Normal file
255
LibTSM/Data/Mill.lua
Normal file
@@ -0,0 +1,255 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Mill = TSM.Init("Data.Mill")
|
||||
local private = {}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Mill Data
|
||||
-- ============================================================================
|
||||
|
||||
local DATA = TSM.IsWowClassic() and {} or {
|
||||
-- ======================================= Common Pigments =======================================
|
||||
["i:39151"] = { -- Alabaster Pigment (Ivory / Moonglow Ink)
|
||||
["i:765"] = 0.5,
|
||||
["i:2447"] = 0.5,
|
||||
["i:2449"] = 0.6,
|
||||
},
|
||||
["i:39343"] = { -- Azure Pigment (Ink of the Sea)
|
||||
["i:39969"] = 0.5,
|
||||
["i:36904"] = 0.5,
|
||||
["i:36907"] = 0.5,
|
||||
["i:36901"] = 0.5,
|
||||
["i:39970"] = 0.5,
|
||||
["i:37921"] = 0.5,
|
||||
["i:36905"] = 0.6,
|
||||
["i:36906"] = 0.6,
|
||||
["i:36903"] = 0.6,
|
||||
},
|
||||
["i:61979"] = { -- Ashen Pigment (Blackfallow Ink)
|
||||
["i:52983"] = 0.5,
|
||||
["i:52984"] = 0.5,
|
||||
["i:52985"] = 0.5,
|
||||
["i:52986"] = 0.5,
|
||||
["i:52987"] = 0.6,
|
||||
["i:52988"] = 0.6,
|
||||
},
|
||||
["i:39334"] = { -- Dusky Pigment (Midnight Ink)
|
||||
["i:785"] = 0.5,
|
||||
["i:2450"] = 0.5,
|
||||
["i:2452"] = 0.5,
|
||||
["i:2453"] = 0.6,
|
||||
["i:3820"] = 0.6,
|
||||
},
|
||||
["i:39339"] = { -- Emerald Pigment (Jadefire Ink)
|
||||
["i:3818"] = 0.5,
|
||||
["i:3821"] = 0.5,
|
||||
["i:3358"] = 0.6,
|
||||
["i:3819"] = 0.6,
|
||||
},
|
||||
["i:39338"] = { -- Golden Pigment (Lion's Ink)
|
||||
["i:3355"] = 0.5,
|
||||
["i:3369"] = 0.5,
|
||||
["i:3356"] = 0.6,
|
||||
["i:3357"] = 0.6,
|
||||
},
|
||||
["i:39342"] = { -- Nether Pigment (Ethereal Ink)
|
||||
["i:22785"] = 0.5,
|
||||
["i:22786"] = 0.5,
|
||||
["i:22787"] = 0.5,
|
||||
["i:22789"] = 0.5,
|
||||
["i:22790"] = 0.6,
|
||||
["i:22791"] = 0.6,
|
||||
["i:22792"] = 0.6,
|
||||
["i:22793"] = 0.6,
|
||||
},
|
||||
["i:79251"] = { -- Shadow Pigment (Ink of Dreams)
|
||||
["i:72237"] = 0.5,
|
||||
["i:72234"] = 0.5,
|
||||
["i:79010"] = 0.5,
|
||||
["i:72235"] = 0.5,
|
||||
["i:89639"] = 0.5,
|
||||
["i:79011"] = 0.6,
|
||||
},
|
||||
["i:39341"] = { -- Silvery Pigment (Shimmering Ink)
|
||||
["i:13463"] = 0.5,
|
||||
["i:13464"] = 0.5,
|
||||
["i:13465"] = 0.6,
|
||||
["i:13466"] = 0.6,
|
||||
["i:13467"] = 0.6,
|
||||
},
|
||||
["i:39340"] = { -- Violet Pigment (Celestial Ink)
|
||||
["i:4625"] = 0.5,
|
||||
["i:8831"] = 0.5,
|
||||
["i:8838"] = 0.5,
|
||||
["i:8839"] = 0.6,
|
||||
["i:8845"] = 0.6,
|
||||
["i:8846"] = 0.6,
|
||||
},
|
||||
["i:114931"] = { -- Cerulean Pigment (Warbinder's Ink)
|
||||
["i:109124"] = 0.42,
|
||||
["i:109125"] = 0.42,
|
||||
["i:109126"] = 0.42,
|
||||
["i:109127"] = 0.42,
|
||||
["i:109128"] = 0.42,
|
||||
["i:109129"] = 0.42,
|
||||
},
|
||||
["i:129032"] = { -- Roseate Pigment (No Legion Ink)
|
||||
["i:124101"] = 0.42,
|
||||
["i:124102"] = 0.42,
|
||||
["i:124103"] = 0.42,
|
||||
["i:124104"] = 0.47,
|
||||
["i:124105"] = 1.22,
|
||||
["i:124106"] = 0.42,
|
||||
["i:128304"] = 0.2,
|
||||
["i:151565"] = 0.43,
|
||||
},
|
||||
-- ======================================= Rare Pigments =======================================
|
||||
["i:43109"] = { -- Icy Pigment (Snowfall Ink)
|
||||
["i:39969"] = 0.05,
|
||||
["i:36904"] = 0.05,
|
||||
["i:36907"] = 0.05,
|
||||
["i:36901"] = 0.05,
|
||||
["i:39970"] = 0.05,
|
||||
["i:37921"] = 0.05,
|
||||
["i:36905"] = 0.1,
|
||||
["i:36906"] = 0.1,
|
||||
["i:36903"] = 0.1,
|
||||
},
|
||||
["i:61980"] = { -- Burning Embers (Inferno Ink)
|
||||
["i:52983"] = 0.05,
|
||||
["i:52984"] = 0.05,
|
||||
["i:52985"] = 0.05,
|
||||
["i:52986"] = 0.05,
|
||||
["i:52987"] = 0.1,
|
||||
["i:52988"] = 0.1,
|
||||
},
|
||||
["i:43104"] = { -- Burnt Pigment (Dawnstar Ink)
|
||||
["i:3356"] = 0.1,
|
||||
["i:3357"] = 0.1,
|
||||
["i:3369"] = 0.05,
|
||||
["i:3355"] = 0.05,
|
||||
},
|
||||
["i:43108"] = { -- Ebon Pigment (Darkflame Ink)
|
||||
["i:22792"] = 0.1,
|
||||
["i:22790"] = 0.1,
|
||||
["i:22791"] = 0.1,
|
||||
["i:22793"] = 0.1,
|
||||
["i:22786"] = 0.05,
|
||||
["i:22785"] = 0.05,
|
||||
["i:22787"] = 0.05,
|
||||
["i:22789"] = 0.05,
|
||||
},
|
||||
["i:43105"] = { -- Indigo Pigment (Royal Ink)
|
||||
["i:3358"] = 0.1,
|
||||
["i:3819"] = 0.1,
|
||||
["i:3821"] = 0.05,
|
||||
["i:3818"] = 0.05,
|
||||
},
|
||||
["i:79253"] = { -- Misty Pigment (Starlight Ink)
|
||||
["i:72237"] = 0.05,
|
||||
["i:72234"] = 0.05,
|
||||
["i:79010"] = 0.05,
|
||||
["i:72235"] = 0.05,
|
||||
["i:79011"] = 0.1,
|
||||
["i:89639"] = 0.05,
|
||||
},
|
||||
["i:43106"] = { -- Ruby Pigment (Fiery Ink)
|
||||
["i:4625"] = 0.05,
|
||||
["i:8838"] = 0.05,
|
||||
["i:8831"] = 0.05,
|
||||
["i:8845"] = 0.1,
|
||||
["i:8846"] = 0.1,
|
||||
["i:8839"] = 0.1,
|
||||
},
|
||||
["i:43107"] = { -- Sapphire Pigment (Ink of the Sky)
|
||||
["i:13463"] = 0.05,
|
||||
["i:13464"] = 0.05,
|
||||
["i:13465"] = 0.1,
|
||||
["i:13466"] = 0.1,
|
||||
["i:13467"] = 0.1,
|
||||
},
|
||||
["i:43103"] = { -- Verdant Pigment (Hunter's Ink)
|
||||
["i:2453"] = 0.1,
|
||||
["i:3820"] = 0.1,
|
||||
["i:2450"] = 0.05,
|
||||
["i:785"] = 0.05,
|
||||
["i:2452"] = 0.05,
|
||||
},
|
||||
["i:129034"] = { -- Sallow Pigment (No Legion Ink)
|
||||
["i:124101"] = 0.04,
|
||||
["i:124102"] = 0.04,
|
||||
["i:124103"] = 0.05,
|
||||
["i:124104"] = 0.05,
|
||||
["i:124105"] = 0.04,
|
||||
["i:124106"] = 2.14,
|
||||
["i:128304"] = 0.0018,
|
||||
["i:151565"] = 0.048,
|
||||
},
|
||||
-- ======================================= BFA Pigments ========================================
|
||||
["i:153669"] = { -- Viridescent Pigment
|
||||
["i:152505"] = 0.1325,
|
||||
["i:152506"] = 0.1325,
|
||||
["i:152507"] = 0.1325,
|
||||
["i:152508"] = 0.1325,
|
||||
["i:152509"] = 0.1325,
|
||||
["i:152511"] = 0.1325,
|
||||
["i:152510"] = 0.325,
|
||||
},
|
||||
["i:153636"] = { -- Crimson Pigment
|
||||
["i:152505"] = 0.315,
|
||||
["i:152506"] = 0.315,
|
||||
["i:152507"] = 0.315,
|
||||
["i:152508"] = 0.315,
|
||||
["i:152509"] = 0.315,
|
||||
["i:152511"] = 0.315,
|
||||
["i:152510"] = 0.315,
|
||||
},
|
||||
["i:153635"] = { -- Ultramarine Pigment
|
||||
["i:152505"] = 0.825,
|
||||
["i:152506"] = 0.825,
|
||||
["i:152507"] = 0.825,
|
||||
["i:152508"] = 0.825,
|
||||
["i:152509"] = 0.825,
|
||||
["i:152511"] = 0.825,
|
||||
["i:152510"] = 0.825,
|
||||
},
|
||||
["i:168662"] = { -- Maroon Pigment
|
||||
["i:168487"] = 0.6,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Mill.TargetItemIterator()
|
||||
return private.TableKeyIterator, DATA, nil
|
||||
end
|
||||
|
||||
function Mill.SourceItemIterator(targetItemString)
|
||||
return private.TableKeyIterator, DATA[targetItemString], nil
|
||||
end
|
||||
|
||||
function Mill.GetRate(targetItemString, sourceItemString)
|
||||
return DATA[targetItemString][sourceItemString]
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.TableKeyIterator(tbl, index)
|
||||
index = next(tbl, index)
|
||||
return index
|
||||
end
|
||||
605
LibTSM/Data/ProfessionInfo.lua
Normal file
605
LibTSM/Data/ProfessionInfo.lua
Normal file
@@ -0,0 +1,605 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local ProfessionInfo = TSM.Init("Data.ProfessionInfo")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Profession Info Data
|
||||
-- ============================================================================
|
||||
|
||||
local PROFESSION_NAMES = {
|
||||
Mining = GetSpellInfo(2575),
|
||||
Smelting = GetSpellInfo(2656),
|
||||
Poisons = GetSpellInfo(2842),
|
||||
}
|
||||
local CLASSIC_SUB_NAMES = {
|
||||
[APPRENTICE] = true,
|
||||
[JOURNEYMAN] = true,
|
||||
[EXPERT] = true,
|
||||
[ARTISAN] = true,
|
||||
["大师级"] = true, -- zhCN ARTISAN
|
||||
["Мастеровой"] = true, -- ruRU ARTISAN
|
||||
}
|
||||
local VELLUM_ITEM_STRING = "i:38682"
|
||||
local ENGINEERING_TINKERS = {
|
||||
[54736] = true, -- Engineering: EMP Generator
|
||||
[54793] = true, -- Engineering: Frag Belt
|
||||
[55002] = true, -- Engineering: Flexweave Underlay
|
||||
[55016] = true, -- Engineering: Nitro Boosts
|
||||
[67839] = true, -- Engineering: Mind Amplification Dish
|
||||
[82200] = true, -- Engineering: Spinal healing Injector
|
||||
[84424] = true, -- Engineering: Invisibility Field
|
||||
[84425] = true, -- Engineering: Cardboard Assasin
|
||||
[84427] = true, -- Engineering: Grounded Plasma Shield
|
||||
[109099] = true, -- Engineering: Watergliding Jets
|
||||
[126392] = true, -- Engineering: Goblin Glider
|
||||
}
|
||||
local MASS_MILLING_RECIPES = {
|
||||
[190381] = "i:114931", -- Frostweed
|
||||
[190382] = "i:114931", -- Fireweed
|
||||
[190383] = "i:114931", -- Gorgrond Flytrap
|
||||
[190384] = "i:114931", -- Starflower
|
||||
[190385] = "i:114931", -- Nargrand Arrowbloom
|
||||
[190386] = "i:114931", -- Talador Orchid
|
||||
[209658] = "i:129032", -- Aethril
|
||||
[209659] = "i:129032", -- Dreamleaf
|
||||
[209660] = "i:129032", -- Foxflower
|
||||
[209661] = "i:129032", -- Fjarnskaggl
|
||||
[209662] = "i:129032", -- Starlight Rose
|
||||
[209664] = "i:129034", -- Felwort
|
||||
[210116] = "i:129032", -- Yseralline Seeds
|
||||
[247861] = "i:129034", -- Astral Glory
|
||||
}
|
||||
local ENCHANTING_RECIPIES = {
|
||||
-- Scraped from Wowhead (http://www.wowhead.com/items/consumables/item-enhancements-permanent?filter=86;4;0) using the following javascript:
|
||||
-- for (i=0; i<listviewitems.length; i++) console.log("["+listviewitems[i].sourcemore[0].ti+"] = \"i:"+listviewitems[i].id+"\", -- "+listviewitems[i].name.substr(1));
|
||||
[298009] = "i:168446", -- Enchant Ring - Accord of Critical Strike Rank 1
|
||||
[298010] = "i:168446", -- Enchant Ring - Accord of Critical Strike Rank 2
|
||||
[298011] = "i:168446", -- Enchant Ring - Accord of Critical Strike Rank 3
|
||||
[297989] = "i:168447", -- Enchant Ring - Accord of Haste Rank 1
|
||||
[297994] = "i:168447", -- Enchant Ring - Accord of Haste Rank 2
|
||||
[298016] = "i:168447", -- Enchant Ring - Accord of Haste Rank 3
|
||||
[297995] = "i:168448", -- Enchant Ring - Accord of Mastery Rank 1
|
||||
[298001] = "i:168448", -- Enchant Ring - Accord of Mastery Rank 2
|
||||
[298002] = "i:168448", -- Enchant Ring - Accord of Mastery Rank 3
|
||||
[297993] = "i:168449", -- Enchant Ring - Accord of Versatility Rank 1
|
||||
[297991] = "i:168449", -- Enchant Ring - Accord of Versatility Rank 2
|
||||
[297999] = "i:168449", -- Enchant Ring - Accord of Versatility Rank 3
|
||||
[298440] = "i:168596", -- Enchant Weapon - Force Multiplier Rank 1
|
||||
[298439] = "i:168596", -- Enchant Weapon - Force Multiplier Rank 2
|
||||
[300788] = "i:168596", -- Enchant Weapon - Force Multiplier Rank 3
|
||||
[298433] = "i:168593", -- Enchant Weapon - Machinist's Brilliance Rank 1
|
||||
[300769] = "i:168593", -- Enchant Weapon - Machinist's Brilliance Rank 2
|
||||
[300770] = "i:168593", -- Enchant Weapon - Machinist's Brilliance Rank 3
|
||||
[298442] = "i:168598", -- Enchant Weapon - Naga Hide Rank 1
|
||||
[298441] = "i:168598", -- Enchant Weapon - Naga Hide Rank 2
|
||||
[300789] = "i:168598", -- Enchant Weapon - Naga Hide Rank 3
|
||||
[298438] = "i:168592", -- Enchant Weapon - Oceanic Restoration Rank 1
|
||||
[298437] = "i:168592", -- Enchant Weapon - Oceanic Restoration Rank 2
|
||||
[298515] = "i:168592", -- Enchant Weapon - Oceanic Restoration Rank 3
|
||||
[255075] = "i:153442", -- Enchant Ring - Pact of Critical Strike Rank 1
|
||||
[255090] = "i:153442", -- Enchant Ring - Pact of Critical Strike Rank 2
|
||||
[255098] = "i:153442", -- Enchant Ring - Pact of Critical Strike Rank 3
|
||||
[255076] = "i:153443", -- Enchant Ring - Pact of Haste Rank 1
|
||||
[255091] = "i:153443", -- Enchant Ring - Pact of Haste Rank 2
|
||||
[255099] = "i:153443", -- Enchant Ring - Pact of Haste Rank 3
|
||||
[255077] = "i:153444", -- Enchant Ring - Pact of Mastery Rank 1
|
||||
[255092] = "i:153444", -- Enchant Ring - Pact of Mastery Rank 2
|
||||
[255100] = "i:153444", -- Enchant Ring - Pact of Mastery Rank 3
|
||||
[255078] = "i:153445", -- Enchant Ring - Pact of Versatility Rank 1
|
||||
[255093] = "i:153445", -- Enchant Ring - Pact of Versatility Rank 2
|
||||
[255101] = "i:153445", -- Enchant Ring - Pact of Versatility Rank 3
|
||||
[255071] = "i:153438", -- Enchant Ring - Seal of Critical Strike Rank 1
|
||||
[255086] = "i:153438", -- Enchant Ring - Seal of Critical Strike Rank 2
|
||||
[255094] = "i:153438", -- Enchant Ring - Seal of Critical Strike Rank 3
|
||||
[255072] = "i:153439", -- Enchant Ring - Seal of Haste Rank 1
|
||||
[255087] = "i:153439", -- Enchant Ring - Seal of Haste Rank 2
|
||||
[255095] = "i:153439", -- Enchant Ring - Seal of Haste Rank 3
|
||||
[255073] = "i:153440", -- Enchant Ring - Seal of Mastery Rank 1
|
||||
[255088] = "i:153440", -- Enchant Ring - Seal of Mastery Rank 2
|
||||
[255096] = "i:153440", -- Enchant Ring - Seal of Mastery Rank 3
|
||||
[255074] = "i:153441", -- Enchant Ring - Seal of Versatility Rank 1
|
||||
[255089] = "i:153441", -- Enchant Ring - Seal of Versatility Rank 2
|
||||
[255097] = "i:153441", -- Enchant Ring - Seal of Versatility Rank 3
|
||||
[255103] = "i:153476", -- Enchant Weapon - Coastal Surge Rank 1
|
||||
[255104] = "i:153476", -- Enchant Weapon - Coastal Surge Rank 2
|
||||
[255105] = "i:153476", -- Enchant Weapon - Coastal Surge Rank 3
|
||||
[255110] = "i:153478", -- Enchant Weapon - Siphoning Rank 1
|
||||
[255111] = "i:153478", -- Enchant Weapon - Siphoning Rank 2
|
||||
[255112] = "i:153478", -- Enchant Weapon - Siphoning Rank 3
|
||||
[255141] = "i:153480", -- Enchant Weapon - Gale-Force Striking Rank 1
|
||||
[255142] = "i:153480", -- Enchant Weapon - Gale-Force Striking Rank 2
|
||||
[255143] = "i:153480", -- Enchant Weapon - Gale-Force Striking Rank 3
|
||||
[268907] = "i:159785", -- Enchant Weapon - Deadly Navigation Rank 1
|
||||
[268908] = "i:159785", -- Enchant Weapon - Deadly Navigation Rank 2
|
||||
[268909] = "i:159785", -- Enchant Weapon - Deadly Navigation Rank 3
|
||||
[268894] = "i:159786", -- Enchant Weapon - Quick Navigation Rank 1
|
||||
[268895] = "i:159786", -- Enchant Weapon - Quick Navigation Rank 2
|
||||
[268897] = "i:159786", -- Enchant Weapon - Quick Navigation Rank 3
|
||||
[268901] = "i:159787", -- Enchant Weapon - Masterful Navigation Rank 1
|
||||
[268902] = "i:159787", -- Enchant Weapon - Masterful Navigation Rank 2
|
||||
[268903] = "i:159787", -- Enchant Weapon - Masterful Navigation Rank 3
|
||||
[268852] = "i:159788", -- Enchant Weapon - Versatile Navigation Rank 1
|
||||
[268878] = "i:159788", -- Enchant Weapon - Versatile Navigation Rank 2
|
||||
[268879] = "i:159788", -- Enchant Weapon - Versatile Navigation Rank 3
|
||||
[268913] = "i:159789", -- Enchant Weapon - Stalwart Navigation Rank 1
|
||||
[268914] = "i:159789", -- Enchant Weapon - Stalwart Navigation Rank 2
|
||||
[268915] = "i:159789", -- Enchant Weapon - Stalwart Navigation Rank 3
|
||||
[255129] = "i:153479", -- Enchant Weapon - Torrent of Elements Rank 1
|
||||
[255130] = "i:153479", -- Enchant Weapon - Torrent of Elements Rank 2
|
||||
[255131] = "i:153479", -- Enchant Weapon - Torrent of Elements Rank 3
|
||||
[255035] = "i:153430", -- Enchant Gloves - Kul Tiran Herbalism
|
||||
[255040] = "i:153431", -- Enchant Gloves - Kul Tiran Mining
|
||||
[255065] = "i:153434", -- Enchant Gloves - Kul Tiran Skinning
|
||||
[255070] = "i:153437", -- Enchant Gloves - Kul Tiran Crafting
|
||||
[267458] = "i:159464", -- Enchant Gloves - Zandalari Herbalism
|
||||
[267482] = "i:159466", -- Enchant Gloves - Zandalari Mining
|
||||
[267486] = "i:159467", -- Enchant Gloves - Zandalari Skinning
|
||||
[267498] = "i:159471", -- Enchant Gloves - Zandalari Crafting
|
||||
[123125] = "i:141910", -- Enchant Neck - Mark of the Ancient Priestess
|
||||
[255066] = "i:153435", -- Enchant Gloves - Kul Tiran Surveying
|
||||
[267490] = "i:159468", -- Enchant Gloves - Zandalari Surveying
|
||||
[158914] = "i:110638", -- Enchant Ring - Gift of Critical Strike
|
||||
[158915] = "i:110639", -- Enchant Ring - Gift of Haste
|
||||
[158916] = "i:110640", -- Enchant Ring - Gift of Mastery
|
||||
[158918] = "i:110642", -- Enchant Ring - Gift of Versatility
|
||||
[158899] = "i:110645", -- Enchant Neck - Gift of Critical Strike
|
||||
[158900] = "i:110646", -- Enchant Neck - Gift of Haste
|
||||
[158901] = "i:110647", -- Enchant Neck - Gift of Mastery
|
||||
[158903] = "i:110649", -- Enchant Neck - Gift of Versatility
|
||||
[158884] = "i:110652", -- Enchant Cloak - Gift of Critical Strike
|
||||
[158885] = "i:110653", -- Enchant Cloak - Gift of Haste
|
||||
[158886] = "i:110654", -- Enchant Cloak - Gift of Mastery
|
||||
[158889] = "i:110656", -- Enchant Cloak - Gift of Versatility
|
||||
[159235] = "i:110682", -- Enchant Weapon - Mark of the Thunderlord
|
||||
[159236] = "i:112093", -- Enchant Weapon - Mark of the Shattered Hand
|
||||
[159673] = "i:112115", -- Enchant Weapon - Mark of Shadowmoon
|
||||
[159674] = "i:112160", -- Enchant Weapon - Mark of Blackrock
|
||||
[159671] = "i:112164", -- Enchant Weapon - Mark of Warsong
|
||||
[159672] = "i:112165", -- Enchant Weapon - Mark of the Frostwolf
|
||||
[173323] = "i:118015", -- Enchant Weapon - Mark of Bleeding Hollow
|
||||
[158907] = "i:110617", -- Enchant Ring - Breath of Critical Strike
|
||||
[158908] = "i:110618", -- Enchant Ring - Breath of Haste
|
||||
[158909] = "i:110619", -- Enchant Ring - Breath of Mastery
|
||||
[158911] = "i:110621", -- Enchant Ring - Breath of Versatility
|
||||
[158892] = "i:110624", -- Enchant Neck - Breath of Critical Strike
|
||||
[158893] = "i:110625", -- Enchant Neck - Breath of Haste
|
||||
[158894] = "i:110626", -- Enchant Neck - Breath of Mastery
|
||||
[158896] = "i:110628", -- Enchant Neck - Breath of Versatility
|
||||
[158877] = "i:110631", -- Enchant Cloak - Breath of Critical Strike
|
||||
[158878] = "i:110632", -- Enchant Cloak - Breath of Haste
|
||||
[158879] = "i:110633", -- Enchant Cloak - Breath of Mastery
|
||||
[158881] = "i:110635", -- Enchant Cloak - Breath of Versatility
|
||||
[104425] = "i:74723", -- Enchant Weapon - Windsong
|
||||
[104427] = "i:74724", -- Enchant Weapon - Jade Spirit
|
||||
[104430] = "i:74725", -- Enchant Weapon - Elemental Force
|
||||
[104434] = "i:74726", -- Enchant Weapon - Dancing Steel
|
||||
[104440] = "i:74727", -- Enchant Weapon - Colossus
|
||||
[104442] = "i:74728", -- Enchant Weapon - River's Song
|
||||
[104338] = "i:74700", -- Enchant Bracer - Mastery
|
||||
[104385] = "i:74701", -- Enchant Bracer - Major Dodge
|
||||
[104389] = "i:74703", -- Enchant Bracer - Super Intellect
|
||||
[104390] = "i:74704", -- Enchant Bracer - Exceptional Strength
|
||||
[104391] = "i:74705", -- Enchant Bracer - Greater Agility
|
||||
[104392] = "i:74706", -- Enchant Chest - Super Resilience
|
||||
[104393] = "i:74707", -- Enchant Chest - Mighty Versatility
|
||||
[104395] = "i:74708", -- Enchant Chest - Glorious Stats
|
||||
[104397] = "i:74709", -- Enchant Chest - Superior Stamina
|
||||
[104398] = "i:74710", -- Enchant Cloak - Accuracy
|
||||
[104401] = "i:74711", -- Enchant Cloak - Greater Protection
|
||||
[104403] = "i:74712", -- Enchant Cloak - Superior Intellect
|
||||
[104404] = "i:74713", -- Enchant Cloak - Superior Critical Strike
|
||||
[104407] = "i:74715", -- Enchant Boots - Greater Haste
|
||||
[104408] = "i:74716", -- Enchant Boots - Greater Precision
|
||||
[104409] = "i:74717", -- Enchant Boots - Blurred Speed
|
||||
[104414] = "i:74718", -- Enchant Boots - Pandaren's Step
|
||||
[104416] = "i:74719", -- Enchant Gloves - Greater Haste
|
||||
[104417] = "i:74720", -- Enchant Gloves - Superior Haste
|
||||
[104419] = "i:74721", -- Enchant Gloves - Super Strength
|
||||
[104420] = "i:74722", -- Enchant Gloves - Superior Mastery
|
||||
[104445] = "i:74729", -- Enchant Off-Hand - Major Intellect
|
||||
[130758] = "i:89737", -- Enchant Shield - Greater Parry
|
||||
[74195] = "i:52747", -- Enchant Weapon - Mending
|
||||
[96264] = "i:68784", -- Enchant Bracer - Agility
|
||||
[96261] = "i:68785", -- Enchant Bracer - Major Strength
|
||||
[96262] = "i:68786", -- Enchant Bracer - Mighty Intellect
|
||||
[74132] = "i:52687", -- Enchant Gloves - Mastery
|
||||
[74189] = "i:52743", -- Enchant Boots - Earthen Vitality
|
||||
[74191] = "i:52744", -- Enchant Chest - Mighty Stats
|
||||
[74192] = "i:52745", -- Enchant Cloak - Lesser Power
|
||||
[74193] = "i:52746", -- Enchant Bracer - Speed
|
||||
[74197] = "i:52748", -- Enchant Weapon - Avalanche
|
||||
[74198] = "i:52749", -- Enchant Gloves - Haste
|
||||
[74199] = "i:52750", -- Enchant Boots - Haste
|
||||
[74200] = "i:52751", -- Enchant Chest - Stamina
|
||||
[74201] = "i:52752", -- Enchant Bracer - Critical Strike
|
||||
[74202] = "i:52753", -- Enchant Cloak - Intellect
|
||||
[74207] = "i:52754", -- Enchant Shield - Protection
|
||||
[74211] = "i:52755", -- Enchant Weapon - Elemental Slayer
|
||||
[74212] = "i:52756", -- Enchant Gloves - Exceptional Strength
|
||||
[74213] = "i:52757", -- Enchant Boots - Major Agility
|
||||
[74214] = "i:52758", -- Enchant Chest - Mighty Resilience
|
||||
[74220] = "i:52759", -- Enchant Gloves - Greater Haste
|
||||
[74223] = "i:52760", -- Enchant Weapon - Hurricane
|
||||
[74225] = "i:52761", -- Enchant Weapon - Heartsong
|
||||
[74226] = "i:52762", -- Enchant Shield - Mastery
|
||||
[74229] = "i:52763", -- Enchant Bracer - Superior Dodge
|
||||
[74230] = "i:52764", -- Enchant Cloak - Critical Strike
|
||||
[74231] = "i:52765", -- Enchant Chest - Exceptional Versatility
|
||||
[74232] = "i:52766", -- Enchant Bracer - Precision
|
||||
[74234] = "i:52767", -- Enchant Cloak - Protection
|
||||
[74235] = "i:52768", -- Enchant Off-Hand - Superior Intellect
|
||||
[74236] = "i:52769", -- Enchant Boots - Precision
|
||||
[74237] = "i:52770", -- Enchant Bracer - Exceptional Versatility
|
||||
[74238] = "i:52771", -- Enchant Boots - Mastery
|
||||
[74239] = "i:52772", -- Enchant Bracer - Greater Haste
|
||||
[74240] = "i:52773", -- Enchant Cloak - Greater Intellect
|
||||
[74242] = "i:52774", -- Enchant Weapon - Power Torrent
|
||||
[74244] = "i:52775", -- Enchant Weapon - Windwalk
|
||||
[74246] = "i:52776", -- Enchant Weapon - Landslide
|
||||
[74247] = "i:52777", -- Enchant Cloak - Greater Critical Strike
|
||||
[74248] = "i:52778", -- Enchant Bracer - Greater Critical Strike
|
||||
[74250] = "i:52779", -- Enchant Chest - Peerless Stats
|
||||
[74251] = "i:52780", -- Enchant Chest - Greater Stamina
|
||||
[74252] = "i:52781", -- Enchant Boots - Assassin's Step
|
||||
[74253] = "i:52782", -- Enchant Boots - Lavawalker
|
||||
[74254] = "i:52783", -- Enchant Gloves - Mighty Strength
|
||||
[74255] = "i:52784", -- Enchant Gloves - Greater Mastery
|
||||
[74256] = "i:52785", -- Enchant Bracer - Greater Speed
|
||||
[95471] = "i:68134", -- Enchant 2H Weapon - Mighty Agility
|
||||
[42974] = "i:38948", -- Enchant Weapon - Executioner
|
||||
[44510] = "i:38963", -- Enchant Weapon - Exceptional Versatility
|
||||
[44524] = "i:38965", -- Enchant Weapon - Icebreaker
|
||||
[44576] = "i:38972", -- Enchant Weapon - Lifeward
|
||||
[44595] = "i:38981", -- Enchant 2H Weapon - Scourgebane
|
||||
[44621] = "i:38988", -- Enchant Weapon - Giant Slayer
|
||||
[44629] = "i:38991", -- Enchant Weapon - Exceptional Spellpower
|
||||
[44630] = "i:38992", -- Enchant 2H Weapon - Greater Savagery
|
||||
[44633] = "i:38995", -- Enchant Weapon - Exceptional Agility
|
||||
[46578] = "i:38998", -- Enchant Weapon - Deathfrost
|
||||
[59625] = "i:43987", -- Enchant Weapon - Black Magic
|
||||
[60621] = "i:44453", -- Enchant Weapon - Greater Potency
|
||||
[60691] = "i:44463", -- Enchant 2H Weapon - Massacre
|
||||
[60707] = "i:44466", -- Enchant Weapon - Superior Potency
|
||||
[60714] = "i:44467", -- Enchant Weapon - Mighty Spellpower
|
||||
[59621] = "i:44493", -- Enchant Weapon - Berserking
|
||||
[59619] = "i:44497", -- Enchant Weapon - Accuracy
|
||||
[62948] = "i:45056", -- Enchant Staff - Greater Spellpower
|
||||
[62959] = "i:45060", -- Enchant Staff - Spellpower
|
||||
[27958] = "i:38912", -- Enchant Chest - Exceptional Mana
|
||||
[44484] = "i:38951", -- Enchant Gloves - Haste
|
||||
[44488] = "i:38953", -- Enchant Gloves - Precision
|
||||
[44489] = "i:38954", -- Enchant Shield - Dodge
|
||||
[44492] = "i:38955", -- Enchant Chest - Mighty Health
|
||||
[44500] = "i:38959", -- Enchant Cloak - Superior Agility
|
||||
[44508] = "i:38961", -- Enchant Boots - Greater Versatility
|
||||
[44509] = "i:38962", -- Enchant Chest - Greater Versatility
|
||||
[44513] = "i:38964", -- Enchant Gloves - Greater Assault
|
||||
[44528] = "i:38966", -- Enchant Boots - Greater Fortitude
|
||||
[44529] = "i:38967", -- Enchant Gloves - Major Agility
|
||||
[44555] = "i:38968", -- Enchant Bracer - Exceptional Intellect
|
||||
[60616] = "i:38971", -- Enchant Bracer - Assault
|
||||
[44582] = "i:38973", -- Enchant Cloak - Minor Power
|
||||
[44584] = "i:38974", -- Enchant Boots - Greater Vitality
|
||||
[44588] = "i:38975", -- Enchant Chest - Exceptional Resilience
|
||||
[44589] = "i:38976", -- Enchant Boots - Superior Agility
|
||||
[44591] = "i:38978", -- Enchant Cloak - Superior Dodge
|
||||
[44592] = "i:38979", -- Enchant Gloves - Exceptional Spellpower
|
||||
[44593] = "i:38980", -- Enchant Bracer - Major Versatility
|
||||
[44598] = "i:38984", -- Enchant Bracer - Haste
|
||||
[60623] = "i:38986", -- Enchant Boots - Icewalker
|
||||
[44616] = "i:38987", -- Enchant Bracer - Greater Stats
|
||||
[44623] = "i:38989", -- Enchant Chest - Super Stats
|
||||
[44625] = "i:38990", -- Enchant Gloves - Armsman
|
||||
[44631] = "i:38993", -- Enchant Cloak - Shadow Armor
|
||||
[44635] = "i:38997", -- Enchant Bracer - Greater Spellpower
|
||||
[47672] = "i:39001", -- Enchant Cloak - Mighty Stamina
|
||||
[47766] = "i:39002", -- Enchant Chest - Greater Dodge
|
||||
[47898] = "i:39003", -- Enchant Cloak - Greater Speed
|
||||
[47899] = "i:39004", -- Enchant Cloak - Wisdom
|
||||
[47900] = "i:39005", -- Enchant Chest - Super Health
|
||||
[47901] = "i:39006", -- Enchant Boots - Tuskarr's Vitality
|
||||
[60606] = "i:44449", -- Enchant Boots - Assault
|
||||
[60653] = "i:44455", -- Shield Enchant - Greater Intellect
|
||||
[60609] = "i:44456", -- Enchant Cloak - Speed
|
||||
[60663] = "i:44457", -- Enchant Cloak - Major Agility
|
||||
[60668] = "i:44458", -- Enchant Gloves - Crusher
|
||||
[60692] = "i:44465", -- Enchant Chest - Powerful Stats
|
||||
[60763] = "i:44469", -- Enchant Boots - Greater Assault
|
||||
[60767] = "i:44470", -- Enchant Bracer - Superior Spellpower
|
||||
[44575] = "i:44815", -- Enchant Bracer - Greater Assault
|
||||
[62256] = "i:44947", -- Enchant Bracer - Major Stamina
|
||||
[27967] = "i:38917", -- Enchant Weapon - Major Striking
|
||||
[27968] = "i:38918", -- Enchant Weapon - Major Intellect
|
||||
[27971] = "i:38919", -- Enchant 2H Weapon - Savagery
|
||||
[27972] = "i:38920", -- Enchant Weapon - Potency
|
||||
[27975] = "i:38921", -- Enchant Weapon - Major Spellpower
|
||||
[27977] = "i:38922", -- Enchant 2H Weapon - Major Agility
|
||||
[27981] = "i:38923", -- Enchant Weapon - Sunfire
|
||||
[27982] = "i:38924", -- Enchant Weapon - Soulfrost
|
||||
[27984] = "i:38925", -- Enchant Weapon - Mongoose
|
||||
[28003] = "i:38926", -- Enchant Weapon - Spellsurge
|
||||
[28004] = "i:38927", -- Enchant Weapon - Battlemaster
|
||||
[34010] = "i:38946", -- Enchant Weapon - Major Healing
|
||||
[42620] = "i:38947", -- Enchant Weapon - Greater Agility
|
||||
[27951] = "i:37603", -- Enchant Boots - Dexterity
|
||||
[25086] = "i:38895", -- Enchant Cloak - Dodge
|
||||
[27899] = "i:38897", -- Enchant Bracer - Brawn
|
||||
[27905] = "i:38898", -- Enchant Bracer - Stats
|
||||
[27906] = "i:38899", -- Enchant Bracer - Greater Dodge
|
||||
[27911] = "i:38900", -- Enchant Bracer - Superior Healing
|
||||
[27913] = "i:38901", -- Enchant Bracer - Versatility Prime
|
||||
[27914] = "i:38902", -- Enchant Bracer - Fortitude
|
||||
[27917] = "i:38903", -- Enchant Bracer - Spellpower
|
||||
[27944] = "i:38904", -- Enchant Shield - Lesser Dodge
|
||||
[27945] = "i:38905", -- Enchant Shield - Intellect
|
||||
[27946] = "i:38906", -- Enchant Shield - Parry
|
||||
[27948] = "i:38908", -- Enchant Boots - Vitality
|
||||
[27950] = "i:38909", -- Enchant Boots - Fortitude
|
||||
[27954] = "i:38910", -- Enchant Boots - Surefooted
|
||||
[27957] = "i:38911", -- Enchant Chest - Exceptional Health
|
||||
[27960] = "i:38913", -- Enchant Chest - Exceptional Stats
|
||||
[27961] = "i:38914", -- Enchant Cloak - Major Armor
|
||||
[33990] = "i:38928", -- Enchant Chest - Major Versatility
|
||||
[33991] = "i:38929", -- Enchant Chest - Versatility Prime
|
||||
[33992] = "i:38930", -- Enchant Chest - Major Resilience
|
||||
[33993] = "i:38931", -- Enchant Gloves - Blasting
|
||||
[33994] = "i:38932", -- Enchant Gloves - Precise Strikes
|
||||
[33995] = "i:38933", -- Enchant Gloves - Major Strength
|
||||
[33996] = "i:38934", -- Enchant Gloves - Assault
|
||||
[33997] = "i:38935", -- Enchant Gloves - Major Spellpower
|
||||
[33999] = "i:38936", -- Enchant Gloves - Major Healing
|
||||
[34001] = "i:38937", -- Enchant Bracer - Major Intellect
|
||||
[34002] = "i:38938", -- Enchant Bracer - Lesser Assault
|
||||
[34003] = "i:38939", -- Enchant Cloak - PvP Power
|
||||
[34004] = "i:38940", -- Enchant Cloak - Greater Agility
|
||||
[34007] = "i:38943", -- Enchant Boots - Cat's Swiftness
|
||||
[34008] = "i:38944", -- Enchant Boots - Boar's Speed
|
||||
[34009] = "i:38945", -- Enchant Shield - Major Stamina
|
||||
[44383] = "i:38949", -- Enchant Shield - Resilience
|
||||
[46594] = "i:38999", -- Enchant Chest - Dodge
|
||||
[47051] = "i:39000", -- Enchant Cloak - Greater Dodge
|
||||
[7745] = "i:38772", -- Enchant 2H Weapon - Minor Impact
|
||||
[7786] = "i:38779", -- Enchant Weapon - Minor Beastslayer
|
||||
[7788] = "i:38780", -- Enchant Weapon - Minor Striking
|
||||
[7793] = "i:38781", -- Enchant 2H Weapon - Lesser Intellect
|
||||
[13380] = "i:38788", -- Enchant 2H Weapon - Lesser Versatility
|
||||
[13503] = "i:38794", -- Enchant Weapon - Lesser Striking
|
||||
[13529] = "i:38796", -- Enchant 2H Weapon - Lesser Impact
|
||||
[13653] = "i:38813", -- Enchant Weapon - Lesser Beastslayer
|
||||
[13655] = "i:38814", -- Enchant Weapon - Lesser Elemental Slayer
|
||||
[13693] = "i:38821", -- Enchant Weapon - Striking
|
||||
[13695] = "i:38822", -- Enchant 2H Weapon - Impact
|
||||
[13898] = "i:38838", -- Enchant Weapon - Fiery Weapon
|
||||
[13915] = "i:38840", -- Enchant Weapon - Demonslaying
|
||||
[13937] = "i:38845", -- Enchant 2H Weapon - Greater Impact
|
||||
[13943] = "i:38848", -- Enchant Weapon - Greater Striking
|
||||
[20029] = "i:38868", -- Enchant Weapon - Icy Chill
|
||||
[20030] = "i:38869", -- Enchant 2H Weapon - Superior Impact
|
||||
[20031] = "i:38870", -- Enchant Weapon - Superior Striking
|
||||
[20032] = "i:38871", -- Enchant Weapon - Lifestealing
|
||||
[20033] = "i:38872", -- Enchant Weapon - Unholy Weapon
|
||||
[20034] = "i:38873", -- Enchant Weapon - Crusader
|
||||
[20035] = "i:38874", -- Enchant 2H Weapon - Major Versatility
|
||||
[20036] = "i:38875", -- Enchant 2H Weapon - Major Intellect
|
||||
[21931] = "i:38876", -- Enchant Weapon - Winter's Might
|
||||
[22749] = "i:38877", -- Enchant Weapon - Spellpower
|
||||
[22750] = "i:38878", -- Enchant Weapon - Healing Power
|
||||
[23799] = "i:38879", -- Enchant Weapon - Strength
|
||||
[23800] = "i:38880", -- Enchant Weapon - Agility
|
||||
[23803] = "i:38883", -- Enchant Weapon - Mighty Versatility
|
||||
[23804] = "i:38884", -- Enchant Weapon - Mighty Intellect
|
||||
[27837] = "i:38896", -- Enchant 2H Weapon - Agility
|
||||
[64441] = "i:46026", -- Enchant Weapon - Blade Ward
|
||||
[64579] = "i:46098", -- Enchant Weapon - Blood Draining
|
||||
[7418] = "i:38679", -- Enchant Bracer - Minor Health
|
||||
[7420] = "i:38766", -- Enchant Chest - Minor Health
|
||||
[7426] = "i:38767", -- Enchant Chest - Minor Absorption
|
||||
[7428] = "i:38768", -- Enchant Bracer - Minor Dodge
|
||||
[7443] = "i:38769", -- Enchant Chest - Minor Mana
|
||||
[7457] = "i:38771", -- Enchant Bracer - Minor Stamina
|
||||
[7748] = "i:38773", -- Enchant Chest - Lesser Health
|
||||
[7766] = "i:38774", -- Enchant Bracer - Minor Versatility
|
||||
[7771] = "i:38775", -- Enchant Cloak - Minor Protection
|
||||
[7776] = "i:38776", -- Enchant Chest - Lesser Mana
|
||||
[7779] = "i:38777", -- Enchant Bracer - Minor Agility
|
||||
[7782] = "i:38778", -- Enchant Bracer - Minor Strength
|
||||
[7857] = "i:38782", -- Enchant Chest - Health
|
||||
[7859] = "i:38783", -- Enchant Bracer - Lesser Versatility
|
||||
[7863] = "i:38785", -- Enchant Boots - Minor Stamina
|
||||
[7867] = "i:38786", -- Enchant Boots - Minor Agility
|
||||
[13378] = "i:38787", -- Enchant Shield - Minor Stamina
|
||||
[13419] = "i:38789", -- Enchant Cloak - Minor Agility
|
||||
[13421] = "i:38790", -- Enchant Cloak - Lesser Protection
|
||||
[13464] = "i:38791", -- Enchant Shield - Lesser Protection
|
||||
[13485] = "i:38792", -- Enchant Shield - Lesser Versatility
|
||||
[13501] = "i:38793", -- Enchant Bracer - Lesser Stamina
|
||||
[13536] = "i:38797", -- Enchant Bracer - Lesser Strength
|
||||
[13538] = "i:38798", -- Enchant Chest - Lesser Absorption
|
||||
[13607] = "i:38799", -- Enchant Chest - Mana
|
||||
[13612] = "i:38800", -- Enchant Gloves - Mining
|
||||
[13617] = "i:38801", -- Enchant Gloves - Herbalism
|
||||
[13620] = "i:38802", -- Enchant Gloves - Fishing
|
||||
[13622] = "i:38803", -- Enchant Bracer - Lesser Intellect
|
||||
[13626] = "i:38804", -- Enchant Chest - Minor Stats
|
||||
[13631] = "i:38805", -- Enchant Shield - Lesser Stamina
|
||||
[13635] = "i:38806", -- Enchant Cloak - Defense
|
||||
[13637] = "i:38807", -- Enchant Boots - Lesser Agility
|
||||
[13640] = "i:38808", -- Enchant Chest - Greater Health
|
||||
[13642] = "i:38809", -- Enchant Bracer - Versatility
|
||||
[13644] = "i:38810", -- Enchant Boots - Lesser Stamina
|
||||
[13646] = "i:38811", -- Enchant Bracer - Lesser Dodge
|
||||
[13648] = "i:38812", -- Enchant Bracer - Stamina
|
||||
[13659] = "i:38816", -- Enchant Shield - Versatility
|
||||
[13661] = "i:38817", -- Enchant Bracer - Strength
|
||||
[13663] = "i:38818", -- Enchant Chest - Greater Mana
|
||||
[13687] = "i:38819", -- Enchant Boots - Lesser Versatility
|
||||
[13689] = "i:38820", -- Enchant Shield - Lesser Parry
|
||||
[13698] = "i:38823", -- Enchant Gloves - Skinning
|
||||
[13700] = "i:38824", -- Enchant Chest - Lesser Stats
|
||||
[13746] = "i:38825", -- Enchant Cloak - Greater Defense
|
||||
[13815] = "i:38827", -- Enchant Gloves - Agility
|
||||
[13817] = "i:38828", -- Enchant Shield - Stamina
|
||||
[13822] = "i:38829", -- Enchant Bracer - Intellect
|
||||
[13836] = "i:38830", -- Enchant Boots - Stamina
|
||||
[13841] = "i:38831", -- Enchant Gloves - Advanced Mining
|
||||
[13846] = "i:38832", -- Enchant Bracer - Greater Versatility
|
||||
[13858] = "i:38833", -- Enchant Chest - Superior Health
|
||||
[13868] = "i:38834", -- Enchant Gloves - Advanced Herbalism
|
||||
[13882] = "i:38835", -- Enchant Cloak - Lesser Agility
|
||||
[13887] = "i:38836", -- Enchant Gloves - Strength
|
||||
[13890] = "i:38837", -- Enchant Boots - Minor Speed
|
||||
[13905] = "i:38839", -- Enchant Shield - Greater Versatility
|
||||
[13917] = "i:38841", -- Enchant Chest - Superior Mana
|
||||
[13931] = "i:38842", -- Enchant Bracer - Dodge
|
||||
[13935] = "i:38844", -- Enchant Boots - Agility
|
||||
[13939] = "i:38846", -- Enchant Bracer - Greater Strength
|
||||
[13941] = "i:38847", -- Enchant Chest - Stats
|
||||
[13945] = "i:38849", -- Enchant Bracer - Greater Stamina
|
||||
[13947] = "i:38850", -- Enchant Gloves - Riding Skill
|
||||
[13948] = "i:38851", -- Enchant Gloves - Minor Haste
|
||||
[20008] = "i:38852", -- Enchant Bracer - Greater Intellect
|
||||
[20009] = "i:38853", -- Enchant Bracer - Superior Versatility
|
||||
[20010] = "i:38854", -- Enchant Bracer - Superior Strength
|
||||
[20011] = "i:38855", -- Enchant Bracer - Superior Stamina
|
||||
[20012] = "i:38856", -- Enchant Gloves - Greater Agility
|
||||
[20013] = "i:38857", -- Enchant Gloves - Greater Strength
|
||||
[20015] = "i:38859", -- Enchant Cloak - Superior Defense
|
||||
[20016] = "i:38860", -- Enchant Shield - Vitality
|
||||
[20017] = "i:38861", -- Enchant Shield - Greater Stamina
|
||||
[20020] = "i:38862", -- Enchant Boots - Greater Stamina
|
||||
[20023] = "i:38863", -- Enchant Boots - Greater Agility
|
||||
[20024] = "i:38864", -- Enchant Boots - Versatility
|
||||
[20025] = "i:38865", -- Enchant Chest - Greater Stats
|
||||
[20026] = "i:38866", -- Enchant Chest - Major Health
|
||||
[20028] = "i:38867", -- Enchant Chest - Major Mana
|
||||
[23801] = "i:38881", -- Enchant Bracer - Argent Versatility
|
||||
[23802] = "i:38882", -- Enchant Bracer - Healing Power
|
||||
[25072] = "i:38885", -- Enchant Gloves - Threat
|
||||
[25073] = "i:38886", -- Enchant Gloves - Shadow Power
|
||||
[25074] = "i:38887", -- Enchant Gloves - Frost Power
|
||||
[25078] = "i:38888", -- Enchant Gloves - Fire Power
|
||||
[25079] = "i:38889", -- Enchant Gloves - Healing Power
|
||||
[25080] = "i:38890", -- Enchant Gloves - Superior Agility
|
||||
[25083] = "i:38893", -- Enchant Cloak - Stealth
|
||||
[25084] = "i:38894", -- Enchant Cloak - Subtlety
|
||||
[44506] = "i:38960", -- Enchant Gloves - Gatherer
|
||||
[63746] = "i:45628", -- Enchant Boots - Lesser Accuracy
|
||||
[71692] = "i:50816", -- Enchant Gloves - Angler
|
||||
[190954] = "i:128554", -- Enchant Shoulder - Boon of the Scavenger
|
||||
[190988] = "i:128558", -- Enchant Gloves - Legion Herbalism
|
||||
[190989] = "i:128559", -- Enchant Gloves - Legion Mining
|
||||
[190990] = "i:128560", -- Enchant Gloves - Legion Skinning
|
||||
[190991] = "i:128561", -- Enchant Gloves - Legion Surveying
|
||||
[190869] = "i:128540", -- Enchant Ring - Word of Versatility Rank 1
|
||||
[190995] = "i:128540", -- Enchant Ring - Word of Versatility Rank 2
|
||||
[191012] = "i:128540", -- Enchant Ring - Word of Versatility Rank 3
|
||||
[190866] = "i:128537", -- Enchant Ring - Word of Critical Strike Rank 1
|
||||
[190992] = "i:128537", -- Enchant Ring - Word of Critical Strike Rank 2
|
||||
[191009] = "i:128537", -- Enchant Ring - Word of Critical Strike Rank 3
|
||||
[190875] = "i:128546", -- Enchant Cloak - Word of Agility Rank 1
|
||||
[191001] = "i:128546", -- Enchant Cloak - Word of Agility Rank 2
|
||||
[191018] = "i:128546", -- Enchant Cloak - Word of Agility Rank 3
|
||||
[190867] = "i:128538", -- Enchant Ring - Word of Haste Rank 1
|
||||
[190993] = "i:128538", -- Enchant Ring - Word of Haste Rank 2
|
||||
[191010] = "i:128538", -- Enchant Ring - Word of Haste Rank 3
|
||||
[190876] = "i:128547", -- Enchant Cloak - Word of Intellect Rank 1
|
||||
[191002] = "i:128547", -- Enchant Cloak - Word of Intellect Rank 2
|
||||
[191019] = "i:128547", -- Enchant Cloak - Word of Intellect Rank 3
|
||||
[190868] = "i:128539", -- Enchant Ring - Word of Mastery Rank 1
|
||||
[190994] = "i:128539", -- Enchant Ring - Word of Mastery Rank 2
|
||||
[191011] = "i:128539", -- Enchant Ring - Word of Mastery Rank 3
|
||||
[190874] = "i:128545", -- Enchant Cloak - Word of Strength Rank 1
|
||||
[191000] = "i:128545", -- Enchant Cloak - Word of Strength Rank 2
|
||||
[191017] = "i:128545", -- Enchant Cloak - Word of Strength Rank 3
|
||||
[228408] = "i:141910", -- Enchant Neck - Mark Of The Ancient Priestess Rank 1
|
||||
[228409] = "i:141910", -- Enchant Neck - Mark Of The Ancient Priestess Rank 2
|
||||
[228410] = "i:141910", -- Enchant Neck - Mark Of The Ancient Priestess Rank 3
|
||||
[190892] = "i:128551", -- Enchant Neck - Mark Of The Claw Rank 1
|
||||
[191006] = "i:128551", -- Enchant Neck - Mark Of The Claw Rank 2
|
||||
[191023] = "i:128551", -- Enchant Neck - Mark Of The Claw Rank 3
|
||||
[190893] = "i:128552", -- Enchant Neck - Mark Of The Distant Army Rank 1
|
||||
[191007] = "i:128552", -- Enchant Neck - Mark Of The Distant Army Rank 2
|
||||
[191024] = "i:128552", -- Enchant Neck - Mark Of The Distant Army Rank 3
|
||||
[228402] = "i:141908", -- Enchant Neck - Mark Of The Heavy Hide Rank 1
|
||||
[228403] = "i:141908", -- Enchant Neck - Mark Of The Heavy Hide Rank 2
|
||||
[228404] = "i:141908", -- Enchant Neck - Mark Of The Heavy Hide Rank 3
|
||||
[190894] = "i:128553", -- Enchant Neck - Mark of the Hidden Satyr Rank 1
|
||||
[191008] = "i:128553", -- Enchant Neck - Mark of the Hidden Satyr Rank 2
|
||||
[191025] = "i:128553", -- Enchant Neck - Mark of the Hidden Satyr Rank 3
|
||||
[228405] = "i:141909", -- Enchant Neck - Mark of the Trained Soldier Rank 1
|
||||
[228406] = "i:141909", -- Enchant Neck - Mark of the Trained Soldier Rank 2
|
||||
[228407] = "i:141909", -- Enchant Neck - Mark of the Trained Soldier Rank 3
|
||||
[190870] = "i:128541", -- Enchant Ring - Binding Of Critical Strike Rank 1
|
||||
[190996] = "i:128541", -- Enchant Ring - Binding Of Critical Strike Rank 2
|
||||
[191013] = "i:128541", -- Enchant Ring - Binding Of Critical Strike Rank 3
|
||||
[190871] = "i:128542", -- Enchant Ring - Binding Of Haste Rank 1
|
||||
[190997] = "i:128542", -- Enchant Ring - Binding Of Haste Rank 2
|
||||
[191014] = "i:128542", -- Enchant Ring - Binding Of Haste Rank 3
|
||||
[190872] = "i:128543", -- Enchant Ring - Binding Of Mastery Rank 1
|
||||
[190998] = "i:128543", -- Enchant Ring - Binding Of Mastery Rank 2
|
||||
[191015] = "i:128543", -- Enchant Ring - Binding Of Mastery Rank 3
|
||||
[190873] = "i:128544", -- Enchant Ring - Binding Of Versatility Rank 1
|
||||
[190999] = "i:128544", -- Enchant Ring - Binding Of Versatility Rank 2
|
||||
[191016] = "i:128544", -- Enchant Ring - Binding Of Versatility Rank 3
|
||||
[190877] = "i:128548", -- Enchant Cloak - Binding Of Strength Rank 1
|
||||
[191003] = "i:128548", -- Enchant Cloak - Binding Of Strength Rank 2
|
||||
[191020] = "i:128548", -- Enchant Cloak - Binding Of Strength Rank 3
|
||||
[190878] = "i:128549", -- Enchant Cloak - Binding Of Agility Rank 1
|
||||
[191004] = "i:128549", -- Enchant Cloak - Binding Of Agility Rank 2
|
||||
[191021] = "i:128549", -- Enchant Cloak - Binding Of Agility Rank 3
|
||||
[190879] = "i:128550", -- Enchant Cloak - Binding Of Intellect Rank 1
|
||||
[191005] = "i:128550", -- Enchant Cloak - Binding Of Intellect Rank 2
|
||||
[191022] = "i:128550", -- Enchant Cloak - Binding Of Intellect Rank 3
|
||||
[235698] = "i:144307", -- Enchant Neck - Mark of the Deadly Rank 1
|
||||
[235702] = "i:144307", -- Enchant Neck - Mark of the Deadly Rank 2
|
||||
[235706] = "i:144307", -- Enchant Neck - Mark of the Deadly Rank 3
|
||||
[235695] = "i:144304", -- Enchant Neck - Mark of the Master Rank 1
|
||||
[235699] = "i:144304", -- Enchant Neck - Mark of the Master Rank 2
|
||||
[235703] = "i:144304", -- Enchant Neck - Mark of the Master Rank 3
|
||||
[235697] = "i:144306", -- Enchant Neck - Mark of the Quick Rank 1
|
||||
[235701] = "i:144306", -- Enchant Neck - Mark of the Quick Rank 2
|
||||
[235705] = "i:144306", -- Enchant Neck - Mark of the Quick Rank 3
|
||||
[235696] = "i:144305", -- Enchant Neck - Mark of the Versatile Rank 1
|
||||
[235704] = "i:144305", -- Enchant Neck - Mark of the Versatile Rank 2
|
||||
[235700] = "i:144305", -- Enchant Neck - Mark of the Versatile Rank 3
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function ProfessionInfo.GetName(key)
|
||||
local name = PROFESSION_NAMES[key]
|
||||
assert(name)
|
||||
return name
|
||||
end
|
||||
|
||||
function ProfessionInfo.IsSubNameClassic(str)
|
||||
assert(TSM.IsWowClassic())
|
||||
return CLASSIC_SUB_NAMES[str] or false
|
||||
end
|
||||
|
||||
function ProfessionInfo.GetVellumItemString()
|
||||
return VELLUM_ITEM_STRING
|
||||
end
|
||||
|
||||
function ProfessionInfo.IsEngineeringTinker(spellId)
|
||||
return ENGINEERING_TINKERS[spellId] or false
|
||||
end
|
||||
|
||||
function ProfessionInfo.IsMassMill(spellId)
|
||||
return MASS_MILLING_RECIPES[spellId] and true or false
|
||||
end
|
||||
|
||||
function ProfessionInfo.GetIndirectCraftResult(spellId)
|
||||
return ENCHANTING_RECIPIES[spellId] or MASS_MILLING_RECIPES[spellId] or nil
|
||||
end
|
||||
512
LibTSM/Data/Prospect.lua
Normal file
512
LibTSM/Data/Prospect.lua
Normal file
@@ -0,0 +1,512 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Prospect = TSM.Init("Data.Prospect")
|
||||
local private = {}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Prospect Data
|
||||
-- ============================================================================
|
||||
|
||||
local DATA = TSM.IsWowClassic() and {} or {
|
||||
-- ======================================== Uncommon Gems ======================================
|
||||
["i:774"] = { -- Malachite
|
||||
["i:2770"] = {matRate = 0.5000, minAmount = 1, maxAmount = 1, amountOfMats = 0.1000}, -- Copper Ore
|
||||
},
|
||||
["i:818"] = { -- Tigerseye
|
||||
["i:2770"] = {matRate = 0.5000, minAmount = 1, maxAmount = 1, amountOfMats = 0.1000}, -- Copper Ore
|
||||
},
|
||||
["i:1210"] = { -- Shadowgem
|
||||
["i:2771"] = {matRate = 0.3800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0800}, -- Tin Ore
|
||||
["i:2770"] = {matRate = 0.1000, minAmount = 1, maxAmount = 1, amountOfMats = 0.0200}, -- Copper Ore
|
||||
},
|
||||
["i:1206"] = { -- Moss Agate
|
||||
["i:2771"] = {matRate = 0.3800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0800}, -- Tin Ore
|
||||
},
|
||||
["i:1705"] = { -- Lesser Moonstone
|
||||
["i:2771"] = {matRate = 0.3800, minAmount = 1, maxAmount = 2, amountOfMats = 0.080}, -- Tin Ore
|
||||
["i:2772"] = {matRate = 0.3500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0725}, -- Iron Ore
|
||||
},
|
||||
["i:1529"] = { -- Jade
|
||||
["i:2772"] = {matRate = 0.3500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0725}, -- Iron Ore
|
||||
["i:2771"] = {matRate = 0.0375, minAmount = 1, maxAmount = 1, amountOfMats = 0.0075}, -- Tin Ore
|
||||
},
|
||||
["i:3864"] = { -- Citrine
|
||||
["i:2772"] = {matRate = 0.3500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0725}, -- Iron Ore
|
||||
["i:3858"] = {matRate = 0.3500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0725}, -- Mithril Ore
|
||||
["i:2771"] = {matRate = 0.0375, minAmount = 1, maxAmount = 1, amountOfMats = 0.0075}, -- Tin Ore
|
||||
},
|
||||
["i:7909"] = { -- Aquamarine
|
||||
["i:3858"] = {matRate = 0.3500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0725}, -- Mithril Ore
|
||||
["i:2772"] = {matRate = 0.0500, minAmount = 1, maxAmount = 1, amountOfMats = 0.0100}, -- Iron Ore
|
||||
["i:2771"] = {matRate = 0.0375, minAmount = 1, maxAmount = 1, amountOfMats = 0.0075}, -- Tin Ore
|
||||
},
|
||||
["i:7910"] = { -- Star Ruby
|
||||
[ "i:3858"] = {matRate = 0.3500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0725}, -- Mithril Ore
|
||||
["i:10620"] = {matRate = 0.1550, minAmount = 1, maxAmount = 2, amountOfMats = 0.0320}, -- Thorium Ore
|
||||
[ "i:2772"] = {matRate = 0.0500, minAmount = 1, maxAmount = 1, amountOfMats = 0.0100}, -- Iron Ore
|
||||
},
|
||||
["i:12361"] = { -- Blue Sapphire
|
||||
["i:10620"] = {matRate = 0.3100, minAmount = 1, maxAmount = 2, amountOfMats = 0.0660}, -- Thorium Ore
|
||||
[ "i:3858"] = {matRate = 0.0250, minAmount = 1, maxAmount = 1, amountOfMats = 0.0050}, -- Mithril Ore
|
||||
},
|
||||
["i:12799"] = { -- Large Opal
|
||||
["i:10620"] = {matRate = 0.3100, minAmount = 1, maxAmount = 2, amountOfMats = 0.0660}, -- Thorium Ore
|
||||
[ "i:3858"] = {matRate = 0.0250, minAmount = 1, maxAmount = 1, amountOfMats = 0.0050}, -- Mithril Ore
|
||||
},
|
||||
["i:12800"] = { -- Azerothian Diamond
|
||||
["i:10620"] = {matRate = 0.3100, minAmount = 1, maxAmount = 2, amountOfMats = 0.0660}, -- Thorium Ore
|
||||
[ "i:3858"] = {matRate = 0.0250, minAmount = 1, maxAmount = 1, amountOfMats = 0.0050}, -- Mithril Ore
|
||||
},
|
||||
["i:12364"] = { -- Huge Emerald
|
||||
["i:10620"] = {matRate = 0.3100, minAmount = 1, maxAmount = 2, amountOfMats = 0.0660}, -- Thorium Ore
|
||||
[ "i:3858"] = {matRate = 0.0250, minAmount = 1, maxAmount = 1, amountOfMats = 0.0050}, -- Mithril Ore
|
||||
},
|
||||
["i:23117"] = { -- Azure Moonstone
|
||||
["i:23424"] = {matRate = 0.1800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0365}, -- Fel Iron Ore
|
||||
["i:23425"] = {matRate = 0.5000, minAmount = 1, maxAmount = 2, amountOfMats = 0.0365}, -- Adamantite Ore
|
||||
},
|
||||
["i:23077"] = { -- Blood Garnet
|
||||
["i:23424"] = {matRate = 0.1800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0365}, -- Fel Iron Ore
|
||||
["i:23425"] = {matRate = 0.1800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0365}, -- Adamantite Ore
|
||||
},
|
||||
["i:23079"] = { -- Deep Peridot
|
||||
["i:23424"] = {matRate = 0.1800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0365}, -- Fel Iron Ore
|
||||
["i:23425"] = {matRate = 0.1800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0365}, -- Adamantite Ore
|
||||
},
|
||||
["i:21929"] = { -- Flame Spessarite
|
||||
["i:23424"] = {matRate = 0.1800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0365}, -- Fel Iron Ore
|
||||
["i:23425"] = {matRate = 0.1800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0365}, -- Adamantite Ore
|
||||
},
|
||||
["i:23112"] = { -- Golden Draenite
|
||||
["i:23424"] = {matRate = 0.1800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0365}, -- Fel Iron Ore
|
||||
["i:23425"] = {matRate = 0.1800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0365}, -- Adamantite Ore
|
||||
},
|
||||
["i:23107"] = { -- Shadow Draenite
|
||||
["i:23424"] = {matRate = 0.1800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0365}, -- Fel Iron Ore
|
||||
["i:23425"] = {matRate = 0.1800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0365}, -- Adamantite Ore
|
||||
},
|
||||
["i:36917"] = { -- Bloodstone
|
||||
["i:36909"] = {matRate = 0.2500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0528}, -- Cobalt Ore
|
||||
["i:36912"] = {matRate = 0.1800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0365}, -- Saronite Ore
|
||||
["i:36910"] = {matRate = 0.2500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0525}, -- Titanium Ore
|
||||
},
|
||||
["i:36923"] = { -- Chalcedony
|
||||
["i:36909"] = {matRate = 0.2500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0528}, -- Cobalt Ore
|
||||
["i:36912"] = {matRate = 0.1800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0365}, -- Saronite Ore
|
||||
["i:36910"] = {matRate = 0.2500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0525}, -- Titanium Ore
|
||||
},
|
||||
["i:36932"] = { -- Dark Jade
|
||||
["i:36909"] = {matRate = 0.2500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0528}, -- Cobalt Ore
|
||||
["i:36912"] = {matRate = 0.1800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0365}, -- Saronite Ore
|
||||
["i:36910"] = {matRate = 0.2500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0525}, -- Titanium Ore
|
||||
},
|
||||
["i:36929"] = { -- Huge Citrine
|
||||
["i:36909"] = {matRate = 0.2500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0528}, -- Cobalt Ore
|
||||
["i:36912"] = {matRate = 0.1800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0365}, -- Saronite Ore
|
||||
["i:36910"] = {matRate = 0.2500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0525}, -- Titanium Ore
|
||||
},
|
||||
["i:36926"] = { -- Shadow Crystal
|
||||
["i:36909"] = {matRate = 0.2500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0528}, -- Cobalt Ore
|
||||
["i:36912"] = {matRate = 0.1800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0365}, -- Saronite Ore
|
||||
["i:36910"] = {matRate = 0.2500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0525}, -- Titanium Ore
|
||||
},
|
||||
["i:36920"] = { -- Sun Crystal
|
||||
["i:36909"] = {matRate = 0.2500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0528}, -- Cobalt Ore
|
||||
["i:36912"] = {matRate = 0.1800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0365}, -- Saronite Ore
|
||||
["i:36910"] = {matRate = 0.2500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0525}, -- Titanium Ore
|
||||
},
|
||||
["i:52182"] = { -- Jasper
|
||||
["i:53038"] = {matRate = 0.2500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0528}, -- Obsidium Ore
|
||||
["i:52185"] = {matRate = 0.1800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0365}, -- Elementium Ore
|
||||
["i:52183"] = {matRate = 0.1650, minAmount = 1, maxAmount = 1, amountOfMats = 0.0330}, -- Pyrite Ore
|
||||
},
|
||||
["i:52180"] = { -- Nightstone
|
||||
["i:53038"] = {matRate = 0.2500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0528}, -- Obsidium Ore
|
||||
["i:52185"] = {matRate = 0.1800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0365}, -- Elementium Ore
|
||||
["i:52183"] = {matRate = 0.1650, minAmount = 1, maxAmount = 1, amountOfMats = 0.0330}, -- Pyrite Ore
|
||||
},
|
||||
["i:52178"] = { -- Zephyrite
|
||||
["i:53038"] = {matRate = 0.2500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0528}, -- Obsidium Ore
|
||||
["i:52185"] = {matRate = 0.1800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0365}, -- Elementium Ore
|
||||
["i:52183"] = {matRate = 0.1650, minAmount = 1, maxAmount = 1, amountOfMats = 0.0330}, -- Pyrite Ore
|
||||
},
|
||||
["i:52179"] = { -- Alicite
|
||||
["i:53038"] = {matRate = 0.2500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0528}, -- Obsidium Ore
|
||||
["i:52185"] = {matRate = 0.1800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0365}, -- Elementium Ore
|
||||
["i:52183"] = {matRate = 0.1650, minAmount = 1, maxAmount = 1, amountOfMats = 0.0330}, -- Pyrite Ore
|
||||
},
|
||||
["i:52177"] = { -- Carnelian
|
||||
["i:53038"] = {matRate = 0.2500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0528}, -- Obsidium Ore
|
||||
["i:52185"] = {matRate = 0.1800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0365}, -- Elementium Ore
|
||||
["i:52183"] = {matRate = 0.1650, minAmount = 1, maxAmount = 1, amountOfMats = 0.0330}, -- Pyrite Ore
|
||||
},
|
||||
["i:52181"] = { -- Hessonite
|
||||
["i:53038"] = {matRate = 0.2500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0528}, -- Obsidium Ore
|
||||
["i:52185"] = {matRate = 0.1800, minAmount = 1, maxAmount = 2, amountOfMats = 0.0365}, -- Elementium Ore
|
||||
["i:52183"] = {matRate = 0.1650, minAmount = 1, maxAmount = 1, amountOfMats = 0.0330}, -- Pyrite Ore
|
||||
},
|
||||
["i:76130"] = { -- Tiger Opal
|
||||
["i:72092"] = {matRate = 0.2350, minAmount = 1, maxAmount = 2, amountOfMats = 0.0485}, -- Ghost Iron Ore
|
||||
["i:72093"] = {matRate = 0.2350, minAmount = 1, maxAmount = 2, amountOfMats = 0.0505}, -- Kyparite
|
||||
["i:72103"] = {matRate = 0.1700, minAmount = 1, maxAmount = 1, amountOfMats = 0.0340}, -- White Trillium Ore
|
||||
["i:72094"] = {matRate = 0.1700, minAmount = 1, maxAmount = 1, amountOfMats = 0.0340}, -- Black Trillium Ore
|
||||
},
|
||||
["i:76133"] = { -- Lapis Lazuli
|
||||
["i:72092"] = {matRate = 0.2350, minAmount = 1, maxAmount = 2, amountOfMats = 0.0485}, -- Ghost Iron Ore
|
||||
["i:72093"] = {matRate = 0.2350, minAmount = 1, maxAmount = 2, amountOfMats = 0.0505}, -- Kyparite
|
||||
["i:72103"] = {matRate = 0.1700, minAmount = 1, maxAmount = 1, amountOfMats = 0.0340}, -- White Trillium Ore
|
||||
["i:72094"] = {matRate = 0.1700, minAmount = 1, maxAmount = 1, amountOfMats = 0.0340}, -- Black Trillium Ore
|
||||
},
|
||||
["i:76134"] = { -- Sunstone
|
||||
["i:72092"] = {matRate = 0.2350, minAmount = 1, maxAmount = 2, amountOfMats = 0.0485}, -- Ghost Iron Ore
|
||||
["i:72093"] = {matRate = 0.2350, minAmount = 1, maxAmount = 2, amountOfMats = 0.0505}, -- Kyparite
|
||||
["i:72103"] = {matRate = 0.1700, minAmount = 1, maxAmount = 1, amountOfMats = 0.0340}, -- White Trillium Ore
|
||||
["i:72094"] = {matRate = 0.1700, minAmount = 1, maxAmount = 1, amountOfMats = 0.0340}, -- Black Trillium Ore
|
||||
},
|
||||
["i:76135"] = { -- Roguestone
|
||||
["i:72092"] = {matRate = 0.2350, minAmount = 1, maxAmount = 2, amountOfMats = 0.0485}, -- Ghost Iron Ore
|
||||
["i:72093"] = {matRate = 0.2350, minAmount = 1, maxAmount = 2, amountOfMats = 0.0505}, -- Kyparite
|
||||
["i:72103"] = {matRate = 0.1700, minAmount = 1, maxAmount = 1, amountOfMats = 0.0340}, -- White Trillium Ore
|
||||
["i:72094"] = {matRate = 0.1700, minAmount = 1, maxAmount = 1, amountOfMats = 0.0340}, -- Black Trillium Ore
|
||||
},
|
||||
["i:76136"] = { -- Pandarian Garnet
|
||||
["i:72092"] = {matRate = 0.2350, minAmount = 1, maxAmount = 2, amountOfMats = 0.0485}, -- Ghost Iron Ore
|
||||
["i:72093"] = {matRate = 0.2350, minAmount = 1, maxAmount = 2, amountOfMats = 0.0505}, -- Kyparite
|
||||
["i:72103"] = {matRate = 0.1700, minAmount = 1, maxAmount = 1, amountOfMats = 0.0340}, -- White Trillium Ore
|
||||
["i:72094"] = {matRate = 0.1700, minAmount = 1, maxAmount = 1, amountOfMats = 0.0340}, -- Black Trillium Ore
|
||||
},
|
||||
["i:76137"] = { -- Alexandrite
|
||||
["i:72092"] = {matRate = 0.2350, minAmount = 1, maxAmount = 2, amountOfMats = 0.0485}, -- Ghost Iron Ore
|
||||
["i:72093"] = {matRate = 0.2350, minAmount = 1, maxAmount = 2, amountOfMats = 0.0505}, -- Kyparite
|
||||
["i:72103"] = {matRate = 0.1700, minAmount = 1, maxAmount = 1, amountOfMats = 0.0340}, -- White Trillium Ore
|
||||
["i:72094"] = {matRate = 0.1700, minAmount = 1, maxAmount = 1, amountOfMats = 0.0340}, -- Black Trillium Ore
|
||||
},
|
||||
["i:130173"] = { -- Deep Amber
|
||||
["i:123918"] = {matRate = 0.0500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0110}, -- Leystone Ore
|
||||
["i:123919"] = {matRate = 0.0600, minAmount = 2, maxAmount = 5, amountOfMats = 0.0420}, -- Felslate
|
||||
},
|
||||
["i:130174"] = { -- Azsunite
|
||||
["i:123918"] = {matRate = 0.0500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0110}, -- Leystone Ore
|
||||
["i:123919"] = {matRate = 0.0600, minAmount = 2, maxAmount = 5, amountOfMats = 0.0420}, -- Felslate
|
||||
},
|
||||
["i:130176"] = { -- Skystone
|
||||
["i:123918"] = {matRate = 0.0500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0110}, -- Leystone Ore
|
||||
["i:123919"] = {matRate = 0.0600, minAmount = 2, maxAmount = 5, amountOfMats = 0.0420}, -- Felslate
|
||||
},
|
||||
["i:130177"] = { -- Queen's Opal
|
||||
["i:123918"] = {matRate = 0.0500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0110}, -- Leystone Ore
|
||||
["i:123919"] = {matRate = 0.0600, minAmount = 2, maxAmount = 5, amountOfMats = 0.0420}, -- Felslate
|
||||
},
|
||||
["i:130175"] = { -- Chaotic Spinel
|
||||
["i:123918"] = {matRate = 0.3500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0080}, -- Leystone Ore
|
||||
["i:123919"] = {matRate = 0.2500, minAmount = 2, maxAmount = 5, amountOfMats = 0.0175}, -- Felslate
|
||||
},
|
||||
["i:130172"] = { -- Sangrite
|
||||
["i:123918"] = {matRate = 0.2500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0060}, -- Leystone Ore
|
||||
["i:123919"] = {matRate = 0.3500, minAmount = 2, maxAmount = 5, amountOfMats = 0.0245}, -- Felslate
|
||||
},
|
||||
["i:153700"] = { -- Golden Beryl
|
||||
["i:152512"] = {matRate = 0.1800, minAmount = 1, maxAmount = 4, amountOfMats = 0.0552}, -- Monelite Ore
|
||||
["i:152579"] = {matRate = 0.1950, minAmount = 1, maxAmount = 4, amountOfMats = 0.0603}, -- Storm Silver Ore
|
||||
["i:152513"] = {matRate = 0.2100, minAmount = 1, maxAmount = 4, amountOfMats = 0.0660}, -- Platinum Ore
|
||||
},
|
||||
["i:153701"] = { -- Rubellite
|
||||
["i:152512"] = {matRate = 0.1800, minAmount = 1, maxAmount = 4, amountOfMats = 0.0552}, -- Monelite Ore
|
||||
["i:152579"] = {matRate = 0.1950, minAmount = 1, maxAmount = 4, amountOfMats = 0.0603}, -- Storm Silver Ore
|
||||
["i:152513"] = {matRate = 0.2100, minAmount = 1, maxAmount = 4, amountOfMats = 0.0660}, -- Platinum Ore
|
||||
},
|
||||
["i:153702"] = { -- Kubiline
|
||||
["i:152512"] = {matRate = 0.1800, minAmount = 1, maxAmount = 4, amountOfMats = 0.0552}, -- Monelite Ore
|
||||
["i:152579"] = {matRate = 0.1950, minAmount = 1, maxAmount = 4, amountOfMats = 0.0603}, -- Storm Silver Ore
|
||||
["i:152513"] = {matRate = 0.2100, minAmount = 1, maxAmount = 4, amountOfMats = 0.0660}, -- Platinum Ore
|
||||
},
|
||||
["i:153703"] = { -- Solstone
|
||||
["i:152512"] = {matRate = 0.1800, minAmount = 1, maxAmount = 4, amountOfMats = 0.0552}, -- Monelite Ore
|
||||
["i:152579"] = {matRate = 0.1950, minAmount = 1, maxAmount = 4, amountOfMats = 0.0603}, -- Storm Silver Ore
|
||||
["i:152513"] = {matRate = 0.2100, minAmount = 1, maxAmount = 4, amountOfMats = 0.0660}, -- Platinum Ore
|
||||
},
|
||||
["i:153704"] = { -- Viridium
|
||||
["i:152512"] = {matRate = 0.1800, minAmount = 1, maxAmount = 4, amountOfMats = 0.0552}, -- Monelite Ore
|
||||
["i:152579"] = {matRate = 0.1950, minAmount = 1, maxAmount = 4, amountOfMats = 0.0603}, -- Storm Silver Ore
|
||||
["i:152513"] = {matRate = 0.2100, minAmount = 1, maxAmount = 4, amountOfMats = 0.0660}, -- Platinum Ore
|
||||
},
|
||||
["i:153705"] = { -- Kyanite
|
||||
["i:152512"] = {matRate = 0.1800, minAmount = 1, maxAmount = 4, amountOfMats = 0.0552}, -- Monelite Ore
|
||||
["i:152579"] = {matRate = 0.1950, minAmount = 1, maxAmount = 4, amountOfMats = 0.0603}, -- Storm Silver Ore
|
||||
["i:152513"] = {matRate = 0.2100, minAmount = 1, maxAmount = 4, amountOfMats = 0.0660}, -- Platinum Ore
|
||||
},
|
||||
-- ========================================== Rare Gems ========================================
|
||||
["i:23440"] = { -- Dawnstone
|
||||
["i:23424"] = {matRate = 0.0125, minAmount = 1, maxAmount = 1, amountOfMats = 0.0025}, -- Fel Iron Ore
|
||||
["i:23425"] = {matRate = 0.0400, minAmount = 1, maxAmount = 1, amountOfMats = 0.0080}, -- Adamantite Ore
|
||||
},
|
||||
["i:23436"] = { -- Living Ruby
|
||||
["i:23424"] = {matRate = 0.0125, minAmount = 1, maxAmount = 1, amountOfMats = 0.0025}, -- Fel Iron Ore
|
||||
["i:23425"] = {matRate = 0.0400, minAmount = 1, maxAmount = 1, amountOfMats = 0.0080}, -- Adamantite Ore
|
||||
},
|
||||
["i:23441"] = { -- Nightseye
|
||||
["i:23424"] = {matRate = 0.0125, minAmount = 1, maxAmount = 1, amountOfMats = 0.0025}, -- Fel Iron Ore
|
||||
["i:23425"] = {matRate = 0.0400, minAmount = 1, maxAmount = 1, amountOfMats = 0.0080}, -- Adamantite Ore
|
||||
},
|
||||
["i:23439"] = { -- Noble Topaz
|
||||
["i:23424"] = {matRate = 0.0125, minAmount = 1, maxAmount = 1, amountOfMats = 0.0025}, -- Fel Iron Ore
|
||||
["i:23425"] = {matRate = 0.0400, minAmount = 1, maxAmount = 1, amountOfMats = 0.0080}, -- Adamantite Ore
|
||||
},
|
||||
["i:23438"] = { -- Star of Elune
|
||||
["i:23424"] = {matRate = 0.0125, minAmount = 1, maxAmount = 1, amountOfMats = 0.0025}, -- Fel Iron Ore
|
||||
["i:23425"] = {matRate = 0.0400, minAmount = 1, maxAmount = 1, amountOfMats = 0.0080}, -- Adamantite Ore
|
||||
},
|
||||
["i:23437"] = { -- Talasite
|
||||
["i:23424"] = {matRate = 0.0125, minAmount = 1, maxAmount = 1, amountOfMats = 0.0025}, -- Fel Iron Ore
|
||||
["i:23425"] = {matRate = 0.0400, minAmount = 1, maxAmount = 1, amountOfMats = 0.0080}, -- Adamantite Ore
|
||||
},
|
||||
["i:36921"] = { -- Autumn's Glow
|
||||
["i:36909"] = {matRate = 0.0125, minAmount = 1, maxAmount = 2, amountOfMats = 0.0025}, -- Cobalt Ore
|
||||
["i:36912"] = {matRate = 0.0500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0100}, -- Saronite Ore
|
||||
["i:36910"] = {matRate = 0.0500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0100}, -- Titanium Ore
|
||||
},
|
||||
["i:36933"] = { -- Forest Emerald
|
||||
["i:36909"] = {matRate = 0.0125, minAmount = 1, maxAmount = 2, amountOfMats = 0.0025}, -- Cobalt Ore
|
||||
["i:36912"] = {matRate = 0.0500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0100}, -- Saronite Ore
|
||||
["i:36910"] = {matRate = 0.0500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0100}, -- Titanium Ore
|
||||
},
|
||||
["i:36930"] = { -- Monarch Topaz
|
||||
["i:36909"] = {matRate = 0.0125, minAmount = 1, maxAmount = 2, amountOfMats = 0.0025}, -- Cobalt Ore
|
||||
["i:36912"] = {matRate = 0.0500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0100}, -- Saronite Ore
|
||||
["i:36910"] = {matRate = 0.0500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0100}, -- Titanium Ore
|
||||
},
|
||||
["i:36918"] = { -- Scarlet Ruby
|
||||
["i:36909"] = {matRate = 0.0125, minAmount = 1, maxAmount = 2, amountOfMats = 0.0025}, -- Cobalt Ore
|
||||
["i:36912"] = {matRate = 0.0500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0100}, -- Saronite Ore
|
||||
["i:36910"] = {matRate = 0.0500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0100}, -- Titanium Ore
|
||||
},
|
||||
["i:36924"] = { -- Sky Sapphire
|
||||
["i:36909"] = {matRate = 0.0125, minAmount = 1, maxAmount = 2, amountOfMats = 0.0025}, -- Cobalt Ore
|
||||
["i:36912"] = {matRate = 0.0500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0100}, -- Saronite Ore
|
||||
["i:36910"] = {matRate = 0.0500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0100}, -- Titanium Ore
|
||||
},
|
||||
["i:36927"] = { -- Twilight Opal
|
||||
["i:36909"] = {matRate = 0.0125, minAmount = 1, maxAmount = 2, amountOfMats = 0.0025}, -- Cobalt Ore
|
||||
["i:36912"] = {matRate = 0.0500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0100}, -- Saronite Ore
|
||||
["i:36910"] = {matRate = 0.0500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0100}, -- Titanium Ore
|
||||
},
|
||||
["i:52192"] = { -- Dream Emerald
|
||||
["i:53038"] = {matRate = 0.0125, minAmount = 1, maxAmount = 1, amountOfMats = 0.0025}, -- Obsidium Ore
|
||||
["i:52185"] = {matRate = 0.0450, minAmount = 1, maxAmount = 2, amountOfMats = 0.0091}, -- Elementium Ore
|
||||
["i:52183"] = {matRate = 0.0750, minAmount = 1, maxAmount = 2, amountOfMats = 0.0152}, -- Pyrite Ore
|
||||
},
|
||||
["i:52193"] = { -- Ember Topaz
|
||||
["i:53038"] = {matRate = 0.0125, minAmount = 1, maxAmount = 1, amountOfMats = 0.0025}, -- Obsidium Ore
|
||||
["i:52185"] = {matRate = 0.0450, minAmount = 1, maxAmount = 2, amountOfMats = 0.0091}, -- Elementium Ore
|
||||
["i:52183"] = {matRate = 0.0750, minAmount = 1, maxAmount = 2, amountOfMats = 0.0152}, -- Pyrite Ore
|
||||
},
|
||||
["i:52190"] = { -- Inferno Ruby
|
||||
["i:53038"] = {matRate = 0.0125, minAmount = 1, maxAmount = 1, amountOfMats = 0.0025}, -- Obsidium Ore
|
||||
["i:52185"] = {matRate = 0.0450, minAmount = 1, maxAmount = 2, amountOfMats = 0.0091}, -- Elementium Ore
|
||||
["i:52183"] = {matRate = 0.0750, minAmount = 1, maxAmount = 2, amountOfMats = 0.0152}, -- Pyrite Ore
|
||||
},
|
||||
["i:52195"] = { -- Amberjewel
|
||||
["i:53038"] = {matRate = 0.0125, minAmount = 1, maxAmount = 1, amountOfMats = 0.0025}, -- Obsidium Ore
|
||||
["i:52185"] = {matRate = 0.0450, minAmount = 1, maxAmount = 2, amountOfMats = 0.0091}, -- Elementium Ore
|
||||
["i:52183"] = {matRate = 0.0750, minAmount = 1, maxAmount = 2, amountOfMats = 0.0152}, -- Pyrite Ore
|
||||
},
|
||||
["i:52194"] = { -- Demonseye
|
||||
["i:53038"] = {matRate = 0.0125, minAmount = 1, maxAmount = 1, amountOfMats = 0.0025}, -- Obsidium Ore
|
||||
["i:52185"] = {matRate = 0.0450, minAmount = 1, maxAmount = 2, amountOfMats = 0.0091}, -- Elementium Ore
|
||||
["i:52183"] = {matRate = 0.0750, minAmount = 1, maxAmount = 2, amountOfMats = 0.0152}, -- Pyrite Ore
|
||||
},
|
||||
["i:52191"] = { -- Ocean Sapphire
|
||||
["i:53038"] = {matRate = 0.0125, minAmount = 1, maxAmount = 1, amountOfMats = 0.0025}, -- Obsidium Ore
|
||||
["i:52185"] = {matRate = 0.0450, minAmount = 1, maxAmount = 2, amountOfMats = 0.0091}, -- Elementium Ore
|
||||
["i:52183"] = {matRate = 0.0750, minAmount = 1, maxAmount = 2, amountOfMats = 0.0152}, -- Pyrite Ore
|
||||
},
|
||||
["i:76131"] = { -- Primordial Ruby
|
||||
["i:72092"] = {matRate = 0.0450, minAmount = 1, maxAmount = 2, amountOfMats = 0.0090}, -- Ghost Iron Ore
|
||||
["i:72093"] = {matRate = 0.0450, minAmount = 1, maxAmount = 2, amountOfMats = 0.0090}, -- Kyparite
|
||||
["i:72103"] = {matRate = 0.1650, minAmount = 1, maxAmount = 2, amountOfMats = 0.0340}, -- White Trillium Ore
|
||||
["i:72094"] = {matRate = 0.1650, minAmount = 1, maxAmount = 2, amountOfMats = 0.0340}, -- Black Trillium Ore
|
||||
},
|
||||
["i:76138"] = { -- River's Heart
|
||||
["i:72092"] = {matRate = 0.0450, minAmount = 1, maxAmount = 2, amountOfMats = 0.0090}, -- Ghost Iron Ore
|
||||
["i:72093"] = {matRate = 0.0450, minAmount = 1, maxAmount = 2, amountOfMats = 0.0090}, -- Kyparite
|
||||
["i:72103"] = {matRate = 0.1650, minAmount = 1, maxAmount = 2, amountOfMats = 0.0340}, -- White Trillium Ore
|
||||
["i:72094"] = {matRate = 0.1650, minAmount = 1, maxAmount = 2, amountOfMats = 0.0340}, -- Black Trillium Ore
|
||||
},
|
||||
["i:76139"] = { -- Wild Jade
|
||||
["i:72092"] = {matRate = 0.0450, minAmount = 1, maxAmount = 2, amountOfMats = 0.0090}, -- Ghost Iron Ore
|
||||
["i:72093"] = {matRate = 0.0450, minAmount = 1, maxAmount = 2, amountOfMats = 0.0090}, -- Kyparite
|
||||
["i:72103"] = {matRate = 0.1650, minAmount = 1, maxAmount = 2, amountOfMats = 0.0340}, -- White Trillium Ore
|
||||
["i:72094"] = {matRate = 0.1650, minAmount = 1, maxAmount = 2, amountOfMats = 0.0340}, -- Black Trillium Ore
|
||||
},
|
||||
["i:76140"] = { -- Vermillion Onyx
|
||||
["i:72092"] = {matRate = 0.0450, minAmount = 1, maxAmount = 2, amountOfMats = 0.0090}, -- Ghost Iron Ore
|
||||
["i:72093"] = {matRate = 0.0450, minAmount = 1, maxAmount = 2, amountOfMats = 0.0090}, -- Kyparite
|
||||
["i:72103"] = {matRate = 0.1650, minAmount = 1, maxAmount = 2, amountOfMats = 0.0340}, -- White Trillium Ore
|
||||
["i:72094"] = {matRate = 0.1650, minAmount = 1, maxAmount = 2, amountOfMats = 0.0340}, -- Black Trillium Ore
|
||||
},
|
||||
["i:76141"] = { -- Imperial Amethyst
|
||||
["i:72092"] = {matRate = 0.0450, minAmount = 1, maxAmount = 2, amountOfMats = 0.0090}, -- Ghost Iron Ore
|
||||
["i:72093"] = {matRate = 0.0450, minAmount = 1, maxAmount = 2, amountOfMats = 0.0090}, -- Kyparite
|
||||
["i:72103"] = {matRate = 0.1650, minAmount = 1, maxAmount = 2, amountOfMats = 0.0340}, -- White Trillium Ore
|
||||
["i:72094"] = {matRate = 0.1650, minAmount = 1, maxAmount = 2, amountOfMats = 0.0340}, -- Black Trillium Ore
|
||||
},
|
||||
["i:76142"] = { -- Sun's Radiance
|
||||
["i:72092"] = {matRate = 0.0450, minAmount = 1, maxAmount = 2, amountOfMats = 0.0090}, -- Ghost Iron Ore
|
||||
["i:72093"] = {matRate = 0.0450, minAmount = 1, maxAmount = 2, amountOfMats = 0.0090}, -- Kyparite
|
||||
["i:72103"] = {matRate = 0.1650, minAmount = 1, maxAmount = 2, amountOfMats = 0.0340}, -- White Trillium Ore
|
||||
["i:72094"] = {matRate = 0.1650, minAmount = 1, maxAmount = 2, amountOfMats = 0.0340}, -- Black Trillium Ore
|
||||
},
|
||||
["i:130179"] = { -- Eye of Prophecy
|
||||
["i:123918"] = {matRate = 0.0125, minAmount = 1, maxAmount = 1, amountOfMats = 0.0025}, -- Leystone Ore
|
||||
["i:123919"] = {matRate = 0.0125, minAmount = 2, maxAmount = 5, amountOfMats = 0.0083}, -- Felslate
|
||||
},
|
||||
["i:130180"] = { -- Dawnlight
|
||||
["i:123918"] = {matRate = 0.0125, minAmount = 1, maxAmount = 1, amountOfMats = 0.0025}, -- Leystone Ore
|
||||
["i:123919"] = {matRate = 0.0125, minAmount = 2, maxAmount = 5, amountOfMats = 0.0083}, -- Felslate
|
||||
},
|
||||
["i:130182"] = { -- Maelstrom Sapphire
|
||||
["i:123918"] = {matRate = 0.0125, minAmount = 1, maxAmount = 1, amountOfMats = 0.0025}, -- Leystone Ore
|
||||
["i:123919"] = {matRate = 0.0125, minAmount = 2, maxAmount = 5, amountOfMats = 0.0083}, -- Felslate
|
||||
},
|
||||
["i:130183"] = { -- Shadowruby
|
||||
["i:123918"] = {matRate = 0.0125, minAmount = 1, maxAmount = 1, amountOfMats = 0.0025}, -- Leystone Ore
|
||||
["i:123919"] = {matRate = 0.0125, minAmount = 2, maxAmount = 5, amountOfMats = 0.0083}, -- Felslate
|
||||
},
|
||||
["i:130178"] = { -- FuryStone
|
||||
["i:123918"] = {matRate = 0.0050, minAmount = 1, maxAmount = 1, amountOfMats = 0.0010}, -- Leystone Ore
|
||||
["i:123919"] = {matRate = 0.0075, minAmount = 2, maxAmount = 5, amountOfMats = 0.0048}, -- Felslate
|
||||
},
|
||||
["i:130181"] = { -- Pandemonite
|
||||
["i:123918"] = {matRate = 0.0075, minAmount = 1, maxAmount = 1, amountOfMats = 0.0015}, -- Leystone Ore
|
||||
["i:123919"] = {matRate = 0.0050, minAmount = 2, maxAmount = 5, amountOfMats = 0.0033}, -- Felslate
|
||||
},
|
||||
["i:154120"] = { -- Owlseye
|
||||
["i:152512"] = {matRate = 0.0450, minAmount = 1, maxAmount = 2, amountOfMats = 0.0092}, -- Monelite Ore
|
||||
["i:152579"] = {matRate = 0.0750, minAmount = 1, maxAmount = 2, amountOfMats = 0.0152}, -- Storm Silver Ore
|
||||
["i:152513"] = {matRate = 0.1150, minAmount = 1, maxAmount = 2, amountOfMats = 0.0237}, -- Platinum Ore
|
||||
},
|
||||
["i:154121"] = { -- Scarlet Diamond
|
||||
["i:152512"] = {matRate = 0.0450, minAmount = 1, maxAmount = 2, amountOfMats = 0.0092}, -- Monelite Ore
|
||||
["i:152579"] = {matRate = 0.0750, minAmount = 1, maxAmount = 2, amountOfMats = 0.0152}, -- Storm Silver Ore
|
||||
["i:152513"] = {matRate = 0.1150, minAmount = 1, maxAmount = 2, amountOfMats = 0.0237}, -- Platinum Ore
|
||||
},
|
||||
["i:154122"] = { -- Tidal Amethyst
|
||||
["i:152512"] = {matRate = 0.0450, minAmount = 1, maxAmount = 2, amountOfMats = 0.0092}, -- Monelite Ore
|
||||
["i:152579"] = {matRate = 0.0750, minAmount = 1, maxAmount = 2, amountOfMats = 0.0152}, -- Storm Silver Ore
|
||||
["i:152513"] = {matRate = 0.1150, minAmount = 1, maxAmount = 2, amountOfMats = 0.0237}, -- Platinum Ore
|
||||
},
|
||||
["i:154123"] = { -- Amberblaze
|
||||
["i:152512"] = {matRate = 0.0450, minAmount = 1, maxAmount = 2, amountOfMats = 0.0092}, -- Monelite Ore
|
||||
["i:152579"] = {matRate = 0.0750, minAmount = 1, maxAmount = 2, amountOfMats = 0.0152}, -- Storm Silver Ore
|
||||
["i:152513"] = {matRate = 0.1150, minAmount = 1, maxAmount = 2, amountOfMats = 0.0237}, -- Platinum Ore
|
||||
},
|
||||
["i:154124"] = { -- Laribole
|
||||
["i:152512"] = {matRate = 0.0450, minAmount = 1, maxAmount = 2, amountOfMats = 0.0092}, -- Monelite Ore
|
||||
["i:152579"] = {matRate = 0.0750, minAmount = 1, maxAmount = 2, amountOfMats = 0.0152}, -- Storm Silver Ore
|
||||
["i:152513"] = {matRate = 0.1150, minAmount = 1, maxAmount = 2, amountOfMats = 0.0237}, -- Platinum Ore
|
||||
},
|
||||
["i:154125"] = { -- Royal Quartz
|
||||
["i:152512"] = {matRate = 0.0450, minAmount = 1, maxAmount = 2, amountOfMats = 0.0092}, -- Monelite Ore
|
||||
["i:152579"] = {matRate = 0.0750, minAmount = 1, maxAmount = 2, amountOfMats = 0.0152}, -- Storm Silver Ore
|
||||
["i:152513"] = {matRate = 0.1150, minAmount = 1, maxAmount = 2, amountOfMats = 0.0237}, -- Platinum Ore
|
||||
},
|
||||
-- ========================================== Epic Gems ========================================
|
||||
["i:36931"] = { -- Ametrine
|
||||
["i:36910"] = {matRate = 0.0500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0100}, -- Titanium Ore
|
||||
},
|
||||
["i:36919"] = { -- Cardinal Ruby
|
||||
["i:36910"] = {matRate = 0.0500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0100}, -- Titanium Ore
|
||||
},
|
||||
["i:36928"] = { -- Dreadstone
|
||||
["i:36910"] = {matRate = 0.0500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0100}, -- Titanium Ore
|
||||
},
|
||||
["i:36934"] = { -- Eye of Zul
|
||||
["i:36910"] = {matRate = 0.0500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0100}, -- Titanium Ore
|
||||
},
|
||||
["i:36922"] = { -- King's Amber
|
||||
["i:36910"] = {matRate = 0.0500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0100}, -- Titanium Ore
|
||||
},
|
||||
["i:36925"] = { -- Majestic Zircon
|
||||
["i:36910"] = {matRate = 0.0500, minAmount = 1, maxAmount = 2, amountOfMats = 0.0100}, -- Titanium Ore
|
||||
},
|
||||
["i:151719"] = { -- Lightsphene
|
||||
["i:151564"] = {matRate = 0.0300, minAmount = 1, maxAmount = 1, amountOfMats = 0.0060}, -- Empyrium
|
||||
},
|
||||
["i:151720"] = { -- Chemirine
|
||||
["i:151564"] = {matRate = 0.0300, minAmount = 1, maxAmount = 1, amountOfMats = 0.0060}, -- Empyrium
|
||||
},
|
||||
["i:151722"] = { -- Florid Malachite
|
||||
["i:151564"] = {matRate = 0.0300, minAmount = 1, maxAmount = 1, amountOfMats = 0.0060}, -- Empyrium
|
||||
},
|
||||
["i:151721"] = { -- Hesselian
|
||||
["i:151564"] = {matRate = 0.0300, minAmount = 1, maxAmount = 1, amountOfMats = 0.0060}, -- Empyrium
|
||||
},
|
||||
["i:151718"] = { -- Argulite
|
||||
["i:151564"] = {matRate = 0.0250, minAmount = 1, maxAmount = 1, amountOfMats = 0.0050}, -- Empyrium
|
||||
},
|
||||
["i:151579"] = { -- Labradorite
|
||||
["i:151564"] = {matRate = 0.0225, minAmount = 1, maxAmount = 1, amountOfMats = 0.0045}, -- Empyrium
|
||||
},
|
||||
["i:153706"] = { -- Kraken's Eye
|
||||
["i:152512"] = {matRate = 0.0400, minAmount = 1, maxAmount = 1, amountOfMats = 0.0080}, -- Monelite Ore
|
||||
["i:152579"] = {matRate = 0.0375, minAmount = 1, maxAmount = 1, amountOfMats = 0.0075}, -- Storm Silver Ore
|
||||
["i:152513"] = {matRate = 0.0450, minAmount = 1, maxAmount = 1, amountOfMats = 0.0090}, -- Platinum Ore
|
||||
},
|
||||
["i:168188"] = { -- Sage Agate
|
||||
["i:168185"] = {matRate = 0.1500, minAmount = 1, maxAmount = 1, amountOfMats = 0.0300}, -- Osmenite Ore
|
||||
},
|
||||
["i:168193"] = { -- Azsharine
|
||||
["i:168185"] = {matRate = 0.1500, minAmount = 1, maxAmount = 1, amountOfMats = 0.0300}, -- Osmenite Ore
|
||||
},
|
||||
["i:168189"] = { -- Dark Opal
|
||||
["i:168185"] = {matRate = 0.1500, minAmount = 1, maxAmount = 1, amountOfMats = 0.0300}, -- Osmenite Ore
|
||||
},
|
||||
["i:168190"] = { -- Lava Lazuli
|
||||
["i:168185"] = {matRate = 0.1500, minAmount = 1, maxAmount = 1, amountOfMats = 0.0300}, -- Osmenite Ore
|
||||
},
|
||||
["i:168191"] = { -- Sea Currant
|
||||
["i:168185"] = {matRate = 0.1500, minAmount = 1, maxAmount = 1, amountOfMats = 0.0300}, -- Osmenite Ore
|
||||
},
|
||||
["i:168192"] = { -- Sand Spinel
|
||||
["i:168185"] = {matRate = 0.1500, minAmount = 1, maxAmount = 1, amountOfMats = 0.0300}, -- Osmenite Ore
|
||||
},
|
||||
["i:168635"] = { -- Leviathan's Eye
|
||||
["i:168185"] = {matRate = 0.1000, minAmount = 1, maxAmount = 1, amountOfMats = 0.0200}, -- Osmenite Ore
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Prospect.TargetItemIterator()
|
||||
return private.TableKeyIterator, DATA, nil
|
||||
end
|
||||
|
||||
function Prospect.SourceItemIterator(targetItemString)
|
||||
return private.TableKeyIterator, DATA[targetItemString], nil
|
||||
end
|
||||
|
||||
function Prospect.GetRate(targetItemString, sourceItemString)
|
||||
return DATA[targetItemString][sourceItemString].amountOfMats, DATA[targetItemString][sourceItemString].matRate, DATA[targetItemString][sourceItemString].minAmount, DATA[targetItemString][sourceItemString].maxAmount
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.TableKeyIterator(tbl, index)
|
||||
index = next(tbl, index)
|
||||
return index
|
||||
end
|
||||
331
LibTSM/Data/Transform.lua
Normal file
331
LibTSM/Data/Transform.lua
Normal file
@@ -0,0 +1,331 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Transform = TSM.Init("Data.Transform")
|
||||
local private = {}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Transform Data
|
||||
-- ============================================================================
|
||||
|
||||
local DATA = {
|
||||
-- ================================== Essences / Illusion Dust =================================
|
||||
["i:52719"] = {
|
||||
["i:52718"] = 1/3, -- Greater Celestial Essence
|
||||
},
|
||||
["i:52718"] = {
|
||||
["i:52719"] = 3, -- Lesser Celestial Essence
|
||||
},
|
||||
["i:34055"] = {
|
||||
["i:34056"] = 1/3, -- Greater Cosmic Essence
|
||||
},
|
||||
["i:34056"] = {
|
||||
["i:34055"] = 3, -- Lesser Cosmic Essence
|
||||
},
|
||||
["i:22446"] = {
|
||||
["i:22447"] = 1/3, -- Greater Planar Essence
|
||||
},
|
||||
["i:22447"] = {
|
||||
["i:22446"] = 3, -- Lesser Planar Essence
|
||||
},
|
||||
["i:16203"] = {
|
||||
["i:16202"] = 1/3, -- Greater Eternal Essence
|
||||
},
|
||||
["i:16202"] = {
|
||||
["i:16203"] = 3, -- Lesser Eternal Essence
|
||||
},
|
||||
["i:11175"] = TSM.IsWowClassic() and {
|
||||
["i:11174"] = 1/3, -- Greater Nether Essence
|
||||
} or nil,
|
||||
["i:11174"] = TSM.IsWowClassic() and {
|
||||
["i:11175"] = 3, -- Lesser Nether Essence
|
||||
} or nil,
|
||||
["i:11135"] = TSM.IsWowClassic() and {
|
||||
["i:11134"] = 1/3, -- Greater Mystic Essence
|
||||
} or nil,
|
||||
["i:11134"] = TSM.IsWowClassic() and {
|
||||
["i:11135"] = 3, -- Lesser Mystic Essence
|
||||
} or nil,
|
||||
["i:11082"] = TSM.IsWowClassic() and {
|
||||
["i:10998"] = 1/3, -- Greater Astral Essence
|
||||
} or nil,
|
||||
["i:10998"] = TSM.IsWowClassic() and {
|
||||
["i:11082"] = 3, -- Lesser Astral Essence
|
||||
} or nil,
|
||||
["i:10939"] = {
|
||||
["i:10938"] = 1/3, -- Greater Magic Essence
|
||||
},
|
||||
["i:10938"] = {
|
||||
["i:10939"] = 3, -- Lesser Magic Essence
|
||||
},
|
||||
["i:16204"] = {
|
||||
["i:156930"] = 1/3, -- Rich Illusion Dust
|
||||
},
|
||||
["i:156930"] = {
|
||||
["i:16204"] = 3, -- Light Illusion Dust
|
||||
},
|
||||
-- ============================================ Shards =========================================
|
||||
["i:52721"] = {
|
||||
["i:52720"] = 1/3, -- Heavenly Shard
|
||||
},
|
||||
["i:34052"] = {
|
||||
["i:34053"] = 1/3, -- Dream Shard
|
||||
},
|
||||
["i:74247"] = {
|
||||
["i:74252"] = 1/3, -- Ethereal Shard
|
||||
},
|
||||
["i:111245"] = {
|
||||
["i:115502"] = 0.1, -- Luminous Shard
|
||||
},
|
||||
-- =========================================== Crystals ========================================
|
||||
["i:113588"] = {
|
||||
["i:115504"] = 0.1, -- Temporal Crystal
|
||||
},
|
||||
-- ======================================== Primals / Motes ====================================
|
||||
["i:21885"] = {
|
||||
["i:22578"] = 0.1, -- Water
|
||||
},
|
||||
["i:22456"] = {
|
||||
["i:22577"] = 0.1, -- Shadow
|
||||
},
|
||||
["i:22457"] = {
|
||||
["i:22576"] = 0.1, -- Mana
|
||||
},
|
||||
["i:21886"] = {
|
||||
["i:22575"] = 0.1, -- Life
|
||||
},
|
||||
["i:21884"] = {
|
||||
["i:22574"] = 0.1, -- Fire
|
||||
},
|
||||
["i:22452"] = {
|
||||
["i:22573"] = 0.1, -- Earth
|
||||
},
|
||||
["i:22451"] = {
|
||||
["i:22572"] = 0.1, -- Air
|
||||
},
|
||||
-- ===================================== Crystalized / Eternal =================================
|
||||
["i:37700"] = {
|
||||
["i:35623"] = 10, -- Air
|
||||
},
|
||||
["i:35623"] = {
|
||||
["i:37700"] = 0.1, -- Air
|
||||
},
|
||||
["i:37701"] = {
|
||||
["i:35624"] = 10, -- Earth
|
||||
},
|
||||
["i:35624"] = {
|
||||
["i:37701"] = 0.1, -- Earth
|
||||
},
|
||||
["i:37702"] = {
|
||||
["i:36860"] = 10, -- Fire
|
||||
},
|
||||
["i:36860"] = {
|
||||
["i:37702"] = 0.1, -- Fire
|
||||
},
|
||||
["i:37703"] = {
|
||||
["i:35627"] = 10, -- Shadow
|
||||
},
|
||||
["i:35627"] = {
|
||||
["i:37703"] = 0.1, -- Shadow
|
||||
},
|
||||
["i:37704"] = {
|
||||
["i:35625"] = 10, -- Life
|
||||
},
|
||||
["i:35625"] = {
|
||||
["i:37704"] = 0.1, -- Life
|
||||
},
|
||||
["i:37705"] = {
|
||||
["i:35622"] = 10, -- Water
|
||||
},
|
||||
["i:35622"] = {
|
||||
["i:37705"] = 0.1, -- Water
|
||||
},
|
||||
-- ========================================= Wod Fish ==========================================
|
||||
["i:109137"] = {
|
||||
["i:111601"] = 4, -- Enormous Crescent Saberfish
|
||||
["i:111595"] = 2, -- Crescent Saberfish
|
||||
["i:111589"] = 1, -- Small Crescent Saberfish
|
||||
},
|
||||
["i:109138"] = {
|
||||
["i:111676"] = 4, -- Enormous Jawless Skulker
|
||||
["i:111669"] = 2, -- Jawless Skulker
|
||||
["i:111650"] = 1, -- Small Jawless Skulker
|
||||
},
|
||||
["i:109139"] = {
|
||||
["i:111675"] = 4, -- Enormous Fat Sleeper
|
||||
["i:111668"] = 2, -- Fat Sleeper
|
||||
["i:111651"] = 1, -- Small Fat Sleeper
|
||||
},
|
||||
["i:109140"] = {
|
||||
["i:111674"] = 4, -- Enormous Blind Lake Sturgeon
|
||||
["i:111667"] = 2, -- Blind Lake Sturgeon
|
||||
["i:111652"] = 1, -- Small Blind Lake Sturgeon
|
||||
},
|
||||
["i:109141"] = {
|
||||
["i:111673"] = 4, -- Enormous Fire Ammonite
|
||||
["i:111666"] = 2, -- Fire Ammonite
|
||||
["i:111656"] = 1, -- Small Fire Ammonite
|
||||
},
|
||||
["i:109142"] = {
|
||||
["i:111672"] = 4, -- Enormous Sea Scorpion
|
||||
["i:111665"] = 2, -- Sea Scorpion
|
||||
["i:111658"] = 1, -- Small Sea Scorpion
|
||||
},
|
||||
["i:109143"] = {
|
||||
["i:111671"] = 4, -- Enormous Abyssal Gulper Eel
|
||||
["i:111664"] = 2, -- Abyssal Gulper Eel
|
||||
["i:111659"] = 1, -- Small Abyssal Gulper Eel
|
||||
},
|
||||
["i:109144"] = {
|
||||
["i:111670"] = 4, -- Enormous Blackwater Whiptail
|
||||
["i:111663"] = 2, -- Blackwater Whiptail
|
||||
["i:111662"] = 1, -- Small Blackwater Whiptail
|
||||
},
|
||||
-- ========================================== Aromatic Fish Oil (BFA) ===========================
|
||||
["i:160711"] = {
|
||||
["i:152543"] = 1, -- Sand Shifter
|
||||
["i:152544"] = 1, -- Slimy Mackerel
|
||||
["i:152545"] = 1, -- Frenzied Fangtooth
|
||||
["i:152546"] = 1, -- Lane Snapper
|
||||
["i:152547"] = 1, -- Great Sea Catfish
|
||||
["i:152548"] = 1, -- Tiragarde Perch
|
||||
["i:152549"] = 1, -- Redtail Loach
|
||||
["i:168302"] = 1, -- Viper Fish
|
||||
["i:168646"] = 1, -- Mauve Stinger
|
||||
["i:174327"] = 1, -- Malformed Gnasher
|
||||
["i:174328"] = 1, -- Aberrant Voidfin
|
||||
},
|
||||
-- ========================================== Ore Nuggets =======================================
|
||||
["i:2771"] = {
|
||||
["i:108295"] = 0.1, -- Tin Ore
|
||||
},
|
||||
["i:2772"] = {
|
||||
["i:108297"] = 0.1, -- Iron Ore
|
||||
},
|
||||
["i:2775"] = {
|
||||
["i:108294"] = 0.1, -- Silver Ore
|
||||
},
|
||||
["i:2776"] = {
|
||||
["i:108296"] = 0.1, -- Gold Ore
|
||||
},
|
||||
["i:3858"] = {
|
||||
["i:108300"] = 0.1, -- Mithril Ore
|
||||
},
|
||||
["i:7911"] = {
|
||||
["i:108299"] = 0.1, -- Truesilver Ore
|
||||
},
|
||||
["i:10620"] = {
|
||||
["i:108298"] = 0.1, -- Thorium Ore
|
||||
},
|
||||
["i:23424"] = {
|
||||
["i:108301"] = 0.1, -- Fel Iron Ore
|
||||
},
|
||||
["i:23425"] = {
|
||||
["i:108302"] = 0.1, -- Adamantite Ore
|
||||
},
|
||||
["i:23426"] = {
|
||||
["i:108304"] = 0.1, -- Khorium Ore
|
||||
},
|
||||
["i:23427"] = {
|
||||
["i:108303"] = 0.1, -- Eternium Ore
|
||||
},
|
||||
["i:36909"] = {
|
||||
["i:108305"] = 0.1, -- Cobalt Ore
|
||||
},
|
||||
["i:36910"] = {
|
||||
["i:108391"] = 0.1, -- Titanium Ore
|
||||
},
|
||||
["i:36912"] = {
|
||||
["i:108306"] = 0.1, -- Saronite Ore
|
||||
},
|
||||
["i:52183"] = {
|
||||
["i:108309"] = 0.1, -- Pyrite Ore
|
||||
},
|
||||
["i:52185"] = {
|
||||
["i:108308"] = 0.1, -- Elementium Ore
|
||||
},
|
||||
["i:53038"] = {
|
||||
["i:108307"] = 0.1, -- Obsidium Ore
|
||||
},
|
||||
["i:72092"] = {
|
||||
["i:97512"] = 0.1, -- Ghost Iron Ore
|
||||
},
|
||||
["i:109119"] = {
|
||||
["i:109991"] = 0.1, -- True Iron Ore
|
||||
},
|
||||
-- =========================================== Herb Parts ======================================
|
||||
["i:2449"] = {
|
||||
["i:108319"] = 0.1, -- Earthroot Stem
|
||||
},
|
||||
["i:2453"] = {
|
||||
["i:108322"] = 0.1, -- Bruiseweed Stem
|
||||
},
|
||||
["i:3357"] = {
|
||||
["i:108325"] = 0.1, -- Liferoot Stem
|
||||
},
|
||||
["i:3358"] = {
|
||||
["i:108326"] = 0.1, -- Khadgar's Whisker Stem
|
||||
},
|
||||
["i:3819"] = {
|
||||
["i:108329"] = 0.1, -- Dragon's Teeth Stem
|
||||
},
|
||||
["i:8839"] = {
|
||||
["i:108336"] = 0.1, -- Blindweed Stem
|
||||
},
|
||||
["i:22792"] = {
|
||||
["i:108350"] = 0.1, -- Nightmare Vine Stem
|
||||
},
|
||||
["i:36903"] = {
|
||||
["i:108353"] = 0.1, -- Adder's Tongue Stem
|
||||
},
|
||||
["i:52985"] = {
|
||||
["i:108362"] = 0.1, -- Azshara's Veil Stem
|
||||
},
|
||||
["i:52988"] = {
|
||||
["i:108365"] = 0.1, -- Whiptail Stem
|
||||
},
|
||||
["i:72235"] = {
|
||||
["i:97621"] = 0.1, -- Silkweed Stem
|
||||
},
|
||||
["i:109124"] = {
|
||||
["i:109624"] = 0.1, -- Broken Frostweed Stem
|
||||
},
|
||||
["i:109125"] = {
|
||||
["i:109625"] = 0.1, -- Broken Fireweed Stem
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Transform.TargetItemIterator()
|
||||
return private.TableKeyIterator, DATA, nil
|
||||
end
|
||||
|
||||
function Transform.SourceItemIterator(targetItemString)
|
||||
return private.TableKeyIterator, DATA[targetItemString], nil
|
||||
end
|
||||
|
||||
function Transform.GetRate(targetItemString, sourceItemString)
|
||||
return DATA[targetItemString][sourceItemString]
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.TableKeyIterator(tbl, index)
|
||||
index = next(tbl, index)
|
||||
return index
|
||||
end
|
||||
215
LibTSM/Data/VendorSell.lua
Normal file
215
LibTSM/Data/VendorSell.lua
Normal file
@@ -0,0 +1,215 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local VendorSell = TSM.Init("Data.VendorSell")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Vendor Sell Data
|
||||
-- ============================================================================
|
||||
|
||||
-- Scraped from Wowhead using the following javascript and then manually pruned to remove limited quantity items and fill in missing prices:
|
||||
-- x = listviewitems.sort((a,b) => a.id - b.id); for (i=0; i<x.length; i++) console.log("[\"i:"+x[i].id+"\"] = "+x[i].cost[0]+", -- "+x[i].name);
|
||||
local VENDOR_SELL_PRICES = TSM.IsWowClassic() and {
|
||||
-- https://classic.wowhead.com/items/side:3?filter=92:87:186:86;1:11:1:12;0:0:0:0
|
||||
["i:159"] = 25, -- Refreshing Spring Water
|
||||
["i:1179"] = 125, -- Ice Cold Milk
|
||||
["i:2320"] = 10, -- Coarse Thread
|
||||
["i:2321"] = 100, -- Fine Thread
|
||||
["i:2324"] = 25, -- Bleach
|
||||
["i:2325"] = 1000, -- Black Dye
|
||||
["i:2596"] = 120, -- Skin of Dwarven Stout
|
||||
["i:2604"] = 50, -- Red Dye
|
||||
["i:2605"] = 100, -- Green Dye
|
||||
["i:2665"] = 20, -- Stormwind Seasoning Herbs
|
||||
["i:2678"] = 10, -- Mild Spices
|
||||
["i:2692"] = 40, -- Hot Spices
|
||||
["i:2880"] = 100, -- Weak Flux
|
||||
["i:2894"] = 50, -- Rhapsody Malt
|
||||
["i:3371"] = 20, -- Empty Vial
|
||||
["i:3372"] = 200, -- Leaded Vial
|
||||
["i:3466"] = 2000, -- Strong Flux
|
||||
["i:3713"] = 160, -- Soothing Spices
|
||||
["i:3857"] = 500, -- Coal
|
||||
["i:4289"] = 50, -- Salt
|
||||
["i:4291"] = 500, -- Silken Thread
|
||||
["i:4340"] = 350, -- Gray Dye
|
||||
["i:4341"] = 500, -- Yellow Dye
|
||||
["i:4342"] = 2500, -- Purple Dye
|
||||
["i:4399"] = 200, -- Wooden Stock
|
||||
["i:4400"] = 2000, -- Heavy Stock
|
||||
["i:4470"] = 38, -- Simple Wood
|
||||
["i:4536"] = 25, -- Shiny Red Apple
|
||||
["i:5140"] = 25, -- Flash Powder
|
||||
["i:6217"] = 124, -- Copper Rod
|
||||
["i:6260"] = 50, -- Blue Dye
|
||||
["i:6261"] = 1000, -- Orange Dye
|
||||
["i:6530"] = 100, -- Nightcrawlers
|
||||
["i:8343"] = 2000, -- Heavy Silken Thread
|
||||
["i:8925"] = 2500, -- Crystal Vial
|
||||
["i:10290"] = 2500, -- Pink Dye
|
||||
["i:10647"] = 2000, -- Engineer's Ink
|
||||
["i:10648"] = 500, -- Blank Parchment
|
||||
["i:11291"] = 4500, -- Star Wood
|
||||
["i:14341"] = 5000, -- Rune Thread
|
||||
["i:16583"] = 10000, -- Demonic Figurine
|
||||
["i:17020"] = 1000, -- Arcane Powder
|
||||
["i:17021"] = 700, -- Wild Berries
|
||||
["i:17026"] = 1000, -- Wild Thornroot
|
||||
["i:17028"] = 700, -- Holy Candle
|
||||
["i:17029"] = 1000, -- Sacred Candle
|
||||
["i:17030"] = 2000, -- Ankh
|
||||
["i:17031"] = 1000, -- Rune of Teleportation
|
||||
["i:17032"] = 2000, -- Rune of Portals
|
||||
["i:17033"] = 2000, -- Symbol of Divinity
|
||||
["i:17034"] = 200, -- Maple Seed
|
||||
["i:17035"] = 400, -- Stranglethorn Seed
|
||||
["i:17036"] = 800, -- Ashwood Seed
|
||||
["i:17037"] = 1400, -- Hornbeam Seed
|
||||
["i:17038"] = 2000, -- Ironwood Seed
|
||||
["i:17194"] = 10, -- Holiday Spices
|
||||
["i:17196"] = 50, -- Holiday Spirits
|
||||
["i:17202"] = 10, -- Snowball
|
||||
["i:18256"] = 30000, -- Imbued Vial
|
||||
["i:18567"] = 150000, -- Elemental Flux
|
||||
["i:21177"] = 3000, -- Symbol of Kings
|
||||
} or {
|
||||
-- https://www.wowhead.com/items/side:3/live-only:on?filter=92:87:186:86;1:11:1:12;0:0:0:0
|
||||
["i:159"] = 25, -- Refreshing Spring Water
|
||||
["i:1179"] = 125, -- Ice Cold Milk
|
||||
["i:2320"] = 10, -- Coarse Thread
|
||||
["i:2321"] = 100, -- Fine Thread
|
||||
["i:2324"] = 25, -- Bleach
|
||||
["i:2325"] = 1000, -- Black Dye
|
||||
["i:2593"] = 150, -- Flask of Stormwind Tawny
|
||||
["i:2594"] = 1500, -- Flagon of Dwarven Mead
|
||||
["i:2595"] = 2000, -- Jug of Badlands Bourbon
|
||||
["i:2596"] = 120, -- Skin of Dwarven Stout
|
||||
["i:2604"] = 50, -- Red Dye
|
||||
["i:2605"] = 100, -- Green Dye
|
||||
["i:2678"] = 10, -- Mild Spices
|
||||
["i:2880"] = 100, -- Weak Flux
|
||||
["i:2901"] = 81, -- Mining Pick
|
||||
["i:3371"] = 150, -- Crystal Vial
|
||||
["i:3466"] = 2000, -- Strong Flux
|
||||
["i:3857"] = 500, -- Coal
|
||||
["i:4289"] = 50, -- Salt
|
||||
["i:4291"] = 500, -- Silken Thread
|
||||
["i:4340"] = 350, -- Gray Dye
|
||||
["i:4341"] = 500, -- Yellow Dye
|
||||
["i:4342"] = 2500, -- Purple Dye
|
||||
["i:4399"] = 200, -- Wooden Stock
|
||||
["i:4400"] = 2000, -- Heavy Stock
|
||||
["i:4470"] = 38, -- Simple Wood
|
||||
["i:4537"] = 125, -- Tel'Abim Banana
|
||||
["i:5956"] = 18, -- Blacksmith Hammer
|
||||
["i:6217"] = 124, -- Copper Rod
|
||||
["i:6260"] = 50, -- Blue Dye
|
||||
["i:6261"] = 1000, -- Orange Dye
|
||||
["i:6530"] = 100, -- Nightcrawlers
|
||||
["i:7005"] = 82, -- Skinning Knife
|
||||
["i:8343"] = 2000, -- Heavy Silken Thread
|
||||
["i:10290"] = 2500, -- Pink Dye
|
||||
["i:10647"] = 2000, -- Engineer's Ink
|
||||
["i:11291"] = 4500, -- Star Wood
|
||||
["i:14341"] = 5000, -- Rune Thread
|
||||
["i:17194"] = 10, -- Holiday Spices
|
||||
["i:17196"] = 50, -- Holiday Spirits
|
||||
["i:17202"] = 10, -- Snowball
|
||||
["i:18567"] = 30000, -- Elemental Flux
|
||||
["i:23572"] = 500000, -- Primal Nether
|
||||
["i:27860"] = 6400, -- Purified Draenic Water
|
||||
["i:30183"] = 700000, -- Nether Vortex
|
||||
["i:30817"] = 25, -- Simple Flour
|
||||
["i:34249"] = 1000000, -- Hula Girl Doll
|
||||
["i:34412"] = 1000, -- Sparkling Apple Cider
|
||||
["i:35948"] = 16000, -- Savory Snowplum
|
||||
["i:35949"] = 8500, -- Tundra Berries
|
||||
["i:38426"] = 30000, -- Eternium Thread
|
||||
["i:39354"] = 15, -- Light Parchment
|
||||
["i:39684"] = 9000, -- Hair Trigger
|
||||
["i:40533"] = 50000, -- Walnut Stock
|
||||
["i:43102"] = 750000, -- Frozen Orb
|
||||
["i:44499"] = 30000000, -- Salvaged Iron Golem Parts
|
||||
["i:44500"] = 15000000, -- Elementium-Plated Exhaust Pipe
|
||||
["i:44501"] = 10000000, -- Goblin-Machined Piston
|
||||
["i:44835"] = 10, -- Autumnal Herbs
|
||||
["i:44853"] = 25, -- Honey
|
||||
["i:44854"] = 25, -- Tangy Wetland Cranberries
|
||||
["i:44855"] = 25, -- Teldrassil Sweet Potato
|
||||
["i:46784"] = 25, -- Ripe Elwynn Pumpkin
|
||||
["i:46793"] = 25, -- Tangy Southfury Cranberries
|
||||
["i:46796"] = 25, -- Ripe Tirisfal Pumpkin
|
||||
["i:46797"] = 25, -- Mulgore Sweet Potato
|
||||
["i:49908"] = 1500000, -- Primordial Saronite
|
||||
["i:52188"] = 15000, -- Jeweler's Setting
|
||||
["i:58265"] = 20000, -- Highland Pomegranate
|
||||
["i:58278"] = 16000, -- Tropical Sunfruit
|
||||
["i:62323"] = 60000, -- Deathwing Scale Fragment
|
||||
["i:65892"] = 50000000, -- Pyrium-Laced Crystalline Vial
|
||||
["i:65893"] = 30000000, -- Sands of Time
|
||||
["i:67319"] = 328990, -- Preserved Ogre Eye
|
||||
["i:67335"] = 445561, -- Silver Charm Bracelet
|
||||
["i:74659"] = 30000, -- Farm Chicken
|
||||
["i:74660"] = 15000, -- Pandaren Peach
|
||||
["i:74832"] = 12000, -- Barley
|
||||
["i:74845"] = 35000, -- Ginseng
|
||||
["i:74851"] = 14000, -- Rice
|
||||
["i:74852"] = 16000, -- Yak Milk
|
||||
["i:74854"] = 7000, -- Instant Noodles
|
||||
["i:79740"] = 23, -- Plain Wooden Staff
|
||||
["i:80433"] = 2000000, -- Blood Spirit
|
||||
["i:83092"] = 200000000, -- Orb of Mystery
|
||||
["i:85583"] = 12000, -- Needle Mushrooms
|
||||
["i:85584"] = 17000, -- Silkworm Pupa
|
||||
["i:85585"] = 27000, -- Red Beans
|
||||
["i:102539"] = 5000, -- Fresh Strawberries
|
||||
["i:102540"] = 5000, -- Fresh Mangos
|
||||
["i:124436"] = 40000, -- Foxflower Flux
|
||||
["i:127037"] = 5000, -- Runic Catgut
|
||||
["i:127681"] = 5000, -- Sharp Spritethorn
|
||||
["i:133588"] = 25000, -- Flaked Sea Salt
|
||||
["i:133589"] = 25000, -- Dalapeño Pepper
|
||||
["i:133590"] = 25000, -- Muskenbutter
|
||||
["i:133591"] = 25000, -- River Onion
|
||||
["i:133592"] = 25000, -- Stonedark Snail
|
||||
["i:133593"] = 25000, -- Royal Olive
|
||||
["i:136629"] = 173300, -- Felgibber Shotgun
|
||||
["i:136630"] = 118500, -- "Twirling Bottom" Repeater
|
||||
["i:136631"] = 450000, -- Surface-to-Infernal Rocket Launcher
|
||||
["i:136632"] = 210800, -- Chaos Blaster
|
||||
["i:136633"] = 25000, -- Loose Trigger
|
||||
["i:136636"] = 57500, -- Sniping Scope
|
||||
["i:136637"] = 11500, -- Oversized Blasting Cap
|
||||
["i:136638"] = 89500, -- True Iron Barrel
|
||||
["i:158186"] = 250, -- Distilled Water
|
||||
["i:158205"] = 1000, -- Acacia Powder
|
||||
["i:159959"] = 6000, -- Nylon Thread
|
||||
["i:160059"] = 250, -- Amber Tanning Oil
|
||||
["i:160298"] = 3000, -- Durable Flux
|
||||
["i:160398"] = 25000, -- Choral Honey
|
||||
["i:160399"] = 25000, -- Wild Flour
|
||||
["i:160400"] = 25000, -- Foosaka
|
||||
["i:160502"] = 11500, -- Chemical Blasting Cap
|
||||
["i:160705"] = 50, -- Major's Frothy Coffee
|
||||
["i:160709"] = 25000, -- Fresh Potato
|
||||
["i:160710"] = 25000, -- Wild Berries
|
||||
["i:160712"] = 25000, -- Powdered Sugar
|
||||
["i:161131"] = 300000000, -- Barely Stable Azerite Reactor
|
||||
["i:163569"] = 100, -- Insulated Wiring
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function VendorSell.Iterator()
|
||||
return pairs(VENDOR_SELL_PRICES)
|
||||
end
|
||||
92
LibTSM/Data/VendorTrade.lua
Normal file
92
LibTSM/Data/VendorTrade.lua
Normal file
@@ -0,0 +1,92 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local VendorTrade = TSM.Init("Data.VendorTrade")
|
||||
local private = {}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Vendor Trade Data
|
||||
-- ============================================================================
|
||||
|
||||
local DATA = TSM.IsWowClassic() and {} or {
|
||||
["i:37101"] = {
|
||||
["i:173058"] = 1, -- Ivory Ink
|
||||
},
|
||||
["i:39469"] = {
|
||||
["i:173058"] = 1, -- Moonglow Ink
|
||||
},
|
||||
["i:39774"] = {
|
||||
["i:173058"] = 1, -- Midnight Ink
|
||||
},
|
||||
["i:43116"] = {
|
||||
["i:173058"] = 1, -- Lion's Ink
|
||||
},
|
||||
["i:43118"] = {
|
||||
["i:173058"] = 1, -- Jadefire Ink
|
||||
},
|
||||
["i:43120"] = {
|
||||
["i:173058"] = 1, -- Celestial Ink
|
||||
},
|
||||
["i:43122"] = {
|
||||
["i:173058"] = 1, -- Shimmering Ink
|
||||
},
|
||||
["i:43124"] = {
|
||||
["i:173058"] = 1, -- Ethereal Ink
|
||||
},
|
||||
["i:43126"] = {
|
||||
["i:173058"] = 1, -- Ink of the Sea
|
||||
},
|
||||
["i:43127"] = {
|
||||
["i:173058"] = 0.1, -- Snowfall Ink
|
||||
},
|
||||
["i:61978"] = {
|
||||
["i:173058"] = 1, -- Blackfallow Ink
|
||||
},
|
||||
["i:61981"] = {
|
||||
["i:173058"] = 0.1, -- Inferno Ink
|
||||
},
|
||||
["i:79254"] = {
|
||||
["i:173058"] = 1, -- Ink of Dreams
|
||||
},
|
||||
["i:79255"] = {
|
||||
["i:173058"] = 0.1, -- Starlight Ink
|
||||
},
|
||||
["i:113111"] = {
|
||||
["i:173058"] = 1, -- Warbinder's Ink
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function VendorTrade.TargetItemIterator()
|
||||
return private.TableKeyIterator, DATA, nil
|
||||
end
|
||||
|
||||
function VendorTrade.SourceItemIterator(targetItemString)
|
||||
return private.TableKeyIterator, DATA[targetItemString], nil
|
||||
end
|
||||
|
||||
function VendorTrade.GetRate(targetItemString, sourceItemString)
|
||||
return DATA[targetItemString][sourceItemString]
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.TableKeyIterator(tbl, index)
|
||||
index = next(tbl, index)
|
||||
return index
|
||||
end
|
||||
74
LibTSM/Service/AltTracking.lua
Normal file
74
LibTSM/Service/AltTracking.lua
Normal file
@@ -0,0 +1,74 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local AltTracking = TSM.Init("Service.AltTracking")
|
||||
local Database = TSM.Include("Util.Database")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Vararg = TSM.Include("Util.Vararg")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local private = {
|
||||
quantityDB = nil,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
AltTracking:OnSettingsLoad(function()
|
||||
private.quantityDB = Database.NewSchema("INVENTORY_ALT_QUANTITY")
|
||||
:AddUniqueStringField("itemString")
|
||||
:AddNumberField("quantity")
|
||||
:Commit()
|
||||
|
||||
local altItemQuantity = TempTable.Acquire()
|
||||
for _, factionrealm, character, syncScopeKey in Settings.ConnectedFactionrealmAltCharacterIterator() do
|
||||
for _, key in Vararg.Iterator("bagQuantity", "bankQuantity", "reagentBankQuantity", "auctionQuantity", "mailQuantity") do
|
||||
for itemString, quantity in pairs(Settings.Get("sync", syncScopeKey, "internalData", key)) do
|
||||
altItemQuantity[itemString] = (altItemQuantity[itemString] or 0) + quantity
|
||||
end
|
||||
end
|
||||
local pendingMailLookup = Settings.Get("factionrealm", factionrealm, "internalData", "pendingMail")
|
||||
local pendingMail = pendingMailLookup[character]
|
||||
local isValid = true
|
||||
if pendingMail then
|
||||
for itemString, quantity in pairs(pendingMail) do
|
||||
if type(quantity) ~= "number" then
|
||||
isValid = false
|
||||
break
|
||||
end
|
||||
altItemQuantity[itemString] = (altItemQuantity[itemString] or 0) + quantity
|
||||
end
|
||||
end
|
||||
if not isValid then
|
||||
pendingMailLookup[character] = nil
|
||||
end
|
||||
end
|
||||
private.quantityDB:BulkInsertStart()
|
||||
for itemString, quantity in pairs(altItemQuantity) do
|
||||
private.quantityDB:BulkInsertNewRow(itemString, quantity)
|
||||
end
|
||||
private.quantityDB:BulkInsertEnd()
|
||||
TempTable.Release(altItemQuantity)
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function AltTracking.BaseItemIterator()
|
||||
return private.quantityDB:NewQuery()
|
||||
:Select("itemString")
|
||||
:IteratorAndRelease()
|
||||
end
|
||||
|
||||
function AltTracking.GetQuantityByBaseItemString(baseItemString)
|
||||
return private.quantityDB:GetUniqueRowField("itemString", baseItemString, "quantity") or 0
|
||||
end
|
||||
734
LibTSM/Service/AuctionHouseWrapper.lua
Normal file
734
LibTSM/Service/AuctionHouseWrapper.lua
Normal file
@@ -0,0 +1,734 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local AuctionHouseWrapper = TSM.Init("Service.AuctionHouseWrapper")
|
||||
local LibTSMClass = TSM.Include("LibTSMClass")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local Table = TSM.Include("Util.Table")
|
||||
local Future = TSM.Include("Util.Future")
|
||||
local Vararg = TSM.Include("Util.Vararg")
|
||||
local Analytics = TSM.Include("Util.Analytics")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
local Debug = TSM.Include("Util.Debug")
|
||||
local APIWrapper = LibTSMClass.DefineClass("APIWrapper")
|
||||
local private = {
|
||||
wrappers = {},
|
||||
events = {},
|
||||
argsTemp = {},
|
||||
sortsPartsTemp = {},
|
||||
itemKeyPartsTemp = {},
|
||||
searchQueryAPITimes = {},
|
||||
isAHOpen = false,
|
||||
lastResponseReceived = 0,
|
||||
hookedTime = {},
|
||||
lastAuctionCanceledAuctionId = nil,
|
||||
lastAuctionCanceledTime = 0,
|
||||
auctionIdUpdateCallbacks = {},
|
||||
}
|
||||
local API_TIMEOUT = 5
|
||||
local GET_ALL_TIMEOUT = 30
|
||||
local SEARCH_QUERY_THROTTLE_INTERVAL = 60
|
||||
local SEARCH_QUERY_THROTTLE_MAX = 100
|
||||
local EMPTY_SORTS_TABLE = {}
|
||||
local ITEM_KEY_KEYS = {
|
||||
"itemID",
|
||||
"itemLevel",
|
||||
"itemSuffix",
|
||||
"battlePetSpeciesID",
|
||||
}
|
||||
local SILENT_EVENTS = {
|
||||
AUCTION_ITEM_LIST_UPDATE = true,
|
||||
REPLICATE_ITEM_LIST_UPDATE = true,
|
||||
}
|
||||
local GENERIC_EVENTS = {
|
||||
CHAT_MSG_SYSTEM = 1,
|
||||
UI_ERROR_MESSAGE = 2,
|
||||
}
|
||||
local GENERIC_EVENT_SEP = "/"
|
||||
local API_EVENT_INFO = TSM.IsWowClassic() and
|
||||
{ -- Classic
|
||||
QueryAuctionItems = {
|
||||
AUCTION_ITEM_LIST_UPDATE = { result = true },
|
||||
},
|
||||
} or
|
||||
{ -- Retail
|
||||
SendBrowseQuery = {
|
||||
AUCTION_HOUSE_BROWSE_RESULTS_UPDATED = { result = true },
|
||||
},
|
||||
SearchForFavorites = {
|
||||
AUCTION_HOUSE_BROWSE_RESULTS_UPDATED = { result = true },
|
||||
},
|
||||
SearchForItemKeys = {
|
||||
AUCTION_HOUSE_BROWSE_RESULTS_UPDATED = { result = true },
|
||||
},
|
||||
ReplicateItems = {
|
||||
REPLICATE_ITEM_LIST_UPDATE = { result = true },
|
||||
},
|
||||
RequestMoreBrowseResults = {
|
||||
AUCTION_HOUSE_BROWSE_RESULTS_ADDED = { result = 1 },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_DATABASE_ERROR] = { timeoutChange = 1 },
|
||||
},
|
||||
SendSearchQuery = {
|
||||
COMMODITY_SEARCH_RESULTS_UPDATED = { result = true, eventArgIndex = 1, apiArgIndex = 1, apiArgKey = "itemID" },
|
||||
ITEM_SEARCH_RESULTS_UPDATED = { result = true, eventArgIndex = 1, apiArgIndex = 1 },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_DATABASE_ERROR] = { timeoutChange = 1 },
|
||||
},
|
||||
SendSellSearchQuery = {
|
||||
COMMODITY_SEARCH_RESULTS_UPDATED = { result = true, eventArgIndex = 1, apiArgIndex = 1, apiArgKey = "itemID" },
|
||||
ITEM_SEARCH_RESULTS_UPDATED = { result = true, eventArgIndex = 1, apiArgIndex = 1 },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_DATABASE_ERROR] = { timeoutChange = 1 },
|
||||
},
|
||||
RequestMoreCommoditySearchResults = {
|
||||
COMMODITY_SEARCH_RESULTS_ADDED = { result = true },
|
||||
},
|
||||
RequestMoreItemSearchResults = {
|
||||
ITEM_SEARCH_RESULTS_ADDED = { result = true },
|
||||
},
|
||||
RefreshCommoditySearchResults = {
|
||||
COMMODITY_SEARCH_RESULTS_UPDATED = { result = true },
|
||||
},
|
||||
RefreshItemSearchResults = {
|
||||
ITEM_SEARCH_RESULTS_UPDATED = { result = true },
|
||||
},
|
||||
QueryOwnedAuctions = {
|
||||
OWNED_AUCTIONS_UPDATED = { result = true },
|
||||
},
|
||||
QueryBids = {
|
||||
BIDS_UPDATED = { result = true },
|
||||
},
|
||||
CancelAuction = {
|
||||
AUCTION_CANCELED = { result = true, eventArgIndex = 1, apiArgIndex = 1, compareFunc = function(eventArg, apiArg) return eventArg == 0 or apiArg == eventArg end },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_ITEM_NOT_FOUND] = { result = false },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_NOT_ENOUGH_MONEY] = { result = false },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_DATABASE_ERROR] = { result = false },
|
||||
},
|
||||
StartCommoditiesPurchase = {
|
||||
COMMODITY_PRICE_UPDATED = { result = 2, rawFilterFunc = function(apiArgs, unitPrice, totalPrice) return Math.Ceil((totalPrice / apiArgs[2]), COPPER_PER_SILVER) == apiArgs[3] and true end },
|
||||
COMMODITY_PRICE_UNAVAILABLE = { result = false },
|
||||
},
|
||||
ConfirmCommoditiesPurchase = {
|
||||
COMMODITY_PURCHASE_SUCCEEDED = { result = true },
|
||||
COMMODITY_PURCHASE_FAILED = { result = false },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_DATABASE_ERROR] = { result = false },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_HIGHER_BID] = { result = false },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_ITEM_NOT_FOUND] = { result = false },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_BID_OWN] = { result = false },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_NOT_ENOUGH_MONEY] = { result = false },
|
||||
},
|
||||
PlaceBid = {
|
||||
AUCTION_CANCELED = { result = true, eventArgIndex = 1, apiArgIndex = 1 },
|
||||
["CHAT_MSG_SYSTEM"..GENERIC_EVENT_SEP..ERR_AUCTION_BID_PLACED] = { result = true },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_DATABASE_ERROR] = { result = false },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_HIGHER_BID] = { result = false },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_ITEM_NOT_FOUND] = { result = false },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_BID_OWN] = { result = false },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_NOT_ENOUGH_MONEY] = { result = false },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_ITEM_MAX_COUNT] = { result = false },
|
||||
},
|
||||
PostItem = {
|
||||
AUCTION_HOUSE_AUCTION_CREATED = { result = true, rawFilterFunc = function(apiArgs) return apiArgs[3] <= 1 end },
|
||||
AUCTION_MULTISELL_UPDATE = { result = true, rawFilterFunc = function(apiArgs, createdCount, totalToCreate) return createdCount == totalToCreate end },
|
||||
AUCTION_MULTISELL_FAILURE = { result = false },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_ITEM_NOT_FOUND] = { result = false },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_DATABASE_ERROR] = { result = false },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_REPAIR_ITEM] = { result = nil },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_LIMITED_DURATION_ITEM] = { result = nil },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_USED_CHARGES] = { result = nil },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_WRAPPED_ITEM] = { result = nil },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_BAG] = { result = nil },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_NOT_ENOUGH_MONEY] = { result = nil },
|
||||
},
|
||||
PostCommodity = {
|
||||
AUCTION_HOUSE_AUCTION_CREATED = { result = true },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_ITEM_NOT_FOUND] = { result = false },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_DATABASE_ERROR] = { result = false },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_REPAIR_ITEM] = { result = nil },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_LIMITED_DURATION_ITEM] = { result = nil },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_USED_CHARGES] = { result = nil },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_WRAPPED_ITEM] = { result = nil },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_BAG] = { result = nil },
|
||||
["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_NOT_ENOUGH_MONEY] = { result = nil },
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
AuctionHouseWrapper:OnModuleLoad(function()
|
||||
Event.Register("AUCTION_HOUSE_SHOW", private.AuctionHouseShowHandler)
|
||||
Event.Register("AUCTION_HOUSE_CLOSED", private.AuctionHouseClosedHandler)
|
||||
|
||||
-- setup wrappers
|
||||
for apiName in pairs(API_EVENT_INFO) do
|
||||
private.wrappers[apiName] = APIWrapper(apiName)
|
||||
end
|
||||
|
||||
if not TSM.IsWowClassic() then
|
||||
-- extra hooks to track search query calls since they are limited
|
||||
hooksecurefunc(C_AuctionHouse, "SendSearchQuery", function()
|
||||
tinsert(private.searchQueryAPITimes, GetTime())
|
||||
end)
|
||||
hooksecurefunc(C_AuctionHouse, "SendSellSearchQuery", function()
|
||||
tinsert(private.searchQueryAPITimes, GetTime())
|
||||
end)
|
||||
|
||||
-- events to track auction purchases
|
||||
Event.Register("AUCTION_CANCELED", private.AuctionCanceledHandler)
|
||||
Event.Register("ITEM_SEARCH_RESULTS_UPDATED", private.ItemSearchResultsUpdated)
|
||||
|
||||
-- general events
|
||||
Event.Register("AUCTION_HOUSE_THROTTLED_MESSAGE_RESPONSE_RECEIVED", private.ResponseReceivedHandler)
|
||||
|
||||
-- extra events that are interesting to log
|
||||
Event.Register("AUCTION_HOUSE_NEW_RESULTS_RECEIVED", private.UnusedEventHandler)
|
||||
Event.Register("AUCTION_HOUSE_THROTTLED_MESSAGE_DROPPED", private.UnusedEventHandler)
|
||||
Event.Register("AUCTION_HOUSE_THROTTLED_MESSAGE_QUEUED", private.UnusedEventHandler)
|
||||
Event.Register("AUCTION_HOUSE_THROTTLED_MESSAGE_SENT", private.UnusedEventHandler)
|
||||
if not TSM.IsShadowlands() then
|
||||
Event.Register("AUCTION_HOUSE_THROTTLED_SPECIFIC_SEARCH_READY", private.UnusedEventHandler)
|
||||
end
|
||||
Event.Register("AUCTION_HOUSE_THROTTLED_SYSTEM_READY", private.UnusedEventHandler)
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function AuctionHouseWrapper.RegisterAuctionIdUpdateCallback(callback)
|
||||
tinsert(private.auctionIdUpdateCallbacks, callback)
|
||||
end
|
||||
|
||||
function AuctionHouseWrapper.IsOpen()
|
||||
return private.isAHOpen
|
||||
end
|
||||
|
||||
function AuctionHouseWrapper.GetAndResetTotalHookedTime()
|
||||
local total, topTime, topAddon = 0, nil, nil
|
||||
for addon, hookedTime in pairs(private.hookedTime) do
|
||||
total = total + hookedTime
|
||||
if hookedTime > (topTime or 0) then
|
||||
topTime = hookedTime
|
||||
topAddon = addon
|
||||
end
|
||||
end
|
||||
wipe(private.hookedTime)
|
||||
return total, topAddon, topTime
|
||||
end
|
||||
|
||||
function AuctionHouseWrapper.SendBrowseQuery(query)
|
||||
assert(not TSM.IsWowClassic())
|
||||
if not private.CheckAllIdle() then
|
||||
return
|
||||
end
|
||||
return private.wrappers.SendBrowseQuery:Start(query)
|
||||
end
|
||||
|
||||
function AuctionHouseWrapper.RequestMoreBrowseResults()
|
||||
assert(not TSM.IsWowClassic())
|
||||
if not private.CheckAllIdle() then
|
||||
return
|
||||
end
|
||||
return private.wrappers.RequestMoreBrowseResults:Start()
|
||||
end
|
||||
|
||||
function AuctionHouseWrapper.SendSearchQuery(itemKey, isSell)
|
||||
assert(not TSM.IsWowClassic())
|
||||
if not private.CheckAllIdle() then
|
||||
return
|
||||
end
|
||||
-- remove times which are beyond the throttle interval
|
||||
for i = #private.searchQueryAPITimes, 1, -1 do
|
||||
if GetTime() - private.searchQueryAPITimes[i] >= SEARCH_QUERY_THROTTLE_INTERVAL then
|
||||
tremove(private.searchQueryAPITimes, i)
|
||||
end
|
||||
end
|
||||
if #private.searchQueryAPITimes >= SEARCH_QUERY_THROTTLE_MAX then
|
||||
local delayTime = private.searchQueryAPITimes[1] + SEARCH_QUERY_THROTTLE_INTERVAL - GetTime()
|
||||
assert(delayTime > 0, "Invalid delay time: "..tostring(delayTime))
|
||||
Log.Err("Search query can't be run for another %.3f seconds", delayTime)
|
||||
return nil, delayTime
|
||||
end
|
||||
assert(type(isSell) == "boolean")
|
||||
if isSell then
|
||||
return private.wrappers.SendSellSearchQuery:Start(itemKey, EMPTY_SORTS_TABLE, true)
|
||||
else
|
||||
return private.wrappers.SendSearchQuery:Start(itemKey, EMPTY_SORTS_TABLE, true)
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionHouseWrapper.RequestMoreCommoditySearchResults(itemId)
|
||||
assert(not TSM.IsWowClassic())
|
||||
if not private.CheckAllIdle() then
|
||||
return
|
||||
end
|
||||
return private.wrappers.RequestMoreCommoditySearchResults:Start(itemId)
|
||||
end
|
||||
|
||||
function AuctionHouseWrapper.RequestMoreItemSearchResults(itemKey)
|
||||
assert(not TSM.IsWowClassic())
|
||||
if not private.CheckAllIdle() then
|
||||
return
|
||||
end
|
||||
return private.wrappers.RequestMoreItemSearchResults:Start(itemKey)
|
||||
end
|
||||
|
||||
function AuctionHouseWrapper.QueryOwnedAuctions(sorts)
|
||||
assert(not TSM.IsWowClassic())
|
||||
if not private.CheckAllIdle() then
|
||||
return
|
||||
end
|
||||
return private.wrappers.QueryOwnedAuctions:Start(sorts)
|
||||
end
|
||||
|
||||
function AuctionHouseWrapper.CancelAuction(auctionId)
|
||||
assert(not TSM.IsWowClassic())
|
||||
-- if QueryOwnedAuctions is pending, just cancel it
|
||||
private.wrappers.QueryOwnedAuctions:CancelIfPending()
|
||||
if not private.CheckAllIdle() then
|
||||
return
|
||||
end
|
||||
return private.wrappers.CancelAuction:Start(auctionId)
|
||||
end
|
||||
|
||||
function AuctionHouseWrapper.StartCommoditiesPurchase(itemId, quantity, itemBuyout)
|
||||
assert(not TSM.IsWowClassic())
|
||||
if not private.CheckAllIdle() then
|
||||
return
|
||||
end
|
||||
return private.wrappers.StartCommoditiesPurchase:Start(itemId, quantity, itemBuyout)
|
||||
end
|
||||
|
||||
function AuctionHouseWrapper.ConfirmCommoditiesPurchase(itemId, quantity, totalBuyout)
|
||||
assert(not TSM.IsWowClassic())
|
||||
if not private.CheckAllIdle() then
|
||||
return
|
||||
end
|
||||
return private.wrappers.ConfirmCommoditiesPurchase:Start(itemId, quantity, totalBuyout)
|
||||
end
|
||||
|
||||
function AuctionHouseWrapper.PlaceBid(auctionId, bidBuyout)
|
||||
assert(not TSM.IsWowClassic())
|
||||
if not private.CheckAllIdle() then
|
||||
return
|
||||
end
|
||||
return private.wrappers.PlaceBid:Start(auctionId, bidBuyout)
|
||||
end
|
||||
|
||||
function AuctionHouseWrapper.PostItem(itemLocation, postTime, stackSize, bid, buyout)
|
||||
assert(not TSM.IsWowClassic())
|
||||
if not private.CheckAllIdle() then
|
||||
return
|
||||
end
|
||||
return private.wrappers.PostItem:Start(itemLocation, postTime, stackSize, bid, buyout)
|
||||
end
|
||||
|
||||
function AuctionHouseWrapper.PostCommodity(itemLocation, postTime, stackSize, itemBuyout)
|
||||
assert(not TSM.IsWowClassic())
|
||||
if not private.CheckAllIdle() then
|
||||
return
|
||||
end
|
||||
return private.wrappers.PostCommodity:Start(itemLocation, postTime, stackSize, itemBuyout)
|
||||
end
|
||||
|
||||
function AuctionHouseWrapper.QueryAuctionItems(name, minLevel, maxLevel, page, usable, quality, getAll, exact, filterData)
|
||||
assert(TSM.IsWowClassic())
|
||||
local canSendQuery, canSendGetAll = CanSendAuctionQuery()
|
||||
if not canSendQuery or (getAll and not canSendGetAll) or not private.CheckAllIdle() then
|
||||
return
|
||||
end
|
||||
return private.wrappers.QueryAuctionItems:Start(name, minLevel, maxLevel, page, usable, quality, getAll, exact, filterData)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- APIWrapper Class
|
||||
-- ============================================================================
|
||||
|
||||
function APIWrapper.__init(self, name)
|
||||
self._name = name
|
||||
self._args = {}
|
||||
self._state = "IDLE"
|
||||
self._callTime = nil
|
||||
self._future = Future.New(self._name.."_FUTURE")
|
||||
self._future:SetScript("OnCleanup", function()
|
||||
if self._state == "PENDING_REQUESTED" then
|
||||
-- switch the current call to a hooked call
|
||||
self._state = "PENDING_HOOKED"
|
||||
elseif self._state == "DONE" then
|
||||
self._state = "IDLE"
|
||||
end
|
||||
end)
|
||||
self._timeoutWrapper = function()
|
||||
Log.Err("API timed out: %s(%s)", self._name, private.ArgsToStr(unpack(self._args)))
|
||||
return self:_Done(false)
|
||||
end
|
||||
|
||||
-- hook the API
|
||||
hooksecurefunc(TSM.IsWowClassic() and _G or C_AuctionHouse, self._name, function(...)
|
||||
Log.Info("%s(%s)", self._name, private.ArgsToStr(...))
|
||||
if self:_IsPending() and select("#", ...) == 0 then
|
||||
return
|
||||
end
|
||||
self:CancelIfPending()
|
||||
if self:_HandleAPICall(...) then
|
||||
for _, wrapper in pairs(private.wrappers) do
|
||||
if wrapper ~= self and GetTime() ~= private.lastResponseReceived then
|
||||
wrapper:CancelIfPending()
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- register related events
|
||||
for eventName in pairs(API_EVENT_INFO[self._name]) do
|
||||
private.RegisterForEvent(eventName, self)
|
||||
end
|
||||
end
|
||||
|
||||
function APIWrapper.IsIdle(self)
|
||||
return self._state == "IDLE"
|
||||
end
|
||||
|
||||
function APIWrapper.CancelIfPending(self)
|
||||
if not self:_IsPending() then
|
||||
return
|
||||
end
|
||||
Log.Warn("Canceling pending (%s, %s)", self._name, self._state)
|
||||
self:_Done(false)
|
||||
end
|
||||
|
||||
function APIWrapper.Start(self, ...)
|
||||
if self._state ~= "IDLE" then
|
||||
Log.Err("API already in progress (%s)", self._name)
|
||||
return
|
||||
end
|
||||
self._state = "STARTING"
|
||||
self:_CallAPI(...)
|
||||
return self._future
|
||||
end
|
||||
|
||||
function APIWrapper._IsPending(self)
|
||||
return self._state == "PENDING_REQUESTED" or self._state == "PENDING_HOOKED"
|
||||
end
|
||||
|
||||
function APIWrapper._CallAPI(self, ...)
|
||||
return (TSM.IsWowClassic() and _G or C_AuctionHouse)[self._name](...)
|
||||
end
|
||||
|
||||
function APIWrapper._HandleAPICall(self, ...)
|
||||
self._callTime = GetTime()
|
||||
if self._state == "IDLE" then
|
||||
self._state = "PENDING_HOOKED"
|
||||
self._hookAddon = strmatch(Debug.GetStackLevelLocation(3), "AddOns\\([^\\]+)\\")
|
||||
elseif self._state == "STARTING" then
|
||||
self._future:Start()
|
||||
self._state = "PENDING_REQUESTED"
|
||||
else
|
||||
error("Unexpected state: "..self._state)
|
||||
end
|
||||
Vararg.IntoTable(self._args, ...)
|
||||
local timeout = nil
|
||||
if not private.isAHOpen then
|
||||
timeout = 0
|
||||
elseif self._name == "QueryAuctionItems" and select(7, ...) then
|
||||
timeout = GET_ALL_TIMEOUT
|
||||
else
|
||||
timeout = API_TIMEOUT
|
||||
end
|
||||
Delay.AfterTime(self._name.."_TIMEOUT", timeout, self._timeoutWrapper)
|
||||
return true
|
||||
end
|
||||
|
||||
function APIWrapper._HandleEvent(self, eventName, ...)
|
||||
if self._state ~= "PENDING_REQUESTED" and self._state ~= "PENDING_HOOKED" then
|
||||
return
|
||||
end
|
||||
local eventIsValid, result = self:_ValidateEvent(eventName, ...)
|
||||
if not eventIsValid then
|
||||
Log.Info("Ignoring invalidated event")
|
||||
return
|
||||
end
|
||||
self:_Done(result)
|
||||
end
|
||||
|
||||
function APIWrapper._ValidateEvent(self, eventName, ...)
|
||||
local info = nil
|
||||
if GENERIC_EVENTS[eventName] then
|
||||
local arg = ...
|
||||
info = API_EVENT_INFO[self._name][eventName..GENERIC_EVENT_SEP..arg]
|
||||
else
|
||||
info = API_EVENT_INFO[self._name][eventName]
|
||||
end
|
||||
assert(info)
|
||||
if info.timeoutChange then
|
||||
Delay.Cancel(self._name.."_TIMEOUT")
|
||||
Delay.AfterTime(self._name.."_TIMEOUT", info.timeoutChange, self._timeoutWrapper)
|
||||
return false
|
||||
end
|
||||
local eventIsValid, result = true, nil
|
||||
if type(info.result) == "number" then
|
||||
result = select(info.result, ...)
|
||||
else
|
||||
result = info.result
|
||||
end
|
||||
if info.rawFilterFunc then
|
||||
if not info.rawFilterFunc(self._args, ...) then
|
||||
eventIsValid = false
|
||||
end
|
||||
elseif info.eventArgIndex then
|
||||
local eventValue = select(info.eventArgIndex, ...)
|
||||
local apiValue = self._args[info.apiArgIndex]
|
||||
if info.apiArgKey then
|
||||
apiValue = apiValue[info.apiArgKey]
|
||||
end
|
||||
local argMatches = nil
|
||||
assert(type(eventValue) == type(apiValue))
|
||||
if info.compareFunc then
|
||||
argMatches = info.compareFunc(eventValue, apiValue)
|
||||
elseif private.IsItemKey(eventValue) then
|
||||
argMatches = true
|
||||
for _, key in ipairs(ITEM_KEY_KEYS) do
|
||||
if eventValue[key] ~= apiValue[key] then
|
||||
argMatches = false
|
||||
break
|
||||
end
|
||||
end
|
||||
elseif type(eventValue) == "table" then
|
||||
argMatches = Table.Equal(eventValue, apiValue)
|
||||
else
|
||||
argMatches = eventValue == apiValue
|
||||
end
|
||||
if not argMatches then
|
||||
eventIsValid = false
|
||||
end
|
||||
end
|
||||
return eventIsValid, result
|
||||
end
|
||||
|
||||
function APIWrapper._Done(self, result)
|
||||
wipe(self._args)
|
||||
local hookAddon = self._hookAddon
|
||||
self._hookAddon = nil
|
||||
local totalTime = Math.Round((GetTime() - (self._callTime or GetTime())) * 1000)
|
||||
self._callTime = nil
|
||||
Delay.Cancel(self._name.."_TIMEOUT")
|
||||
if self._state == "PENDING_REQUESTED" then
|
||||
if totalTime > 0 then
|
||||
Analytics.Action("AH_API_TIME", private.GetAnalyticsRegionRealm(), self._name, result and totalTime or -1)
|
||||
end
|
||||
self._state = "DONE"
|
||||
-- need to do this last as it might trigger another API call or OnCleanup on the future
|
||||
self._future:Done(result)
|
||||
elseif self._state == "PENDING_HOOKED" then
|
||||
self._state = "IDLE"
|
||||
if hookAddon then
|
||||
private.hookedTime[hookAddon] = (private.hookedTime[hookAddon] or 0) + totalTime / 1000
|
||||
end
|
||||
else
|
||||
error("Unexpected state: "..self._state)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.AuctionHouseShowHandler()
|
||||
private.isAHOpen = true
|
||||
end
|
||||
|
||||
function private.AuctionHouseClosedHandler()
|
||||
private.isAHOpen = false
|
||||
for _, wrapper in pairs(private.wrappers) do
|
||||
wrapper:CancelIfPending()
|
||||
end
|
||||
end
|
||||
|
||||
function private.IsItemKey(value)
|
||||
if type(value) ~= "table" then
|
||||
return false
|
||||
end
|
||||
for _, key in ipairs(ITEM_KEY_KEYS) do
|
||||
if not value[key] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function private.ItemKeyToStr(itemKey)
|
||||
assert(#private.itemKeyPartsTemp == 0)
|
||||
if itemKey.itemID ~= 0 then
|
||||
tinsert(private.itemKeyPartsTemp, "itemID="..itemKey.itemID)
|
||||
end
|
||||
if itemKey.itemLevel ~= 0 then
|
||||
tinsert(private.itemKeyPartsTemp, "itemLevel="..itemKey.itemLevel)
|
||||
end
|
||||
if itemKey.itemSuffix ~= 0 then
|
||||
tinsert(private.itemKeyPartsTemp, "itemSuffix="..itemKey.itemSuffix)
|
||||
end
|
||||
if itemKey.battlePetSpeciesID ~= 0 then
|
||||
tinsert(private.itemKeyPartsTemp, "battlePetSpeciesID="..itemKey.battlePetSpeciesID)
|
||||
end
|
||||
local result = format("{%s}", table.concat(private.itemKeyPartsTemp, ","))
|
||||
wipe(private.itemKeyPartsTemp)
|
||||
return result
|
||||
end
|
||||
|
||||
function private.SortsToStr(sorts)
|
||||
assert(#private.sortsPartsTemp == 0)
|
||||
for _, sort in ipairs(sorts) do
|
||||
local name = Table.KeyByValue(Enum.AuctionHouseSortOrder, sort.sortOrder) or "?"
|
||||
tinsert(private.sortsPartsTemp, format("%s%s", sort.reverseSort and "-" or "", name))
|
||||
end
|
||||
local result = format("{%s}", table.concat(private.sortsPartsTemp, ","))
|
||||
wipe(private.sortsPartsTemp)
|
||||
return result
|
||||
end
|
||||
|
||||
function private.ArgToStr(arg)
|
||||
if type(arg) == "table" then
|
||||
local count = Table.Count(arg)
|
||||
if private.IsItemKey(arg) then
|
||||
return private.ItemKeyToStr(arg)
|
||||
elseif arg.searchString then
|
||||
return format("{searchString=\"%s\", sorts=%s, minLevel=%s, maxLevel=%s, filters=%s, itemClassFilters=%s}", arg.searchString, private.SortsToStr(arg.sorts), private.ArgToStr(arg.minLevel), private.ArgToStr(arg.maxLevel), private.ArgToStr(arg.filters), private.ArgToStr(arg.itemClassFilters))
|
||||
elseif arg.IsBagAndSlot then
|
||||
return format("{<ItemLocation:(%d,%d)>}", arg:GetBagAndSlot())
|
||||
elseif count == 0 then
|
||||
return "{}"
|
||||
elseif count == #arg then
|
||||
if type(arg[1]) == "table" and arg[1].sortOrder then
|
||||
return format("{sorts=%s}", private.SortsToStr(arg))
|
||||
end
|
||||
return format("{<%d items>}", count)
|
||||
else
|
||||
return "{...}"
|
||||
end
|
||||
else
|
||||
return tostring(arg)
|
||||
end
|
||||
end
|
||||
|
||||
function private.ArgsToStr(...)
|
||||
assert(#private.argsTemp == 0)
|
||||
for _, arg in Vararg.Iterator(...) do
|
||||
tinsert(private.argsTemp, private.ArgToStr(arg))
|
||||
end
|
||||
local result = table.concat(private.argsTemp, ",")
|
||||
wipe(private.argsTemp)
|
||||
return result
|
||||
end
|
||||
|
||||
function private.RegisterForEvent(eventName, wrapper)
|
||||
local genericEventArg = nil
|
||||
eventName, genericEventArg = strsplit(GENERIC_EVENT_SEP, eventName)
|
||||
if not private.events[eventName] then
|
||||
private.events[eventName] = {}
|
||||
Event.Register(eventName, private.EventHandler)
|
||||
end
|
||||
if genericEventArg then
|
||||
private.events[eventName][genericEventArg] = private.events[eventName][genericEventArg] or {}
|
||||
tinsert(private.events[eventName][genericEventArg], wrapper)
|
||||
else
|
||||
tinsert(private.events[eventName], wrapper)
|
||||
end
|
||||
end
|
||||
|
||||
function private.EventHandler(eventName, ...)
|
||||
-- reduce the log spam of generic events by combining the message with the name and discarding arguments
|
||||
local genericEventArg = nil
|
||||
if eventName == "UI_ERROR_MESSAGE" and select(1, ...) == ERR_AUCTION_DATABASE_ERROR then
|
||||
-- log an analytics event for "Internal Auction Error" messages
|
||||
for apiName, wrapper in pairs(private.wrappers) do
|
||||
if not wrapper:IsIdle() then
|
||||
Analytics.Action("AH_INTERNAL_ERROR", private.GetAnalyticsRegionRealm(), apiName)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if GENERIC_EVENTS[eventName] then
|
||||
genericEventArg = select(GENERIC_EVENTS[eventName], ...)
|
||||
assert(genericEventArg)
|
||||
if not private.events[eventName][genericEventArg] then
|
||||
return
|
||||
end
|
||||
private.EventHandlerHelper(private.events[eventName][genericEventArg], eventName, genericEventArg)
|
||||
else
|
||||
private.EventHandlerHelper(private.events[eventName], eventName, ...)
|
||||
end
|
||||
end
|
||||
|
||||
function private.ResponseReceivedHandler(eventName, ...)
|
||||
Log.Info("%s (%s)", eventName, private.ArgsToStr(...))
|
||||
private.lastResponseReceived = GetTime()
|
||||
end
|
||||
|
||||
function private.UnusedEventHandler(eventName, ...)
|
||||
Log.Info("%s (%s)", eventName, private.ArgsToStr(...))
|
||||
end
|
||||
|
||||
function private.EventHandlerHelper(wrappers, eventName, ...)
|
||||
if not SILENT_EVENTS[eventName] then
|
||||
Log.Info("%s (%s)", eventName, private.ArgsToStr(...))
|
||||
end
|
||||
for _, wrapper in ipairs(wrappers) do
|
||||
wrapper:_HandleEvent(eventName, ...)
|
||||
end
|
||||
end
|
||||
|
||||
function private.CheckAllIdle()
|
||||
for apiName, wrapper in pairs(private.wrappers) do
|
||||
if not wrapper:IsIdle() then
|
||||
Log.Err("Another wrapper is pending (%s)", apiName)
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function private.AuctionCanceledHandler(_, auctionId)
|
||||
private.lastAuctionCanceledAuctionId = auctionId
|
||||
private.lastAuctionCanceledTime = GetTime()
|
||||
end
|
||||
|
||||
function private.ItemSearchResultsUpdated(_, itemKey, auctionId)
|
||||
if private.lastAuctionCanceledTime == GetTime() and auctionId then
|
||||
Log.Info("Auction ID changed from %s to %s", tostring(private.lastAuctionCanceledAuctionId), tostring(auctionId))
|
||||
local newResultInfo = nil
|
||||
for i = 1, C_AuctionHouse.GetNumItemSearchResults(itemKey) do
|
||||
local info = C_AuctionHouse.GetItemSearchResultInfo(itemKey, i)
|
||||
if info.auctionID == auctionId then
|
||||
newResultInfo = info
|
||||
break
|
||||
end
|
||||
end
|
||||
if not newResultInfo then
|
||||
Log.Warn("Failed to find new result info")
|
||||
end
|
||||
for _, callback in ipairs(private.auctionIdUpdateCallbacks) do
|
||||
callback(private.lastAuctionCanceledAuctionId, auctionId, newResultInfo)
|
||||
end
|
||||
private.lastAuctionCanceledAuctionId = nil
|
||||
private.lastAuctionCanceledTime = 0
|
||||
end
|
||||
end
|
||||
|
||||
function private.GetAnalyticsRegionRealm()
|
||||
return TSM.GetRegion().."-"..gsub(GetRealmName(), "\226", "'")
|
||||
end
|
||||
65
LibTSM/Service/AuctionScan.lua
Normal file
65
LibTSM/Service/AuctionScan.lua
Normal file
@@ -0,0 +1,65 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Auction scanning functions.
|
||||
-- @module AuctionScan
|
||||
|
||||
local _, TSM = ...
|
||||
local AuctionScan = TSM.Init("Service.AuctionScan")
|
||||
local ScanManager = TSM.Include("Service.AuctionScanClasses.ScanManager")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function AuctionScan.GetManager()
|
||||
return ScanManager.Get()
|
||||
end
|
||||
|
||||
function AuctionScan.CanBid(subRow)
|
||||
if not subRow:IsSubRow() then
|
||||
return false
|
||||
end
|
||||
local _, _, _, isHighBidder = subRow:GetBidInfo()
|
||||
local displayedBid = subRow:GetDisplayedBids()
|
||||
local buyout = subRow:GetBuyouts()
|
||||
if isHighBidder then
|
||||
return false
|
||||
elseif displayedBid == buyout then
|
||||
return false
|
||||
elseif not TSM.IsWowClassic() and subRow:IsCommodity() then
|
||||
return false
|
||||
elseif GetMoney() < subRow:GetRequiredBid() then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function AuctionScan.CanBuyout(subRow, auctionScan)
|
||||
if not subRow:IsSubRow() then
|
||||
return false
|
||||
end
|
||||
local buyout, itemBuyout = subRow:GetBuyouts()
|
||||
if not TSM.IsWowClassic() then
|
||||
buyout = itemBuyout
|
||||
end
|
||||
local itemString = subRow:GetItemString()
|
||||
if buyout == 0 or GetMoney() < buyout then
|
||||
return false
|
||||
elseif not TSM.IsWowClassic() and subRow:IsCommodity() then
|
||||
-- make sure it's the cheapest
|
||||
local isCheapest = false
|
||||
for _, query in auctionScan:QueryIterator() do
|
||||
isCheapest = isCheapest or subRow == query:GetCheapestSubRow(itemString)
|
||||
end
|
||||
if not isCheapest then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
675
LibTSM/Service/AuctionScanClasses/Query.lua
Normal file
675
LibTSM/Service/AuctionScanClasses/Query.lua
Normal file
@@ -0,0 +1,675 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- AuctionQuery Class.
|
||||
-- A class which is used to build a query to scan the auciton house.
|
||||
-- @classmod AuctionQuery
|
||||
|
||||
local _, TSM = ...
|
||||
local Query = TSM.Init("Service.AuctionScanClasses.Query")
|
||||
local String = TSM.Include("Util.String")
|
||||
local ObjectPool = TSM.Include("Util.ObjectPool")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Table = TSM.Include("Util.Table")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local AuctionHouseWrapper = TSM.Include("Service.AuctionHouseWrapper")
|
||||
local Scanner = TSM.Include("Service.AuctionScanClasses.Scanner")
|
||||
local LibTSMClass = TSM.Include("LibTSMClass")
|
||||
local AuctionQuery = LibTSMClass.DefineClass("AuctionQuery")
|
||||
local private = {
|
||||
objectPool = ObjectPool.New("AUCTION_SCAN_QUERY", AuctionQuery),
|
||||
}
|
||||
local ITEM_SPECIFIC = newproxy()
|
||||
local ITEM_BASE = newproxy()
|
||||
local DEFAULT_SORTS = TSM.IsWowClassic() and
|
||||
{ -- classic
|
||||
"seller",
|
||||
"quantity",
|
||||
"unitprice",
|
||||
} or
|
||||
{ -- retail
|
||||
{ sortOrder = Enum.AuctionHouseSortOrder.Price, reverseSort = false },
|
||||
{ sortOrder = Enum.AuctionHouseSortOrder.Name, reverseSort = false },
|
||||
}
|
||||
local EMPTY_SORTS = {}
|
||||
local INV_TYPES = {
|
||||
CHEST = TSM.IsShadowlands() and Enum.InventoryType.IndexChestType or LE_INVENTORY_TYPE_CHEST_TYPE,
|
||||
ROBE = TSM.IsShadowlands() and Enum.InventoryType.IndexRobeType or LE_INVENTORY_TYPE_ROBE_TYPE,
|
||||
NECK = TSM.IsShadowlands() and Enum.InventoryType.IndexNeckType or LE_INVENTORY_TYPE_NECK_TYPE,
|
||||
FINGER = TSM.IsShadowlands() and Enum.InventoryType.IndexFingerType or LE_INVENTORY_TYPE_FINGER_TYPE,
|
||||
TRINKET = TSM.IsShadowlands() and Enum.InventoryType.IndexTrinketType or LE_INVENTORY_TYPE_TRINKET_TYPE,
|
||||
HOLDABLE = TSM.IsShadowlands() and Enum.InventoryType.IndexHoldableType or LE_INVENTORY_TYPE_HOLDABLE_TYPE,
|
||||
BODY = TSM.IsShadowlands() and Enum.InventoryType.IndexBodyType or LE_INVENTORY_TYPE_BODY_TYPE,
|
||||
CLOAK = TSM.IsShadowlands() and Enum.InventoryType.IndexCloakType or LE_INVENTORY_TYPE_CLOAK_TYPE,
|
||||
}
|
||||
assert(Table.Count(INV_TYPES) == 8)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Query.Get()
|
||||
return private.objectPool:Get()
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Class Meta Methods
|
||||
-- ============================================================================
|
||||
|
||||
function AuctionQuery.__init(self)
|
||||
self._str = ""
|
||||
self._strLower = ""
|
||||
self._strMatch = ""
|
||||
self._exact = false
|
||||
self._minQuality = -math.huge
|
||||
self._maxQuality = math.huge
|
||||
self._minLevel = -math.huge
|
||||
self._maxLevel = math.huge
|
||||
self._minItemLevel = -math.huge
|
||||
self._maxItemLevel = math.huge
|
||||
self._class = nil
|
||||
self._subClass = nil
|
||||
self._invType = nil
|
||||
self._classFilter1 = {}
|
||||
self._classFilter2 = {}
|
||||
self._usable = false
|
||||
self._uncollected = false
|
||||
self._upgrades = false
|
||||
self._unlearned = false
|
||||
self._canLearn = false
|
||||
self._minPrice = 0
|
||||
self._maxPrice = math.huge
|
||||
self._items = {}
|
||||
self._customFilters = {}
|
||||
self._isBrowseDoneFunc = nil
|
||||
self._specifiedPage = nil
|
||||
self._getAll = nil
|
||||
self._resolveSellers = false
|
||||
self._callback = nil
|
||||
self._queryTemp = {}
|
||||
self._filtersTemp = {}
|
||||
self._classFiltersTemp = {}
|
||||
self._browseResults = {}
|
||||
self._page = 0
|
||||
end
|
||||
|
||||
function AuctionQuery.Release(self)
|
||||
self._str = ""
|
||||
self._strLower = ""
|
||||
self._strMatch = ""
|
||||
self._exact = false
|
||||
self._minQuality = -math.huge
|
||||
self._maxQuality = math.huge
|
||||
self._minLevel = -math.huge
|
||||
self._maxLevel = math.huge
|
||||
self._minItemLevel = -math.huge
|
||||
self._maxItemLevel = math.huge
|
||||
self._class = nil
|
||||
self._subClass = nil
|
||||
self._invType = nil
|
||||
wipe(self._classFilter1)
|
||||
wipe(self._classFilter2)
|
||||
self._usable = false
|
||||
self._uncollected = false
|
||||
self._upgrades = false
|
||||
self._unlearned = false
|
||||
self._canLearn = false
|
||||
self._minPrice = 0
|
||||
self._maxPrice = math.huge
|
||||
wipe(self._items)
|
||||
wipe(self._customFilters)
|
||||
self._isBrowseDoneFunc = nil
|
||||
self._specifiedPage = nil
|
||||
self._getAll = nil
|
||||
self._resolveSellers = false
|
||||
self._callback = nil
|
||||
wipe(self._queryTemp)
|
||||
wipe(self._filtersTemp)
|
||||
wipe(self._classFiltersTemp)
|
||||
for _, row in pairs(self._browseResults) do
|
||||
row:Release()
|
||||
end
|
||||
wipe(self._browseResults)
|
||||
self._page = 0
|
||||
private.objectPool:Recycle(self)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Public Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
function AuctionQuery.SetStr(self, str, exact)
|
||||
self._str = str or ""
|
||||
self._strLower = strlower(self._str)
|
||||
self._strMatch = String.Escape(self._strLower)
|
||||
self._exact = exact or false
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetQualityRange(self, minQuality, maxQuality)
|
||||
self._minQuality = minQuality or -math.huge
|
||||
self._maxQuality = maxQuality or math.huge
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetLevelRange(self, minLevel, maxLevel)
|
||||
self._minLevel = minLevel or -math.huge
|
||||
self._maxLevel = maxLevel or math.huge
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetItemLevelRange(self, minItemLevel, maxItemLevel)
|
||||
self._minItemLevel = minItemLevel or -math.huge
|
||||
self._maxItemLevel = maxItemLevel or math.huge
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetClass(self, class, subClass, invType)
|
||||
self._class = class or nil
|
||||
self._subClass = subClass or nil
|
||||
self._invType = invType or nil
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetUsable(self, usable)
|
||||
self._usable = usable or false
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetUncollected(self, uncollected)
|
||||
self._uncollected = uncollected or false
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetUpgrades(self, upgrades)
|
||||
self._upgrades = upgrades or false
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetUnlearned(self, unlearned)
|
||||
self._unlearned = unlearned or false
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetCanLearn(self, canLearn)
|
||||
self._canLearn = canLearn or false
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetPriceRange(self, minPrice, maxPrice)
|
||||
self._minPrice = minPrice or 0
|
||||
self._maxPrice = maxPrice or math.huge
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetItems(self, items)
|
||||
wipe(self._items)
|
||||
if type(items) == "table" then
|
||||
for _, itemString in ipairs(items) do
|
||||
local baseItemString = ItemString.GetBaseFast(itemString)
|
||||
self._items[itemString] = ITEM_SPECIFIC
|
||||
if baseItemString ~= itemString then
|
||||
self._items[baseItemString] = self._items[baseItemString] or ITEM_BASE
|
||||
end
|
||||
end
|
||||
elseif type(items) == "string" then
|
||||
local itemString = items
|
||||
local baseItemString = ItemString.GetBaseFast(itemString)
|
||||
self._items[itemString] = ITEM_SPECIFIC
|
||||
if baseItemString ~= itemString then
|
||||
self._items[baseItemString] = self._items[baseItemString] or ITEM_BASE
|
||||
end
|
||||
elseif items ~= nil then
|
||||
error("Invalid items type: "..tostring(items))
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.AddCustomFilter(self, func)
|
||||
self._customFilters[func] = true
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetIsBrowseDoneFunction(self, func)
|
||||
self._isBrowseDoneFunc = func
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetPage(self, page)
|
||||
if page == nil then
|
||||
self._specifiedPage = nil
|
||||
elseif type(page) == "number" or page == "FIRST" or page == "LAST" then
|
||||
assert(TSM.IsWowClassic())
|
||||
self._specifiedPage = page
|
||||
else
|
||||
error("Invalid page: "..tostring(page))
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetGetAll(self, getAll)
|
||||
-- only currently support GetAll on classic
|
||||
assert(not getAll or TSM.IsWowClassic())
|
||||
self._getAll = getAll
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetResolveSellers(self, resolveSellers)
|
||||
self._resolveSellers = resolveSellers
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetCallback(self, callback)
|
||||
self._callback = callback
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.Browse(self, forceNoScan)
|
||||
assert(not TSM.IsWowClassic() or not forceNoScan)
|
||||
|
||||
local noScan = forceNoScan or false
|
||||
if not TSM.IsWowClassic() then
|
||||
local numItems = 0
|
||||
for _, itemType in pairs(self._items) do
|
||||
if itemType == ITEM_SPECIFIC then
|
||||
numItems = numItems + 1
|
||||
end
|
||||
end
|
||||
if numItems > 0 and numItems < 500 then
|
||||
-- it's faster to just issue individual item searches instead of a browse query
|
||||
noScan = true
|
||||
end
|
||||
end
|
||||
|
||||
if noScan then
|
||||
assert(not TSM.IsWowClassic())
|
||||
local itemKeys = TempTable.Acquire()
|
||||
for itemString in pairs(self._items) do
|
||||
if itemString == ItemString.GetBaseFast(itemString) then
|
||||
local itemId, battlePetSpeciesId = nil, nil
|
||||
if ItemString.IsPet(itemString) then
|
||||
itemId = ItemString.ToId(ItemString.GetPetCage())
|
||||
battlePetSpeciesId = ItemString.ToId(itemString)
|
||||
else
|
||||
itemId = ItemString.ToId(itemString)
|
||||
battlePetSpeciesId = 0
|
||||
end
|
||||
local itemKey = C_AuctionHouse.MakeItemKey(itemId, 0, 0, battlePetSpeciesId)
|
||||
-- FIX for 9.0.1 bug where MakeItemKey randomly adds an itemLevel which breaks scanning
|
||||
itemKey.itemLevel = 0
|
||||
tinsert(itemKeys, itemKey)
|
||||
end
|
||||
end
|
||||
local future = Scanner.BrowseNoScan(self, itemKeys, self._browseResults, self._callback)
|
||||
TempTable.Release(itemKeys)
|
||||
return future
|
||||
else
|
||||
self._page = 0
|
||||
return Scanner.Browse(self, self._resolveSellers, self._browseResults, self._callback)
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionQuery.GetSearchProgress(self)
|
||||
if TSM.IsWowClassic() then
|
||||
return 1
|
||||
end
|
||||
local progress, totalNum = 0, 0
|
||||
for _, row in pairs(self._browseResults) do
|
||||
progress = progress + row:_GetSearchProgress()
|
||||
totalNum = totalNum + 1
|
||||
end
|
||||
if totalNum == 0 then
|
||||
return 0
|
||||
end
|
||||
return progress / totalNum
|
||||
end
|
||||
|
||||
function AuctionQuery.GetBrowseResults(self, baseItemString)
|
||||
return self._browseResults[baseItemString]
|
||||
end
|
||||
|
||||
function AuctionQuery.ItemSubRowIterator(self, itemString)
|
||||
local result = TempTable.Acquire()
|
||||
local baseItemString = ItemString.GetBaseFast(itemString)
|
||||
local isBaseItemString = itemString == baseItemString
|
||||
local row = self._browseResults[baseItemString]
|
||||
if row then
|
||||
for _, subRow in row:SubRowIterator() do
|
||||
local subRowBaseItemString = subRow:GetBaseItemString()
|
||||
local subRowItemString = subRow:GetItemString()
|
||||
if (isBaseItemString and subRowBaseItemString == itemString) or (not isBaseItemString and subRowItemString == itemString) then
|
||||
tinsert(result, subRow)
|
||||
end
|
||||
end
|
||||
end
|
||||
return TempTable.Iterator(result)
|
||||
end
|
||||
|
||||
function AuctionQuery.GetCheapestSubRow(self, itemString)
|
||||
assert(not TSM.IsWowClassic())
|
||||
local cheapest, cheapestItemBuyout = nil, nil
|
||||
for _, subRow in self:ItemSubRowIterator(itemString) do
|
||||
local quantity = subRow:GetQuantities()
|
||||
local _, numOwnerItems = subRow:GetOwnerInfo()
|
||||
local _, itemBuyout = subRow:GetBuyouts()
|
||||
if numOwnerItems ~= quantity and itemBuyout < (cheapestItemBuyout or math.huge) then
|
||||
cheapest = subRow
|
||||
cheapestItemBuyout = itemBuyout
|
||||
end
|
||||
end
|
||||
return cheapest
|
||||
end
|
||||
|
||||
function AuctionQuery.BrowseResultsIterator(self)
|
||||
return pairs(self._browseResults)
|
||||
end
|
||||
|
||||
function AuctionQuery.RemoveResultRow(self, row)
|
||||
local baseItemString = row:GetBaseItemString()
|
||||
assert(baseItemString and self._browseResults[baseItemString])
|
||||
self._browseResults[baseItemString] = nil
|
||||
row:Release()
|
||||
if self._callback then
|
||||
self._callback(self)
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionQuery.Search(self, row, useCachedData)
|
||||
assert(not TSM.IsWowClassic())
|
||||
assert(self._browseResults)
|
||||
return Scanner.Search(self, self._resolveSellers, useCachedData, row, self._callback)
|
||||
end
|
||||
|
||||
function AuctionQuery.CancelBrowseOrSearch(self)
|
||||
Scanner.Cancel()
|
||||
end
|
||||
|
||||
function AuctionQuery.ItemIterator(self)
|
||||
return private.ItemIteratorHelper, self._items, nil
|
||||
end
|
||||
|
||||
function AuctionQuery.WipeBrowseResults(self)
|
||||
for _, row in pairs(self._browseResults) do
|
||||
row:Release()
|
||||
end
|
||||
wipe(self._browseResults)
|
||||
if self._callback then
|
||||
self._callback(self)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
function AuctionQuery._SetSort(self)
|
||||
if not TSM.IsWowClassic() then
|
||||
return true
|
||||
end
|
||||
|
||||
local sorts = (type(self._specifiedPage) == "string" or self._getAll) and EMPTY_SORTS or DEFAULT_SORTS
|
||||
|
||||
if GetAuctionSort("list", #sorts + 1) == nil then
|
||||
local properlySorted = true
|
||||
for i, col in ipairs(sorts) do
|
||||
local sortCol, sortReversed = GetAuctionSort("list", #sorts - i + 1)
|
||||
-- we never care to reverse a sort so if it's reversed then it's not properly sorted
|
||||
if sortCol ~= col or sortReversed then
|
||||
properlySorted = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if properlySorted then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
SortAuctionClearSort("list")
|
||||
for _, col in ipairs(sorts) do
|
||||
SortAuctionSetSort("list", col, false)
|
||||
end
|
||||
SortAuctionApplySort("list")
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function AuctionQuery._SendWowQuery(self)
|
||||
-- build the class filters
|
||||
wipe(self._classFiltersTemp)
|
||||
wipe(self._classFilter1)
|
||||
wipe(self._classFilter2)
|
||||
if self._invType == INV_TYPES.CHEST or self._invType == INV_TYPES.ROBE then
|
||||
-- default AH only sends in queries for robe chest type, we need to mimic this when using a chest filter
|
||||
self._classFilter1.classID = LE_ITEM_CLASS_ARMOR
|
||||
self._classFilter1.subClassID = self._subClass
|
||||
self._classFilter1.inventoryType = INV_TYPES.CHEST
|
||||
tinsert(self._classFiltersTemp, self._classFilter1)
|
||||
self._classFilter2.classID = LE_ITEM_CLASS_ARMOR
|
||||
self._classFilter2.subClassID = self._subClass
|
||||
self._classFilter2.inventoryType = INV_TYPES.ROBE
|
||||
tinsert(self._classFiltersTemp, self._classFilter2)
|
||||
elseif self._invType == INV_TYPES.NECK or self._invType == INV_TYPES.FINGER or self._invType == INV_TYPES.TRINKET or self._invType == INV_TYPES.HOLDABLE or self._invType == INV_TYPES.BODY then
|
||||
self._classFilter1.classID = LE_ITEM_CLASS_ARMOR
|
||||
self._classFilter1.subClassID = LE_ITEM_ARMOR_GENERIC
|
||||
self._classFilter1.inventoryType = self._invType
|
||||
tinsert(self._classFiltersTemp, self._classFilter1)
|
||||
elseif self._invType == INV_TYPES.CLOAK then
|
||||
self._classFilter1.classID = LE_ITEM_CLASS_ARMOR
|
||||
self._classFilter1.subClassID = LE_ITEM_ARMOR_CLOTH
|
||||
self._classFilter1.inventoryType = self._invType
|
||||
tinsert(self._classFiltersTemp, self._classFilter1)
|
||||
elseif self._class then
|
||||
self._classFilter1.classID = self._class
|
||||
self._classFilter1.subClassID = self._subClass
|
||||
self._classFilter1.inventoryType = self._invType
|
||||
tinsert(self._classFiltersTemp, self._classFilter1)
|
||||
end
|
||||
|
||||
-- build the query
|
||||
local minLevel = self._minLevel ~= -math.huge and self._minLevel or nil
|
||||
local maxLevel = self._maxLevel ~= math.huge and self._maxLevel or nil
|
||||
if TSM.IsWowClassic() then
|
||||
if self._specifiedPage == "LAST" then
|
||||
self._page = max(ceil(select(2, GetNumAuctionItems("list")) / NUM_AUCTION_ITEMS_PER_PAGE) - 1, 0)
|
||||
elseif self._specifiedPage == "FIRST" then
|
||||
self._page = 0
|
||||
elseif self._specifiedPage then
|
||||
self._page = self._specifiedPage
|
||||
end
|
||||
local minQuality = self._minQuality == -math.huge and 0 or self._minQuality
|
||||
return AuctionHouseWrapper.QueryAuctionItems(self._str, minLevel, maxLevel, self._page, self._usable, minQuality, self._getAll, self._exact, self._classFiltersTemp)
|
||||
else
|
||||
wipe(self._filtersTemp)
|
||||
if self._uncollected then
|
||||
tinsert(self._filtersTemp, Enum.AuctionHouseFilter.UncollectedOnly)
|
||||
end
|
||||
if self._usable then
|
||||
tinsert(self._filtersTemp, Enum.AuctionHouseFilter.UsableOnly)
|
||||
end
|
||||
if self._upgrades then
|
||||
tinsert(self._filtersTemp, Enum.AuctionHouseFilter.UpgradesOnly)
|
||||
end
|
||||
if self._exact then
|
||||
tinsert(self._filtersTemp, Enum.AuctionHouseFilter.ExactMatch)
|
||||
end
|
||||
local minQuality = self._minQuality == -math.huge and 0 or self._minQuality
|
||||
for i = minQuality + Enum.AuctionHouseFilter.PoorQuality, min(self._maxQuality + Enum.AuctionHouseFilter.PoorQuality, Enum.AuctionHouseFilter.ArtifactQuality) do
|
||||
tinsert(self._filtersTemp, i)
|
||||
end
|
||||
wipe(self._queryTemp)
|
||||
self._queryTemp.searchString = self._str
|
||||
self._queryTemp.minLevel = minLevel
|
||||
self._queryTemp.maxLevel = maxLevel
|
||||
self._queryTemp.sorts = DEFAULT_SORTS
|
||||
self._queryTemp.filters = self._filtersTemp
|
||||
self._queryTemp.itemClassFilters = self._classFiltersTemp
|
||||
return AuctionHouseWrapper.SendBrowseQuery(self._queryTemp)
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionQuery._IsFiltered(self, row, isSubRow, itemKey)
|
||||
local baseItemString = row:GetBaseItemString()
|
||||
local itemString = row:GetItemString()
|
||||
assert(baseItemString)
|
||||
local name, quality, itemLevel, maxItemLevel = row:GetItemInfo(itemKey)
|
||||
local _, itemBuyout, minItemBuyout = row:GetBuyouts(itemKey)
|
||||
if row:IsSubRow() and itemBuyout == 0 then
|
||||
_, itemBuyout = row:GetBidInfo()
|
||||
end
|
||||
|
||||
if next(self._items) then
|
||||
if not self._items[baseItemString] then
|
||||
return true
|
||||
end
|
||||
if isSubRow and itemString and self._items[itemString] ~= ITEM_SPECIFIC and self._items[baseItemString] ~= ITEM_SPECIFIC then
|
||||
return true
|
||||
elseif not isSubRow and itemString and not self._items[itemString] then
|
||||
return true
|
||||
end
|
||||
end
|
||||
if self._str ~= "" and name then
|
||||
name = strlower(name)
|
||||
if not strmatch(name, self._strMatch) or (self._exact and name ~= self._strLower) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
if self._minLevel ~= -math.huge or self._maxLevel ~= math.huge then
|
||||
local minLevel = TSM.IsShadowlands() and ItemString.IsPet(baseItemString) and (itemLevel or maxItemLevel) or ItemInfo.GetMinLevel(baseItemString)
|
||||
if minLevel < self._minLevel or minLevel > self._maxLevel then
|
||||
return true
|
||||
end
|
||||
end
|
||||
if itemLevel and (itemLevel < self._minItemLevel or itemLevel > self._maxItemLevel) then
|
||||
return true
|
||||
end
|
||||
if maxItemLevel and maxItemLevel < self._minItemLevel then
|
||||
return true
|
||||
end
|
||||
if quality and (quality < self._minQuality or quality > self._maxQuality) then
|
||||
return true
|
||||
end
|
||||
if self._class and ItemInfo.GetClassId(baseItemString) ~= self._class then
|
||||
return true
|
||||
end
|
||||
if self._subClass and ItemInfo.GetSubClassId(baseItemString) ~= self._subClass then
|
||||
return true
|
||||
end
|
||||
if self._invType and ItemInfo.GetInvSlotId(baseItemString) ~= self._invType then
|
||||
return true
|
||||
end
|
||||
if self._unlearned and CanIMogIt:PlayerKnowsTransmog(ItemInfo.GetLink(baseItemString)) then
|
||||
return true
|
||||
end
|
||||
if self._canLearn and not CanIMogIt:CharacterCanLearnTransmog(ItemInfo.GetLink(baseItemString)) then
|
||||
return true
|
||||
end
|
||||
if itemBuyout and (itemBuyout < self._minPrice or itemBuyout > self._maxPrice) then
|
||||
return true
|
||||
end
|
||||
if minItemBuyout and minItemBuyout > self._maxPrice then
|
||||
return true
|
||||
end
|
||||
for func in pairs(self._customFilters) do
|
||||
if func(self, row, isSubRow, itemKey) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function AuctionQuery._BrowseIsDone(self, isRetry)
|
||||
if TSM.IsWowClassic() then
|
||||
local numAuctions, totalAuctions = GetNumAuctionItems("list")
|
||||
if totalAuctions <= NUM_AUCTION_ITEMS_PER_PAGE and numAuctions ~= totalAuctions then
|
||||
-- there are cases where we get (0, 1) from the API - no idea why so just assume we're not done
|
||||
return false
|
||||
end
|
||||
local numPages = ceil(totalAuctions / NUM_AUCTION_ITEMS_PER_PAGE)
|
||||
if self._getAll then
|
||||
return true
|
||||
end
|
||||
if self._specifiedPage then
|
||||
if isRetry then
|
||||
return false
|
||||
end
|
||||
-- check if we're on the right page
|
||||
local specifiedPage = (self._specifiedPage == "FIRST" and 0) or (self._specifiedPage == "LAST" and numPages - 1) or self._specifiedPage
|
||||
return self._page == specifiedPage
|
||||
elseif self._isBrowseDoneFunc and self._isBrowseDoneFunc(self) then
|
||||
return true
|
||||
else
|
||||
return self._page >= numPages
|
||||
end
|
||||
else
|
||||
if self._isBrowseDoneFunc and self._isBrowseDoneFunc(self) then
|
||||
return true
|
||||
end
|
||||
return C_AuctionHouse.HasFullBrowseResults()
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionQuery._BrowseIsPageValid(self)
|
||||
if TSM.IsWowClassic() then
|
||||
if self._specifiedPage then
|
||||
return self:_BrowseIsDone()
|
||||
else
|
||||
return true
|
||||
end
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionQuery._BrowseRequestMore(self, isRetry)
|
||||
if TSM.IsWowClassic() then
|
||||
assert(not self._getAll)
|
||||
if self._specifiedPage then
|
||||
return self:_SendWowQuery()
|
||||
end
|
||||
if not isRetry then
|
||||
self._page = self._page + 1
|
||||
end
|
||||
return self:_SendWowQuery()
|
||||
else
|
||||
return AuctionHouseWrapper.RequestMoreBrowseResults()
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionQuery._OnSubRowRemoved(self, row)
|
||||
local baseItemString = row:GetBaseItemString()
|
||||
assert(row == self._browseResults[baseItemString])
|
||||
if row:GetNumSubRows() == 0 then
|
||||
self._browseResults[baseItemString] = nil
|
||||
row:Release()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.ItemIteratorHelper(items, index)
|
||||
while true do
|
||||
local itemString, itemType = next(items, index)
|
||||
if not itemString then
|
||||
return
|
||||
elseif itemType == ITEM_SPECIFIC then
|
||||
return itemString
|
||||
end
|
||||
index = itemString
|
||||
end
|
||||
end
|
||||
134
LibTSM/Service/AuctionScanClasses/QueryUtil.lua
Normal file
134
LibTSM/Service/AuctionScanClasses/QueryUtil.lua
Normal file
@@ -0,0 +1,134 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local QueryUtil = TSM.Init("Service.AuctionScanClasses.QueryUtil")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local Threading = TSM.Include("Service.Threading")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local Query = TSM.Include("Service.AuctionScanClasses.Query")
|
||||
local private = {
|
||||
itemListSortValue = {},
|
||||
}
|
||||
local MAX_ITEM_INFO_RETRIES = 30
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function QueryUtil.GenerateThreaded(itemList, callback, context)
|
||||
-- get all the item info into the game's cache
|
||||
for _ = 1, MAX_ITEM_INFO_RETRIES do
|
||||
local isMissingItemInfo = false
|
||||
for _, itemString in ipairs(itemList) do
|
||||
if not private.HasInfo(itemString) then
|
||||
isMissingItemInfo = true
|
||||
end
|
||||
Threading.Yield()
|
||||
end
|
||||
if not isMissingItemInfo then
|
||||
break
|
||||
end
|
||||
Threading.Sleep(0.1)
|
||||
end
|
||||
|
||||
-- remove items we're missing info for
|
||||
for i = #itemList, 1, -1 do
|
||||
if not private.HasInfo(itemList[i]) then
|
||||
Log.Err("Missing item info for %s", itemList[i])
|
||||
tremove(itemList, i)
|
||||
end
|
||||
Threading.Yield()
|
||||
end
|
||||
if #itemList == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
-- add all the items
|
||||
if TSM.IsWowClassic() then
|
||||
for _, itemString in ipairs(itemList) do
|
||||
private.GenerateQuery(callback, context, itemString, private.GetItemQueryInfo(itemString))
|
||||
end
|
||||
else
|
||||
-- sort the item list so all base items are grouped together but keep relative ordering between base items the same
|
||||
wipe(private.itemListSortValue)
|
||||
for i, itemString in ipairs(itemList) do
|
||||
local baseItemString = ItemString.GetBaseFast(itemString)
|
||||
private.itemListSortValue[baseItemString] = private.itemListSortValue[baseItemString] or i
|
||||
private.itemListSortValue[itemString] = private.itemListSortValue[baseItemString]
|
||||
end
|
||||
sort(itemList, private.ItemListSortHelper)
|
||||
local currentBaseItemString = nil
|
||||
local currentItems = TempTable.Acquire()
|
||||
for _, itemString in ipairs(itemList) do
|
||||
local baseItemString = ItemString.GetBaseFast(itemString)
|
||||
assert(baseItemString)
|
||||
if baseItemString == currentBaseItemString then
|
||||
-- same base item
|
||||
tinsert(currentItems, itemString)
|
||||
else
|
||||
-- new base item
|
||||
if currentBaseItemString then
|
||||
private.GenerateQuery(callback, context, currentItems, ItemInfo.GetName(currentBaseItemString))
|
||||
wipe(currentItems)
|
||||
end
|
||||
currentBaseItemString = baseItemString
|
||||
tinsert(currentItems, itemString)
|
||||
end
|
||||
end
|
||||
if currentBaseItemString then
|
||||
private.GenerateQuery(callback, context, currentItems, ItemInfo.GetName(currentBaseItemString))
|
||||
wipe(currentItems)
|
||||
end
|
||||
TempTable.Release(currentItems)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.GetItemQueryInfo(itemString)
|
||||
local name = ItemInfo.GetName(itemString)
|
||||
local level = ItemInfo.GetMinLevel(itemString) or 0
|
||||
local quality = ItemInfo.GetQuality(itemString)
|
||||
local classId = ItemInfo.GetClassId(itemString) or 0
|
||||
local subClassId = ItemInfo.GetSubClassId(itemString) or 0
|
||||
-- Ignoring level because level can now vary
|
||||
if itemString == ItemString.GetBase(itemString) and (classId == LE_ITEM_CLASS_WEAPON or classId == LE_ITEM_CLASS_ARMOR or (classId == LE_ITEM_CLASS_GEM and subClassId == LE_ITEM_GEM_ARTIFACTRELIC)) then
|
||||
level = nil
|
||||
end
|
||||
return name, level, level, quality, classId, subClassId
|
||||
end
|
||||
|
||||
function private.HasInfo(itemString)
|
||||
return ItemInfo.GetName(itemString) and ItemInfo.GetQuality(itemString) and ItemInfo.GetMinLevel(itemString)
|
||||
end
|
||||
|
||||
function private.GenerateQuery(callback, context, items, name, minLevel, maxLevel, quality, class, subClass)
|
||||
local query = Query.Get()
|
||||
:SetStr(name, false)
|
||||
:SetQualityRange(quality, quality)
|
||||
:SetLevelRange(minLevel, maxLevel)
|
||||
:SetClass(class, subClass)
|
||||
:SetItems(items)
|
||||
callback(query, context)
|
||||
end
|
||||
|
||||
function private.ItemListSortHelper(a, b)
|
||||
local aSortValue = private.itemListSortValue[a]
|
||||
local bSortValue = private.itemListSortValue[b]
|
||||
if aSortValue ~= bSortValue then
|
||||
return aSortValue < bSortValue
|
||||
end
|
||||
return a < b
|
||||
end
|
||||
671
LibTSM/Service/AuctionScanClasses/ResultRow.lua
Normal file
671
LibTSM/Service/AuctionScanClasses/ResultRow.lua
Normal file
@@ -0,0 +1,671 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local ResultRow = TSM.Init("Service.AuctionScanClasses.ResultRow")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local ObjectPool = TSM.Include("Util.ObjectPool")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Table = TSM.Include("Util.Table")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local LibTSMClass = TSM.Include("LibTSMClass")
|
||||
local Util = TSM.Include("Service.AuctionScanClasses.Util")
|
||||
local AuctionHouseWrapper = TSM.Include("Service.AuctionHouseWrapper")
|
||||
local ResultSubRow = TSM.Include("Service.AuctionScanClasses.ResultSubRow")
|
||||
local ResultRowWrapper = LibTSMClass.DefineClass("ResultRowWrapper")
|
||||
local private = {
|
||||
objectPool = ObjectPool.New("AUCTION_SCAN_RESULT_ROW", ResultRowWrapper),
|
||||
}
|
||||
local SUB_ROW_SEARCH_INDEX_MULTIPLIER = 1000000
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function ResultRow.Get(query, itemKey, minPrice, totalQuantity)
|
||||
local row = private.objectPool:Get()
|
||||
row:_Acquire(query, itemKey, minPrice, totalQuantity)
|
||||
return row
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- ResultRowWrapper - Meta Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
function ResultRowWrapper.__init(self)
|
||||
self._query = nil
|
||||
self._items = {}
|
||||
self._baseItemString = nil
|
||||
self._canHaveNonBaseItemString = nil
|
||||
self._minPrice = nil
|
||||
self._hasItemInfo = nil
|
||||
self._isCommodity = nil
|
||||
self._notFiltered = false
|
||||
self._searchIndex = nil
|
||||
self._subRows = {}
|
||||
self._minBrowseId = nil
|
||||
end
|
||||
|
||||
function ResultRowWrapper._Acquire(self, query, item, minPrice, totalQuantity)
|
||||
self._query = query
|
||||
if TSM.IsWowClassic() then
|
||||
assert(not minPrice and not totalQuantity)
|
||||
tinsert(self._items, item)
|
||||
self._baseItemString = ItemString.GetBase(item)
|
||||
else
|
||||
item._minPrice = minPrice
|
||||
item._totalQuantity = totalQuantity
|
||||
tinsert(self._items, item)
|
||||
self._baseItemString = ItemString.GetBaseFromItemKey(item)
|
||||
end
|
||||
self._canHaveNonBaseItemString = nil
|
||||
self._minPrice = nil
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- ResultRowWrapper - Public Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
function ResultRowWrapper.Merge(self, item, minPrice, totalQuantity)
|
||||
-- check if we already have this item
|
||||
for i = 1, #self._items do
|
||||
if item == self._items[i] then
|
||||
return
|
||||
end
|
||||
if type(item) == "table" then
|
||||
local isEqual = true
|
||||
for k in pairs(item) do
|
||||
if item[k] ~= self._items[i][k] then
|
||||
isEqual = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if isEqual then
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
self._hasItemInfo = nil
|
||||
if TSM.IsWowClassic() then
|
||||
assert(not minPrice and not totalQuantity)
|
||||
assert(self._baseItemString == ItemString.GetBase(item))
|
||||
tinsert(self._items, item)
|
||||
self._notFiltered = false
|
||||
else
|
||||
assert(self._baseItemString == ItemString.GetBaseFromItemKey(item))
|
||||
item._minPrice = minPrice
|
||||
item._totalQuantity = totalQuantity
|
||||
tinsert(self._items, item)
|
||||
self._notFiltered = false
|
||||
end
|
||||
self._canHaveNonBaseItemString = nil
|
||||
end
|
||||
|
||||
function ResultRowWrapper.Release(self)
|
||||
wipe(self._items)
|
||||
self._baseItemString = nil
|
||||
self._canHaveNonBaseItemString = nil
|
||||
self._minPrice = nil
|
||||
self._hasItemInfo = nil
|
||||
self._isCommodity = nil
|
||||
self._notFiltered = false
|
||||
self._searchIndex = nil
|
||||
self._minBrowseId = nil
|
||||
for _, subRow in pairs(self._subRows) do
|
||||
subRow:Release()
|
||||
end
|
||||
wipe(self._subRows)
|
||||
private.objectPool:Recycle(self)
|
||||
end
|
||||
|
||||
function ResultRowWrapper.IsSubRow(self)
|
||||
return false
|
||||
end
|
||||
|
||||
function ResultRowWrapper.PopulateBrowseData(self)
|
||||
assert(self._baseItemString)
|
||||
if self._hasItemInfo then
|
||||
-- already have our item info
|
||||
return true
|
||||
elseif not Util.HasItemInfo(self._baseItemString) then
|
||||
-- don't have item info yet
|
||||
return false
|
||||
end
|
||||
|
||||
if not TSM.IsWowClassic() then
|
||||
-- cache the commodity status since it's referenced a ton
|
||||
if self._isCommodity == nil then
|
||||
self._isCommodity = ItemInfo.IsCommodity(self._baseItemString)
|
||||
assert(self._isCommodity ~= nil)
|
||||
end
|
||||
end
|
||||
|
||||
-- check if we have info for all the items and try to fetch it if not
|
||||
local missingInfo = false
|
||||
for _, item in ipairs(self._items) do
|
||||
if TSM.IsWowClassic() then
|
||||
if not Util.HasItemInfo(ItemString.Get(item)) then
|
||||
missingInfo = true
|
||||
end
|
||||
else
|
||||
if not item._itemKeyInfo then
|
||||
item._itemKeyInfo = C_AuctionHouse.GetItemKeyInfo(item, true)
|
||||
if not item._itemKeyInfo then
|
||||
missingInfo = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if missingInfo then
|
||||
return false
|
||||
end
|
||||
|
||||
self._hasItemInfo = true
|
||||
return true
|
||||
end
|
||||
|
||||
function ResultRowWrapper.IsFiltered(self, query)
|
||||
assert(#self._items > 0)
|
||||
if self._notFiltered then
|
||||
return false
|
||||
end
|
||||
|
||||
-- check if the whole row is filtered
|
||||
if query:_IsFiltered(self, false) then
|
||||
return true
|
||||
end
|
||||
|
||||
-- filter our items
|
||||
for i = #self._items, 1, -1 do
|
||||
if query:_IsFiltered(self, false, self._items[i]) then
|
||||
tremove(self._items, i)
|
||||
end
|
||||
end
|
||||
self._canHaveNonBaseItemString = nil
|
||||
self._minPrice = nil
|
||||
if #self._items == 0 then
|
||||
-- no more items, so the entire row is filtered
|
||||
return true
|
||||
end
|
||||
|
||||
-- not filtered (cache this result)
|
||||
self._notFiltered = true
|
||||
return false
|
||||
end
|
||||
|
||||
function ResultRowWrapper.SearchReset(self)
|
||||
assert(not TSM.IsWowClassic())
|
||||
assert(#self._items > 0)
|
||||
self._searchIndex = 1
|
||||
end
|
||||
|
||||
function ResultRowWrapper.SearchNext(self)
|
||||
assert(not TSM.IsWowClassic())
|
||||
assert(self._searchIndex)
|
||||
if self._searchIndex == #self._items then
|
||||
self._searchIndex = nil
|
||||
return false
|
||||
end
|
||||
self._searchIndex = self._searchIndex + 1
|
||||
return true
|
||||
end
|
||||
|
||||
function ResultRowWrapper.SearchIsReady(self)
|
||||
assert(not TSM.IsWowClassic())
|
||||
assert(self._searchIndex)
|
||||
-- the client needs to have the item key info cached before we can run the search
|
||||
return C_AuctionHouse.GetItemKeyInfo(self._items[self._searchIndex], true) and true or false
|
||||
end
|
||||
|
||||
function ResultRowWrapper.SearchSend(self)
|
||||
assert(not TSM.IsWowClassic())
|
||||
assert(self._searchIndex)
|
||||
local itemKey = self._items[self._searchIndex]
|
||||
-- send a sell query if we don't have browse results for the itemKey
|
||||
-- for some reason sell queries don't work for commodities or pets
|
||||
local isSellQuery = not self._isCommodity and not ItemString.IsPet(self._baseItemString) and not itemKey._totalQuantity
|
||||
return AuctionHouseWrapper.SendSearchQuery(itemKey, isSellQuery)
|
||||
end
|
||||
|
||||
function ResultRowWrapper.HasCachedSearchData(self)
|
||||
local itemKey = self._items[self._searchIndex]
|
||||
if self._isCommodity then
|
||||
return C_AuctionHouse.HasFullCommoditySearchResults(itemKey.itemID)
|
||||
else
|
||||
return C_AuctionHouse.HasFullItemSearchResults(itemKey)
|
||||
end
|
||||
end
|
||||
|
||||
function ResultRowWrapper.SearchCheckStatus(self)
|
||||
assert(not TSM.IsWowClassic())
|
||||
assert(self._searchIndex)
|
||||
local itemKey = self._items[self._searchIndex]
|
||||
|
||||
-- check if we have the full results
|
||||
local hasFullResults = nil
|
||||
if self._isCommodity then
|
||||
hasFullResults = C_AuctionHouse.HasFullCommoditySearchResults(itemKey.itemID)
|
||||
else
|
||||
hasFullResults = C_AuctionHouse.HasFullItemSearchResults(itemKey)
|
||||
end
|
||||
if hasFullResults then
|
||||
return true
|
||||
end
|
||||
|
||||
-- request more results
|
||||
if self._isCommodity then
|
||||
return false, AuctionHouseWrapper.RequestMoreCommoditySearchResults(itemKey.itemID)
|
||||
else
|
||||
return false, AuctionHouseWrapper.RequestMoreItemSearchResults(itemKey)
|
||||
end
|
||||
end
|
||||
|
||||
function ResultRowWrapper.PopulateSubRows(self, browseId, index, itemLink)
|
||||
if TSM.IsWowClassic() then
|
||||
-- remove any prior results with a different browseId
|
||||
assert(index and not self._searchIndex)
|
||||
local subRow = ResultSubRow.Get(self)
|
||||
subRow:_SetRawData(index, browseId, itemLink)
|
||||
local _, hashNoSeller = subRow:GetHashes()
|
||||
if self._minBrowseId and self._minBrowseId ~= browseId then
|
||||
-- check if this subRow already exists with a prior browseId
|
||||
for i, existingSubRow in ipairs(self._subRows) do
|
||||
local _, existingHashNoSeller = existingSubRow:GetHashes()
|
||||
local _, _, existingBrowseId = existingSubRow:GetListingInfo()
|
||||
if hashNoSeller == existingHashNoSeller and browseId ~= existingBrowseId then
|
||||
-- replace the existing subRow
|
||||
existingSubRow:Release()
|
||||
self._subRows[i] = subRow
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
tinsert(self._subRows, subRow)
|
||||
else
|
||||
assert(self._searchIndex and not index)
|
||||
local subRowOffset = self._searchIndex * SUB_ROW_SEARCH_INDEX_MULTIPLIER
|
||||
local itemKey = self._items[self._searchIndex]
|
||||
local numAuctions = nil
|
||||
if self:IsCommodity() then
|
||||
numAuctions = C_AuctionHouse.GetNumCommoditySearchResults(itemKey.itemID)
|
||||
else
|
||||
numAuctions = C_AuctionHouse.GetNumItemSearchResults(itemKey)
|
||||
end
|
||||
if itemKey._numAuctions and numAuctions ~= itemKey._numAuctions then
|
||||
-- the results changed so clear out our existing data
|
||||
for i = itemKey._numAuctions, 1, -1 do
|
||||
if i > numAuctions then
|
||||
self._subRows[subRowOffset + i]:Release()
|
||||
self._subRows[subRowOffset + i] = nil
|
||||
else
|
||||
self._subRows[subRowOffset + i]:_SetRawData(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
itemKey._numAuctions = numAuctions
|
||||
for i = 1, numAuctions do
|
||||
self._subRows[subRowOffset + i] = self._subRows[subRowOffset + i] or ResultSubRow.Get(self)
|
||||
local subRow = self._subRows[subRowOffset + i]
|
||||
if not subRow:HasRawData() or not subRow:HasOwners() then
|
||||
local result = nil
|
||||
if self:IsCommodity() then
|
||||
result = C_AuctionHouse.GetCommoditySearchResultInfo(itemKey.itemID, i)
|
||||
else
|
||||
result = C_AuctionHouse.GetItemSearchResultInfo(itemKey, i)
|
||||
end
|
||||
subRow:_SetRawData(result, browseId)
|
||||
end
|
||||
end
|
||||
end
|
||||
self._minBrowseId = min(self._minBrowseId or math.huge, browseId)
|
||||
end
|
||||
|
||||
function ResultRowWrapper.FilterSubRows(self, query)
|
||||
local subRowOffset = TSM.IsWowClassic() and 0 or (self._searchIndex * SUB_ROW_SEARCH_INDEX_MULTIPLIER)
|
||||
if TSM.IsWowClassic() then
|
||||
for i = #self._subRows, 1, -1 do
|
||||
if query:_IsFiltered(self._subRows[i], true) then
|
||||
self:_RemoveSubRowByIndex(i)
|
||||
end
|
||||
end
|
||||
else
|
||||
local itemKey = self._items[self._searchIndex]
|
||||
for j = itemKey._numAuctions, 1, -1 do
|
||||
local subRow = self._subRows[subRowOffset + j]
|
||||
if query:_IsFiltered(subRow, true) then
|
||||
self:_RemoveSubRowByIndex(j)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- merge subRows with identical hashes
|
||||
local numSubRows = nil
|
||||
local hashIndexLookup = TempTable.Acquire()
|
||||
local index = 1
|
||||
while true do
|
||||
numSubRows = TSM.IsWowClassic() and #self._subRows or self._items[self._searchIndex]._numAuctions
|
||||
if index > numSubRows then
|
||||
break
|
||||
end
|
||||
local subRow = self._subRows[subRowOffset + index]
|
||||
local hash = subRow:GetHashes()
|
||||
local prevIndex = hashIndexLookup[hash]
|
||||
if prevIndex then
|
||||
-- there was a previous subRow with the same hash
|
||||
self._subRows[subRowOffset + prevIndex]:Merge(subRow)
|
||||
-- remove this subRow
|
||||
self:_RemoveSubRowByIndex(index)
|
||||
else
|
||||
hashIndexLookup[hash] = index
|
||||
index = index + 1
|
||||
end
|
||||
end
|
||||
TempTable.Release(hashIndexLookup)
|
||||
return numSubRows == 0
|
||||
end
|
||||
|
||||
function ResultRowWrapper.GetNumSubRows(self)
|
||||
if TSM.IsWowClassic() then
|
||||
return #self._subRows
|
||||
else
|
||||
local result = 0
|
||||
for _, itemKey in ipairs(self._items) do
|
||||
result = result + (itemKey._numAuctions or 0)
|
||||
end
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
function ResultRowWrapper.SubRowIterator(self, searchOnly)
|
||||
if TSM.IsWowClassic() then
|
||||
return ipairs(self._subRows)
|
||||
else
|
||||
if searchOnly then
|
||||
local result = TempTable.Acquire()
|
||||
assert(self._searchIndex)
|
||||
for i = 1, self._items[self._searchIndex]._numAuctions do
|
||||
local subRow = self._subRows[self._searchIndex * SUB_ROW_SEARCH_INDEX_MULTIPLIER + i]
|
||||
assert(subRow)
|
||||
tinsert(result, subRow)
|
||||
end
|
||||
return TempTable.Iterator(result)
|
||||
else
|
||||
return private.SubRowIteratorHelper, self, SUB_ROW_SEARCH_INDEX_MULTIPLIER
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ResultRowWrapper.IsCommodity(self)
|
||||
assert(self._isCommodity ~= nil)
|
||||
return self._isCommodity
|
||||
end
|
||||
|
||||
function ResultRowWrapper.HasItemInfo(self)
|
||||
return self._hasItemInfo
|
||||
end
|
||||
|
||||
function ResultRowWrapper.GetBaseItemString(self)
|
||||
return self._baseItemString
|
||||
end
|
||||
|
||||
function ResultRowWrapper.GetItemString(self)
|
||||
if TSM.IsWowClassic() or not self._hasItemInfo or self._canHaveNonBaseItemString then
|
||||
return nil
|
||||
end
|
||||
if self._canHaveNonBaseItemString == nil then
|
||||
for _, itemKey in ipairs(self._items) do
|
||||
if ItemInfo.CanHaveVariations(self._baseItemString) or itemKey.battlePetSpeciesID ~= 0 or itemKey.itemSuffix ~= 0 or itemKey.itemLevel ~= 0 then
|
||||
-- this item can have variations, so we don't know its itemString
|
||||
self._canHaveNonBaseItemString = true
|
||||
return nil
|
||||
end
|
||||
end
|
||||
self._canHaveNonBaseItemString = false
|
||||
end
|
||||
return self._baseItemString
|
||||
end
|
||||
|
||||
function ResultRowWrapper.GetItemInfo(self, itemKey)
|
||||
if TSM.IsWowClassic() or not self._hasItemInfo then
|
||||
return nil, nil, nil, nil
|
||||
end
|
||||
itemKey = itemKey or (#self._items == 1 and self._items[1] or nil)
|
||||
assert(not itemKey or itemKey._itemKeyInfo)
|
||||
local baseItemString = self:GetBaseItemString()
|
||||
local itemString = self:GetItemString()
|
||||
local itemName, quality, itemLevel, maxItemLevel = nil, nil, nil, nil
|
||||
if itemString then
|
||||
-- this item can't have variations, so we can know the name / level / quality
|
||||
itemName = ItemInfo.GetName(baseItemString)
|
||||
itemLevel = ItemInfo.GetItemLevel(baseItemString)
|
||||
quality = ItemInfo.GetQuality(baseItemString)
|
||||
assert(itemName and itemLevel and quality)
|
||||
else
|
||||
if itemKey and not itemKey._totalQuantity then
|
||||
-- if we didn't do a browse, then don't use this itemKey
|
||||
itemKey = nil
|
||||
end
|
||||
if itemKey then
|
||||
-- grab the name from the itemKeyInfo
|
||||
itemName = itemKey._itemKeyInfo.itemName
|
||||
assert(itemName)
|
||||
end
|
||||
local hasSingleAuction = itemKey and itemKey._totalQuantity == 1
|
||||
if hasSingleAuction then
|
||||
-- grab the quality from the itemKeyInfo since there's only one listing
|
||||
quality = itemKey._itemKeyInfo.quality
|
||||
assert(quality)
|
||||
end
|
||||
if not ItemString.IsPet(self._baseItemString) then
|
||||
-- for non-pets, we can maybe grab the itemLevel from the itemKey
|
||||
if itemKey then
|
||||
itemLevel = itemKey.itemLevel ~= 0 and itemKey.itemLevel or nil
|
||||
else
|
||||
-- only use the itemLevel from the itemKeys if they are all the same
|
||||
local itemKeyItemLevel = self._items[1].itemLevel
|
||||
for i = 2, #self._items do
|
||||
if self._items[i].itemLevel ~= itemKeyItemLevel then
|
||||
itemKeyItemLevel = nil
|
||||
break
|
||||
end
|
||||
end
|
||||
itemLevel = (itemKeyItemLevel or 0) ~= 0 and itemKeyItemLevel or nil
|
||||
end
|
||||
elseif itemKey and itemKey._itemKeyInfo.battlePetLink then
|
||||
if hasSingleAuction then
|
||||
-- grab the itemLevel from the link since there's only one listing
|
||||
itemLevel = ItemInfo.GetItemLevel(itemKey._itemKeyInfo.battlePetLink)
|
||||
assert(itemLevel)
|
||||
else
|
||||
-- grab the maxItemLevel from the link
|
||||
maxItemLevel = ItemInfo.GetItemLevel(itemKey._itemKeyInfo.battlePetLink)
|
||||
assert(maxItemLevel)
|
||||
end
|
||||
end
|
||||
end
|
||||
return itemName, quality, itemLevel, maxItemLevel
|
||||
end
|
||||
|
||||
function ResultRowWrapper.GetBuyouts(self, resultItemKey)
|
||||
if TSM.IsWowClassic() then
|
||||
return nil, nil, nil
|
||||
end
|
||||
assert(#self._items > 0)
|
||||
if resultItemKey then
|
||||
return nil, nil, resultItemKey._minPrice
|
||||
else
|
||||
if self._minPrice == nil then
|
||||
for _, itemKey in ipairs(self._items) do
|
||||
if not itemKey._minPrice then
|
||||
self._minPrice = -1
|
||||
return nil, nil, nil
|
||||
end
|
||||
self._minPrice = min(self._minPrice or math.huge, itemKey._minPrice)
|
||||
end
|
||||
elseif self._minPrice == -1 then
|
||||
return nil, nil, nil
|
||||
end
|
||||
return nil, nil, self._minPrice
|
||||
end
|
||||
end
|
||||
|
||||
function ResultRowWrapper.GetQuantities(self)
|
||||
local totalQuantity = 0
|
||||
if TSM.IsWowClassic() then
|
||||
for _, subRow in ipairs(self._subRows) do
|
||||
local quantity, numAuctions = subRow:GetQuantities()
|
||||
totalQuantity = totalQuantity + quantity * numAuctions
|
||||
end
|
||||
else
|
||||
for _, itemKey in ipairs(self._items) do
|
||||
if not itemKey._totalQuantity then
|
||||
return
|
||||
end
|
||||
totalQuantity = totalQuantity + itemKey._totalQuantity
|
||||
end
|
||||
end
|
||||
return totalQuantity, 1
|
||||
end
|
||||
|
||||
function ResultRowWrapper.GetMaxQuantities(self)
|
||||
assert(self:IsCommodity())
|
||||
local totalQuantity = 0
|
||||
for _, subRow in self:SubRowIterator() do
|
||||
local _, numOwnerItems = subRow:GetOwnerInfo()
|
||||
local quantityAvailable = subRow:GetQuantities() - numOwnerItems
|
||||
totalQuantity = totalQuantity + quantityAvailable
|
||||
end
|
||||
return totalQuantity
|
||||
end
|
||||
|
||||
function ResultRowWrapper.RemoveSubRow(self, subRow)
|
||||
local index = Table.KeyByValue(self._subRows, subRow)
|
||||
if TSM.IsWowClassic() then
|
||||
self:_RemoveSubRowByIndex(index)
|
||||
else
|
||||
local searchIndex = floor(index / SUB_ROW_SEARCH_INDEX_MULTIPLIER)
|
||||
index = index % SUB_ROW_SEARCH_INDEX_MULTIPLIER
|
||||
assert(self._subRows[searchIndex * SUB_ROW_SEARCH_INDEX_MULTIPLIER + index] == subRow)
|
||||
local prevSearchIndex = self._searchIndex
|
||||
self._searchIndex = searchIndex
|
||||
self:_RemoveSubRowByIndex(index)
|
||||
self._searchIndex = prevSearchIndex
|
||||
end
|
||||
self._query:_OnSubRowRemoved(self)
|
||||
end
|
||||
|
||||
function ResultRowWrapper.WipeSearchResults(self)
|
||||
wipe(self._subRows)
|
||||
if not TSM.IsWowClassic() then
|
||||
for _, itemKey in ipairs(self._items) do
|
||||
itemKey._numAuctions = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ResultRowWrapper.GetQuery(self)
|
||||
return self._query
|
||||
end
|
||||
|
||||
function ResultRowWrapper.DecrementQuantity(self, amount)
|
||||
assert(self:IsCommodity() and not TSM.IsWowClassic() and #self._items == 1)
|
||||
local index = 1
|
||||
while amount > 0 do
|
||||
local subRow = self._subRows[index + SUB_ROW_SEARCH_INDEX_MULTIPLIER]
|
||||
assert(subRow)
|
||||
local _, numOwnerItems = subRow:GetOwnerInfo()
|
||||
local quantityAvailable = subRow:GetQuantities() - numOwnerItems
|
||||
if quantityAvailable > 0 then
|
||||
local usedQuantity = min(quantityAvailable, amount)
|
||||
local prevItemBuyout = floor(subRow._buyout / subRow._quantity)
|
||||
amount = amount - usedQuantity
|
||||
subRow._quantity = subRow._quantity - usedQuantity
|
||||
subRow._buyout = prevItemBuyout * subRow._quantity
|
||||
subRow._minBid = subRow._buyout
|
||||
if numOwnerItems == 0 and subRow._quantity == 0 then
|
||||
self:RemoveSubRow(subRow)
|
||||
else
|
||||
index = index + 1
|
||||
end
|
||||
else
|
||||
index = index + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ResultRowWrapper.GetMinBrowseId(self)
|
||||
return self._minBrowseId
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- ResultRowWrapper - Private Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
function ResultRowWrapper._RemoveSubRowByIndex(self, index)
|
||||
if TSM.IsWowClassic() then
|
||||
self._subRows[index]:Release()
|
||||
tremove(self._subRows, index)
|
||||
else
|
||||
local subRowOffset = self._searchIndex * SUB_ROW_SEARCH_INDEX_MULTIPLIER
|
||||
local itemKey = self._items[self._searchIndex]
|
||||
self._subRows[subRowOffset + index]:Release()
|
||||
self._subRows[subRowOffset + index] = nil
|
||||
-- shift the other subRows for this item down
|
||||
for i = index, itemKey._numAuctions - 1 do
|
||||
self._subRows[subRowOffset + i] = self._subRows[subRowOffset + i + 1]
|
||||
end
|
||||
self._subRows[subRowOffset + itemKey._numAuctions] = nil
|
||||
itemKey._numAuctions = itemKey._numAuctions - 1
|
||||
end
|
||||
end
|
||||
|
||||
function ResultRowWrapper._GetSearchProgress(self)
|
||||
assert(not TSM.IsWowClassic())
|
||||
if #self._items == 0 then
|
||||
return 0
|
||||
end
|
||||
local numSearched = 0
|
||||
for _, itemKey in ipairs(self._items) do
|
||||
if itemKey._numAuctions then
|
||||
numSearched = numSearched + 1
|
||||
end
|
||||
end
|
||||
return numSearched / #self._items
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.SubRowIteratorHelper(row, index)
|
||||
local searchIndex = floor(index / SUB_ROW_SEARCH_INDEX_MULTIPLIER)
|
||||
local subRowIndex = index % SUB_ROW_SEARCH_INDEX_MULTIPLIER
|
||||
while true do
|
||||
local itemKey = row._items[searchIndex]
|
||||
if not itemKey then
|
||||
return
|
||||
end
|
||||
|
||||
if subRowIndex >= (itemKey._numAuctions or 0) then
|
||||
searchIndex = searchIndex + 1
|
||||
subRowIndex = 0
|
||||
else
|
||||
subRowIndex = subRowIndex + 1
|
||||
index = searchIndex * SUB_ROW_SEARCH_INDEX_MULTIPLIER + subRowIndex
|
||||
return index, row._subRows[index]
|
||||
end
|
||||
end
|
||||
end
|
||||
364
LibTSM/Service/AuctionScanClasses/ResultSubRow.lua
Normal file
364
LibTSM/Service/AuctionScanClasses/ResultSubRow.lua
Normal file
@@ -0,0 +1,364 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local ResultSubRow = TSM.Init("Service.AuctionScanClasses.ResultSubRow")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local ObjectPool = TSM.Include("Util.ObjectPool")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local LibTSMClass = TSM.Include("LibTSMClass")
|
||||
local Util = TSM.Include("Service.AuctionScanClasses.Util")
|
||||
local ResultSubRowWrapper = LibTSMClass.DefineClass("ResultSubRowWrapper")
|
||||
local private = {
|
||||
objectPool = ObjectPool.New("AUCTION_SCAN_RESULT_SUB_ROW", ResultSubRowWrapper),
|
||||
ownersTemp = {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function ResultSubRow.Get(resultRow)
|
||||
local subRow = private.objectPool:Get()
|
||||
subRow:_Acquire(resultRow)
|
||||
return subRow
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- ResultSubRowWrapper - Meta Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
function ResultSubRowWrapper.__init(self)
|
||||
self._resultRow = nil
|
||||
self._itemLink = nil
|
||||
self._buyout = nil
|
||||
self._minBid = nil
|
||||
self._currentBid = nil
|
||||
self._minIncrement = nil
|
||||
self._isHighBidder = nil
|
||||
self._quantity = nil
|
||||
self._timeLeft = nil
|
||||
self._ownerStr = nil
|
||||
self._hasOwners = false
|
||||
self._numOwnerItems = nil
|
||||
self._auctionId = nil
|
||||
self._hash = nil
|
||||
self._hashNoSeller = nil
|
||||
self._browseId = nil
|
||||
self._numAuctions = 1
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper._Acquire(self, resultRow)
|
||||
self._resultRow = resultRow
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- ResultSubRowWrapper - Public Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
function ResultSubRowWrapper.Merge(self, other)
|
||||
if TSM.IsWowClassic() then
|
||||
self._numAuctions = self._numAuctions + other._numAuctions
|
||||
else
|
||||
if self:IsCommodity() then
|
||||
self._quantity = self._quantity + other._quantity
|
||||
self._numOwnerItems = self._numOwnerItems + other._numOwnerItems
|
||||
else
|
||||
self._numAuctions = self._numAuctions + other._numAuctions
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.Release(self)
|
||||
self._resultRow = nil
|
||||
self._numAuctions = 1
|
||||
self:_SetRawData(nil)
|
||||
private.objectPool:Recycle(self)
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.IsSubRow(self)
|
||||
return true
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.HasRawData(self)
|
||||
return self._timeLeft and true or false
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.HasOwners(self)
|
||||
return self._hasOwners
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.HasItemString(self)
|
||||
assert(self:HasRawData())
|
||||
local itemString = ItemString.Get(self._itemLink)
|
||||
if not Util.HasItemInfo(itemString) then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.IsCommodity(self)
|
||||
return self._resultRow:IsCommodity()
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetResultRow(self)
|
||||
return self._resultRow
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetBaseItemString(self)
|
||||
return self._resultRow:GetBaseItemString()
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetItemString(self)
|
||||
assert(self:HasRawData())
|
||||
local itemString = ItemString.Get(self._itemLink)
|
||||
return itemString or self._resultRow:GetItemString()
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetItemInfo(self)
|
||||
assert(self:HasItemString())
|
||||
local itemString = ItemString.Get(self._itemLink)
|
||||
local itemName = ItemInfo.GetName(itemString)
|
||||
local quality = ItemInfo.GetQuality(itemString)
|
||||
local itemLevel = ItemInfo.GetItemLevel(itemString)
|
||||
assert(itemName and quality and itemLevel)
|
||||
return itemName, quality, itemLevel, nil
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetBuyouts(self)
|
||||
assert(self:HasRawData())
|
||||
return self._buyout, floor(self._buyout / self._quantity), nil
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetBidInfo(self)
|
||||
assert(self:HasRawData())
|
||||
local itemMinBid = Math.Floor(self._minBid / self._quantity, TSM.IsWowClassic() and 1 or COPPER_PER_SILVER)
|
||||
return self._minBid, itemMinBid, self._currentBid, self._isHighBidder, self._minIncrement
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetRequiredBid(self)
|
||||
local requiredBid = nil
|
||||
if TSM.IsWowClassic() then
|
||||
requiredBid = self._currentBid == 0 and self._minBid or (self._currentBid + self._minIncrement)
|
||||
else
|
||||
requiredBid = self._minBid
|
||||
end
|
||||
return requiredBid
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetDisplayedBids(self)
|
||||
local displayedBid = self._currentBid == 0 and self._minBid or self._currentBid
|
||||
local itemDisplayedBid = Math.Floor(displayedBid / self._quantity, TSM.IsWowClassic() and 1 or COPPER_PER_SILVER)
|
||||
return displayedBid, itemDisplayedBid
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetLinks(self)
|
||||
assert(self:HasRawData())
|
||||
local rawLink = self._itemLink
|
||||
local itemLink = ItemInfo.GeneralizeLink(rawLink)
|
||||
return itemLink, rawLink
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetListingInfo(self)
|
||||
assert(self:HasRawData())
|
||||
return self._timeLeft, self._auctionId, self._browseId
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetQuantities(self)
|
||||
assert(self:HasRawData())
|
||||
return self._quantity, self._numAuctions
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetOwnerInfo(self)
|
||||
assert(self:HasRawData())
|
||||
return self._ownerStr, self._numOwnerItems
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetHashes(self)
|
||||
if not self._hash then
|
||||
assert(self:HasRawData())
|
||||
if TSM.IsWowClassic() then
|
||||
self._hash = strjoin("~", tostringall(self._itemLink, self._minBid, self._minIncrement, self._buyout, self._currentBid, self._ownerStr, self._timeLeft, self._quantity, self._isHighBidder))
|
||||
self._hashNoSeller = strjoin("~", tostringall(self._itemLink, self._minBid, self._minIncrement, self._buyout, self._currentBid, self._timeLeft, self._quantity, self._isHighBidder))
|
||||
else
|
||||
local baseItemString = self:GetBaseItemString()
|
||||
local itemMinBid = Math.Floor(self._minBid / self._quantity, COPPER_PER_SILVER)
|
||||
local itemBuyout = floor(self._buyout / self._quantity)
|
||||
local itemKeyId, itemKeySpeciesId = nil, nil
|
||||
if ItemString.IsPet(baseItemString) then
|
||||
itemKeyId = ItemString.ToId(ItemString.GetPetCage())
|
||||
itemKeySpeciesId = ItemString.ToId(baseItemString)
|
||||
elseif ItemString.IsItem(baseItemString) then
|
||||
itemKeyId = ItemString.ToId(baseItemString)
|
||||
itemKeySpeciesId = 0
|
||||
else
|
||||
error("Invalid baseItemString: "..tostring(baseItemString))
|
||||
end
|
||||
if self:IsCommodity() then
|
||||
self._hash = strjoin("~", tostringall(itemKeyId, itemBuyout, self._auctionId, self._ownerStr))
|
||||
self._hashNoSeller = strjoin("~", tostringall(itemKeyId, itemBuyout, self._auctionId))
|
||||
else
|
||||
self._hash = strjoin("~", tostringall(itemKeyId, itemKeySpeciesId, self._itemLink, itemMinBid, itemBuyout, self._currentBid, self._quantity, self._isHighBidder, self._ownerStr, self._auctionId))
|
||||
self._hashNoSeller = strjoin("~", tostringall(itemKeyId, itemKeySpeciesId, self._itemLink, itemMinBid, itemBuyout, self._currentBid, self._quantity, self._isHighBidder, self._auctionId))
|
||||
end
|
||||
end
|
||||
end
|
||||
return self._hash, self._hashNoSeller
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.EqualsIndex(self, index, noSeller)
|
||||
assert(TSM.IsWowClassic())
|
||||
local _, _, stackSize, _, _, _, _, minBid, minIncrement, buyout, bid, isHighBidder, _, seller, sellerFull = GetAuctionItemInfo("list", index)
|
||||
seller = Util.FixSellerName(seller, sellerFull) or "?"
|
||||
-- this is to get around a bug in Blizzard's code where the minIncrement value will be inconsistent for auctions where the player is the highest bidder
|
||||
minIncrement = isHighBidder and 0 or minIncrement
|
||||
if minBid ~= self._minBid or minIncrement ~= self._minIncrement or buyout ~= self._buyout or bid ~= self._currentBid or stackSize == self._quantity and isHighBidder ~= self._isHighBidder then
|
||||
return false
|
||||
elseif not noSeller and seller ~= self._ownerStr then
|
||||
return false
|
||||
elseif GetAuctionItemLink("list", index) ~= self._itemLink then
|
||||
return false
|
||||
elseif GetAuctionItemTimeLeft("list", index) ~= self._timeLeft then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.DecrementQuantity(self, amount)
|
||||
if TSM.IsWowClassic() then
|
||||
assert(amount == self._quantity)
|
||||
self._numAuctions = self._numAuctions - 1
|
||||
if self._numAuctions == 0 then
|
||||
self._resultRow:RemoveSubRow(self)
|
||||
end
|
||||
else
|
||||
if self:IsCommodity() then
|
||||
self._resultRow:DecrementQuantity(amount)
|
||||
else
|
||||
assert(amount == 1 and amount == self._quantity)
|
||||
self._numAuctions = self._numAuctions - 1
|
||||
assert(self._numOwnerItems <= self._numAuctions)
|
||||
if self._numAuctions == 0 then
|
||||
self._resultRow:RemoveSubRow(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.UpdateResultInfo(self, newAuctionId, newResultInfo)
|
||||
if newResultInfo then
|
||||
self:_SetRawData(newResultInfo, self._browseId)
|
||||
else
|
||||
self._auctionId = newAuctionId
|
||||
self._hash = nil
|
||||
self._hashNoSeller = nil
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- ResultRowWrapper - Private Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
function ResultSubRowWrapper._SetRawData(self, data, browseId, itemLink)
|
||||
self._hash = nil
|
||||
self._hashNoSeller = nil
|
||||
self._browseId = browseId
|
||||
if data then
|
||||
if TSM.IsWowClassic() then
|
||||
local _, _, stackSize, _, _, _, _, minBid, minIncrement, buyout, bid, isHighBidder, _, seller, sellerFull = GetAuctionItemInfo("list", data)
|
||||
seller = Util.FixSellerName(seller, sellerFull)
|
||||
-- this is to get around a bug in Blizzard's code where the minIncrement value will be inconsistent for auctions where the player is the highest bidder
|
||||
minIncrement = isHighBidder and 0 or minIncrement
|
||||
self._itemLink = itemLink
|
||||
self._buyout = buyout
|
||||
self._minBid = minBid
|
||||
self._currentBid = bid
|
||||
self._minIncrement = minIncrement
|
||||
self._isHighBidder = isHighBidder
|
||||
self._quantity = stackSize
|
||||
self._timeLeft = GetAuctionItemTimeLeft("list", data)
|
||||
self._ownerStr = seller or "?"
|
||||
self._hasOwners = seller and true or false
|
||||
self._numOwnerItems = 0
|
||||
self._auctionId = 0
|
||||
else
|
||||
if self._resultRow:IsCommodity() then
|
||||
local baseItemString = self._resultRow:GetBaseItemString()
|
||||
self._itemLink = ItemInfo.GetLink(baseItemString)
|
||||
else
|
||||
self._itemLink = data.itemLink
|
||||
end
|
||||
|
||||
if self:IsCommodity() then
|
||||
self._quantity = data.quantity
|
||||
self._buyout = data.unitPrice * data.quantity
|
||||
self._minBid = self._buyout
|
||||
self._currentBid = 0
|
||||
self._minIncrement = 0
|
||||
self._isHighBidder = data.bidder and data.bidder == UnitGUID("player") or false
|
||||
self._numOwnerItems = data.numOwnerItems or 0
|
||||
-- convert the timeLeftSeconds to regular timeLeft
|
||||
if data.timeLeftSeconds < 60 * 60 then
|
||||
self._timeLeft = 1
|
||||
elseif data.timeLeftSeconds < 2 * 60 * 60 then
|
||||
self._timeLeft = 2
|
||||
elseif data.timeLeftSeconds < 12 * 60 * 60 then
|
||||
self._timeLeft = 3
|
||||
else
|
||||
self._timeLeft = 4
|
||||
end
|
||||
else
|
||||
self._quantity = 1
|
||||
self._numAuctions = data.quantity
|
||||
self._buyout = data.buyoutAmount or 0
|
||||
self._minBid = data.minBid or data.buyoutAmount
|
||||
self._currentBid = data.bidAmount or 0
|
||||
self._minIncrement = 0
|
||||
self._isHighBidder = false
|
||||
self._numOwnerItems = data.containsAccountItem and data.quantity or 0
|
||||
self._timeLeft = data.timeLeft + 1
|
||||
end
|
||||
|
||||
self._hasOwners = #data.owners > 0
|
||||
assert(#private.ownersTemp == 0)
|
||||
for _, owner in ipairs(data.owners) do
|
||||
if owner == "player" then
|
||||
owner = UnitName("player")
|
||||
elseif owner == "" then
|
||||
owner = "?"
|
||||
self._hasOwners = false
|
||||
end
|
||||
tinsert(private.ownersTemp, owner)
|
||||
end
|
||||
self._ownerStr = table.concat(private.ownersTemp, ",")
|
||||
wipe(private.ownersTemp)
|
||||
self._auctionId = data.auctionID
|
||||
end
|
||||
assert(self._itemLink and self._quantity and self._buyout and self._minBid and self._currentBid and self._numOwnerItems and self._timeLeft and self._ownerStr and self._auctionId)
|
||||
else
|
||||
self._itemLink = nil
|
||||
self._buyout = nil
|
||||
self._minBid = nil
|
||||
self._currentBid = nil
|
||||
self._minIncrement = nil
|
||||
self._isHighBidder = nil
|
||||
self._quantity = nil
|
||||
self._timeLeft = nil
|
||||
self._ownerStr = nil
|
||||
self._hasOwners = false
|
||||
self._numOwnerItems = nil
|
||||
self._auctionId = nil
|
||||
end
|
||||
end
|
||||
620
LibTSM/Service/AuctionScanClasses/ScanManager.lua
Normal file
620
LibTSM/Service/AuctionScanClasses/ScanManager.lua
Normal file
@@ -0,0 +1,620 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
-- This file contains code for scanning the auction house
|
||||
local _, TSM = ...
|
||||
local ScanManager = TSM.Init("Service.AuctionScanClasses.ScanManager")
|
||||
local L = TSM.Include("Locale").GetTable()
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
local ObjectPool = TSM.Include("Util.ObjectPool")
|
||||
local AuctionHouseWrapper = TSM.Include("Service.AuctionHouseWrapper")
|
||||
local Threading = TSM.Include("Service.Threading")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local Query = TSM.Include("Service.AuctionScanClasses.Query")
|
||||
local QueryUtil = TSM.Include("Service.AuctionScanClasses.QueryUtil")
|
||||
local AuctionScanManager = TSM.Include("LibTSMClass").DefineClass("AuctionScanManager")
|
||||
local private = {
|
||||
objectPool = ObjectPool.New("AUCTION_SCAN_MANAGER", AuctionScanManager),
|
||||
}
|
||||
-- arbitrary estimate that finishing the browse request is worth 10% of the query's progress
|
||||
local BROWSE_PROGRESS = 0.1
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function ScanManager.Get()
|
||||
return private.objectPool:Get()
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Class Meta Methods
|
||||
-- ============================================================================
|
||||
|
||||
function AuctionScanManager.__init(self)
|
||||
self._resolveSellers = nil
|
||||
self._ignoreItemLevel = nil
|
||||
self._queries = {}
|
||||
self._queriesScanned = 0
|
||||
self._queryDidBrowse = false
|
||||
self._onProgressUpdateHandler = nil
|
||||
self._onQueryDoneHandler = nil
|
||||
self._resultsUpdateCallbacks = {}
|
||||
self._nextSearchItemFunction = nil
|
||||
self._currentSearchChangedCallback = nil
|
||||
self._findResult = {}
|
||||
self._cancelled = false
|
||||
self._shouldPause = false
|
||||
self._paused = false
|
||||
self._scanQuery = nil
|
||||
self._findQuery = nil
|
||||
self._numItems = nil
|
||||
self._queryCallback = function(query, searchRow)
|
||||
for func in pairs(self._resultsUpdateCallbacks) do
|
||||
func(self, query, searchRow)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionScanManager._Release(self)
|
||||
self._resolveSellers = nil
|
||||
self._ignoreItemLevel = nil
|
||||
for _, query in ipairs(self._queries) do
|
||||
query:Release()
|
||||
end
|
||||
wipe(self._queries)
|
||||
self._queriesScanned = 0
|
||||
self._queryDidBrowse = false
|
||||
self._onProgressUpdateHandler = nil
|
||||
self._onQueryDoneHandler = nil
|
||||
wipe(self._resultsUpdateCallbacks)
|
||||
self._nextSearchItemFunction = nil
|
||||
self._currentSearchChangedCallback = nil
|
||||
self._cancelled = false
|
||||
self._shouldPause = false
|
||||
self._paused = false
|
||||
wipe(self._findResult)
|
||||
self._scanQuery = nil
|
||||
if self._findQuery then
|
||||
self._findQuery:Release()
|
||||
self._findQuery = nil
|
||||
end
|
||||
self._numItems = nil
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Public Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
function AuctionScanManager.Release(self)
|
||||
self:_Release()
|
||||
private.objectPool:Recycle(self)
|
||||
end
|
||||
|
||||
function AuctionScanManager.SetResolveSellers(self, resolveSellers)
|
||||
self._resolveSellers = resolveSellers
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionScanManager.SetIgnoreItemLevel(self, ignoreItemLevel)
|
||||
self._ignoreItemLevel = ignoreItemLevel
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionScanManager.SetScript(self, script, handler)
|
||||
if script == "OnProgressUpdate" then
|
||||
self._onProgressUpdateHandler = handler
|
||||
elseif script == "OnQueryDone" then
|
||||
self._onQueryDoneHandler = handler
|
||||
elseif script == "OnCurrentSearchChanged" then
|
||||
self._currentSearchChangedCallback = handler
|
||||
else
|
||||
error("Unknown AuctionScanManager script: "..tostring(script))
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionScanManager.AddResultsUpdateCallback(self, func)
|
||||
self._resultsUpdateCallbacks[func] = true
|
||||
end
|
||||
|
||||
function AuctionScanManager.RemoveResultsUpdateCallback(self, func)
|
||||
self._resultsUpdateCallbacks[func] = nil
|
||||
end
|
||||
|
||||
function AuctionScanManager.SetNextSearchItemFunction(self, func)
|
||||
self._nextSearchItemFunction = func
|
||||
end
|
||||
|
||||
function AuctionScanManager.GetNumQueries(self)
|
||||
return #self._queries
|
||||
end
|
||||
|
||||
function AuctionScanManager.QueryIterator(self, offset)
|
||||
return private.QueryIteratorHelper, self._queries, offset or 0
|
||||
end
|
||||
|
||||
function AuctionScanManager.NewQuery(self)
|
||||
local query = Query.Get()
|
||||
self:_AddQuery(query)
|
||||
return query
|
||||
end
|
||||
|
||||
function AuctionScanManager.AddItemListQueriesThreaded(self, itemList)
|
||||
assert(Threading.IsThreadContext())
|
||||
-- remove duplicates
|
||||
local usedItems = TempTable.Acquire()
|
||||
for i = #itemList, 1, -1 do
|
||||
local itemString = itemList[i]
|
||||
if usedItems[itemString] then
|
||||
tremove(itemList, i)
|
||||
end
|
||||
usedItems[itemString] = true
|
||||
end
|
||||
TempTable.Release(usedItems)
|
||||
self._numItems = #itemList
|
||||
QueryUtil.GenerateThreaded(itemList, private.NewQueryCallback, self)
|
||||
end
|
||||
|
||||
function AuctionScanManager.ScanQueriesThreaded(self)
|
||||
assert(Threading.IsThreadContext())
|
||||
self._queriesScanned = 0
|
||||
self._cancelled = false
|
||||
AuctionHouseWrapper.GetAndResetTotalHookedTime()
|
||||
self:_NotifyProgressUpdate()
|
||||
|
||||
-- loop through each filter to perform
|
||||
local allSuccess = true
|
||||
while self._queriesScanned < #self._queries do
|
||||
local query = self._queries[self._queriesScanned + 1]
|
||||
-- run the browse query
|
||||
local querySuccess, numNewResults = self:_ProcessQuery(query)
|
||||
if not querySuccess then
|
||||
allSuccess = false
|
||||
break
|
||||
end
|
||||
self._queriesScanned = self._queriesScanned + 1
|
||||
self:_NotifyProgressUpdate()
|
||||
if self._onQueryDoneHandler then
|
||||
self:_onQueryDoneHandler(query, numNewResults)
|
||||
end
|
||||
self:_Pause()
|
||||
end
|
||||
|
||||
if allSuccess then
|
||||
local hookedTime, topAddon, topTime = AuctionHouseWrapper.GetAndResetTotalHookedTime()
|
||||
if hookedTime > 1 and topAddon ~= "Blizzard_AuctionHouseUI" then
|
||||
Log.PrintfUser(L["Scan was slowed down by %s seconds by other AH addons (%s seconds by %s)."], Math.Round(hookedTime, 0.1), Math.Round(topTime, 0.1), topAddon)
|
||||
end
|
||||
end
|
||||
return allSuccess
|
||||
end
|
||||
|
||||
function AuctionScanManager.FindAuctionThreaded(self, findSubRow, noSeller)
|
||||
assert(Threading.IsThreadContext())
|
||||
wipe(self._findResult)
|
||||
if TSM.IsWowClassic() then
|
||||
return self:_FindAuctionThreaded(findSubRow, noSeller)
|
||||
else
|
||||
return self:_FindAuctionThreaded83(findSubRow, noSeller)
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionScanManager.PrepareForBidOrBuyout(self, index, subRow, noSeller, quantity, itemBuyout)
|
||||
if TSM.IsWowClassic() then
|
||||
return subRow:EqualsIndex(index, noSeller)
|
||||
else
|
||||
local itemString = subRow:GetItemString()
|
||||
if ItemInfo.IsCommodity(itemString) then
|
||||
local future = AuctionHouseWrapper.StartCommoditiesPurchase(ItemString.ToId(itemString), quantity, itemBuyout)
|
||||
if not future then
|
||||
return false
|
||||
end
|
||||
return true, future
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionScanManager.PlaceBidOrBuyout(self, index, bidBuyout, subRow, quantity)
|
||||
if TSM.IsWowClassic() then
|
||||
PlaceAuctionBid("list", index, bidBuyout)
|
||||
return true
|
||||
else
|
||||
local itemString = subRow:GetItemString()
|
||||
local future = nil
|
||||
if ItemInfo.IsCommodity(itemString) then
|
||||
local itemId = ItemString.ToId(itemString)
|
||||
future = AuctionHouseWrapper.ConfirmCommoditiesPurchase(itemId, quantity)
|
||||
else
|
||||
local _, auctionId = subRow:GetListingInfo()
|
||||
future = AuctionHouseWrapper.PlaceBid(auctionId, bidBuyout)
|
||||
quantity = 1
|
||||
end
|
||||
if not future then
|
||||
return false
|
||||
end
|
||||
-- TODO: return this future and record the buyout once the future is done
|
||||
future:Cancel()
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionScanManager.GetProgress(self)
|
||||
local numQueries = self:GetNumQueries()
|
||||
if self._queriesScanned == numQueries then
|
||||
return 1
|
||||
end
|
||||
local currentQuery = self._queries[self._queriesScanned + 1]
|
||||
local searchProgress = nil
|
||||
if not self._queryDidBrowse or TSM.IsWowClassic() then
|
||||
searchProgress = 0
|
||||
else
|
||||
searchProgress = currentQuery:GetSearchProgress() * (1 - BROWSE_PROGRESS) + BROWSE_PROGRESS
|
||||
end
|
||||
local queryStep = 1 / numQueries
|
||||
local progress = min((self._queriesScanned + searchProgress) * queryStep, 1)
|
||||
return progress, self._paused
|
||||
end
|
||||
|
||||
function AuctionScanManager.Cancel(self)
|
||||
self._cancelled = true
|
||||
if self._scanQuery then
|
||||
self._scanQuery:CancelBrowseOrSearch()
|
||||
self._scanQuery = nil
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionScanManager.SetPaused(self, paused)
|
||||
self._shouldPause = paused
|
||||
if self._scanQuery then
|
||||
self._scanQuery:CancelBrowseOrSearch()
|
||||
self._scanQuery = nil
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionScanManager.GetNumItems(self)
|
||||
return self._numItems
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
function AuctionScanManager._AddQuery(self, query)
|
||||
query:SetResolveSellers(self._resolveSellers)
|
||||
query:SetCallback(self._queryCallback)
|
||||
tinsert(self._queries, query)
|
||||
end
|
||||
|
||||
function AuctionScanManager._IsCancelled(self)
|
||||
return self._cancelled
|
||||
end
|
||||
|
||||
function AuctionScanManager._Pause(self)
|
||||
if not self._shouldPause then
|
||||
return
|
||||
end
|
||||
self._paused = true
|
||||
self:_NotifyProgressUpdate()
|
||||
if self._currentSearchChangedCallback then
|
||||
self:_currentSearchChangedCallback()
|
||||
end
|
||||
while self._shouldPause do
|
||||
Threading.Yield(true)
|
||||
end
|
||||
self._paused = false
|
||||
self:_NotifyProgressUpdate()
|
||||
if self._currentSearchChangedCallback then
|
||||
self:_currentSearchChangedCallback()
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionScanManager._NotifyProgressUpdate(self)
|
||||
if self._onProgressUpdateHandler then
|
||||
self:_onProgressUpdateHandler()
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionScanManager._ProcessQuery(self, query)
|
||||
local prevMaxBrowseId = 0
|
||||
for _, row in query:BrowseResultsIterator() do
|
||||
prevMaxBrowseId = max(prevMaxBrowseId, row:GetMinBrowseId())
|
||||
end
|
||||
|
||||
-- run the browse query
|
||||
self._queryDidBrowse = false
|
||||
while not self:_DoBrowse(query) do
|
||||
if self._shouldPause then
|
||||
-- this browse failed due to a pause request, so try again after we're resumed
|
||||
self:_Pause()
|
||||
-- wipe the browse results since we're going to do another search
|
||||
query:WipeBrowseResults()
|
||||
else
|
||||
return false, 0
|
||||
end
|
||||
end
|
||||
self._queryDidBrowse = true
|
||||
self:_NotifyProgressUpdate()
|
||||
|
||||
local numNewResults = 0
|
||||
if TSM.IsWowClassic() then
|
||||
for _, row in query:BrowseResultsIterator() do
|
||||
if row:GetMinBrowseId() > prevMaxBrowseId then
|
||||
numNewResults = numNewResults + row:GetNumSubRows()
|
||||
end
|
||||
end
|
||||
return true, numNewResults
|
||||
end
|
||||
|
||||
local rows = Threading.AcquireSafeTempTable()
|
||||
for baseItemString, row in query:BrowseResultsIterator() do
|
||||
rows[baseItemString] = row
|
||||
end
|
||||
while true do
|
||||
local baseItemString, row = nil, nil
|
||||
if self._nextSearchItemFunction then
|
||||
baseItemString = self._nextSearchItemFunction()
|
||||
row = baseItemString and rows[baseItemString]
|
||||
end
|
||||
if not row then
|
||||
baseItemString, row = next(rows)
|
||||
end
|
||||
if not row then
|
||||
break
|
||||
end
|
||||
rows[baseItemString] = nil
|
||||
if self._currentSearchChangedCallback then
|
||||
self:_currentSearchChangedCallback(baseItemString)
|
||||
end
|
||||
-- store all the existing auctionIds so we can see what changed
|
||||
local prevAuctionIds = Threading.AcquireSafeTempTable()
|
||||
for _, subRow in row:SubRowIterator() do
|
||||
local _, auctionId = subRow:GetListingInfo()
|
||||
assert(not prevAuctionIds[auctionId])
|
||||
prevAuctionIds[auctionId] = true
|
||||
end
|
||||
-- send the query for this item
|
||||
while not self:_DoSearch(query, row) do
|
||||
if self._shouldPause then
|
||||
-- this search failed due to a pause request, so try again after we're resumed
|
||||
self:_Pause()
|
||||
-- wipe the search results since we're going to do another search
|
||||
row:WipeSearchResults()
|
||||
else
|
||||
Threading.ReleaseSafeTempTable(prevAuctionIds)
|
||||
Threading.ReleaseSafeTempTable(rows)
|
||||
return false, numNewResults
|
||||
end
|
||||
end
|
||||
|
||||
local numSubRows = row:GetNumSubRows()
|
||||
for _, subRow in row:SubRowIterator() do
|
||||
local _, auctionId = subRow:GetListingInfo()
|
||||
if not prevAuctionIds[auctionId] then
|
||||
numNewResults = numNewResults + 1
|
||||
end
|
||||
end
|
||||
Threading.ReleaseSafeTempTable(prevAuctionIds)
|
||||
if numSubRows == 0 then
|
||||
-- remove this row since there are no search results
|
||||
query:RemoveResultRow(row)
|
||||
end
|
||||
|
||||
self:_NotifyProgressUpdate()
|
||||
self:_Pause()
|
||||
Threading.Yield()
|
||||
end
|
||||
Threading.ReleaseSafeTempTable(rows)
|
||||
return true, numNewResults
|
||||
end
|
||||
|
||||
function AuctionScanManager._DoBrowse(self, query, ...)
|
||||
return self:_DoBrowseSearchHelper(query, query:Browse(...))
|
||||
end
|
||||
|
||||
function AuctionScanManager._DoSearch(self, query, ...)
|
||||
return self:_DoBrowseSearchHelper(query, query:Search(...))
|
||||
end
|
||||
|
||||
function AuctionScanManager._DoBrowseSearchHelper(self, query, future)
|
||||
if not future then
|
||||
return false
|
||||
end
|
||||
self._scanQuery = query
|
||||
local result = Threading.WaitForFuture(future)
|
||||
self._scanQuery = nil
|
||||
Threading.Yield()
|
||||
return result
|
||||
end
|
||||
|
||||
function AuctionScanManager._FindAuctionThreaded(self, row, noSeller)
|
||||
self._cancelled = false
|
||||
-- make sure we're not in the middle of a query where the results are going to change on us
|
||||
Threading.WaitForFunction(CanSendAuctionQuery)
|
||||
|
||||
-- search the current page for the auction
|
||||
if self:_FindAuctionOnCurrentPage(row, noSeller) then
|
||||
Log.Info("Found on current page")
|
||||
return self._findResult
|
||||
end
|
||||
|
||||
-- search for the item
|
||||
local page, maxPage = 0, nil
|
||||
while true do
|
||||
-- query the AH
|
||||
if self._findQuery then
|
||||
self._findQuery:Release()
|
||||
end
|
||||
local itemString = row:GetItemString()
|
||||
local level = ItemInfo.GetMinLevel(itemString)
|
||||
local quality = ItemInfo.GetQuality(itemString)
|
||||
assert(level and quality)
|
||||
self._findQuery = Query.Get()
|
||||
:SetStr(ItemInfo.GetName(itemString), true)
|
||||
:SetQualityRange(quality, quality)
|
||||
:SetLevelRange(level, level)
|
||||
:SetClass(ItemInfo.GetClassId(itemString), ItemInfo.GetSubClassId(itemString))
|
||||
:SetItems(itemString)
|
||||
:SetResolveSellers(not noSeller)
|
||||
:SetPage(page)
|
||||
local filterSuccess = self:_DoBrowse(self._findQuery)
|
||||
if self._findQuery then
|
||||
self._findQuery:Release()
|
||||
self._findQuery = nil
|
||||
end
|
||||
if not filterSuccess then
|
||||
break
|
||||
end
|
||||
-- search this page for the row
|
||||
if self:_FindAuctionOnCurrentPage(row, noSeller) then
|
||||
Log.Info("Found auction (%d)", page)
|
||||
return self._findResult
|
||||
elseif self:_IsCancelled() then
|
||||
break
|
||||
end
|
||||
|
||||
local numPages = ceil(select(2, GetNumAuctionItems("list")) / NUM_AUCTION_ITEMS_PER_PAGE)
|
||||
local canBeLater = private.FindAuctionCanBeOnLaterPage(row)
|
||||
maxPage = maxPage or numPages - 1
|
||||
if not canBeLater and page < maxPage then
|
||||
maxPage = page
|
||||
end
|
||||
if canBeLater and page < maxPage then
|
||||
Log.Info("Trying next page (%d)", page + 1)
|
||||
page = page + 1
|
||||
else
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionScanManager._FindAuctionOnCurrentPage(self, subRow, noSeller)
|
||||
local found = false
|
||||
for i = 1, GetNumAuctionItems("list") do
|
||||
if subRow:EqualsIndex(i, noSeller) then
|
||||
tinsert(self._findResult, i)
|
||||
found = true
|
||||
end
|
||||
end
|
||||
return found
|
||||
end
|
||||
|
||||
function AuctionScanManager._FindAuctionThreaded83(self, findSubRow, noSeller)
|
||||
assert(findSubRow:IsSubRow())
|
||||
self._cancelled = false
|
||||
noSeller = noSeller or findSubRow:IsCommodity()
|
||||
|
||||
local row = findSubRow:GetResultRow()
|
||||
local findHash, findHashNoSeller = findSubRow:GetHashes()
|
||||
|
||||
if not self:_DoSearch(row:GetQuery(), row, false) then
|
||||
return nil
|
||||
end
|
||||
local result = nil
|
||||
-- first try to find a subRow with a full matching hash
|
||||
for _, subRow in row:SubRowIterator() do
|
||||
local quantity, numAuctions = subRow:GetQuantities()
|
||||
local hash = subRow:GetHashes()
|
||||
if hash == findHash then
|
||||
result = (result or 0) + quantity * numAuctions
|
||||
end
|
||||
end
|
||||
if result then
|
||||
return result
|
||||
end
|
||||
-- next try to find the first subRow with a matching no-seller hash
|
||||
local firstHash = nil
|
||||
for _, subRow in row:SubRowIterator() do
|
||||
local quantity, numAuctions = subRow:GetQuantities()
|
||||
local hash, hashNoSeller = subRow:GetHashes()
|
||||
if (not firstHash or hash == firstHash) and hashNoSeller == findHashNoSeller then
|
||||
firstHash = hash
|
||||
result = (result or 0) + quantity * numAuctions
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.NewQueryCallback(query, self)
|
||||
self:_AddQuery(query)
|
||||
end
|
||||
|
||||
function private.FindAuctionCanBeOnLaterPage(row)
|
||||
local pageAuctions = GetNumAuctionItems("list")
|
||||
if pageAuctions == 0 then
|
||||
-- there are no auctions on this page, so it cannot be on a later one
|
||||
return false
|
||||
end
|
||||
local _, _, stackSize, _, _, _, _, _, _, buyout, _, _, _, seller, sellerFull = GetAuctionItemInfo("list", pageAuctions)
|
||||
|
||||
local itemBuyout = (buyout > 0) and floor(buyout / stackSize) or 0
|
||||
local _, rowItemBuyout = row:GetBuyouts()
|
||||
if rowItemBuyout > itemBuyout then
|
||||
-- item must be on a later page since it would be sorted after the last auction on this page
|
||||
return true
|
||||
elseif rowItemBuyout < itemBuyout then
|
||||
-- item cannot be on a later page since it would be sorted before the last auction on this page
|
||||
return false
|
||||
end
|
||||
|
||||
local rowStackSize = row:GetQuantities()
|
||||
if rowStackSize > stackSize then
|
||||
-- item must be on a later page since it would be sorted after the last auction on this page
|
||||
return true
|
||||
elseif rowStackSize < stackSize then
|
||||
-- item cannot be on a later page since it would be sorted before the last auction on this page
|
||||
return false
|
||||
end
|
||||
|
||||
seller = private.FixSellerName(seller, sellerFull) or "?"
|
||||
local rowSeller = row:GetOwnerInfo()
|
||||
if rowSeller > seller then
|
||||
-- item must be on a later page since it would be sorted after the last auction on this page
|
||||
return true
|
||||
elseif rowSeller < seller then
|
||||
-- item cannot be on a later page since it would be sorted before the last auction on this page
|
||||
return false
|
||||
end
|
||||
|
||||
-- all the things we are sorting on are the same, so the auction could be on a later page
|
||||
return true
|
||||
end
|
||||
|
||||
function private.FixSellerName(seller, sellerFull)
|
||||
local realm = GetRealmName()
|
||||
if sellerFull and strjoin("-", seller, realm) ~= sellerFull then
|
||||
return sellerFull
|
||||
else
|
||||
return seller
|
||||
end
|
||||
end
|
||||
|
||||
function private.QueryIteratorHelper(tbl, index)
|
||||
index = index + 1
|
||||
if index > #tbl then
|
||||
return
|
||||
end
|
||||
return index, tbl[index]
|
||||
end
|
||||
511
LibTSM/Service/AuctionScanClasses/Scanner.lua
Normal file
511
LibTSM/Service/AuctionScanClasses/Scanner.lua
Normal file
@@ -0,0 +1,511 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Scanner = TSM.Init("Service.AuctionScanClasses.Scanner")
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local FSM = TSM.Include("Util.FSM")
|
||||
local Future = TSM.Include("Util.Future")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local AuctionHouseWrapper = TSM.Include("Service.AuctionHouseWrapper")
|
||||
local Util = TSM.Include("Service.AuctionScanClasses.Util")
|
||||
local ResultRow = TSM.Include("Service.AuctionScanClasses.ResultRow")
|
||||
local private = {
|
||||
resolveSellers = nil,
|
||||
pendingFuture = nil,
|
||||
query = nil,
|
||||
browseResults = nil,
|
||||
callback = nil,
|
||||
browseId = 1,
|
||||
browseIsNoScan = false,
|
||||
browseIndex = 1,
|
||||
browsePendingIndexes = {},
|
||||
searchRow = nil,
|
||||
useCachedData = nil,
|
||||
retryCount = 0,
|
||||
requestFuture = Future.New("AUCTION_SCANNER_FUTURE"),
|
||||
requestResult = nil,
|
||||
fsm = nil,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Scanner:OnModuleLoad(function()
|
||||
private.requestFuture:SetScript("OnCleanup", function()
|
||||
Delay.Cancel("AUCTION_SCANNER_DONE")
|
||||
private.fsm:ProcessEvent("EV_CANCEL")
|
||||
end)
|
||||
|
||||
if TSM.IsWowClassic() then
|
||||
Event.Register("AUCTION_ITEM_LIST_UPDATE", function()
|
||||
private.fsm:SetLoggingEnabled(false)
|
||||
private.fsm:ProcessEvent("EV_BROWSE_RESULTS_UPDATED")
|
||||
private.fsm:SetLoggingEnabled(true)
|
||||
end)
|
||||
else
|
||||
Event.Register("COMMODITY_SEARCH_RESULTS_UPDATED", function()
|
||||
private.fsm:ProcessEvent("EV_SEARCH_RESULTS_UPDATED")
|
||||
end)
|
||||
Event.Register("ITEM_SEARCH_RESULTS_UPDATED", function()
|
||||
private.fsm:ProcessEvent("EV_SEARCH_RESULTS_UPDATED")
|
||||
end)
|
||||
end
|
||||
|
||||
private.fsm = FSM.New("AUCTION_SCANNER_FSM")
|
||||
:AddState(FSM.NewState("ST_INIT")
|
||||
:SetOnEnter(function()
|
||||
private.query = nil
|
||||
private.resolveSellers = nil
|
||||
private.useCachedData = nil
|
||||
private.searchRow = nil
|
||||
private.callback = nil
|
||||
private.retryCount = 0
|
||||
Delay.Cancel("AUCTION_SCANNER_RETRY")
|
||||
if private.pendingFuture then
|
||||
private.pendingFuture:Cancel()
|
||||
private.pendingFuture = nil
|
||||
end
|
||||
end)
|
||||
:AddTransition("ST_BROWSE_SORT")
|
||||
:AddTransition("ST_BROWSE_CHECKING")
|
||||
:AddTransition("ST_SEARCH_GET_KEY")
|
||||
:AddEvent("EV_START_BROWSE", function(_, query, resolveSellers, browseResults, callback)
|
||||
assert(not private.query)
|
||||
private.query = query
|
||||
private.resolveSellers = resolveSellers
|
||||
private.browseResults = browseResults
|
||||
private.browseId = private.browseId + 1
|
||||
private.browseIsNoScan = false
|
||||
private.callback = callback
|
||||
return "ST_BROWSE_SORT"
|
||||
end)
|
||||
:AddEvent("EV_START_BROWSE_NO_SCAN", function(_, query, itemKeys, browseResults, callback)
|
||||
assert(not TSM.IsWowClassic())
|
||||
assert(not private.query)
|
||||
private.query = query
|
||||
private.browseResults = browseResults
|
||||
private.browseId = private.browseId + 1
|
||||
private.browseIsNoScan = true
|
||||
private.callback = callback
|
||||
for _, itemKey in ipairs(itemKeys) do
|
||||
local baseItemString = ItemString.GetBaseFromItemKey(itemKey)
|
||||
private.ProcessBrowseResult(baseItemString, itemKey)
|
||||
end
|
||||
return "ST_BROWSE_CHECKING"
|
||||
end)
|
||||
:AddEvent("EV_START_SEARCH", function(_, query, resolveSellers, useCachedData, searchRow, callback)
|
||||
assert(not TSM.IsWowClassic())
|
||||
assert(not private.query)
|
||||
private.query = query
|
||||
private.resolveSellers = resolveSellers
|
||||
private.useCachedData = useCachedData
|
||||
private.searchRow = searchRow
|
||||
private.callback = callback
|
||||
private.searchRow:SearchReset()
|
||||
return "ST_SEARCH_GET_KEY"
|
||||
end)
|
||||
)
|
||||
:AddState(FSM.NewState("ST_BROWSE_SORT")
|
||||
:SetOnEnter(function()
|
||||
if not private.query:_SetSort() then
|
||||
Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.5, private.RetryHandler)
|
||||
return
|
||||
end
|
||||
return "ST_BROWSE_SEND"
|
||||
end)
|
||||
:AddTransition("ST_BROWSE_SORT")
|
||||
:AddTransition("ST_BROWSE_SEND")
|
||||
:AddTransition("ST_CANCELING")
|
||||
:AddEventTransition("EV_RETRY", "ST_BROWSE_SORT")
|
||||
:AddEventTransition("EV_CANCEL", "ST_CANCELING")
|
||||
)
|
||||
:AddState(FSM.NewState("ST_BROWSE_SEND")
|
||||
:SetOnEnter(function()
|
||||
private.HandleAuctionHouseWrapperResult(private.query:_SendWowQuery())
|
||||
end)
|
||||
:AddTransition("ST_BROWSE_SEND")
|
||||
:AddTransition("ST_BROWSE_CHECKING")
|
||||
:AddTransition("ST_CANCELING")
|
||||
:AddEvent("EV_FUTURE_SUCCESS", function()
|
||||
if TSM.IsWowClassic() then
|
||||
private.browseIndex = 1
|
||||
wipe(private.browsePendingIndexes)
|
||||
else
|
||||
for _, result in ipairs(C_AuctionHouse.GetBrowseResults()) do
|
||||
local baseItemString = ItemString.GetBaseFromItemKey(result.itemKey)
|
||||
private.ProcessBrowseResult(baseItemString, result.itemKey, result.minPrice, result.totalQuantity)
|
||||
end
|
||||
end
|
||||
return "ST_BROWSE_CHECKING"
|
||||
end)
|
||||
:AddEventTransition("EV_RETRY", "ST_BROWSE_SEND")
|
||||
:AddEventTransition("EV_CANCEL", "ST_CANCELING")
|
||||
)
|
||||
:AddState(FSM.NewState("ST_BROWSE_CHECKING")
|
||||
:SetOnEnter(function()
|
||||
if not private.query:_BrowseIsPageValid() then
|
||||
-- this page isn't valid, so go to the next page
|
||||
return "ST_BROWSE_REQUEST_MORE"
|
||||
elseif not private.CheckBrowseResults() then
|
||||
-- result's aren't valid yet, so check again
|
||||
Delay.AfterFrame("AUCTION_SCANNER_RETRY", 1, private.RetryHandler)
|
||||
return
|
||||
end
|
||||
-- we're done with this set of browse results
|
||||
if private.callback then
|
||||
private.callback(private.query)
|
||||
end
|
||||
if private.browseIsNoScan or private.query:_BrowseIsDone() then
|
||||
-- we're done
|
||||
return "ST_BROWSE_DONE"
|
||||
else
|
||||
-- move on to the next page
|
||||
return "ST_BROWSE_REQUEST_MORE"
|
||||
end
|
||||
end)
|
||||
:AddTransition("ST_BROWSE_CHECKING")
|
||||
:AddTransition("ST_BROWSE_DONE")
|
||||
:AddTransition("ST_BROWSE_REQUEST_MORE")
|
||||
:AddTransition("ST_CANCELING")
|
||||
:AddEventTransition("EV_RETRY", "ST_BROWSE_CHECKING")
|
||||
:AddEventTransition("EV_BROWSE_RESULTS_UPDATED", "ST_BROWSE_CHECKING")
|
||||
:AddEventTransition("EV_CANCEL", "ST_CANCELING")
|
||||
)
|
||||
:AddState(FSM.NewState("ST_BROWSE_REQUEST_MORE")
|
||||
:SetOnEnter(function(_, isRetry)
|
||||
if private.query:_BrowseIsDone(isRetry) then
|
||||
return "ST_BROWSE_CHECKING"
|
||||
else
|
||||
private.HandleAuctionHouseWrapperResult(private.query:_BrowseRequestMore(isRetry))
|
||||
end
|
||||
end)
|
||||
:AddTransition("ST_BROWSE_REQUEST_MORE")
|
||||
:AddTransition("ST_BROWSE_CHECKING")
|
||||
:AddTransition("ST_CANCELING")
|
||||
:AddEvent("EV_FUTURE_SUCCESS", function(_, ...)
|
||||
if TSM.IsWowClassic() then
|
||||
private.browseIndex = 1
|
||||
wipe(private.browsePendingIndexes)
|
||||
else
|
||||
local newResults = ...
|
||||
for _, result in ipairs(newResults) do
|
||||
local baseItemString = ItemString.GetBaseFromItemKey(result.itemKey)
|
||||
private.ProcessBrowseResult(baseItemString, result.itemKey, result.minPrice, result.totalQuantity)
|
||||
end
|
||||
end
|
||||
return "ST_BROWSE_CHECKING"
|
||||
end)
|
||||
:AddEvent("EV_RETRY", function()
|
||||
return "ST_BROWSE_REQUEST_MORE", true
|
||||
end)
|
||||
:AddEventTransition("EV_CANCEL", "ST_CANCELING")
|
||||
)
|
||||
:AddState(FSM.NewState("ST_BROWSE_DONE")
|
||||
:SetOnEnter(function()
|
||||
private.HandleRequestDone(true)
|
||||
return "ST_INIT"
|
||||
end)
|
||||
:AddTransition("ST_INIT")
|
||||
)
|
||||
:AddState(FSM.NewState("ST_SEARCH_GET_KEY")
|
||||
:SetOnEnter(function()
|
||||
assert(not TSM.IsWowClassic())
|
||||
if not private.searchRow:SearchIsReady() then
|
||||
Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.1, private.RetryHandler)
|
||||
return
|
||||
end
|
||||
return "ST_SEARCH_SEND"
|
||||
end)
|
||||
:AddTransition("ST_SEARCH_GET_KEY")
|
||||
:AddTransition("ST_SEARCH_SEND")
|
||||
:AddTransition("ST_CANCELING")
|
||||
:AddEventTransition("EV_FUTURE_SUCCESS", "ST_SEARCH_SEND")
|
||||
:AddEventTransition("EV_RETRY", "ST_SEARCH_GET_KEY")
|
||||
:AddEventTransition("EV_CANCEL", "ST_CANCELING")
|
||||
)
|
||||
:AddState(FSM.NewState("ST_SEARCH_SEND")
|
||||
:SetOnEnter(function()
|
||||
assert(not TSM.IsWowClassic())
|
||||
if not AuctionHouseWrapper.IsOpen() then
|
||||
return "ST_CANCELING"
|
||||
end
|
||||
if private.useCachedData and private.searchRow:HasCachedSearchData() then
|
||||
return "ST_SEARCH_REQUEST_MORE"
|
||||
end
|
||||
local future, delayTime = private.searchRow:SearchSend()
|
||||
if future then
|
||||
private.HandleAuctionHouseWrapperResult(future)
|
||||
else
|
||||
if not delayTime then
|
||||
Log.Err("Failed to send search query - retrying")
|
||||
delayTime = 0.5
|
||||
end
|
||||
-- try again after a delay
|
||||
Delay.AfterTime("AUCTION_SCANNER_RETRY", delayTime, private.RetryHandler)
|
||||
end
|
||||
end)
|
||||
:AddTransition("ST_SEARCH_SEND")
|
||||
:AddTransition("ST_SEARCH_REQUEST_MORE")
|
||||
:AddTransition("ST_CANCELING")
|
||||
:AddEventTransition("EV_FUTURE_SUCCESS", "ST_SEARCH_REQUEST_MORE")
|
||||
:AddEventTransition("EV_RETRY", "ST_SEARCH_SEND")
|
||||
:AddEventTransition("EV_CANCEL", "ST_CANCELING")
|
||||
)
|
||||
:AddState(FSM.NewState("ST_SEARCH_REQUEST_MORE")
|
||||
:SetOnEnter(function()
|
||||
assert(not TSM.IsWowClassic())
|
||||
local baseItemString = private.searchRow:GetBaseItemString()
|
||||
-- get if the item is a commodity or not
|
||||
local isCommodity = ItemInfo.IsCommodity(baseItemString)
|
||||
if isCommodity == nil then
|
||||
Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.1, private.RetryHandler)
|
||||
return
|
||||
end
|
||||
|
||||
local isDone, future = private.searchRow:SearchCheckStatus()
|
||||
if isDone then
|
||||
return "ST_SEARCH_CHECKING"
|
||||
elseif future then
|
||||
private.HandleAuctionHouseWrapperResult(future)
|
||||
else
|
||||
Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.5, private.RetryHandler)
|
||||
end
|
||||
end)
|
||||
:AddTransition("ST_SEARCH_SEND")
|
||||
:AddTransition("ST_SEARCH_CHECKING")
|
||||
:AddTransition("ST_CANCELING")
|
||||
:AddEventTransition("EV_FUTURE_SUCCESS", "ST_SEARCH_CHECKING")
|
||||
:AddEventTransition("EV_RETRY", "ST_SEARCH_SEND")
|
||||
:AddEventTransition("EV_CANCEL", "ST_CANCELING")
|
||||
)
|
||||
:AddState(FSM.NewState("ST_SEARCH_CHECKING")
|
||||
:SetOnEnter(function()
|
||||
assert(not TSM.IsWowClassic())
|
||||
Delay.Cancel("AUCTION_SCANNER_RETRY")
|
||||
private.searchRow:PopulateSubRows(private.browseId)
|
||||
|
||||
-- check if all the sub rows have their data
|
||||
local isDone = true
|
||||
for _, subRow in private.searchRow:SubRowIterator(true) do
|
||||
if not subRow:HasRawData() or not subRow:HasItemString() then
|
||||
isDone = false
|
||||
elseif private.resolveSellers and not subRow:HasOwners() and not private.query:_IsFiltered(subRow, true) then
|
||||
-- waiting for owner info
|
||||
isDone = false
|
||||
end
|
||||
end
|
||||
|
||||
if not isDone and private.retryCount >= 100 then
|
||||
-- out of retries, so give up
|
||||
return "ST_SEARCH_DONE", false
|
||||
elseif not isDone then
|
||||
-- we'll try again
|
||||
private.retryCount = private.retryCount + 1
|
||||
Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.5, private.RetryHandler)
|
||||
return
|
||||
end
|
||||
|
||||
-- filter the sub rows we don't care about
|
||||
private.searchRow:FilterSubRows(private.query)
|
||||
|
||||
if private.callback then
|
||||
private.callback(private.query, private.searchRow)
|
||||
end
|
||||
if private.searchRow:SearchNext() then
|
||||
-- there is more to search
|
||||
return "ST_SEARCH_GET_KEY"
|
||||
else
|
||||
-- scanned everything
|
||||
return "ST_SEARCH_DONE", true
|
||||
end
|
||||
end)
|
||||
:AddTransition("ST_SEARCH_GET_KEY")
|
||||
:AddTransition("ST_SEARCH_CHECKING")
|
||||
:AddTransition("ST_SEARCH_DONE")
|
||||
:AddTransition("ST_CANCELING")
|
||||
:AddEventTransition("EV_RETRY", "ST_SEARCH_CHECKING")
|
||||
:AddEventTransition("EV_SEARCH_RESULTS_UPDATED", "ST_SEARCH_CHECKING")
|
||||
:AddEventTransition("EV_CANCEL", "ST_CANCELING")
|
||||
)
|
||||
:AddState(FSM.NewState("ST_SEARCH_DONE")
|
||||
:SetOnEnter(function(_, result)
|
||||
assert(not TSM.IsWowClassic())
|
||||
private.HandleRequestDone(result)
|
||||
return "ST_INIT"
|
||||
end)
|
||||
:AddTransition("ST_INIT")
|
||||
)
|
||||
:AddState(FSM.NewState("ST_CANCELING")
|
||||
:SetOnEnter(function()
|
||||
Delay.Cancel("AUCTION_SCANNER_DONE")
|
||||
return "ST_INIT"
|
||||
end)
|
||||
:AddTransition("ST_INIT")
|
||||
)
|
||||
:Init("ST_INIT", nil)
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Scanner.Browse(query, resolveSellers, results, callback)
|
||||
private.requestFuture:Start()
|
||||
private.fsm:ProcessEvent("EV_START_BROWSE", query, resolveSellers, results, callback)
|
||||
return private.requestFuture
|
||||
end
|
||||
|
||||
function Scanner.BrowseNoScan(query, itemKeys, results, callback)
|
||||
assert(not TSM.IsWowClassic())
|
||||
private.requestFuture:Start()
|
||||
private.fsm:ProcessEvent("EV_START_BROWSE_NO_SCAN", query, itemKeys, results, callback)
|
||||
return private.requestFuture
|
||||
end
|
||||
|
||||
function Scanner.Search(query, resolveSellers, useCachedData, browseRow, callback)
|
||||
assert(not TSM.IsWowClassic())
|
||||
private.requestFuture:Start()
|
||||
private.fsm:ProcessEvent("EV_START_SEARCH", query, resolveSellers, useCachedData, browseRow, callback)
|
||||
return private.requestFuture
|
||||
end
|
||||
|
||||
function Scanner.Cancel()
|
||||
private.requestFuture:Done(false)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.ProcessBrowseResult(baseItemString, ...)
|
||||
if private.browseResults[baseItemString] then
|
||||
private.browseResults[baseItemString]:Merge(...)
|
||||
else
|
||||
private.browseResults[baseItemString] = ResultRow.Get(private.query, ...)
|
||||
end
|
||||
return private.browseResults[baseItemString]
|
||||
end
|
||||
|
||||
function private.PendingFutureDoneHandler()
|
||||
local result = private.pendingFuture:GetValue()
|
||||
private.pendingFuture = nil
|
||||
if result then
|
||||
private.fsm:ProcessEvent("EV_FUTURE_SUCCESS", result)
|
||||
else
|
||||
Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.1, private.RetryHandler)
|
||||
end
|
||||
end
|
||||
|
||||
function private.RetryHandler()
|
||||
private.fsm:SetLoggingEnabled(false)
|
||||
private.fsm:ProcessEvent("EV_RETRY")
|
||||
private.fsm:SetLoggingEnabled(true)
|
||||
end
|
||||
|
||||
function private.RequestDoneHandler()
|
||||
local result = private.requestResult
|
||||
private.requestResult = nil
|
||||
private.requestFuture:Done(result)
|
||||
end
|
||||
|
||||
function private.HandleAuctionHouseWrapperResult(future)
|
||||
if future then
|
||||
private.pendingFuture = future
|
||||
private.pendingFuture:SetScript("OnDone", private.PendingFutureDoneHandler)
|
||||
else
|
||||
Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.1, private.RetryHandler)
|
||||
end
|
||||
end
|
||||
|
||||
function private.HandleRequestDone(result)
|
||||
private.requestResult = result
|
||||
-- delay a bit so that we complete our current FSM transition
|
||||
Delay.AfterTime("AUCTION_SCANNER_DONE", 0, private.RequestDoneHandler)
|
||||
end
|
||||
|
||||
function private.CheckBrowseResults()
|
||||
if TSM.IsWowClassic() then
|
||||
-- process as many auctions as we can
|
||||
local numAuctions = GetNumAuctionItems("list")
|
||||
for i = #private.browsePendingIndexes, 1, -1 do
|
||||
local index = private.browsePendingIndexes[i]
|
||||
if private.ProcessBrowseResultClassic(index) then
|
||||
tremove(private.browsePendingIndexes, i)
|
||||
end
|
||||
end
|
||||
local index = private.browseIndex
|
||||
while index <= numAuctions and #private.browsePendingIndexes < 50 do
|
||||
if not private.ProcessBrowseResultClassic(index) then
|
||||
tinsert(private.browsePendingIndexes, index)
|
||||
end
|
||||
index = index + 1
|
||||
end
|
||||
private.browseIndex = index
|
||||
if private.browseIndex <= numAuctions or #private.browsePendingIndexes > 0 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- check if there's data still pending
|
||||
local hasPendingData = false
|
||||
for _, row in pairs(private.browseResults) do
|
||||
if not row:PopulateBrowseData() then
|
||||
hasPendingData = true
|
||||
-- keep going so we issue requests for all pending rows
|
||||
end
|
||||
end
|
||||
if hasPendingData then
|
||||
return false
|
||||
end
|
||||
|
||||
-- filter the results
|
||||
local numRemoved = 0
|
||||
for baseItemString, row in pairs(private.browseResults) do
|
||||
-- filter the itemKeys we don't care about and rows which don't match the query
|
||||
if row:IsFiltered(private.query) then
|
||||
private.browseResults[baseItemString] = nil
|
||||
numRemoved = numRemoved + 1
|
||||
end
|
||||
if TSM.IsWowClassic() then
|
||||
if row:FilterSubRows(private.query) then
|
||||
-- no more subRows, so filter the entire row
|
||||
private.browseResults[baseItemString] = nil
|
||||
numRemoved = numRemoved + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
if numRemoved > 0 then
|
||||
Log.Info("Removed %d results", numRemoved)
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function private.ProcessBrowseResultClassic(index)
|
||||
local rawName, _, stackSize, _, _, _, _, _, _, buyout, _, _, _, seller, sellerFull = GetAuctionItemInfo("list", index)
|
||||
local itemLink = GetAuctionItemLink("list", index)
|
||||
local baseItemString = ItemString.GetBase(itemLink)
|
||||
local timeLeft = GetAuctionItemTimeLeft("list", index)
|
||||
seller = Util.FixSellerName(seller, sellerFull)
|
||||
if not rawName or rawName == "" or not baseItemString or not buyout or not stackSize or not timeLeft or (not seller and private.resolveSellers) then
|
||||
return false
|
||||
end
|
||||
local row = private.ProcessBrowseResult(baseItemString, itemLink)
|
||||
-- amazingly, GetAuctionItemLink could return nil the next time it's called (within the same frame), so pass through our itemLink
|
||||
row:PopulateSubRows(private.browseId, index, itemLink)
|
||||
return true
|
||||
end
|
||||
38
LibTSM/Service/AuctionScanClasses/Util.lua
Normal file
38
LibTSM/Service/AuctionScanClasses/Util.lua
Normal file
@@ -0,0 +1,38 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Util = TSM.Init("Service.AuctionScanClasses.Util")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Util.HasItemInfo(itemString)
|
||||
local itemName = ItemInfo.GetName(itemString)
|
||||
local itemLevel = ItemInfo.GetItemLevel(itemString)
|
||||
local quality = ItemInfo.GetQuality(itemString)
|
||||
local minLevel = ItemInfo.GetMinLevel(itemString)
|
||||
local hasIsCommodity = TSM.IsWowClassic() or ItemInfo.IsCommodity(itemString) ~= nil
|
||||
local hasCanHaveVariations = ItemInfo.CanHaveVariations(itemString) ~= nil
|
||||
local result = itemName and itemLevel and quality and minLevel and hasIsCommodity and hasCanHaveVariations
|
||||
if not result then
|
||||
ItemInfo.FetchInfo(itemString)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function Util.FixSellerName(seller, sellerFull)
|
||||
local realm = GetRealmName()
|
||||
if sellerFull and strjoin("-", seller, realm) ~= sellerFull then
|
||||
return sellerFull
|
||||
else
|
||||
return seller
|
||||
end
|
||||
end
|
||||
626
LibTSM/Service/AuctionTracking.lua
Normal file
626
LibTSM/Service/AuctionTracking.lua
Normal file
@@ -0,0 +1,626 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local AuctionTracking = TSM.Init("Service.AuctionTracking")
|
||||
local L = TSM.Include("Locale").GetTable()
|
||||
local Database = TSM.Include("Util.Database")
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Sound = TSM.Include("Util.Sound")
|
||||
local Money = TSM.Include("Util.Money")
|
||||
local Analytics = TSM.Include("Util.Analytics")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local AuctionHouseWrapper = TSM.Include("Service.AuctionHouseWrapper")
|
||||
local private = {
|
||||
settings = nil,
|
||||
indexDB = nil,
|
||||
quantityDB = nil,
|
||||
updateQuery = nil, -- luacheck: ignore 1004 - just stored for GC reasons
|
||||
isAHOpen = false,
|
||||
callbacks = {},
|
||||
expiresCallbacks = {},
|
||||
indexUpdates = {
|
||||
list = {},
|
||||
pending = {},
|
||||
},
|
||||
cancelAuctionId = nil,
|
||||
lastScanNum = nil,
|
||||
ignoreUpdateEvent = nil,
|
||||
lastPurchase = {},
|
||||
prevLineId = nil,
|
||||
prevLineResult = nil,
|
||||
origChatFrameOnEvent = nil,
|
||||
pendingFuture = nil,
|
||||
auctionIdToLink = {},
|
||||
auctionIdToItemBuyout = {},
|
||||
prevLogTime = 0,
|
||||
prevLogNum = math.huge,
|
||||
}
|
||||
local PLAYER_NAME = UnitName("player")
|
||||
local SALE_HINT_SEP = "\001"
|
||||
local SALE_HINT_EXPIRE_TIME = 33 * 24 * 60 * 60
|
||||
local SORT_ORDER = not TSM.IsWowClassic() and {
|
||||
{ sortOrder = Enum.AuctionHouseSortOrder.Name, reverseSort = false },
|
||||
{ sortOrder = Enum.AuctionHouseSortOrder.Price, reverseSort = false },
|
||||
}
|
||||
local AUCTIONABLE_WOW_TOKEN_ITEM_ID = 122270
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
AuctionTracking:OnSettingsLoad(function()
|
||||
private.settings = Settings.NewView()
|
||||
:AddKey("char", "internalData", "auctionSaleHints")
|
||||
:AddKey("char", "internalData", "auctionPrices")
|
||||
:AddKey("char", "internalData", "auctionMessages")
|
||||
:AddKey("factionrealm", "internalData", "expiringAuction")
|
||||
:AddKey("sync", "internalData", "auctionQuantity")
|
||||
:AddKey("global", "coreOptions", "auctionSaleSound")
|
||||
Event.Register("AUCTION_HOUSE_SHOW", private.AuctionHouseShowHandler)
|
||||
Event.Register("AUCTION_HOUSE_CLOSED", private.AuctionHouseClosedHandler)
|
||||
if TSM.IsWowClassic() then
|
||||
Event.Register("AUCTION_OWNED_LIST_UPDATE", private.AuctionOwnedListUpdateHandler)
|
||||
else
|
||||
Event.Register("OWNED_AUCTIONS_UPDATED", private.AuctionOwnedListUpdateHandler)
|
||||
Event.Register("AUCTION_CANCELED", private.AuctionCanceledHandler)
|
||||
end
|
||||
private.indexDB = Database.NewSchema("AUCTION_TRACKING_INDEXES")
|
||||
:AddUniqueNumberField("index")
|
||||
:AddStringField("itemString")
|
||||
:AddSmartMapField("baseItemString", ItemString.GetBaseMap(), "itemString")
|
||||
:AddStringField("itemLink")
|
||||
:AddNumberField("itemTexture")
|
||||
:AddStringField("itemName")
|
||||
:AddNumberField("itemQuality")
|
||||
:AddNumberField("duration")
|
||||
:AddStringField("highBidder")
|
||||
:AddNumberField("currentBid")
|
||||
:AddNumberField("buyout")
|
||||
:AddNumberField("stackSize")
|
||||
:AddNumberField("saleStatus")
|
||||
:AddNumberField("auctionId")
|
||||
:AddIndex("index")
|
||||
:AddIndex("saleStatus")
|
||||
:AddIndex("auctionId")
|
||||
:Commit()
|
||||
private.quantityDB = Database.NewSchema("AUCTION_TRACKING_QUANTITY")
|
||||
:AddUniqueStringField("itemString")
|
||||
:AddNumberField("quantity")
|
||||
:Commit()
|
||||
private.updateQuery = private.indexDB:NewQuery()
|
||||
:SetUpdateCallback(private.OnCallbackQueryUpdated)
|
||||
|
||||
private.RebuildQuantityDB()
|
||||
for info, timestamp in pairs(private.settings.auctionSaleHints) do
|
||||
if time() > timestamp + SALE_HINT_EXPIRE_TIME then
|
||||
private.settings.auctionSaleHints[info] = nil
|
||||
end
|
||||
end
|
||||
|
||||
if TSM.IsWowClassic() then
|
||||
hooksecurefunc("PostAuction", function(_, _, duration)
|
||||
private.PostAuctionHookHandler(duration)
|
||||
end)
|
||||
else
|
||||
hooksecurefunc(C_AuctionHouse, "PostCommodity", function(_, duration)
|
||||
private.PostAuctionHookHandler(duration)
|
||||
end)
|
||||
hooksecurefunc(C_AuctionHouse, "PostItem", function(_, duration)
|
||||
private.PostAuctionHookHandler(duration)
|
||||
end)
|
||||
hooksecurefunc(C_AuctionHouse, "CancelAuction", function(auctionId)
|
||||
private.cancelAuctionId = auctionId
|
||||
end)
|
||||
end
|
||||
|
||||
-- setup enhanced sale / buy messages
|
||||
ChatFrame_AddMessageEventFilter("CHAT_MSG_SYSTEM", private.FilterSystemMsg)
|
||||
if TSM.IsWowClassic() then
|
||||
hooksecurefunc("PlaceAuctionBid", function(_, index, amountPaid)
|
||||
local link = GetAuctionItemLink("list", index)
|
||||
local name, _, stackSize, _, _, _, _, _, _, buyout = GetAuctionItemInfo("list", index)
|
||||
if amountPaid == buyout then
|
||||
wipe(private.lastPurchase)
|
||||
private.lastPurchase.name = name
|
||||
private.lastPurchase.link = link
|
||||
private.lastPurchase.stackSize = stackSize
|
||||
private.lastPurchase.buyout = buyout
|
||||
end
|
||||
end)
|
||||
else
|
||||
Event.Register("ITEM_SEARCH_RESULTS_UPDATED", function(_, itemKey)
|
||||
wipe(private.auctionIdToLink)
|
||||
wipe(private.auctionIdToItemBuyout)
|
||||
for i = 1, C_AuctionHouse.GetNumItemSearchResults(itemKey) do
|
||||
local info = C_AuctionHouse.GetItemSearchResultInfo(itemKey, i)
|
||||
if info.buyoutAmount then
|
||||
private.auctionIdToLink[info.auctionID] = info.itemLink
|
||||
private.auctionIdToItemBuyout[info.auctionID] = info.buyoutAmount
|
||||
end
|
||||
end
|
||||
end)
|
||||
hooksecurefunc(C_AuctionHouse, "PlaceBid", function(auctionId, bidPlaced)
|
||||
local link = private.auctionIdToLink[auctionId]
|
||||
local buyout = private.auctionIdToItemBuyout[auctionId]
|
||||
if not link or buyout ~= bidPlaced then
|
||||
return
|
||||
end
|
||||
wipe(private.lastPurchase)
|
||||
private.lastPurchase.name = ItemInfo.GetName(link)
|
||||
private.lastPurchase.link = link
|
||||
private.lastPurchase.stackSize = 1
|
||||
private.lastPurchase.buyout = bidPlaced
|
||||
end)
|
||||
hooksecurefunc(C_AuctionHouse, "ConfirmCommoditiesPurchase", function(itemId, quantity)
|
||||
local link = ItemInfo.GetLink("i:"..itemId)
|
||||
if not link then
|
||||
return
|
||||
end
|
||||
local origQuantity = quantity
|
||||
local buyout = 0
|
||||
for i = 1, C_AuctionHouse.GetNumCommoditySearchResults(itemId) do
|
||||
local info = C_AuctionHouse.GetCommoditySearchResultInfo(itemId, i)
|
||||
local resultQuantity = min(quantity, info.quantity - info.numOwnerItems)
|
||||
buyout = buyout + resultQuantity * info.unitPrice
|
||||
quantity = quantity - resultQuantity
|
||||
if quantity == 0 then
|
||||
break
|
||||
end
|
||||
end
|
||||
if quantity > 0 then
|
||||
return
|
||||
end
|
||||
private.lastPurchase.name = ItemInfo.GetName(link)
|
||||
private.lastPurchase.link = link
|
||||
private.lastPurchase.stackSize = origQuantity
|
||||
private.lastPurchase.buyout = buyout
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|
||||
AuctionTracking:OnGameDataLoad(function()
|
||||
-- setup auction created / cancelled filtering
|
||||
-- NOTE: this is delayed until the game is loaded to avoid taint issues
|
||||
local ElvUIChat, ElvUIChatIsEnabled = nil, nil
|
||||
if IsAddOnLoaded("ElvUI") and ElvUI then
|
||||
ElvUIChat = ElvUI[1]:GetModule("Chat")
|
||||
if ElvUI[3].chat.enable then
|
||||
ElvUIChatIsEnabled = true
|
||||
end
|
||||
end
|
||||
if ElvUIChatIsEnabled then
|
||||
private.origChatFrameOnEvent = ElvUIChat.ChatFrame_OnEvent
|
||||
ElvUIChat.ChatFrame_OnEvent = private.ChatFrameOnEvent
|
||||
else
|
||||
private.origChatFrameOnEvent = ChatFrame_OnEvent
|
||||
ChatFrame_OnEvent = private.ChatFrameOnEvent
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function AuctionTracking.RegisterCallback(callback)
|
||||
tinsert(private.callbacks, callback)
|
||||
end
|
||||
|
||||
function AuctionTracking.RegisterExpiresCallback(callback)
|
||||
tinsert(private.expiresCallbacks, callback)
|
||||
end
|
||||
|
||||
function AuctionTracking.DatabaseFieldIterator()
|
||||
return private.indexDB:FieldIterator()
|
||||
end
|
||||
|
||||
function AuctionTracking.BaseItemIterator()
|
||||
return private.quantityDB:NewQuery()
|
||||
:Select("itemString")
|
||||
:IteratorAndRelease()
|
||||
end
|
||||
|
||||
function AuctionTracking.CreateQuery()
|
||||
return private.indexDB:NewQuery()
|
||||
end
|
||||
|
||||
function AuctionTracking.CreateQueryUnsold()
|
||||
return AuctionTracking.CreateQuery()
|
||||
:Equal("saleStatus", 0)
|
||||
end
|
||||
|
||||
function AuctionTracking.CreateQueryUnsoldItem(itemString)
|
||||
return AuctionTracking.CreateQueryUnsold()
|
||||
:Equal(itemString == ItemString.GetBaseFast(itemString) and "baseItemString" or "itemString", itemString)
|
||||
end
|
||||
|
||||
function AuctionTracking.GetSaleHintItemString(name, stackSize, buyout)
|
||||
for info in pairs(private.settings.auctionSaleHints) do
|
||||
local infoName, itemString, infoStackSize, infoBuyout = strsplit(SALE_HINT_SEP, info)
|
||||
if infoName == name and tonumber(infoStackSize) == stackSize and tonumber(infoBuyout) == buyout then
|
||||
return itemString
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionTracking.GetQuantityByBaseItemString(baseItemString)
|
||||
return private.quantityDB:GetUniqueRowField("itemString", baseItemString, "quantity") or 0
|
||||
end
|
||||
|
||||
function AuctionTracking.QueryOwnedAuctions()
|
||||
if not private.isAHOpen then
|
||||
return
|
||||
end
|
||||
if TSM.IsWowClassic() then
|
||||
GetOwnerAuctionItems()
|
||||
else
|
||||
if private.pendingFuture then
|
||||
return
|
||||
end
|
||||
private.pendingFuture = AuctionHouseWrapper.QueryOwnedAuctions(SORT_ORDER)
|
||||
if not private.pendingFuture then
|
||||
Delay.AfterTime(0.5, AuctionTracking.QueryOwnedAuctions)
|
||||
return
|
||||
end
|
||||
private.pendingFuture:SetScript("OnDone", private.PendingFutureOnDone)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Event Handlers
|
||||
-- ============================================================================
|
||||
|
||||
function private.AuctionHouseShowHandler()
|
||||
private.isAHOpen = true
|
||||
if TSM.IsWowClassic() then
|
||||
AuctionTracking.QueryOwnedAuctions()
|
||||
-- We don't always get AUCTION_OWNED_LIST_UPDATE events, so do our own scanning if needed
|
||||
Delay.AfterTime("AUCTION_BACKGROUND_SCAN", 1, private.DoBackgroundScan, 1)
|
||||
else
|
||||
Delay.AfterTime(0.1, AuctionTracking.QueryOwnedAuctions)
|
||||
end
|
||||
end
|
||||
|
||||
function private.AuctionHouseClosedHandler()
|
||||
private.isAHOpen = false
|
||||
Delay.Cancel("AUCTION_BACKGROUND_SCAN")
|
||||
end
|
||||
|
||||
function private.DoBackgroundScan()
|
||||
if private.GetNumOwnedAuctions() ~= private.lastScanNum then
|
||||
private.AuctionOwnedListUpdateHandler()
|
||||
end
|
||||
end
|
||||
|
||||
function private.AuctionOwnedListUpdateHandler()
|
||||
if private.ignoreUpdateEvent then
|
||||
return
|
||||
end
|
||||
wipe(private.indexUpdates.pending)
|
||||
wipe(private.indexUpdates.list)
|
||||
local numOwned = private.GetNumOwnedAuctions()
|
||||
for i = 1, numOwned do
|
||||
if not private.indexUpdates.pending[i] then
|
||||
private.indexUpdates.pending[i] = true
|
||||
tinsert(private.indexUpdates.list, i)
|
||||
end
|
||||
end
|
||||
if numOwned == 0 and private.settings.expiringAuction[PLAYER_NAME] then
|
||||
private.settings.expiringAuction[PLAYER_NAME] = nil
|
||||
for _, callback in ipairs(private.expiresCallbacks) do
|
||||
callback()
|
||||
end
|
||||
end
|
||||
Delay.AfterFrame("AUCTION_OWNED_LIST_SCAN", 2, private.AuctionOwnedListUpdateDelayed)
|
||||
end
|
||||
|
||||
function private.AuctionCanceledHandler(_, auctionId)
|
||||
if not private.cancelAuctionId or auctionId ~= 0 then
|
||||
-- an auction was bought, so rescan the owned auctions
|
||||
AuctionTracking.QueryOwnedAuctions()
|
||||
return
|
||||
end
|
||||
local row = private.indexDB:NewQuery()
|
||||
:Equal("auctionId", private.cancelAuctionId)
|
||||
:GetFirstResultAndRelease()
|
||||
private.cancelAuctionId = nil
|
||||
if not row then
|
||||
return
|
||||
end
|
||||
|
||||
local baseItemString = row:GetField("baseItemString")
|
||||
local stackSize = row:GetField("stackSize")
|
||||
assert(stackSize <= private.settings.auctionQuantity[baseItemString])
|
||||
private.settings.auctionQuantity[baseItemString] = private.settings.auctionQuantity[baseItemString] - stackSize
|
||||
private.RebuildQuantityDB()
|
||||
private.indexDB:DeleteRow(row)
|
||||
row:Release()
|
||||
end
|
||||
|
||||
function private.AuctionOwnedListUpdateDelayed()
|
||||
if not private.isAHOpen then
|
||||
return
|
||||
elseif AuctionFrame and AuctionFrame:IsVisible() and AuctionFrame.selectedTab == 3 then
|
||||
-- default UI auctions tab is visible, so scan later
|
||||
Delay.AfterFrame("AUCTION_OWNED_LIST_SCAN", 2, private.AuctionOwnedListUpdateDelayed)
|
||||
return
|
||||
elseif not TSM.IsWowClassic() and not C_AuctionHouse.HasFullOwnedAuctionResults() then
|
||||
-- don't have all the results yet, so try again in a moment
|
||||
Delay.AfterFrame("AUCTION_OWNED_LIST_SCAN", 0.1, private.AuctionOwnedListUpdateDelayed)
|
||||
return
|
||||
end
|
||||
if TSM.IsWowClassic() then
|
||||
-- check if we need to change the sort
|
||||
local needsSort = false
|
||||
local numColumns = #AuctionSort.owner_duration
|
||||
for i, info in ipairs(AuctionSort.owner_duration) do
|
||||
local col, reversed = GetAuctionSort("owner", numColumns - i + 1)
|
||||
-- we want to do the opposite order
|
||||
reversed = not reversed
|
||||
if col ~= info.column or info.reverse ~= reversed then
|
||||
needsSort = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if needsSort then
|
||||
Log.Info("Sorting owner auctions")
|
||||
-- ignore events while changing the sort
|
||||
private.ignoreUpdateEvent = true
|
||||
AuctionFrame_SetSort("owner", "duration", true)
|
||||
SortAuctionApplySort("owner")
|
||||
private.ignoreUpdateEvent = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- scan the auctions
|
||||
local shouldLog = GetTime() - private.prevLogTime > 5
|
||||
if shouldLog then
|
||||
private.prevLogTime = GetTime()
|
||||
end
|
||||
wipe(private.settings.auctionQuantity)
|
||||
private.indexDB:TruncateAndBulkInsertStart()
|
||||
local expire = math.huge
|
||||
for i = #private.indexUpdates.list, 1, -1 do
|
||||
local index = private.indexUpdates.list[i]
|
||||
local auctionId, link, name, texture, stackSize, quality, minBid, buyout, bid, highBidder, saleStatus, duration, shouldIgnore = private.GetOwnedAuctionInfo(index)
|
||||
if shouldIgnore then
|
||||
private.indexUpdates.pending[index] = nil
|
||||
tremove(private.indexUpdates.list, i)
|
||||
else
|
||||
name = name or ItemInfo.GetName(link)
|
||||
texture = texture or ItemInfo.GetTexture(link)
|
||||
quality = quality or ItemInfo.GetQuality(link)
|
||||
if link and name and texture and quality then
|
||||
assert(saleStatus == 0 or saleStatus == 1)
|
||||
highBidder = highBidder or ""
|
||||
local itemString = ItemString.Get(link)
|
||||
local currentBid = highBidder ~= "" and bid or minBid
|
||||
if not currentBid and saleStatus == 1 and not TSM.IsWowClassic() then
|
||||
-- sometimes wow doesn't tell us the current bid on sold auctions on retail
|
||||
currentBid = 0
|
||||
end
|
||||
if saleStatus == 0 then
|
||||
if TSM.IsWowClassic() then
|
||||
if duration == 1 then -- 30 min
|
||||
expire = min(expire, time() + 0.5 * 60 * 60)
|
||||
elseif duration == 2 then -- 2 hours
|
||||
expire = min(expire, time() + 2 * 60 * 60)
|
||||
elseif duration == 3 then -- 12 hours
|
||||
expire = min(expire, time() + 12 * 60 * 60)
|
||||
end
|
||||
else
|
||||
duration = time() + duration
|
||||
expire = min(expire, duration)
|
||||
end
|
||||
local baseItemString = ItemString.GetBaseFast(itemString)
|
||||
private.settings.auctionQuantity[baseItemString] = (private.settings.auctionQuantity[baseItemString] or 0) + stackSize
|
||||
local hintInfo = strjoin(SALE_HINT_SEP, ItemInfo.GetName(link), itemString, stackSize, buyout)
|
||||
private.settings.auctionSaleHints[hintInfo] = time()
|
||||
else
|
||||
duration = time() + duration
|
||||
end
|
||||
private.indexUpdates.pending[index] = nil
|
||||
tremove(private.indexUpdates.list, i)
|
||||
private.indexDB:BulkInsertNewRow(index, itemString, link, texture, name, quality, duration, highBidder, currentBid, buyout, stackSize, saleStatus, auctionId)
|
||||
elseif shouldLog then
|
||||
Log.Warn("Missing info (%s, %s, %s, %s)", gsub(tostring(link), "\124", "\\124"), tostring(name), tostring(texture), tostring(quality))
|
||||
if link and strmatch(link, "item:") and not TSM.IsWowClassic() then
|
||||
Analytics.Action("AUCTION_TRACKING_MISSING_INFO", link)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
private.RebuildQuantityDB()
|
||||
private.indexDB:BulkInsertEnd()
|
||||
|
||||
if expire ~= math.huge and (private.settings.expiringAuction[PLAYER_NAME] or math.huge) > expire then
|
||||
private.settings.expiringAuction[PLAYER_NAME] = expire
|
||||
for _, callback in ipairs(private.expiresCallbacks) do
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
if shouldLog or #private.indexUpdates.list ~= private.prevLogNum then
|
||||
Log.Info("Scanned auctions (left=%d)", #private.indexUpdates.list)
|
||||
private.prevLogNum = #private.indexUpdates.list
|
||||
end
|
||||
if #private.indexUpdates.list > 0 then
|
||||
-- some failed to scan so try again
|
||||
Delay.AfterFrame("AUCTION_OWNED_LIST_SCAN", 2, private.AuctionOwnedListUpdateDelayed)
|
||||
else
|
||||
private.lastScanNum = private.GetNumOwnedAuctions()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.RebuildQuantityDB()
|
||||
private.quantityDB:TruncateAndBulkInsertStart()
|
||||
for itemString, quantity in pairs(private.settings.auctionQuantity) do
|
||||
if quantity > 0 then
|
||||
private.quantityDB:BulkInsertNewRow(itemString, quantity)
|
||||
else
|
||||
private.settings.auctionQuantity[itemString] = nil
|
||||
end
|
||||
end
|
||||
private.quantityDB:BulkInsertEnd()
|
||||
end
|
||||
|
||||
function private.GetNumOwnedAuctions()
|
||||
if TSM.IsWowClassic() then
|
||||
return GetNumAuctionItems("owner")
|
||||
else
|
||||
return C_AuctionHouse.GetNumOwnedAuctions()
|
||||
end
|
||||
end
|
||||
|
||||
function private.GetOwnedAuctionInfo(index)
|
||||
if TSM.IsWowClassic() then
|
||||
local name, texture, stackSize, quality, _, _, _, minBid, _, buyout, bid, highBidder, _, _, _, saleStatus = GetAuctionItemInfo("owner", index)
|
||||
local link = name and name ~= "" and GetAuctionItemLink("owner", index)
|
||||
if not link then
|
||||
return
|
||||
end
|
||||
local duration = GetAuctionItemTimeLeft("owner", index)
|
||||
return index, link, name, texture, stackSize, quality, minBid, buyout, bid, highBidder, saleStatus, duration
|
||||
else
|
||||
local info = C_AuctionHouse.GetOwnedAuctionInfo(index)
|
||||
if info.itemKey.itemID == AUCTIONABLE_WOW_TOKEN_ITEM_ID then
|
||||
-- this is a token, so just ignore it
|
||||
return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, true
|
||||
end
|
||||
local link = info and info.itemLink
|
||||
if not link then
|
||||
return
|
||||
end
|
||||
local bid = info.bidAmount or info.buyoutAmount
|
||||
local minBid = bid
|
||||
return info.auctionID, link, nil, nil, info.quantity, nil, minBid, info.buyoutAmount or 0, bid, info.bidder or "", info.status, info.timeLeftSeconds
|
||||
end
|
||||
end
|
||||
|
||||
function private.OnCallbackQueryUpdated()
|
||||
for _, callback in ipairs(private.callbacks) do
|
||||
callback()
|
||||
end
|
||||
-- updating the auction prices / messages is very low-priority, so throttle it to at most every 0.5 seconds
|
||||
Delay.AfterTime("UPDATE_AUCTION_PRICES_MESSAGES_THROTTLE", 0.5, private.UpdateAuctionPricesMessages)
|
||||
end
|
||||
|
||||
function private.PostAuctionHookHandler(duration)
|
||||
local days = nil
|
||||
if duration == 1 then
|
||||
days = 0.5
|
||||
elseif duration == 2 then
|
||||
days = 1
|
||||
elseif duration == 3 then
|
||||
days = 2
|
||||
end
|
||||
|
||||
local expiration = time() + (days * 24 * 60 * 60)
|
||||
if (private.settings.expiringAuction[PLAYER_NAME] or math.huge) < expiration then
|
||||
return
|
||||
end
|
||||
private.settings.expiringAuction[PLAYER_NAME] = expiration
|
||||
for _, callback in ipairs(private.expiresCallbacks) do
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
function private.UpdateAuctionPricesMessages()
|
||||
local INVALID_STACK_SIZE = -1
|
||||
-- recycle tables from private.settings.auctionPrices if we can so we're not creating a ton of garbage
|
||||
local freeTables = TempTable.Acquire()
|
||||
for _, tbl in pairs(private.settings.auctionPrices) do
|
||||
wipe(tbl)
|
||||
tinsert(freeTables, tbl)
|
||||
end
|
||||
wipe(private.settings.auctionPrices)
|
||||
wipe(private.settings.auctionMessages)
|
||||
local auctionPrices = TempTable.Acquire()
|
||||
local auctionStackSizes = TempTable.Acquire()
|
||||
local query = AuctionTracking.CreateQueryUnsold()
|
||||
:Select("itemLink", "stackSize", "buyout")
|
||||
:GreaterThan("buyout", 0)
|
||||
:OrderBy("index", true)
|
||||
for _, link, stackSize, buyout in query:IteratorAndRelease() do
|
||||
auctionPrices[link] = auctionPrices[link] or tremove(freeTables) or {}
|
||||
if stackSize ~= auctionStackSizes[link] then
|
||||
auctionStackSizes[link] = stackSize
|
||||
end
|
||||
tinsert(auctionPrices[link], buyout)
|
||||
end
|
||||
for link, prices in pairs(auctionPrices) do
|
||||
local name = ItemInfo.GetName(link)
|
||||
if auctionStackSizes[link] ~= INVALID_STACK_SIZE then
|
||||
sort(prices)
|
||||
private.settings.auctionPrices[link] = prices
|
||||
private.settings.auctionMessages[format(ERR_AUCTION_SOLD_S, name)] = link
|
||||
end
|
||||
end
|
||||
TempTable.Release(freeTables)
|
||||
TempTable.Release(auctionPrices)
|
||||
TempTable.Release(auctionStackSizes)
|
||||
end
|
||||
|
||||
function private.ChatFrameOnEvent(self, event, msg, ...)
|
||||
-- surpress auction created / cancelled spam
|
||||
if event == "CHAT_MSG_SYSTEM" and (msg == ERR_AUCTION_STARTED or msg == ERR_AUCTION_REMOVED) then
|
||||
return
|
||||
end
|
||||
return private.origChatFrameOnEvent(self, event, msg, ...)
|
||||
end
|
||||
|
||||
function private.FilterSystemMsg(_, _, msg, ...)
|
||||
local lineID = select(10, ...)
|
||||
if lineID ~= private.prevLineId then
|
||||
private.prevLineId = lineID
|
||||
private.prevLineResult = nil
|
||||
local link = private.settings.auctionMessages and private.settings.auctionMessages[msg]
|
||||
if private.lastPurchase.name and msg == format(ERR_AUCTION_WON_S, private.lastPurchase.name) then
|
||||
-- we just bought an auction
|
||||
private.prevLineResult = format(L["You won an auction for %sx%d for %s"], private.lastPurchase.link, private.lastPurchase.stackSize, Money.ToString(private.lastPurchase.buyout, "|cffffffff"))
|
||||
return nil, private.prevLineResult, ...
|
||||
elseif link then
|
||||
-- we may have just sold an auction
|
||||
local price = tremove(private.settings.auctionPrices[link], 1)
|
||||
local numAuctions = #private.settings.auctionPrices[link]
|
||||
if not price then
|
||||
-- couldn't determine the price, so just replace the link
|
||||
private.prevLineResult = format(ERR_AUCTION_SOLD_S, link)
|
||||
Sound.PlaySound(private.settings.auctionSaleSound)
|
||||
return nil, private.prevLineResult, ...
|
||||
end
|
||||
if numAuctions == 0 then -- this was the last auction
|
||||
private.settings.auctionMessages[msg] = nil
|
||||
end
|
||||
private.prevLineResult = format(L["Your auction of %s has sold for %s!"], link, Money.ToString(price, "|cffffffff"))
|
||||
Sound.PlaySound(private.settings.auctionSaleSound)
|
||||
return nil, private.prevLineResult, ...
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function private.PendingFutureOnDone()
|
||||
-- we also hook the event, so don't care what the result is
|
||||
private.pendingFuture:GetValue()
|
||||
private.pendingFuture = nil
|
||||
end
|
||||
615
LibTSM/Service/BagTracking.lua
Normal file
615
LibTSM/Service/BagTracking.lua
Normal file
@@ -0,0 +1,615 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local BagTracking = TSM.Init("Service.BagTracking")
|
||||
local Database = TSM.Include("Util.Database")
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local SlotId = TSM.Include("Util.SlotId")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local InventoryInfo = TSM.Include("Service.InventoryInfo")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local private = {
|
||||
slotDB = nil,
|
||||
quantityDB = nil,
|
||||
settings = nil,
|
||||
bagUpdates = {
|
||||
pending = {},
|
||||
bagList = {},
|
||||
bankList = {},
|
||||
},
|
||||
bankSlotUpdates = {
|
||||
pending = {},
|
||||
list = {},
|
||||
},
|
||||
reagentBankSlotUpdates = {
|
||||
pending = {},
|
||||
list = {},
|
||||
},
|
||||
bankOpen = false,
|
||||
isFirstBankOpen = true,
|
||||
callbackQuery = nil, -- luacheck: ignore 1004 - just stored for GC reasons
|
||||
callbacks = {},
|
||||
}
|
||||
local BANK_BAG_SLOTS = {}
|
||||
local BANK_NON_REAGENT_BAG_SLOTS = {}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Population of the Static Data
|
||||
-- ============================================================================
|
||||
|
||||
do
|
||||
BANK_BAG_SLOTS[BANK_CONTAINER] = true
|
||||
BANK_NON_REAGENT_BAG_SLOTS[BANK_CONTAINER] = true
|
||||
for i = NUM_BAG_SLOTS + 1, NUM_BAG_SLOTS + NUM_BANKBAGSLOTS do
|
||||
BANK_BAG_SLOTS[i] = true
|
||||
BANK_NON_REAGENT_BAG_SLOTS[i] = true
|
||||
end
|
||||
if not TSM.IsWowClassic() then
|
||||
BANK_BAG_SLOTS[REAGENTBANK_CONTAINER] = true
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
BagTracking:OnSettingsLoad(function()
|
||||
Event.Register("BAG_UPDATE", private.BagUpdateHandler)
|
||||
Event.Register("BAG_UPDATE_DELAYED", private.BagUpdateDelayedHandler)
|
||||
Event.Register("BANKFRAME_OPENED", private.BankOpenedHandler)
|
||||
Event.Register("BANKFRAME_CLOSED", private.BankClosedHandler)
|
||||
Event.Register("PLAYERBANKSLOTS_CHANGED", private.BankSlotChangedHandler)
|
||||
if not TSM.IsWowClassic() then
|
||||
Event.Register("PLAYERREAGENTBANKSLOTS_CHANGED", private.ReagentBankSlotChangedHandler)
|
||||
end
|
||||
private.slotDB = Database.NewSchema("BAG_TRACKING_SLOTS")
|
||||
:AddUniqueNumberField("slotId")
|
||||
:AddNumberField("bag")
|
||||
:AddNumberField("slot")
|
||||
:AddStringField("itemLink")
|
||||
:AddStringField("itemString")
|
||||
:AddSmartMapField("baseItemString", ItemString.GetBaseMap(), "itemString")
|
||||
:AddNumberField("itemTexture")
|
||||
:AddNumberField("quantity")
|
||||
:AddBooleanField("isBoP")
|
||||
:AddBooleanField("isBoA")
|
||||
:AddIndex("slotId")
|
||||
:AddIndex("bag")
|
||||
:AddIndex("itemString")
|
||||
:AddIndex("baseItemString")
|
||||
:Commit()
|
||||
private.quantityDB = Database.NewSchema("BAG_TRACKING_QUANTITY")
|
||||
:AddUniqueStringField("itemString")
|
||||
:AddNumberField("bagQuantity")
|
||||
:AddNumberField("bankQuantity")
|
||||
:AddNumberField("reagentBankQuantity")
|
||||
:Commit()
|
||||
private.callbackQuery = private.slotDB:NewQuery()
|
||||
:SetUpdateCallback(private.OnCallbackQueryUpdated)
|
||||
private.settings = Settings.NewView()
|
||||
:AddKey("sync", "internalData", "bagQuantity")
|
||||
:AddKey("sync", "internalData", "bankQuantity")
|
||||
:AddKey("sync", "internalData", "reagentBankQuantity")
|
||||
|
||||
local items = TempTable.Acquire()
|
||||
local bagQuantity = TempTable.Acquire()
|
||||
local bankQuantity = TempTable.Acquire()
|
||||
local reagentBankQuantity = TempTable.Acquire()
|
||||
for itemString, quantity in pairs(private.settings.bagQuantity) do
|
||||
if itemString == ItemString.GetBase(itemString) then
|
||||
items[itemString] = true
|
||||
bagQuantity[itemString] = quantity
|
||||
else
|
||||
private.settings.bagQuantity[itemString] = nil
|
||||
end
|
||||
end
|
||||
for itemString, quantity in pairs(private.settings.bankQuantity) do
|
||||
if itemString == ItemString.GetBase(itemString) then
|
||||
items[itemString] = true
|
||||
bankQuantity[itemString] = quantity
|
||||
else
|
||||
private.settings.bankQuantity[itemString] = nil
|
||||
end
|
||||
end
|
||||
for itemString, quantity in pairs(private.settings.reagentBankQuantity) do
|
||||
if itemString == ItemString.GetBase(itemString) then
|
||||
items[itemString] = true
|
||||
reagentBankQuantity[itemString] = quantity
|
||||
else
|
||||
private.settings.reagentBankQuantity[itemString] = nil
|
||||
end
|
||||
end
|
||||
private.quantityDB:BulkInsertStart()
|
||||
for itemString in pairs(items) do
|
||||
local total = (bagQuantity[itemString] or 0) + (bankQuantity[itemString] or 0) + (reagentBankQuantity[itemString] or 0)
|
||||
if total > 0 then
|
||||
private.quantityDB:BulkInsertNewRow(itemString, bagQuantity[itemString] or 0, bankQuantity[itemString] or 0, reagentBankQuantity[itemString] or 0)
|
||||
end
|
||||
end
|
||||
private.quantityDB:BulkInsertEnd()
|
||||
TempTable.Release(items)
|
||||
TempTable.Release(bagQuantity)
|
||||
TempTable.Release(bankQuantity)
|
||||
TempTable.Release(reagentBankQuantity)
|
||||
end)
|
||||
|
||||
BagTracking:OnGameDataLoad(function()
|
||||
-- we'll scan all the bags and reagent bank right away, so wipe the existing quantities
|
||||
wipe(private.settings.bagQuantity)
|
||||
wipe(private.settings.reagentBankQuantity)
|
||||
private.quantityDB:SetQueryUpdatesPaused(true)
|
||||
local query = private.quantityDB:NewQuery()
|
||||
for _, row in query:Iterator() do
|
||||
local oldValue = row:GetField("bagQuantity") + row:GetField("reagentBankQuantity")
|
||||
if row:GetField("bankQuantity") == 0 then
|
||||
-- remove this row
|
||||
assert(oldValue > 0)
|
||||
private.quantityDB:DeleteRow(row)
|
||||
elseif oldValue ~= 0 then
|
||||
-- update this row
|
||||
row:SetField("bagQuantity", 0)
|
||||
:SetField("reagentBankQuantity", 0)
|
||||
:Update()
|
||||
end
|
||||
end
|
||||
query:Release()
|
||||
private.quantityDB:SetQueryUpdatesPaused(false)
|
||||
|
||||
-- WoW does not fire an update event for the backpack when you log in, so trigger one
|
||||
private.BagUpdateHandler(nil, 0)
|
||||
private.BagUpdateDelayedHandler()
|
||||
-- trigger an update event for all bank (initial container) and reagent bank slots since we won't get one otherwise on login
|
||||
assert(GetContainerNumSlots(BANK_CONTAINER) == NUM_BANKGENERIC_SLOTS)
|
||||
for slot = 1, GetContainerNumSlots(BANK_CONTAINER) do
|
||||
private.BankSlotChangedHandler(nil, slot)
|
||||
end
|
||||
if not TSM.IsWowClassic() and IsReagentBankUnlocked() then
|
||||
for slot = 1, GetContainerNumSlots(REAGENTBANK_CONTAINER) do
|
||||
private.ReagentBankSlotChangedHandler(nil, slot)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function BagTracking.RegisterCallback(callback)
|
||||
tinsert(private.callbacks, callback)
|
||||
end
|
||||
|
||||
function BagTracking.BaseItemIterator()
|
||||
return private.quantityDB:NewQuery()
|
||||
:Select("itemString")
|
||||
:IteratorAndRelease()
|
||||
end
|
||||
|
||||
function BagTracking.FilterQueryBags(query)
|
||||
return query
|
||||
:GreaterThanOrEqual("slotId", SlotId.Join(0, 1))
|
||||
:LessThanOrEqual("slotId", SlotId.Join(NUM_BAG_SLOTS + 1, 0))
|
||||
end
|
||||
|
||||
function BagTracking.CreateQueryBags()
|
||||
return BagTracking.FilterQueryBags(private.slotDB:NewQuery())
|
||||
end
|
||||
|
||||
function BagTracking.CreateQueryBagsAuctionable()
|
||||
return BagTracking.CreateQueryBags()
|
||||
:Equal("isBoP", false)
|
||||
:Equal("isBoA", false)
|
||||
:Custom(private.NoUsedChargesQueryFilter)
|
||||
end
|
||||
|
||||
function BagTracking.CreateQueryBagsItem(itemString)
|
||||
local query = BagTracking.CreateQueryBags()
|
||||
if itemString == ItemString.GetBaseFast(itemString) then
|
||||
query:Equal("baseItemString", itemString)
|
||||
else
|
||||
query:Equal("itemString", itemString)
|
||||
end
|
||||
return query
|
||||
end
|
||||
|
||||
function BagTracking.CreateQueryBagsItemAuctionable(itemString)
|
||||
return BagTracking.CreateQueryBagsItem(itemString)
|
||||
:Equal("isBoP", false)
|
||||
:Equal("isBoA", false)
|
||||
:Custom(private.NoUsedChargesQueryFilter)
|
||||
end
|
||||
|
||||
function BagTracking.GetNumMailable(itemString)
|
||||
return BagTracking.CreateQueryBagsItem(itemString)
|
||||
:Equal("isBoP", false)
|
||||
:SumAndRelease("quantity") or 0
|
||||
end
|
||||
|
||||
function BagTracking.CreateQueryBank()
|
||||
return private.slotDB:NewQuery()
|
||||
:InTable("bag", BANK_BAG_SLOTS)
|
||||
end
|
||||
|
||||
function BagTracking.CreateQueryBankItem(itemString)
|
||||
local query = BagTracking.CreateQueryBank()
|
||||
if itemString == ItemString.GetBaseFast(itemString) then
|
||||
query:Equal("baseItemString", itemString)
|
||||
else
|
||||
query:Equal("itemString", itemString)
|
||||
end
|
||||
return query
|
||||
end
|
||||
|
||||
function BagTracking.ForceBankQuantityDeduction(itemString, quantity)
|
||||
if private.bankOpen then
|
||||
return
|
||||
end
|
||||
private.slotDB:SetQueryUpdatesPaused(true)
|
||||
local query = private.slotDB:NewQuery()
|
||||
:Equal("itemString", itemString)
|
||||
:InTable("bag", BANK_NON_REAGENT_BAG_SLOTS)
|
||||
local baseItemString = ItemString.GetBaseFast(itemString)
|
||||
for _, row in query:Iterator() do
|
||||
if quantity > 0 then
|
||||
local rowQuantity, rowBag = row:GetFields("quantity", "bag")
|
||||
if rowQuantity <= quantity then
|
||||
private.ChangeBagItemTotal(rowBag, baseItemString, -rowQuantity)
|
||||
private.slotDB:DeleteRow(row)
|
||||
quantity = quantity - rowQuantity
|
||||
else
|
||||
row:SetField("quantity", rowQuantity - quantity)
|
||||
:Update()
|
||||
private.ChangeBagItemTotal(rowBag, baseItemString, -quantity)
|
||||
quantity = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
query:Release()
|
||||
private.slotDB:SetQueryUpdatesPaused(false)
|
||||
end
|
||||
|
||||
function BagTracking.GetQuantityBySlotId(slotId)
|
||||
return private.slotDB:GetUniqueRowField("slotId", slotId, "quantity")
|
||||
end
|
||||
|
||||
function BagTracking.GetBagsQuantityByBaseItemString(baseItemString)
|
||||
return private.quantityDB:GetUniqueRowField("itemString", baseItemString, "bagQuantity") or 0
|
||||
end
|
||||
|
||||
function BagTracking.GetBankQuantityByBaseItemString(baseItemString)
|
||||
return private.quantityDB:GetUniqueRowField("itemString", baseItemString, "bankQuantity") or 0
|
||||
end
|
||||
|
||||
function BagTracking.GetReagentBankQuantityByBaseItemString(baseItemString)
|
||||
return private.quantityDB:GetUniqueRowField("itemString", baseItemString, "reagentBankQuantity") or 0
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Event Handlers
|
||||
-- ============================================================================
|
||||
|
||||
function private.BankOpenedHandler()
|
||||
if private.isFirstBankOpen then
|
||||
private.isFirstBankOpen = false
|
||||
-- this is the first time opening the bank so we'll scan all the items so wipe our existing quantities
|
||||
wipe(private.settings.bankQuantity)
|
||||
private.quantityDB:SetQueryUpdatesPaused(true)
|
||||
local query = private.quantityDB:NewQuery()
|
||||
for _, row in query:Iterator() do
|
||||
local oldValue = row:GetField("bankQuantity")
|
||||
if row:GetField("bagQuantity") + row:GetField("reagentBankQuantity") == 0 then
|
||||
-- remove this row
|
||||
assert(oldValue > 0)
|
||||
private.quantityDB:DeleteRow(row)
|
||||
elseif oldValue ~= 0 then
|
||||
-- update this row
|
||||
row:SetField("bankQuantity", 0)
|
||||
:Update()
|
||||
end
|
||||
end
|
||||
query:Release()
|
||||
private.quantityDB:SetQueryUpdatesPaused(false)
|
||||
end
|
||||
private.bankOpen = true
|
||||
private.BagUpdateDelayedHandler()
|
||||
private.BankSlotUpdateDelayed()
|
||||
end
|
||||
|
||||
function private.BankClosedHandler()
|
||||
private.bankOpen = false
|
||||
end
|
||||
|
||||
function private.BagUpdateHandler(_, bag)
|
||||
if private.bagUpdates.pending[bag] then
|
||||
return
|
||||
end
|
||||
private.bagUpdates.pending[bag] = true
|
||||
if bag >= BACKPACK_CONTAINER and bag <= NUM_BAG_SLOTS then
|
||||
tinsert(private.bagUpdates.bagList, bag)
|
||||
elseif bag == BANK_CONTAINER or (bag > NUM_BAG_SLOTS and bag <= NUM_BAG_SLOTS + NUM_BANKBAGSLOTS) then
|
||||
tinsert(private.bagUpdates.bankList, bag)
|
||||
elseif bag ~= KEYRING_CONTAINER then
|
||||
error("Unexpected bag: "..tostring(bag))
|
||||
end
|
||||
end
|
||||
|
||||
function private.BagUpdateDelayedHandler()
|
||||
private.slotDB:SetQueryUpdatesPaused(true)
|
||||
|
||||
-- scan any pending bags
|
||||
for i = #private.bagUpdates.bagList, 1, -1 do
|
||||
local bag = private.bagUpdates.bagList[i]
|
||||
if private.ScanBagOrBank(bag) then
|
||||
private.bagUpdates.pending[bag] = nil
|
||||
tremove(private.bagUpdates.bagList, i)
|
||||
end
|
||||
end
|
||||
if #private.bagUpdates.bagList > 0 then
|
||||
-- some failed to scan so try again
|
||||
Delay.AfterFrame("bagBankScan", 2, private.BagUpdateDelayedHandler)
|
||||
end
|
||||
|
||||
if private.bankOpen then
|
||||
-- scan any pending bank bags
|
||||
for i = #private.bagUpdates.bankList, 1, -1 do
|
||||
local bag = private.bagUpdates.bankList[i]
|
||||
if private.ScanBagOrBank(bag) then
|
||||
private.bagUpdates.pending[bag] = nil
|
||||
tremove(private.bagUpdates.bankList, i)
|
||||
end
|
||||
end
|
||||
if #private.bagUpdates.bankList > 0 then
|
||||
-- some failed to scan so try again
|
||||
Delay.AfterFrame("bagBankScan", 2, private.BagUpdateDelayedHandler)
|
||||
end
|
||||
end
|
||||
|
||||
private.slotDB:SetQueryUpdatesPaused(false)
|
||||
end
|
||||
|
||||
function private.BankSlotChangedHandler(_, slot)
|
||||
if slot > NUM_BANKGENERIC_SLOTS then
|
||||
private.BagUpdateHandler(nil, slot - NUM_BANKGENERIC_SLOTS)
|
||||
return
|
||||
end
|
||||
if private.bankSlotUpdates.pending[slot] then
|
||||
return
|
||||
end
|
||||
private.bankSlotUpdates.pending[slot] = true
|
||||
tinsert(private.bankSlotUpdates.list, slot)
|
||||
Delay.AfterFrame("bankSlotScan", 2, private.BankSlotUpdateDelayed)
|
||||
end
|
||||
|
||||
-- this is not a WoW event, but we fake it based on a delay from private.BankSlotChangedHandler
|
||||
function private.BankSlotUpdateDelayed()
|
||||
if not private.bankOpen then
|
||||
return
|
||||
end
|
||||
private.slotDB:SetQueryUpdatesPaused(true)
|
||||
|
||||
-- scan any pending slots
|
||||
for i = #private.bankSlotUpdates.list, 1, -1 do
|
||||
local slot = private.bankSlotUpdates.list[i]
|
||||
if private.ScanBankSlot(slot) then
|
||||
private.bankSlotUpdates.pending[slot] = nil
|
||||
tremove(private.bankSlotUpdates.list, i)
|
||||
end
|
||||
end
|
||||
if #private.bankSlotUpdates.list > 0 then
|
||||
-- some failed to scan so try again
|
||||
Delay.AfterFrame("bankSlotScan", 2, private.BankSlotUpdateDelayed)
|
||||
end
|
||||
|
||||
private.slotDB:SetQueryUpdatesPaused(false)
|
||||
end
|
||||
|
||||
function private.ReagentBankSlotChangedHandler(_, slot)
|
||||
if private.reagentBankSlotUpdates.pending[slot] then
|
||||
return
|
||||
end
|
||||
private.reagentBankSlotUpdates.pending[slot] = true
|
||||
tinsert(private.reagentBankSlotUpdates.list, slot)
|
||||
Delay.AfterFrame("reagentBankSlotScan", 2, private.ReagentBankSlotUpdateDelayed)
|
||||
end
|
||||
|
||||
-- this is not a WoW event, but we fake it based on a delay from private.ReagentBankSlotChangedHandler
|
||||
function private.ReagentBankSlotUpdateDelayed()
|
||||
private.slotDB:SetQueryUpdatesPaused(true)
|
||||
|
||||
-- scan any pending slots
|
||||
for i = #private.reagentBankSlotUpdates.list, 1, -1 do
|
||||
local slot = private.reagentBankSlotUpdates.list[i]
|
||||
if private.ScanReagentBankSlot(slot) then
|
||||
private.reagentBankSlotUpdates.pending[slot] = nil
|
||||
tremove(private.reagentBankSlotUpdates.list, i)
|
||||
end
|
||||
end
|
||||
if #private.reagentBankSlotUpdates.list > 0 then
|
||||
-- some failed to scan so try again
|
||||
Delay.AfterFrame("reagentBankSlotScan", 2, private.ReagentBankSlotUpdateDelayed)
|
||||
end
|
||||
|
||||
private.slotDB:SetQueryUpdatesPaused(false)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Scanning Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.ScanBagOrBank(bag)
|
||||
local numSlots = GetContainerNumSlots(bag)
|
||||
private.RemoveExtraSlots(bag, numSlots)
|
||||
local result = true
|
||||
for slot = 1, numSlots do
|
||||
if not private.ScanBagSlot(bag, slot) then
|
||||
result = false
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function private.ScanBankSlot(slot)
|
||||
return private.ScanBagSlot(BANK_CONTAINER, slot)
|
||||
end
|
||||
|
||||
function private.ScanReagentBankSlot(slot)
|
||||
return private.ScanBagSlot(REAGENTBANK_CONTAINER, slot)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.NoUsedChargesQueryFilter(row)
|
||||
return not InventoryInfo.HasUsedCharges(row:GetFields("bag", "slot"))
|
||||
end
|
||||
|
||||
function private.RemoveExtraSlots(bag, numSlots)
|
||||
-- the number of slots of this bag may have changed, in which case we should remove any higher ones from our DB
|
||||
local query = private.slotDB:NewQuery()
|
||||
:Equal("bag", bag)
|
||||
:GreaterThan("slot", numSlots)
|
||||
for _, row in query:Iterator() do
|
||||
local baseItemString, quantity = row:GetFields("baseItemString", "quantity")
|
||||
private.ChangeBagItemTotal(bag, baseItemString, -quantity)
|
||||
private.slotDB:DeleteRow(row)
|
||||
end
|
||||
query:Release()
|
||||
end
|
||||
|
||||
function private.ScanBagSlot(bag, slot)
|
||||
local texture, quantity, _, _, _, _, link, _, _, itemId = GetContainerItemInfo(bag, slot)
|
||||
if quantity and not itemId then
|
||||
-- we are pending item info for this slot so try again later to scan it
|
||||
return false
|
||||
elseif quantity == 0 then
|
||||
-- this item is going away, so try again later to scan it
|
||||
return false
|
||||
end
|
||||
local baseItemString = link and ItemString.GetBase(link)
|
||||
local slotId = SlotId.Join(bag, slot)
|
||||
local row = private.slotDB:GetUniqueRow("slotId", slotId)
|
||||
if baseItemString then
|
||||
local isBoP, isBoA = nil, nil
|
||||
if row then
|
||||
if row:GetField("itemLink") == link then
|
||||
-- the item didn't change, so use the previous values
|
||||
isBoP, isBoA = row:GetFields("isBoP", "isBoA")
|
||||
else
|
||||
isBoP, isBoA = InventoryInfo.IsSoulbound(bag, slot)
|
||||
if isBoP == nil then
|
||||
Log.Err("Failed to get soulbound info for %d,%d (%s)", bag, slot, link or "?")
|
||||
return false
|
||||
end
|
||||
end
|
||||
-- remove the old row from the item totals
|
||||
local oldBaseItemString, oldQuantity = row:GetFields("baseItemString", "quantity")
|
||||
private.ChangeBagItemTotal(bag, oldBaseItemString, -oldQuantity)
|
||||
else
|
||||
isBoP, isBoA = InventoryInfo.IsSoulbound(bag, slot)
|
||||
if isBoP == nil then
|
||||
Log.Err("Failed to get soulbound info for %d,%d (%s)", bag, slot, link or "?")
|
||||
return false
|
||||
end
|
||||
-- there was nothing here previously so create a new row
|
||||
row = private.slotDB:NewRow()
|
||||
:SetField("slotId", slotId)
|
||||
:SetField("bag", bag)
|
||||
:SetField("slot", slot)
|
||||
end
|
||||
-- update the row
|
||||
row:SetField("itemLink", link)
|
||||
:SetField("itemString", ItemString.Get(link))
|
||||
:SetField("itemTexture", texture or ItemInfo.GetTexture(link))
|
||||
:SetField("quantity", quantity)
|
||||
:SetField("isBoP", isBoP)
|
||||
:SetField("isBoA", isBoA)
|
||||
:CreateOrUpdateAndRelease()
|
||||
-- add to the item totals
|
||||
private.ChangeBagItemTotal(bag, baseItemString, quantity)
|
||||
elseif row then
|
||||
-- nothing here now so delete the row and remove from the item totals
|
||||
local oldBaseItemString, oldQuantity = row:GetFields("baseItemString", "quantity")
|
||||
private.ChangeBagItemTotal(bag, oldBaseItemString, -oldQuantity)
|
||||
private.slotDB:DeleteRow(row)
|
||||
row:Release()
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function private.OnCallbackQueryUpdated()
|
||||
for _, callback in ipairs(private.callbacks) do
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
function private.ChangeBagItemTotal(bag, itemString, changeQuantity)
|
||||
local totalsTable = nil
|
||||
local field = nil
|
||||
if bag >= BACKPACK_CONTAINER and bag <= NUM_BAG_SLOTS then
|
||||
totalsTable = private.settings.bagQuantity
|
||||
field = "bagQuantity"
|
||||
elseif bag == BANK_CONTAINER or (bag > NUM_BAG_SLOTS and bag <= NUM_BAG_SLOTS + NUM_BANKBAGSLOTS) then
|
||||
totalsTable = private.settings.bankQuantity
|
||||
field = "bankQuantity"
|
||||
elseif bag == REAGENTBANK_CONTAINER then
|
||||
totalsTable = private.settings.reagentBankQuantity
|
||||
field = "reagentBankQuantity"
|
||||
else
|
||||
error("Unexpected bag: "..tostring(bag))
|
||||
end
|
||||
totalsTable[itemString] = (totalsTable[itemString] or 0) + changeQuantity
|
||||
private.UpdateQuantity(itemString, field, changeQuantity)
|
||||
assert(totalsTable[itemString] >= 0)
|
||||
if totalsTable[itemString] == 0 then
|
||||
totalsTable[itemString] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function private.UpdateQuantity(itemString, field, quantity)
|
||||
assert(itemString and field and quantity)
|
||||
assert(quantity ~= 0)
|
||||
if not private.quantityDB:HasUniqueRow("itemString", itemString) then
|
||||
-- create a new row
|
||||
private.quantityDB:NewRow()
|
||||
:SetField("itemString", itemString)
|
||||
:SetField("bagQuantity", 0)
|
||||
:SetField("bankQuantity", 0)
|
||||
:SetField("reagentBankQuantity", 0)
|
||||
:Create()
|
||||
end
|
||||
|
||||
local row = private.quantityDB:GetUniqueRow("itemString", itemString)
|
||||
local totalQuantity = row:GetField("bagQuantity") + row:GetField("bankQuantity") + row:GetField("reagentBankQuantity")
|
||||
local oldValue = row:GetField(field)
|
||||
local newValue = oldValue + quantity
|
||||
assert(newValue >= 0)
|
||||
if newValue == 0 and totalQuantity == oldValue then
|
||||
-- remove this row
|
||||
private.quantityDB:DeleteRow(row)
|
||||
else
|
||||
-- update this row
|
||||
row:SetField(field, oldValue + quantity)
|
||||
:Update()
|
||||
end
|
||||
row:Release()
|
||||
end
|
||||
67
LibTSM/Service/BlackMarket.lua
Normal file
67
LibTSM/Service/BlackMarket.lua
Normal file
@@ -0,0 +1,67 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
-- This file contains all the code for TSM's standalone features
|
||||
|
||||
local _, TSM = ...
|
||||
local BlackMarket = TSM.Init("Service.BlackMarket")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local private = {
|
||||
data = nil,
|
||||
time = nil,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
BlackMarket:OnModuleLoad(function()
|
||||
-- setup BMAH scanning
|
||||
if not TSM.IsWowClassic() then
|
||||
Event.Register("BLACK_MARKET_ITEM_UPDATE", private.ScanBMAH)
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function BlackMarket.GetScanData()
|
||||
return private.data, private.time
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Features
|
||||
-- ============================================================================
|
||||
|
||||
function private.ScanBMAH()
|
||||
local numItems = C_BlackMarket.GetNumItems()
|
||||
if not numItems then
|
||||
return
|
||||
end
|
||||
local items = TempTable.Acquire()
|
||||
for i = 1, numItems do
|
||||
local _, _, quantity, _, _, _, _, _, minBid, minIncr, currBid, _, numBids, timeLeft, itemLink, bmId = C_BlackMarket.GetItemInfoByIndex(i)
|
||||
local itemID = ItemString.ToId(itemLink)
|
||||
if itemID then
|
||||
minBid = floor(minBid / COPPER_PER_GOLD)
|
||||
minIncr = floor(minIncr / COPPER_PER_GOLD)
|
||||
currBid = floor(currBid / COPPER_PER_GOLD)
|
||||
tinsert(items, "[" .. strjoin(",", bmId, itemID, quantity, timeLeft, minBid, minIncr, currBid, numBids, time()) .. "]")
|
||||
end
|
||||
end
|
||||
private.data = "[" .. table.concat(items, ",") .. "]"
|
||||
private.time = time()
|
||||
TempTable.Release(items)
|
||||
end
|
||||
182
LibTSM/Service/Conversions.lua
Normal file
182
LibTSM/Service/Conversions.lua
Normal file
@@ -0,0 +1,182 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Conversions = TSM.Init("Service.Conversions")
|
||||
local DisenchantInfo = TSM.Include("Data.DisenchantInfo")
|
||||
local Mill = TSM.Include("Data.Mill")
|
||||
local Prospect = TSM.Include("Data.Prospect")
|
||||
local Transform = TSM.Include("Data.Transform")
|
||||
local VendorTrade = TSM.Include("Data.VendorTrade")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local Table = TSM.Include("Util.Table")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
Conversions.METHOD = {
|
||||
DISENCHANT = newproxy(),
|
||||
MILL = newproxy(),
|
||||
PROSPECT = newproxy(),
|
||||
TRANSFORM = newproxy(),
|
||||
VENDOR_TRADE = newproxy(),
|
||||
CRAFT = newproxy(),
|
||||
}
|
||||
local private = {
|
||||
data = {},
|
||||
sourceItemCache = {},
|
||||
skippedConversions = {},
|
||||
}
|
||||
local MAX_CONVERSION_DEPTH = 3
|
||||
local EMPTY_CONVERSION = newproxy()
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Conversions:OnModuleLoad(function()
|
||||
Table.SetReadOnly(Conversions.METHOD)
|
||||
for itemString in DisenchantInfo.TargetItemIterator() do
|
||||
ItemInfo.FetchInfo(itemString)
|
||||
end
|
||||
for targetItemString in Mill.TargetItemIterator() do
|
||||
for sourceItemString in Mill.SourceItemIterator(targetItemString) do
|
||||
local rate = Mill.GetRate(targetItemString, sourceItemString)
|
||||
private.Add(targetItemString, sourceItemString, Conversions.METHOD.MILL, rate)
|
||||
end
|
||||
end
|
||||
for targetItemString in Prospect.TargetItemIterator() do
|
||||
for sourceItemString in Prospect.SourceItemIterator(targetItemString) do
|
||||
local rate, amount, minAmount, maxAmount = Prospect.GetRate(targetItemString, sourceItemString)
|
||||
private.Add(targetItemString, sourceItemString, Conversions.METHOD.PROSPECT, rate, amount, minAmount, maxAmount)
|
||||
end
|
||||
end
|
||||
for targetItemString in Transform.TargetItemIterator() do
|
||||
for sourceItemString in Transform.SourceItemIterator(targetItemString) do
|
||||
local rate = Transform.GetRate(targetItemString, sourceItemString)
|
||||
private.Add(targetItemString, sourceItemString, Conversions.METHOD.TRANSFORM, rate)
|
||||
end
|
||||
end
|
||||
for targetItemString in VendorTrade.TargetItemIterator() do
|
||||
for sourceItemString in VendorTrade.SourceItemIterator(targetItemString) do
|
||||
local rate = VendorTrade.GetRate(targetItemString, sourceItemString)
|
||||
private.Add(targetItemString, sourceItemString, Conversions.METHOD.VENDOR_TRADE, rate)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Conversions.AddCraft(targetItemString, sourceItemString, rate)
|
||||
private.Add(targetItemString, sourceItemString, Conversions.METHOD.CRAFT, rate)
|
||||
end
|
||||
|
||||
function Conversions.TargetItemsByMethodIterator(sourceItemString, method)
|
||||
local context = TempTable.Acquire()
|
||||
context.sourceItemString = sourceItemString
|
||||
context.method = method
|
||||
return private.TargetItemsByMethodIteratorHelper, context, nil
|
||||
end
|
||||
|
||||
function Conversions.GetTargetItemByName(targetItemName)
|
||||
targetItemName = strlower(targetItemName)
|
||||
for targetItemString in pairs(private.data) do
|
||||
local name = ItemInfo.GetName(targetItemString)
|
||||
if name and strlower(name) == targetItemName then
|
||||
return targetItemString
|
||||
end
|
||||
end
|
||||
for targetItemString in DisenchantInfo.TargetItemIterator() do
|
||||
local name = ItemInfo.GetName(targetItemString)
|
||||
if name and strlower(name) == targetItemName then
|
||||
return targetItemString
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Conversions.GetSourceItems(targetItemString)
|
||||
if not targetItemString or not private.data[targetItemString] or private.sourceItemCache[targetItemString] == EMPTY_CONVERSION then
|
||||
return
|
||||
end
|
||||
if not private.sourceItemCache[targetItemString] then
|
||||
local depthLookup = TempTable.Acquire()
|
||||
depthLookup[targetItemString] = -1 -- set this so we don't loop back through the target item
|
||||
private.sourceItemCache[targetItemString] = {}
|
||||
private.GetSourceItemsHelper(targetItemString, private.sourceItemCache[targetItemString], depthLookup, 0, 1)
|
||||
TempTable.Release(depthLookup)
|
||||
if not next(private.sourceItemCache[targetItemString]) then
|
||||
private.sourceItemCache[targetItemString] = EMPTY_CONVERSION
|
||||
return
|
||||
end
|
||||
end
|
||||
return private.sourceItemCache[targetItemString]
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.Add(targetItemString, sourceItemString, method, rate, amount, minAmount, maxAmount)
|
||||
targetItemString = ItemString.GetBase(targetItemString)
|
||||
sourceItemString = ItemString.GetBase(sourceItemString)
|
||||
assert(targetItemString and sourceItemString)
|
||||
|
||||
private.data[targetItemString] = private.data[targetItemString] or {}
|
||||
if private.data[targetItemString][sourceItemString] then
|
||||
-- if there is more than one way to go from source to target, then just skip all conversions between these items
|
||||
private.skippedConversions[targetItemString..sourceItemString] = true
|
||||
private.data[targetItemString][sourceItemString] = nil
|
||||
end
|
||||
if private.skippedConversions[targetItemString..sourceItemString] then
|
||||
return
|
||||
end
|
||||
|
||||
private.data[targetItemString][sourceItemString] = {
|
||||
method = method,
|
||||
rate = rate,
|
||||
amount = amount,
|
||||
minAmount = minAmount,
|
||||
maxAmount = maxAmount,
|
||||
}
|
||||
ItemInfo.FetchInfo(targetItemString)
|
||||
ItemInfo.FetchInfo(sourceItemString)
|
||||
wipe(private.sourceItemCache)
|
||||
end
|
||||
|
||||
function private.GetSourceItemsHelper(targetItemString, result, depthLookup, currentDepth, currentRate)
|
||||
if currentDepth >= MAX_CONVERSION_DEPTH or not private.data[targetItemString] then
|
||||
return
|
||||
end
|
||||
for sourceItemString, info in pairs(private.data[targetItemString]) do
|
||||
if not result[sourceItemString] or depthLookup[sourceItemString] > currentDepth then
|
||||
local rate = info.rate * currentRate
|
||||
result[sourceItemString] = rate
|
||||
depthLookup[sourceItemString] = currentDepth
|
||||
private.GetSourceItemsHelper(sourceItemString, result, depthLookup, currentDepth + 1, rate)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function private.TargetItemsByMethodIteratorHelper(context, index)
|
||||
while true do
|
||||
index = next(private.data, index)
|
||||
local items = private.data[index]
|
||||
if not items then
|
||||
TempTable.Release(context)
|
||||
return
|
||||
end
|
||||
local info = items[context.sourceItemString]
|
||||
if info and ((not context.method and info.method ~= Conversions.METHOD.CRAFT) or info.method == context.method) then
|
||||
return index, info.rate, info.amount, info.minAmount, info.maxAmount
|
||||
end
|
||||
end
|
||||
end
|
||||
1038
LibTSM/Service/CustomPrice.lua
Normal file
1038
LibTSM/Service/CustomPrice.lua
Normal file
File diff suppressed because it is too large
Load Diff
769
LibTSM/Service/ErrorHandler.lua
Normal file
769
LibTSM/Service/ErrorHandler.lua
Normal file
@@ -0,0 +1,769 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
-- TSM's error handler
|
||||
|
||||
local _, TSM = ...
|
||||
local ErrorHandler = TSM.Init("Service.ErrorHandler")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local String = TSM.Include("Util.String")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local JSON = TSM.Include("Util.JSON")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local L = TSM.Include("Locale").GetTable()
|
||||
local private = {
|
||||
origErrorHandler = nil,
|
||||
errorFrame = nil,
|
||||
isSilent = nil,
|
||||
errorSuppressed = nil,
|
||||
errorReports = {},
|
||||
num = 0,
|
||||
localLinesTemp = {},
|
||||
hitInternalError = false,
|
||||
isManual = nil,
|
||||
ignoreErrors = false,
|
||||
globalNameTranslation = {},
|
||||
}
|
||||
local MAX_ERROR_REPORT_AGE = 7 * 24 * 60 * 60 -- 1 week
|
||||
local MAX_STACK_DEPTH = 50
|
||||
local ADDON_SUITES = {
|
||||
"ArkInventory",
|
||||
"AtlasLoot",
|
||||
"Altoholic",
|
||||
"Auc-",
|
||||
"Bagnon",
|
||||
"BigWigs",
|
||||
"Broker",
|
||||
"ButtonFacade",
|
||||
"Carbonite",
|
||||
"DataStore",
|
||||
"DBM",
|
||||
"Dominos",
|
||||
"DXE",
|
||||
"EveryQuest",
|
||||
"Forte",
|
||||
"FuBar",
|
||||
"GatherMate2",
|
||||
"Grid",
|
||||
"LightHeaded",
|
||||
"LittleWigs",
|
||||
"Masque",
|
||||
"MogIt",
|
||||
"Odyssey",
|
||||
"Overachiever",
|
||||
"PitBull4",
|
||||
"Prat-3.0",
|
||||
"RaidAchievement",
|
||||
"Skada",
|
||||
"SpellFlash",
|
||||
"TidyPlates",
|
||||
"TipTac",
|
||||
"Titan",
|
||||
"UnderHood",
|
||||
"WowPro",
|
||||
"ZOMGBuffs",
|
||||
}
|
||||
local OLD_TSM_MODULES = {
|
||||
"TradeSkillMaster_Accounting",
|
||||
"TradeSkillMaster_AuctionDB",
|
||||
"TradeSkillMaster_Auctioning",
|
||||
"TradeSkillMaster_Crafting",
|
||||
"TradeSkillMaster_Destroying",
|
||||
"TradeSkillMaster_Mailing",
|
||||
"TradeSkillMaster_Shopping",
|
||||
"TradeSkillMaster_Vendoring",
|
||||
"TradeSkillMaster_Warehousing",
|
||||
}
|
||||
local PRINT_PREFIX = "|cffff0000TSM:|r "
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function ErrorHandler.ShowForThread(err, thread)
|
||||
local stackLine = debugstack(thread, 0, 1, 0)
|
||||
local oldModule = strmatch(stackLine, "(lMaster_[A-Za-z]+)")
|
||||
if oldModule and tContains(OLD_TSM_MODULES, "TradeSkil"..oldModule) then
|
||||
-- ignore errors from old modules
|
||||
return
|
||||
end
|
||||
-- show an error, but don't cause an exception to be thrown
|
||||
private.isSilent = true
|
||||
private.ErrorHandler(err, thread)
|
||||
end
|
||||
|
||||
function ErrorHandler.ShowManual()
|
||||
private.isManual = true
|
||||
-- show an error, but don't cause an exception to be thrown
|
||||
private.isSilent = true
|
||||
private.ErrorHandler("Manually triggered error")
|
||||
end
|
||||
|
||||
function ErrorHandler.SaveReports(appDB)
|
||||
if private.errorFrame then
|
||||
private.errorFrame:Hide()
|
||||
end
|
||||
appDB.errorReports = appDB.errorReports or { updateTime = 0, data = {} }
|
||||
if #private.errorReports > 0 then
|
||||
appDB.errorReports.updateTime = private.errorReports[#private.errorReports].timestamp
|
||||
end
|
||||
-- remove any events which are too old
|
||||
for i = #appDB.errorReports.data, 1, -1 do
|
||||
local timestamp = strmatch(appDB.errorReports.data[i], "([0-9]+)%]$") or ""
|
||||
if (tonumber(timestamp) or 0) < time() - MAX_ERROR_REPORT_AGE then
|
||||
tremove(appDB.errorReports.data, i)
|
||||
end
|
||||
end
|
||||
for _, report in ipairs(private.errorReports) do
|
||||
local line = format("[%s,\"%s\",%d]", JSON.Encode(report.errorInfo), report.details, report.timestamp)
|
||||
tinsert(appDB.errorReports.data, line)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Error Handler
|
||||
-- ============================================================================
|
||||
|
||||
function private.ErrorHandler(msg, thread)
|
||||
-- ignore errors while we are handling this error
|
||||
private.ignoreErrors = true
|
||||
local isSilent = private.isSilent
|
||||
private.isSilent = nil
|
||||
local isManual = private.isManual
|
||||
private.isManual = nil
|
||||
private.CreateErrorFrame()
|
||||
|
||||
if type(thread) ~= "thread" then
|
||||
thread = nil
|
||||
end
|
||||
|
||||
if private.errorFrame:IsVisible() and private.errorSuppressed then
|
||||
-- already showing an error and suppressed another one, so silently ignore this one
|
||||
private.ignoreErrors = false
|
||||
return true
|
||||
end
|
||||
|
||||
-- shorten the paths in the error message
|
||||
msg = gsub(msg, "%.%.%.T?r?a?d?e?S?k?i?l?l?M?a?ster([_A-Za-z]*)\\", "TradeSkillMaster%1\\")
|
||||
msg = strsub(msg, strfind(msg, "TradeSkillMaster") or 1)
|
||||
msg = gsub(msg, "TradeSkillMaster([^%.])", "TSM%1")
|
||||
|
||||
-- build our global name translation table
|
||||
wipe(private.globalNameTranslation)
|
||||
pcall(function()
|
||||
local UIElements = TSM.Include("UI.UIElements")
|
||||
local temp = {}
|
||||
UIElements.GetDebugNameTranslation(temp)
|
||||
for k, v in pairs(temp) do
|
||||
private.globalNameTranslation[String.Escape(k)] = v
|
||||
end
|
||||
end)
|
||||
|
||||
-- build stack trace with locals and get addon name
|
||||
local stackInfo = private.GetStackInfo(msg, thread)
|
||||
local addonName = isSilent and "TradeSkillMaster" or nil
|
||||
for _, info in ipairs(stackInfo) do
|
||||
if not addonName then
|
||||
addonName = strmatch(info.file, "[A-Za-z]+%.lua") and private.IsTSMAddon(info.file) or nil
|
||||
end
|
||||
end
|
||||
if not isManual and addonName ~= "TradeSkillMaster" then
|
||||
-- not a TSM error
|
||||
private.ignoreErrors = false
|
||||
return false
|
||||
end
|
||||
|
||||
if not TSM.IsDevVersion() and not isManual then
|
||||
-- log the error (use a format string in case there are '%' characters in the msg)
|
||||
Log.Err("%s", msg)
|
||||
end
|
||||
|
||||
if private.errorFrame:IsVisible() then
|
||||
-- already showing an error, so suppress this one and return
|
||||
private.errorSuppressed = true
|
||||
print(PRINT_PREFIX..L["Additional error suppressed"])
|
||||
return true
|
||||
end
|
||||
|
||||
private.num = private.num + 1
|
||||
local clientVersion, clientBuild = GetBuildInfo()
|
||||
local errorInfo = {
|
||||
msg = #stackInfo > 0 and gsub(msg, String.Escape(stackInfo[1].file)..":"..stackInfo[1].line..": ", "") or msg,
|
||||
stackInfo = stackInfo,
|
||||
time = time(),
|
||||
debugTime = floor(debugprofilestop()),
|
||||
client = format("%s (%s)", clientVersion, clientBuild),
|
||||
locale = GetLocale(),
|
||||
inCombat = tostring(InCombatLockdown() and true or false),
|
||||
version = TSM.GetVersion(),
|
||||
}
|
||||
|
||||
-- temp table info
|
||||
local tempTableLines = {}
|
||||
for _, info in ipairs(TempTable.GetDebugInfo()) do
|
||||
tinsert(tempTableLines, info)
|
||||
end
|
||||
errorInfo.tempTableStr = table.concat(tempTableLines, "\n")
|
||||
|
||||
-- object pool info
|
||||
local status, objectPoolInfo = pcall(function() return TSM.Include("Util.ObjectPool").GetDebugInfo() end)
|
||||
local objectPoolLines = {}
|
||||
if status then
|
||||
for name, objectInfo in pairs(objectPoolInfo) do
|
||||
tinsert(objectPoolLines, format("%s (%d created, %d in use)", name, objectInfo.numCreated, objectInfo.numInUse))
|
||||
for _, info in ipairs(objectInfo.info) do
|
||||
tinsert(objectPoolLines, " "..info)
|
||||
end
|
||||
end
|
||||
end
|
||||
errorInfo.objectPoolStr = status and table.concat(objectPoolLines, "\n") or tostring(objectPoolInfo)
|
||||
|
||||
-- TSM thread info
|
||||
local threadInfoStr = nil
|
||||
status, threadInfoStr = pcall(function() return TSM.Include("Service.Threading").GetDebugStr() end)
|
||||
errorInfo.threadInfoStr = tostring(threadInfoStr)
|
||||
|
||||
-- recent debug log entries
|
||||
local entries = {}
|
||||
for i = Log.Length(), 1, -1 do
|
||||
local severity, location, timeStr, logMsg = Log.Get(i)
|
||||
tinsert(entries, format("%s [%s] {%s} %s", timeStr, severity, location, logMsg))
|
||||
end
|
||||
errorInfo.debugLogStr = table.concat(entries, "\n")
|
||||
|
||||
-- addons
|
||||
local hasAddonSuite = {}
|
||||
local addonsLines = {}
|
||||
for i = 1, GetNumAddOns() do
|
||||
local name, _, _, loadable = GetAddOnInfo(i)
|
||||
if loadable then
|
||||
local version = strtrim(GetAddOnMetadata(name, "X-Curse-Packaged-Version") or GetAddOnMetadata(name, "Version") or "")
|
||||
local loaded = IsAddOnLoaded(i)
|
||||
local isSuite = nil
|
||||
for _, commonTerm in ipairs(ADDON_SUITES) do
|
||||
if strsub(name, 1, #commonTerm) == commonTerm then
|
||||
isSuite = commonTerm
|
||||
break
|
||||
end
|
||||
end
|
||||
local commonTerm = "TradeSkillMaster"
|
||||
if isSuite then
|
||||
if not hasAddonSuite[isSuite] then
|
||||
tinsert(addonsLines, name.." ("..version..")"..(loaded and "" or " [Not Loaded]"))
|
||||
hasAddonSuite[isSuite] = true
|
||||
end
|
||||
elseif strsub(name, 1, #commonTerm) == commonTerm then
|
||||
name = gsub(name, "TradeSkillMaster", "TSM")
|
||||
tinsert(addonsLines, name.." ("..version..")"..(loaded and "" or " [Not Loaded]"))
|
||||
else
|
||||
tinsert(addonsLines, name.." ("..version..")"..(loaded and "" or " [Not Loaded]"))
|
||||
end
|
||||
end
|
||||
end
|
||||
errorInfo.addonsStr = table.concat(addonsLines, "\n")
|
||||
|
||||
-- show this error
|
||||
local stackInfoLines = {}
|
||||
for _, info in ipairs(errorInfo.stackInfo) do
|
||||
local localsStr = info.locals ~= "" and ("\n |cffaaaaaa"..gsub(info.locals, "\n", "\n ").."|r") or ""
|
||||
local locationStr = info.line ~= 0 and strjoin(":", info.file, info.line) or info.file
|
||||
tinsert(stackInfoLines, locationStr.." <"..info.func..">"..localsStr)
|
||||
end
|
||||
private.errorFrame.errorStr = strjoin("\n",
|
||||
private.FormatErrorMessageSection("Message", msg),
|
||||
private.FormatErrorMessageSection("Time", date("%m/%d/%y %H:%M:%S", errorInfo.time).." ("..floor(errorInfo.debugTime)..")"),
|
||||
private.FormatErrorMessageSection("Client", errorInfo.client),
|
||||
private.FormatErrorMessageSection("Locale", errorInfo.locale),
|
||||
private.FormatErrorMessageSection("Combat", errorInfo.inCombat),
|
||||
private.FormatErrorMessageSection("Error Count", private.num),
|
||||
private.FormatErrorMessageSection("Stack Trace", table.concat(stackInfoLines, "\n"), true),
|
||||
private.FormatErrorMessageSection("Temp Tables", errorInfo.tempTableStr, true),
|
||||
private.FormatErrorMessageSection("Object Pools", errorInfo.objectPoolStr, true),
|
||||
private.FormatErrorMessageSection("Running Threads", errorInfo.threadInfoStr, true),
|
||||
private.FormatErrorMessageSection("Debug Log", errorInfo.debugLogStr, true),
|
||||
private.FormatErrorMessageSection("Addons", errorInfo.addonsStr, true)
|
||||
)
|
||||
-- remove unprintable characters
|
||||
private.errorFrame.errorStr = gsub(private.errorFrame.errorStr, "[%z\001-\008\011-\031]", "?")
|
||||
private.errorFrame.errorInfo = errorInfo
|
||||
private.errorFrame.isManual = isManual
|
||||
private.errorFrame:Show()
|
||||
print(PRINT_PREFIX..L["Looks like TradeSkillMaster has encountered an error. Please help the author fix this error by following the instructions shown."])
|
||||
if TSM.__IS_TEST_ENV then
|
||||
print(private.errorFrame.errorStr)
|
||||
end
|
||||
|
||||
private.ignoreErrors = false
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.GetStackInfo(msg, thread)
|
||||
local errLocation = strmatch(msg, "[A-Za-z]+%.lua:[0-9]+")
|
||||
local stackInfo = {}
|
||||
local stackStarted = false
|
||||
for i = 0, MAX_STACK_DEPTH do
|
||||
local prevStackFunc = #stackInfo > 0 and stackInfo[#stackInfo].func or nil
|
||||
local file, line, func, localsStr, newPrevStackFunc = private.GetStackLevelInfo(i, thread, prevStackFunc)
|
||||
if newPrevStackFunc then
|
||||
stackInfo[#stackInfo].func = newPrevStackFunc
|
||||
end
|
||||
if file then
|
||||
if not stackStarted then
|
||||
if errLocation then
|
||||
stackStarted = strmatch(file..":"..line, "[A-Za-z]+%.lua:[0-9]+") == errLocation
|
||||
else
|
||||
stackStarted = i > (thread and 1 or 4) and file ~= "[C]"
|
||||
end
|
||||
end
|
||||
if stackStarted then
|
||||
tinsert(stackInfo, {
|
||||
file = file,
|
||||
line = line,
|
||||
func = func,
|
||||
locals = localsStr,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
return stackInfo
|
||||
end
|
||||
|
||||
function private.GetStackLevelInfo(level, thread, prevStackFunc)
|
||||
local stackLine = nil
|
||||
if thread then
|
||||
stackLine = debugstack(thread, level, 1, 0)
|
||||
else
|
||||
level = level + 1
|
||||
stackLine = debugstack(level, 1, 0)
|
||||
end
|
||||
stackLine = gsub(stackLine, "^%[string \"@([^%.]+%.lua)\"%]", "%1")
|
||||
local locals = debuglocals(level)
|
||||
stackLine = gsub(stackLine, "%.%.%.T?r?a?d?e?S?k?i?l?l?M?a?ster([_A-Za-z]*)\\", "TradeSkillMaster%1\\")
|
||||
stackLine = gsub(stackLine, "%.%.%.", "")
|
||||
stackLine = gsub(stackLine, "`", "<", 1)
|
||||
stackLine = gsub(stackLine, "'", ">", 1)
|
||||
stackLine = strtrim(stackLine)
|
||||
if stackLine == "" then
|
||||
return
|
||||
end
|
||||
|
||||
-- Parse out the file, line, and function name
|
||||
local locationStr, functionStr = strmatch(stackLine, "^(.-): in function (<[^\n]*>)")
|
||||
if not locationStr then
|
||||
locationStr, functionStr = strmatch(stackLine, "^(.-): in (main chunk)")
|
||||
end
|
||||
if not locationStr then
|
||||
return
|
||||
end
|
||||
locationStr = strsub(locationStr, strfind(locationStr, "TradeSkillMaster") or 1)
|
||||
locationStr = gsub(locationStr, "TradeSkillMaster([^%.])", "TSM%1")
|
||||
functionStr = functionStr and gsub(gsub(functionStr, ".*\\", ""), "[<>]", "") or ""
|
||||
local file, line = strmatch(locationStr, "^(.+):(%d+)$")
|
||||
file = file or locationStr
|
||||
line = tonumber(line) or 0
|
||||
|
||||
local func = strsub(functionStr, strfind(functionStr, "`") and 2 or 1, -1) or "?"
|
||||
func = func ~= "" and func or "?"
|
||||
|
||||
if strfind(locationStr, "LibTSMClass%.lua:") then
|
||||
-- ignore stack frames from the class code's wrapper function
|
||||
if func ~= "?" and prevStackFunc and not strmatch(func, "^.+:[0-9]+$") and strmatch(prevStackFunc, "^.+:[0-9]+$") then
|
||||
-- this stack frame includes the class method we were accessing in the previous one, so go back and fix it up
|
||||
local className = locals and strmatch(locals, "\n +str = \"([A-Za-z_0-9]+):[0-9A-F]+\"\n") or "?"
|
||||
prevStackFunc = className.."."..func
|
||||
end
|
||||
return nil, nil, nil, nil, prevStackFunc
|
||||
end
|
||||
|
||||
-- add locals for addon functions (debuglocals() doesn't always work - or ever for threads)
|
||||
local localsStr = locals and private.ParseLocals(locals, file) or ""
|
||||
return file, line, func, localsStr, nil
|
||||
end
|
||||
|
||||
function private.ParseLocals(locals, file)
|
||||
if strmatch(file, "^%[") then
|
||||
return
|
||||
end
|
||||
|
||||
local fileName = strmatch(file, "([A-Za-z%-_0-9]+)%.lua")
|
||||
local isBlizzardFile = strmatch(file, "Interface\\FrameXML\\")
|
||||
local isPrivateTable, isLocaleTable, isPackageTable, isSelfTable = false, false, false, false
|
||||
wipe(private.localLinesTemp)
|
||||
locals = gsub(locals, "<([a-z]+)> {[\n\t ]+}", "<%1> {}")
|
||||
locals = gsub(locals, " = <function> defined @", "@")
|
||||
locals = gsub(locals, "<table> {", "{")
|
||||
|
||||
for localLine in gmatch(locals, "[^\n]+") do
|
||||
local shouldIgnoreLine = false
|
||||
if strmatch(localLine, "^ *%(") then
|
||||
shouldIgnoreLine = true
|
||||
elseif strmatch(localLine, "LibTSMClass%.lua:") then
|
||||
-- ignore class methods
|
||||
shouldIgnoreLine = true
|
||||
elseif strmatch(localLine, "<unnamed> {}$") then
|
||||
-- ignore internal WoW frame members
|
||||
shouldIgnoreLine = true
|
||||
end
|
||||
if not shouldIgnoreLine then
|
||||
local level = #strmatch(localLine, "^ *")
|
||||
localLine = strrep(" ", level)..strtrim(localLine)
|
||||
localLine = gsub(localLine, "Interface\\[Aa]dd[Oo]ns\\TradeSkillMaster", "TSM")
|
||||
localLine = gsub(localLine, "\124", "\\124")
|
||||
for matchStr, replaceStr in pairs(private.globalNameTranslation) do
|
||||
localLine = gsub(localLine, matchStr, replaceStr)
|
||||
end
|
||||
if level > 0 then
|
||||
if isBlizzardFile then
|
||||
-- for Blizzard stack frames, only include level 0 locals
|
||||
shouldIgnoreLine = true
|
||||
elseif strmatch(localLine, "^ *[_]*[A-Z].+@TSM") then
|
||||
-- ignore table methods (based on their name being UpperCamelCase - potentially with leading underscores)
|
||||
shouldIgnoreLine = true
|
||||
elseif isLocaleTable then
|
||||
-- ignore everything within the locale table
|
||||
shouldIgnoreLine = true
|
||||
elseif isPackageTable then
|
||||
-- ignore the package table completely
|
||||
shouldIgnoreLine = true
|
||||
elseif (isSelfTable or isPrivateTable) and strmatch(localLine, "^ *[_a-zA-Z0-9]+ = {}") then
|
||||
-- ignore empty tables within objects or the private table
|
||||
shouldIgnoreLine = true
|
||||
elseif strmatch(localLine, "^%s+0 = <userdata>$") then
|
||||
-- remove userdata table entries
|
||||
shouldIgnoreLine = true
|
||||
end
|
||||
end
|
||||
if not shouldIgnoreLine then
|
||||
tinsert(private.localLinesTemp, localLine)
|
||||
end
|
||||
if level == 0 then
|
||||
isPackageTable = strmatch(localLine, "%s*"..fileName.." = {") and true or false
|
||||
isPrivateTable = strmatch(localLine, "%s*private = {") and true or false
|
||||
isLocaleTable = strmatch(localLine, "%s*L = {") and true or false
|
||||
isSelfTable = strmatch(localLine, "%s*self = {") and true or false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- remove any top-level empty tables
|
||||
local i = #private.localLinesTemp
|
||||
while i > 0 do
|
||||
if i > 1 and private.localLinesTemp[i] == "}" and strmatch(private.localLinesTemp[i - 1], "^[A-Za-z_].* = {$") then
|
||||
tremove(private.localLinesTemp, i)
|
||||
tremove(private.localLinesTemp, i - 1)
|
||||
i = i - 2
|
||||
elseif strmatch(private.localLinesTemp[i], "^[A-Za-z_].* = {}$") then
|
||||
tremove(private.localLinesTemp, i)
|
||||
i = i - 1
|
||||
else
|
||||
i = i - 1
|
||||
end
|
||||
end
|
||||
return #private.localLinesTemp > 0 and table.concat(private.localLinesTemp, "\n") or nil
|
||||
end
|
||||
|
||||
function private.IsTSMAddon(str)
|
||||
if strfind(str, "Auc-Adcanced\\CoreScan.lua") then
|
||||
-- ignore auctioneer errors
|
||||
return nil
|
||||
elseif strfind(str, "Master\\External\\") then
|
||||
-- ignore errors from libraries
|
||||
return nil
|
||||
elseif strfind(str, "Master\\Core\\API.lua") then
|
||||
-- ignore errors from public APIs
|
||||
return nil
|
||||
elseif strfind(str, "Master_AppHelper\\") then
|
||||
return "TradeSkillMaster_AppHelper"
|
||||
elseif strfind(str, "lMaster\\") then
|
||||
return "TradeSkillMaster"
|
||||
elseif strfind(str, "ster\\Core\\UI\\") then
|
||||
return "TradeSkillMaster"
|
||||
elseif strfind(str, "r\\LibTSM\\") then
|
||||
return "TradeSkillMaster"
|
||||
elseif strfind(str, "^TSM\\") then
|
||||
return "TradeSkillMaster"
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function private.AddonBlockedHandler(event, addonName, addonFunc)
|
||||
if not strmatch(addonName, "TradeSkillMaster") then
|
||||
return
|
||||
end
|
||||
-- just log it - it might not be TSM that cause the taint
|
||||
Log.Err("[%s] AddOn '%s' tried to call the protected function '%s'.", event, addonName or "<name>", addonFunc or "<func>")
|
||||
end
|
||||
|
||||
function private.SanitizeString(str)
|
||||
str = gsub(str, "\124cff[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]([^\124]+)\124r", "%1")
|
||||
str = gsub(str, "[\\]+", "/")
|
||||
str = gsub(str, "\"", "'")
|
||||
return str
|
||||
end
|
||||
|
||||
function private.FormatErrorMessageSection(heading, info, isMultiLine)
|
||||
-- replace unprintable characters with "?"
|
||||
info = gsub(info, "[^\t\n -~]", "?")
|
||||
local prefix = nil
|
||||
if isMultiLine then
|
||||
prefix = info ~= "" and "\n " or ""
|
||||
info = gsub(info, "\n", "\n ")
|
||||
else
|
||||
prefix = info ~= "" and " " or ""
|
||||
end
|
||||
return "|cff99ffff"..heading..":|r"..prefix..info
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Error Frame
|
||||
-- ============================================================================
|
||||
|
||||
function private.CreateErrorFrame()
|
||||
if private.errorFrame then
|
||||
return
|
||||
end
|
||||
local STEPS_TEXT = "Steps leading up to the error:\n1) List\n2) Steps\n3) Here"
|
||||
local frame = CreateFrame("Frame", nil, UIParent, TSM.IsShadowlands() and "BackdropTemplate" or nil)
|
||||
private.errorFrame = frame
|
||||
frame:Hide()
|
||||
frame:SetWidth(500)
|
||||
frame:SetHeight(400)
|
||||
frame:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
frame:SetPoint("RIGHT", -100, 0)
|
||||
frame:SetBackdrop({
|
||||
bgFile = "Interface\\Buttons\\WHITE8X8",
|
||||
edgeFile = "Interface\\Buttons\\WHITE8X8",
|
||||
edgeSize = 2,
|
||||
})
|
||||
frame:SetBackdropColor(0, 0, 0, 1)
|
||||
frame:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)
|
||||
frame:SetScript("OnShow", function(self)
|
||||
self.showingError = self.isManual or TSM.IsDevVersion()
|
||||
self.details = STEPS_TEXT
|
||||
if self.showingError then
|
||||
-- this is a dev version so show the error (only)
|
||||
self.text:SetText("Looks like TradeSkillMaster has encountered an error.")
|
||||
self.switchBtn:SetText("Hide Error")
|
||||
self.editBox:SetText(self.errorStr)
|
||||
else
|
||||
self.text:SetText("Looks like TradeSkillMaster has encountered an error. Please provide the steps which lead to this error to help the TSM team fix it, then click either button at the bottom of the window to automatically report this error.")
|
||||
self.switchBtn:SetText("Show Error")
|
||||
self.editBox:SetText(self.details)
|
||||
end
|
||||
end)
|
||||
frame:SetScript("OnHide", function()
|
||||
local details = private.errorFrame.showingError and private.errorFrame.details or private.errorFrame.editBox:GetText()
|
||||
local changedDetails = details ~= STEPS_TEXT
|
||||
if (not TSM.IsDevVersion() and not private.errorFrame.isManual and (changedDetails or private.num == 1)) or IsShiftKeyDown() then
|
||||
tinsert(private.errorReports, {
|
||||
errorInfo = private.errorFrame.errorInfo,
|
||||
details = private.SanitizeString(details),
|
||||
timestamp = time(),
|
||||
})
|
||||
end
|
||||
private.errorSuppressed = nil
|
||||
end)
|
||||
|
||||
local title = frame:CreateFontString()
|
||||
title:SetHeight(20)
|
||||
title:SetPoint("TOPLEFT", 0, -10)
|
||||
title:SetPoint("TOPRIGHT", 0, -10)
|
||||
title:SetFontObject(GameFontNormalLarge)
|
||||
title:SetTextColor(1, 1, 1, 1)
|
||||
title:SetJustifyH("CENTER")
|
||||
title:SetJustifyV("MIDDLE")
|
||||
local status, versionText = pcall(TSM.GetVersion)
|
||||
versionText = status and versionText or "?"
|
||||
title:SetText("TSM Error Window ("..versionText..")")
|
||||
|
||||
local hLine = frame:CreateTexture(nil, "ARTWORK")
|
||||
hLine:SetHeight(2)
|
||||
hLine:SetColorTexture(0.3, 0.3, 0.3, 1)
|
||||
hLine:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -10)
|
||||
hLine:SetPoint("TOPRIGHT", title, "BOTTOMRIGHT", 0, -10)
|
||||
|
||||
local text = frame:CreateFontString()
|
||||
frame.text = text
|
||||
text:SetHeight(45)
|
||||
text:SetPoint("TOPLEFT", hLine, "BOTTOMLEFT", 8, -8)
|
||||
text:SetPoint("TOPRIGHT", hLine, "BOTTOMRIGHT", -8, -8)
|
||||
text:SetFontObject(GameFontNormal)
|
||||
text:SetTextColor(1, 1, 1, 1)
|
||||
text:SetJustifyH("LEFT")
|
||||
text:SetJustifyV("MIDDLE")
|
||||
|
||||
local switchBtn = CreateFrame("Button", nil, frame)
|
||||
frame.switchBtn = switchBtn
|
||||
switchBtn:SetPoint("TOPRIGHT", -4, -10)
|
||||
switchBtn:SetWidth(100)
|
||||
switchBtn:SetHeight(20)
|
||||
local fontString = switchBtn:CreateFontString()
|
||||
fontString:SetFontObject(GameFontNormalSmall)
|
||||
fontString:SetJustifyH("CENTER")
|
||||
fontString:SetJustifyV("MIDDLE")
|
||||
switchBtn:SetFontString(fontString)
|
||||
switchBtn:SetScript("OnClick", function(self)
|
||||
private.errorFrame.showingError = not private.errorFrame.showingError
|
||||
if private.errorFrame.showingError then
|
||||
private.errorFrame.details = private.errorFrame.editBox:GetText()
|
||||
self:SetText("Hide Error")
|
||||
private.errorFrame.editBox:SetText(private.errorFrame.errorStr)
|
||||
else
|
||||
self:SetText("Show Error")
|
||||
private.errorFrame.editBox:SetText(private.errorFrame.details)
|
||||
end
|
||||
end)
|
||||
|
||||
local hLine2 = frame:CreateTexture(nil, "ARTWORK")
|
||||
hLine2:SetHeight(2)
|
||||
hLine2:SetColorTexture(0.3, 0.3, 0.3, 1)
|
||||
hLine2:SetPoint("TOPLEFT", text, "BOTTOMLEFT", -8, -4)
|
||||
hLine2:SetPoint("TOPRIGHT", text, "BOTTOMRIGHT", 8, -4)
|
||||
|
||||
local scrollFrame = CreateFrame("ScrollFrame", nil, frame, "UIPanelScrollFrameTemplate")
|
||||
scrollFrame:SetPoint("TOPLEFT", hLine2, "BOTTOMLEFT", 8, -4)
|
||||
scrollFrame:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -26, 38)
|
||||
|
||||
local editBox = CreateFrame("EditBox", nil, scrollFrame)
|
||||
frame.editBox = editBox
|
||||
editBox:SetWidth(scrollFrame:GetWidth())
|
||||
editBox:SetFontObject(ChatFontNormal)
|
||||
editBox:SetMultiLine(true)
|
||||
editBox:SetAutoFocus(false)
|
||||
editBox:SetMaxLetters(0)
|
||||
editBox:SetTextColor(1, 1, 1, 1)
|
||||
editBox:SetScript("OnUpdate", function(self)
|
||||
local offset = scrollFrame:GetVerticalScroll()
|
||||
self:SetHitRectInsets(0, 0, offset, self:GetHeight() - offset - scrollFrame:GetHeight())
|
||||
end)
|
||||
editBox:SetScript("OnEditFocusGained", function(self)
|
||||
self:HighlightText()
|
||||
end)
|
||||
editBox:SetScript("OnCursorChanged", function(self)
|
||||
if private.errorFrame.showingError and self:HasFocus() then
|
||||
self:HighlightText()
|
||||
end
|
||||
end)
|
||||
editBox:SetScript("OnEscapePressed", function(self)
|
||||
if private.errorFrame.showingError then
|
||||
self:HighlightText(0, 0)
|
||||
end
|
||||
self:ClearFocus()
|
||||
end)
|
||||
scrollFrame:SetScrollChild(editBox)
|
||||
|
||||
local hLine3 = frame:CreateTexture(nil, "ARTWORK")
|
||||
hLine3:SetHeight(2)
|
||||
hLine3:SetColorTexture(0.3, 0.3, 0.3, 1)
|
||||
hLine3:SetPoint("BOTTOMLEFT", frame, 0, 35)
|
||||
hLine3:SetPoint("BOTTOMRIGHT", frame, 0, 35)
|
||||
|
||||
local reloadBtn = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
|
||||
frame.reloadBtn = reloadBtn
|
||||
reloadBtn:SetPoint("BOTTOMLEFT", 4, 4)
|
||||
reloadBtn:SetWidth(120)
|
||||
reloadBtn:SetHeight(30)
|
||||
reloadBtn:SetText(RELOADUI)
|
||||
reloadBtn:SetScript("OnClick", function()
|
||||
frame:Hide()
|
||||
ReloadUI()
|
||||
end)
|
||||
|
||||
local closeBtn = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
|
||||
frame.closeBtn = closeBtn
|
||||
closeBtn:SetPoint("BOTTOMRIGHT", -4, 4)
|
||||
closeBtn:SetWidth(120)
|
||||
closeBtn:SetHeight(30)
|
||||
closeBtn:SetText(DONE)
|
||||
closeBtn:SetScript("OnClick", function()
|
||||
frame:Hide()
|
||||
end)
|
||||
|
||||
local stepsText = frame:CreateFontString()
|
||||
frame.stepsText = stepsText
|
||||
stepsText:SetWidth(200)
|
||||
stepsText:SetHeight(30)
|
||||
stepsText:SetPoint("BOTTOM", 0, 4)
|
||||
stepsText:SetFontObject(GameFontNormal)
|
||||
stepsText:SetTextColor(1, 0, 0, 1)
|
||||
stepsText:SetJustifyH("CENTER")
|
||||
stepsText:SetJustifyV("MIDDLE")
|
||||
stepsText:SetText("Please enter steps before submitting")
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Register Error Handler
|
||||
-- ============================================================================
|
||||
|
||||
do
|
||||
private.origErrorHandler = geterrorhandler()
|
||||
local function ErrorHandlerFunc(errMsg, isBugGrabber)
|
||||
local tsmErrMsg = strtrim(tostring(errMsg))
|
||||
if private.ignoreErrors then
|
||||
-- we're ignoring errors
|
||||
tsmErrMsg = nil
|
||||
elseif strmatch(tsmErrMsg, "auc%-stat%-wowuction") or strmatch(tsmErrMsg, "TheUndermineJournal%.lua") or strmatch(tsmErrMsg, "\\SavedVariables\\TradeSkillMaster") or strmatch(tsmErrMsg, "AddOn TradeSkillMaster[_a-zA-Z]* attempted") or (strmatch(tsmErrMsg, "ItemTooltipClasses\\Wrapper%.lua:98") and strmatch(tsmErrMsg, "SetQuest")) then
|
||||
-- explicitly ignore these errors
|
||||
tsmErrMsg = nil
|
||||
end
|
||||
if tsmErrMsg then
|
||||
-- look at the stack trace to see if this is a TSM error
|
||||
for i = 2, MAX_STACK_DEPTH do
|
||||
local stackLine = debugstack(i, 1, 0)
|
||||
local oldModule = strmatch(stackLine, "(lMaster_[A-Za-z]+)")
|
||||
if oldModule and tContains(OLD_TSM_MODULES, "TradeSkil"..oldModule) then
|
||||
-- ignore errors from old modules
|
||||
return
|
||||
end
|
||||
if not strmatch(stackLine, "^%[C%]:") and not strmatch(stackLine, "%(tail call%):") and not strmatch(stackLine, "^%[string \"[^@]") and not strmatch(stackLine, "lMaster\\External\\[A-Za-z0-9%-_%.]+\\") and not strmatch(stackLine, "SharedXML") and not strmatch(stackLine, "CallbackHandler") and not strmatch(stackLine, "!BugGrabber") and not strmatch(stackLine, "ErrorHandler%.lua") then
|
||||
if not private.IsTSMAddon(stackLine) then
|
||||
tsmErrMsg = nil
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if tsmErrMsg then
|
||||
local status, ret = pcall(private.ErrorHandler, tsmErrMsg)
|
||||
if status and ret then
|
||||
return ret
|
||||
elseif not status and not private.hitInternalError then
|
||||
private.hitInternalError = true
|
||||
print("Internal TSM error: "..tostring(ret))
|
||||
end
|
||||
end
|
||||
local oldModule = strmatch(errMsg, "(lMaster_[A-Za-z]+)")
|
||||
if oldModule and tContains(OLD_TSM_MODULES, "TradeSkil"..oldModule) then
|
||||
-- ignore errors from old modules
|
||||
return
|
||||
end
|
||||
if not isBugGrabber then
|
||||
return private.origErrorHandler and private.origErrorHandler(errMsg) or nil
|
||||
end
|
||||
end
|
||||
seterrorhandler(ErrorHandlerFunc)
|
||||
if BugGrabber and BugGrabber.RegisterCallback then
|
||||
BugGrabber.RegisterCallback({}, "BugGrabber_BugGrabbed", function(_, errObj)
|
||||
ErrorHandlerFunc(errObj.message, true)
|
||||
end)
|
||||
end
|
||||
Event.Register("ADDON_ACTION_FORBIDDEN", private.AddonBlockedHandler)
|
||||
Event.Register("ADDON_ACTION_BLOCKED", private.AddonBlockedHandler)
|
||||
end
|
||||
272
LibTSM/Service/GuildTracking.lua
Normal file
272
LibTSM/Service/GuildTracking.lua
Normal file
@@ -0,0 +1,272 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local GuildTracking = TSM.Init("Service.GuildTracking")
|
||||
local Database = TSM.Include("Util.Database")
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local SlotId = TSM.Include("Util.SlotId")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local private = {
|
||||
settings = nil,
|
||||
slotDB = nil,
|
||||
quantityDB = nil,
|
||||
isOpen = nil,
|
||||
pendingPetSlotIds = {},
|
||||
}
|
||||
local PLAYER_NAME = UnitName("player")
|
||||
local PLAYER_GUILD = nil
|
||||
local MAX_PET_SCANS = 10
|
||||
-- don't use MAX_GUILDBANK_SLOTS_PER_TAB since it isn't available right away
|
||||
local GUILD_BANK_TAB_SLOTS = 98
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
GuildTracking:OnSettingsLoad(function()
|
||||
private.settings = Settings.NewView()
|
||||
:AddKey("factionrealm", "internalData", "characterGuilds")
|
||||
:AddKey("factionrealm", "internalData", "guildVaults")
|
||||
private.slotDB = Database.NewSchema("GUILD_TRACKING_SLOTS")
|
||||
:AddUniqueNumberField("slotId")
|
||||
:AddNumberField("tab")
|
||||
:AddNumberField("slot")
|
||||
:AddStringField("itemString")
|
||||
:AddSmartMapField("baseItemString", ItemString.GetBaseMap(), "itemString")
|
||||
:AddNumberField("quantity")
|
||||
:AddIndex("slotId")
|
||||
:AddIndex("itemString")
|
||||
:Commit()
|
||||
private.quantityDB = Database.NewSchema("GUILD_TRACKING_QUANTITY")
|
||||
:AddUniqueStringField("itemString")
|
||||
:AddNumberField("quantity")
|
||||
:Commit()
|
||||
if not TSM.IsWowClassic() then
|
||||
Event.Register("GUILDBANKFRAME_OPENED", private.GuildBankFrameOpenedHandler)
|
||||
Event.Register("GUILDBANKFRAME_CLOSED", private.GuildBankFrameClosedHandler)
|
||||
Event.Register("GUILDBANKBAGSLOTS_CHANGED", private.GuildBankBagSlotsChangedHandler)
|
||||
Delay.AfterFrame(1, private.GetGuildName)
|
||||
Event.Register("PLAYER_GUILD_UPDATE", private.GetGuildName)
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function GuildTracking.BaseItemIterator()
|
||||
return private.quantityDB:NewQuery()
|
||||
:Select("itemString")
|
||||
:IteratorAndRelease()
|
||||
end
|
||||
|
||||
function GuildTracking.CreateQuery()
|
||||
return private.slotDB:NewQuery()
|
||||
end
|
||||
|
||||
function GuildTracking.CreateQueryItem(itemString)
|
||||
local query = GuildTracking.CreateQuery()
|
||||
if itemString == ItemString.GetBaseFast(itemString) then
|
||||
query:Equal("baseItemString", itemString)
|
||||
else
|
||||
query:Equal("itemString", itemString)
|
||||
end
|
||||
return query
|
||||
end
|
||||
|
||||
function GuildTracking.GetQuantityByBaseItemString(baseItemString)
|
||||
return private.quantityDB:GetUniqueRowField("itemString", baseItemString, "quantity") or 0
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.GetGuildName()
|
||||
if not IsInGuild() then
|
||||
private.settings.characterGuilds[PLAYER_NAME] = nil
|
||||
return
|
||||
end
|
||||
PLAYER_GUILD = GetGuildInfo("player")
|
||||
if not PLAYER_GUILD then
|
||||
-- try again next frame
|
||||
Delay.AfterFrame(1, private.GetGuildName)
|
||||
return
|
||||
end
|
||||
|
||||
private.settings.characterGuilds[PLAYER_NAME] = PLAYER_GUILD
|
||||
|
||||
-- clean up any guilds with no players in them
|
||||
local validGuilds = TempTable.Acquire()
|
||||
for _, character in Settings.CharacterByAccountFactionrealmIterator() do
|
||||
local guild = private.settings.characterGuilds[character]
|
||||
if guild then
|
||||
validGuilds[guild] = true
|
||||
end
|
||||
end
|
||||
for character, guild in pairs(private.settings.characterGuilds) do
|
||||
if not validGuilds[guild] then
|
||||
private.settings.characterGuilds[character] = nil
|
||||
end
|
||||
end
|
||||
for guild in pairs(private.settings.guildVaults) do
|
||||
if not validGuilds[guild] then
|
||||
private.settings.guildVaults[guild] = nil
|
||||
end
|
||||
end
|
||||
TempTable.Release(validGuilds)
|
||||
|
||||
private.settings.guildVaults[PLAYER_GUILD] = private.settings.guildVaults[PLAYER_GUILD] or {}
|
||||
for itemString, quantity in pairs(private.settings.guildVaults[PLAYER_GUILD]) do
|
||||
if quantity <= 0 or itemString ~= ItemString.GetBase(itemString) then
|
||||
private.settings.guildVaults[PLAYER_GUILD][itemString] = nil
|
||||
end
|
||||
end
|
||||
private.RebuildQuantityDB()
|
||||
end
|
||||
|
||||
function private.RebuildQuantityDB()
|
||||
private.quantityDB:TruncateAndBulkInsertStart()
|
||||
for itemString, quantity in pairs(private.settings.guildVaults[PLAYER_GUILD]) do
|
||||
if quantity > 0 then
|
||||
private.quantityDB:BulkInsertNewRow(itemString, quantity)
|
||||
else
|
||||
private.settings.guildVaults[PLAYER_GUILD][itemString] = nil
|
||||
end
|
||||
end
|
||||
private.quantityDB:BulkInsertEnd()
|
||||
end
|
||||
|
||||
function private.GuildBankFrameOpenedHandler()
|
||||
local initialTab = GetCurrentGuildBankTab()
|
||||
for i = 1, GetNumGuildBankTabs() do
|
||||
QueryGuildBankTab(i)
|
||||
end
|
||||
QueryGuildBankTab(initialTab)
|
||||
private.isOpen = true
|
||||
end
|
||||
|
||||
function private.GuildBankFrameClosedHandler()
|
||||
private.isOpen = nil
|
||||
end
|
||||
|
||||
function private.GuildBankBagSlotsChangedHandler()
|
||||
Delay.AfterFrame("guildBankScan", 2, private.GuildBankChangedDelayed)
|
||||
end
|
||||
|
||||
function private.GuildBankChangedDelayed()
|
||||
if not private.isOpen then
|
||||
return
|
||||
end
|
||||
if not PLAYER_GUILD then
|
||||
-- we don't have the guild name yet, so try again after a short delay
|
||||
Delay.AfterFrame("guildBankScan", 2, private.GuildBankChangedDelayed)
|
||||
return
|
||||
end
|
||||
private.ScanGuildBank()
|
||||
end
|
||||
|
||||
function private.ScanGuildBank()
|
||||
wipe(private.settings.guildVaults[PLAYER_GUILD])
|
||||
wipe(private.pendingPetSlotIds)
|
||||
private.slotDB:TruncateAndBulkInsertStart()
|
||||
local didFail = false
|
||||
for tab = 1, GetNumGuildBankTabs() do
|
||||
-- only scan tabs which we have at least enough withdrawals to withdraw every slot
|
||||
local _, _, _, _, numWithdrawals = GetGuildBankTabInfo(tab)
|
||||
if numWithdrawals == -1 or numWithdrawals >= GUILD_BANK_TAB_SLOTS then
|
||||
for slot = 1, GUILD_BANK_TAB_SLOTS do
|
||||
local itemLink = GetGuildBankItemLink(tab, slot)
|
||||
if itemLink then
|
||||
local slotId = SlotId.Join(tab, slot)
|
||||
local baseItemString = ItemString.GetBase(itemLink)
|
||||
if baseItemString == ItemString.GetPetCage() then
|
||||
private.pendingPetSlotIds[slotId] = true
|
||||
baseItemString = nil
|
||||
end
|
||||
if baseItemString then
|
||||
local _, quantity = GetGuildBankItemInfo(tab, slot)
|
||||
if quantity == 0 then
|
||||
-- the info for this slot isn't fully loaded yet
|
||||
Log.Err("Failed to scan guild bank slot (%d)", slotId)
|
||||
didFail = true
|
||||
break
|
||||
end
|
||||
private.settings.guildVaults[PLAYER_GUILD][baseItemString] = (private.settings.guildVaults[PLAYER_GUILD][baseItemString] or 0) + quantity
|
||||
local itemString = ItemString.Get(itemLink)
|
||||
private.slotDB:BulkInsertNewRow(slotId, tab, slot, itemString, quantity)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if didFail then
|
||||
break
|
||||
end
|
||||
end
|
||||
private.RebuildQuantityDB()
|
||||
private.slotDB:BulkInsertEnd()
|
||||
if didFail then
|
||||
Delay.AfterFrame("guildBankScan", 2, private.GuildBankChangedDelayed)
|
||||
elseif next(private.pendingPetSlotIds) then
|
||||
Delay.AfterFrame("guildBankPetScan", 2, private.ScanPetsDeferred)
|
||||
else
|
||||
Delay.Cancel("guildBankPetScan")
|
||||
end
|
||||
end
|
||||
|
||||
function private.ScanPetsDeferred()
|
||||
if not TSMScanTooltip then
|
||||
CreateFrame("GameTooltip", "TSMScanTooltip", UIParent, "GameTooltipTemplate")
|
||||
end
|
||||
TSMScanTooltip:SetOwner(UIParent, "ANCHOR_NONE")
|
||||
TSMScanTooltip:ClearLines()
|
||||
|
||||
local numPetSlotIdsScanned = 0
|
||||
local toRemove = TempTable.Acquire()
|
||||
private.slotDB:BulkInsertStart()
|
||||
for slotId in pairs(private.pendingPetSlotIds) do
|
||||
local tab, slot = SlotId.Split(slotId)
|
||||
local speciesId, level, rarity = TSMScanTooltip:SetGuildBankItem(tab, slot)
|
||||
if speciesId and level and rarity then
|
||||
local itemString = "p:"..speciesId..":"..level..":"..rarity
|
||||
if itemString then
|
||||
tinsert(toRemove, slotId)
|
||||
local _, quantity = GetGuildBankItemInfo(tab, slot)
|
||||
local baseItemString = ItemString.GetBase(itemString)
|
||||
private.settings.guildVaults[PLAYER_GUILD][baseItemString] = (private.settings.guildVaults[PLAYER_GUILD][baseItemString] or 0) + quantity
|
||||
private.slotDB:BulkInsertNewRow(slotId, tab, slot, itemString, quantity)
|
||||
end
|
||||
end
|
||||
-- throttle how many pet slots we scan per call (regardless of whether or not it was successful)
|
||||
numPetSlotIdsScanned = numPetSlotIdsScanned + 1
|
||||
if numPetSlotIdsScanned == MAX_PET_SCANS then
|
||||
break
|
||||
end
|
||||
end
|
||||
private.RebuildQuantityDB()
|
||||
private.slotDB:BulkInsertEnd()
|
||||
Log.Info("Scanned %d pet slots", numPetSlotIdsScanned)
|
||||
for _, slotId in ipairs(toRemove) do
|
||||
private.pendingPetSlotIds[slotId] = nil
|
||||
end
|
||||
TempTable.Release(toRemove)
|
||||
|
||||
if next(private.pendingPetSlotIds) then
|
||||
-- there are more to scan
|
||||
Delay.AfterFrame("guildBankPetScan", 2, private.ScanPetsDeferred)
|
||||
end
|
||||
end
|
||||
149
LibTSM/Service/Inventory.lua
Normal file
149
LibTSM/Service/Inventory.lua
Normal file
@@ -0,0 +1,149 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Inventory = TSM.Init("Service.Inventory")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local CustomPrice = TSM.Include("Service.CustomPrice")
|
||||
local BagTracking = TSM.Include("Service.BagTracking")
|
||||
local AuctionTracking = TSM.Include("Service.AuctionTracking")
|
||||
local MailTracking = TSM.Include("Service.MailTracking")
|
||||
local Sync = TSM.Include("Service.Sync")
|
||||
local private = {
|
||||
settings = nil,
|
||||
callbacks = {},
|
||||
}
|
||||
local PLAYER_NAME = UnitName("player")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Inventory:OnSettingsLoad(function()
|
||||
private.settings = Settings.NewView()
|
||||
:AddKey("factionrealm", "internalData", "pendingMail")
|
||||
:AddKey("factionrealm", "coreOptions", "ignoreGuilds")
|
||||
:AddKey("factionrealm", "internalData", "guildVaults")
|
||||
BagTracking.RegisterCallback(private.QuantityChangedCallback)
|
||||
AuctionTracking.RegisterCallback(private.QuantityChangedCallback)
|
||||
MailTracking.RegisterCallback(private.QuantityChangedCallback)
|
||||
Sync.RegisterMirrorCallback(private.QuantityChangedCallback)
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Inventory.RegisterCallback(callback)
|
||||
tinsert(private.callbacks, callback)
|
||||
end
|
||||
|
||||
function Inventory.GetBagQuantity(itemString, character, factionrealm)
|
||||
return private.InventoryQuantityHelper(itemString, "bagQuantity", character, factionrealm)
|
||||
end
|
||||
|
||||
function Inventory.GetBankQuantity(itemString, character, factionrealm)
|
||||
return private.InventoryQuantityHelper(itemString, "bankQuantity", character, factionrealm)
|
||||
end
|
||||
|
||||
function Inventory.GetReagentBankQuantity(itemString, character, factionrealm)
|
||||
return private.InventoryQuantityHelper(itemString, "reagentBankQuantity", character, factionrealm)
|
||||
end
|
||||
|
||||
function Inventory.GetAuctionQuantity(itemString, character, factionrealm)
|
||||
return private.InventoryQuantityHelper(itemString, "auctionQuantity", character, factionrealm)
|
||||
end
|
||||
|
||||
function Inventory.GetMailQuantity(itemString, character, factionrealm)
|
||||
itemString = ItemString.GetBaseFast(itemString)
|
||||
character = character or PLAYER_NAME
|
||||
local pendingQuantity = itemString and private.settings.pendingMail[character] and private.settings.pendingMail[character][itemString] or 0
|
||||
return private.InventoryQuantityHelper(itemString, "mailQuantity", character, factionrealm) + pendingQuantity
|
||||
end
|
||||
|
||||
function Inventory.GetGuildQuantity(itemString, guild)
|
||||
itemString = ItemString.GetBase(itemString)
|
||||
if not itemString then
|
||||
return 0
|
||||
end
|
||||
guild = guild or (IsInGuild() and GetGuildInfo("player") or nil)
|
||||
if not guild or private.settings.ignoreGuilds[guild] then
|
||||
return 0
|
||||
end
|
||||
return private.settings.guildVaults[guild] and private.settings.guildVaults[guild][itemString] or 0
|
||||
end
|
||||
|
||||
function Inventory.GetPlayerTotals(itemString)
|
||||
itemString = ItemString.GetBaseFast(itemString)
|
||||
local numPlayer, numAlts, numAuctions, numAltAuctions = 0, 0, 0, 0
|
||||
numPlayer = numPlayer + Inventory.GetBagQuantity(itemString)
|
||||
numPlayer = numPlayer + Inventory.GetBankQuantity(itemString)
|
||||
numPlayer = numPlayer + Inventory.GetReagentBankQuantity(itemString)
|
||||
numPlayer = numPlayer + Inventory.GetMailQuantity(itemString)
|
||||
numAuctions = numAuctions + Inventory.GetAuctionQuantity(itemString)
|
||||
for _, factionrealm, character in Settings.ConnectedFactionrealmAltCharacterIterator() do
|
||||
numAlts = numAlts + Inventory.GetBagQuantity(itemString, character, factionrealm)
|
||||
numAlts = numAlts + Inventory.GetBankQuantity(itemString, character, factionrealm)
|
||||
numAlts = numAlts + Inventory.GetReagentBankQuantity(itemString, character, factionrealm)
|
||||
numAlts = numAlts + Inventory.GetMailQuantity(itemString, character, factionrealm)
|
||||
local auctionQuantity = Inventory.GetAuctionQuantity(itemString, character, factionrealm)
|
||||
numAltAuctions = numAltAuctions + auctionQuantity
|
||||
numAuctions = numAuctions + auctionQuantity
|
||||
end
|
||||
return numPlayer, numAlts, numAuctions, numAltAuctions
|
||||
end
|
||||
|
||||
function Inventory.GetGuildTotal(itemString)
|
||||
itemString = ItemString.GetBaseFast(itemString)
|
||||
if not itemString then
|
||||
return 0
|
||||
end
|
||||
local numGuild = 0
|
||||
for guild, data in pairs(private.settings.guildVaults) do
|
||||
if not private.settings.ignoreGuilds[guild] then
|
||||
numGuild = numGuild + (data[itemString] or 0)
|
||||
end
|
||||
end
|
||||
return numGuild
|
||||
end
|
||||
|
||||
function Inventory.GetTotalQuantity(itemString)
|
||||
local numPlayer, numAlts, numAuctions = Inventory.GetPlayerTotals(itemString)
|
||||
local numGuild = Inventory.GetGuildTotal(itemString)
|
||||
return numPlayer + numAlts + numAuctions + numGuild
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.GetCharacterInventoryData(settingKey, character, factionrealm)
|
||||
local scopeKey = character and Settings.GetSyncScopeKeyByCharacter(character, factionrealm) or nil
|
||||
return Settings.Get("sync", scopeKey, "internalData", settingKey)
|
||||
end
|
||||
|
||||
function private.InventoryQuantityHelper(itemString, settingKey, character, factionrealm)
|
||||
itemString = ItemString.GetBase(itemString)
|
||||
if not itemString then
|
||||
return 0
|
||||
end
|
||||
local tbl = private.GetCharacterInventoryData(settingKey, character, factionrealm)
|
||||
return tbl and tbl[itemString] or 0
|
||||
end
|
||||
|
||||
function private.QuantityChangedCallback()
|
||||
CustomPrice.OnSourceChange("NumInventory")
|
||||
for _, callback in ipairs(private.callbacks) do
|
||||
callback()
|
||||
end
|
||||
end
|
||||
232
LibTSM/Service/InventoryInfo.lua
Normal file
232
LibTSM/Service/InventoryInfo.lua
Normal file
@@ -0,0 +1,232 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local InventoryInfo = TSM.Init("Service.InventoryInfo")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local SlotId = TSM.Include("Util.SlotId")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local Table = TSM.Include("Util.Table")
|
||||
local private = {
|
||||
slotIdLocked = {},
|
||||
slotIdSoulboundCached = {},
|
||||
slotIdIsBoP = {},
|
||||
slotIdIsBoA = {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
InventoryInfo:OnModuleLoad(function()
|
||||
Event.Register("ITEM_LOCKED", private.ItemLockedHandler)
|
||||
Event.Register("ITEM_UNLOCKED", private.ItemUnlockedHandler)
|
||||
Event.Register("BAG_UPDATE", private.BagUpdateHandler)
|
||||
Event.Register("PLAYERBANKSLOTS_CHANGED", private.BankSlotChangedHandler)
|
||||
if not TSM.IsWowClassic() then
|
||||
Event.Register("PLAYERREAGENTBANKSLOTS_CHANGED", private.ReagentBankSlotChangedHandler)
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Check if an item will go in a bag.
|
||||
-- @tparam string link The item
|
||||
-- @tparam number bag The bag index
|
||||
-- @treturn boolean Whether or not the item will go in the bag
|
||||
function InventoryInfo.ItemWillGoInBag(link, bag)
|
||||
if not link or not bag then
|
||||
return
|
||||
end
|
||||
if bag == BACKPACK_CONTAINER or bag == BANK_CONTAINER then
|
||||
return true
|
||||
elseif bag == REAGENTBANK_CONTAINER then
|
||||
return IsReagentBankUnlocked() and ItemInfo.IsCraftingReagent(link)
|
||||
end
|
||||
local itemFamily = GetItemFamily(link) or 0
|
||||
if ItemInfo.GetClassId(link) == LE_ITEM_CLASS_CONTAINER then
|
||||
-- bags report their family as what can go inside them, not what they can go inside
|
||||
itemFamily = 0
|
||||
end
|
||||
local _, bagFamily = GetContainerNumFreeSlots(bag)
|
||||
if not bagFamily then
|
||||
return
|
||||
end
|
||||
return bagFamily == 0 or bit.band(itemFamily, bagFamily) > 0
|
||||
end
|
||||
|
||||
function InventoryInfo.IsBagSlotLocked(bag, slot)
|
||||
return private.slotIdLocked[SlotId.Join(bag, slot)]
|
||||
end
|
||||
|
||||
function InventoryInfo.IsSoulbound(bag, slot)
|
||||
local slotId = SlotId.Join(bag, slot)
|
||||
if private.slotIdSoulboundCached[slotId] then
|
||||
return private.slotIdIsBoP[slotId], private.slotIdIsBoA[slotId]
|
||||
end
|
||||
if not TSMScanTooltip then
|
||||
CreateFrame("GameTooltip", "TSMScanTooltip", UIParent, "GameTooltipTemplate")
|
||||
end
|
||||
|
||||
TSMScanTooltip:SetOwner(UIParent, "ANCHOR_NONE")
|
||||
TSMScanTooltip:ClearLines()
|
||||
|
||||
if GetContainerItemID(bag, slot) == ItemString.ToId(ItemString.GetPetCage()) then
|
||||
-- battle pets are never BoP or BoA
|
||||
private.slotIdSoulboundCached[slotId] = true
|
||||
private.slotIdIsBoP[slotId] = false
|
||||
private.slotIdIsBoA[slotId] = false
|
||||
return false, false
|
||||
end
|
||||
|
||||
-- set TSMScanTooltip to show the inventory item
|
||||
if bag == BANK_CONTAINER then
|
||||
TSMScanTooltip:SetInventoryItem("player", BankButtonIDToInvSlotID(slot))
|
||||
elseif bag == REAGENTBANK_CONTAINER then
|
||||
TSMScanTooltip:SetInventoryItem("player", ReagentBankButtonIDToInvSlotID(slot))
|
||||
else
|
||||
TSMScanTooltip:SetBagItem(bag, slot)
|
||||
end
|
||||
|
||||
-- scan the tooltip
|
||||
local numLines = TSMScanTooltip:NumLines()
|
||||
if numLines < 1 then
|
||||
-- the tooltip didn't fully load or there's nothing in this slot
|
||||
return nil, nil
|
||||
end
|
||||
local isBOP, isBOA = false, false
|
||||
for id = 2, numLines do
|
||||
local text = private.GetTooltipText(_G["TSMScanTooltipTextLeft"..id])
|
||||
if text then
|
||||
if (text == ITEM_BIND_ON_PICKUP and id < 4) or text == ITEM_SOULBOUND or text == ITEM_BIND_QUEST then
|
||||
isBOP = true
|
||||
break
|
||||
elseif (text == ITEM_ACCOUNTBOUND or text == ITEM_BIND_TO_ACCOUNT or text == ITEM_BIND_TO_BNETACCOUNT or text == ITEM_BNETACCOUNTBOUND) then
|
||||
isBOA = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
private.slotIdSoulboundCached[slotId] = true
|
||||
private.slotIdIsBoP[slotId] = isBOP
|
||||
private.slotIdIsBoA[slotId] = isBOA
|
||||
return isBOP, isBOA
|
||||
end
|
||||
|
||||
function InventoryInfo.HasUsedCharges(bag, slot)
|
||||
-- figure out if this item has a max number of charges
|
||||
local itemId = GetContainerItemID(bag, slot)
|
||||
if not itemId or itemId == ItemString.ToId(ItemString.GetPetCage()) then
|
||||
return false
|
||||
end
|
||||
if not TSMScanTooltip then
|
||||
CreateFrame("GameTooltip", "TSMScanTooltip", UIParent, "GameTooltipTemplate")
|
||||
end
|
||||
|
||||
TSMScanTooltip:SetOwner(UIParent, "ANCHOR_NONE")
|
||||
TSMScanTooltip:ClearLines()
|
||||
TSMScanTooltip:SetItemByID(itemId)
|
||||
|
||||
local maxCharges = private.GetScanTooltipCharges()
|
||||
if not maxCharges then
|
||||
return false
|
||||
end
|
||||
|
||||
-- set TSMScanTooltip to show the inventory item
|
||||
if bag == BANK_CONTAINER then
|
||||
TSMScanTooltip:SetInventoryItem("player", BankButtonIDToInvSlotID(slot))
|
||||
elseif bag == REAGENTBANK_CONTAINER then
|
||||
TSMScanTooltip:SetInventoryItem("player", ReagentBankButtonIDToInvSlotID(slot))
|
||||
else
|
||||
TSMScanTooltip:SetBagItem(bag, slot)
|
||||
end
|
||||
|
||||
-- check if there are used charges
|
||||
if maxCharges and private.GetScanTooltipCharges() ~= maxCharges then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.ItemLockedHandler(_, bag, slot)
|
||||
if not slot then
|
||||
return
|
||||
end
|
||||
private.slotIdLocked[SlotId.Join(bag, slot)] = true
|
||||
end
|
||||
|
||||
function private.ItemUnlockedHandler(_, bag, slot)
|
||||
if not slot then
|
||||
return
|
||||
end
|
||||
private.slotIdLocked[SlotId.Join(bag, slot)] = nil
|
||||
end
|
||||
|
||||
function private.BagUpdateHandler(_, bag)
|
||||
-- clear the soulbound cache for everything in this bag
|
||||
Table.Filter(private.slotIdSoulboundCached, private.SlotIdSoulboundCachedFilter, bag)
|
||||
end
|
||||
|
||||
function private.BankSlotChangedHandler(_, slot)
|
||||
if slot <= NUM_BANKGENERIC_SLOTS then
|
||||
-- one of the slots of the primary bank container changed, so just clear the cache for this slot
|
||||
private.slotIdSoulboundCached[SlotId.Join(BANK_CONTAINER, slot)] = nil
|
||||
else
|
||||
-- one of the extra bank bags changed, so clear the cache for the entire bag
|
||||
Table.Filter(private.slotIdSoulboundCached, private.SlotIdSoulboundCachedFilter, slot - NUM_BANKGENERIC_SLOTS)
|
||||
end
|
||||
end
|
||||
|
||||
function private.SlotIdSoulboundCachedFilter(slotId, _, bag)
|
||||
return SlotId.Split(slotId) == bag
|
||||
end
|
||||
|
||||
function private.ReagentBankSlotChangedHandler(_, slot)
|
||||
-- clear the soulbound cache for this slot
|
||||
private.slotIdSoulboundCached[SlotId.Join(REAGENTBANK_CONTAINER, slot)] = nil
|
||||
end
|
||||
|
||||
function private.GetTooltipText(text)
|
||||
local textStr = strtrim(text and text:GetText() or "")
|
||||
if textStr == "" then return end
|
||||
|
||||
local r, g, b = text:GetTextColor()
|
||||
return textStr, floor(r * 256), floor(g * 256), floor(b * 256)
|
||||
end
|
||||
|
||||
function private.GetScanTooltipCharges()
|
||||
for id = 2, TSMScanTooltip:NumLines() do
|
||||
local text = private.GetTooltipText(_G["TSMScanTooltipTextLeft"..id])
|
||||
local num = text and strmatch(text, "%d+")
|
||||
local chargesStr = gsub(ITEM_SPELL_CHARGES, "%%d", "%%d+")
|
||||
if strfind(chargesStr, ":") then
|
||||
if num == 1 then
|
||||
chargesStr = gsub(chargesStr, "\1244(.+):.+;", "%1")
|
||||
else
|
||||
chargesStr = gsub(chargesStr, "\1244.+:(.+);", "%1")
|
||||
end
|
||||
end
|
||||
|
||||
local maxCharges = text and strmatch(text, "^"..chargesStr.."$")
|
||||
|
||||
if maxCharges then
|
||||
return maxCharges
|
||||
end
|
||||
end
|
||||
end
|
||||
436
LibTSM/Service/ItemFilter.lua
Normal file
436
LibTSM/Service/ItemFilter.lua
Normal file
@@ -0,0 +1,436 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local ItemFilter = TSM.Init("Service.ItemFilter")
|
||||
local LibTSMClass = TSM.Include("LibTSMClass")
|
||||
local L = TSM.Include("Locale").GetTable()
|
||||
local Filter = LibTSMClass.DefineClass("ItemFilter")
|
||||
local ItemClass = TSM.Include("Data.ItemClass")
|
||||
local Money = TSM.Include("Util.Money")
|
||||
local String = TSM.Include("Util.String")
|
||||
local Vararg = TSM.Include("Util.Vararg")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function ItemFilter.New()
|
||||
return Filter()
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Filter Class
|
||||
-- ============================================================================
|
||||
|
||||
function Filter.__init(self)
|
||||
self._isValid = nil
|
||||
self._str = nil
|
||||
self._escapedStr = nil
|
||||
self._class = nil
|
||||
self._subClass = nil
|
||||
self._invSlotId = nil
|
||||
self._minQuality = nil
|
||||
self._maxQuality = nil
|
||||
self._minLevel = nil
|
||||
self._maxLevel = nil
|
||||
self._minItemLevel = nil
|
||||
self._maxItemLevel = nil
|
||||
self._minPrice = nil
|
||||
self._maxPrice = nil
|
||||
self._maxQuantity = nil
|
||||
self._uncollected = nil
|
||||
self._usable = nil
|
||||
self._upgrades = nil
|
||||
self._unlearned = nil
|
||||
self._canlearn = nil
|
||||
self._exactOnly = nil
|
||||
self._crafting = nil
|
||||
self._disenchant = nil
|
||||
self._item = nil
|
||||
|
||||
self:_Reset()
|
||||
end
|
||||
|
||||
function Filter._Reset(self)
|
||||
self._isValid = nil
|
||||
self._str = ""
|
||||
self._escapedStr = ""
|
||||
self._class = nil
|
||||
self._subClass = nil
|
||||
self._invSlotId = nil
|
||||
self._minQuality = -math.huge
|
||||
self._maxQuality = math.huge
|
||||
self._minLevel = 0
|
||||
self._maxLevel = math.huge
|
||||
self._minItemLevel = 0
|
||||
self._maxItemLevel = math.huge
|
||||
self._minPrice = 0
|
||||
self._maxPrice = math.huge
|
||||
self._maxQuantity = math.huge
|
||||
self._uncollected = nil
|
||||
self._usable = nil
|
||||
self._upgrades = nil
|
||||
self._unlearned = nil
|
||||
self._canlearn = nil
|
||||
self._exactOnly = nil
|
||||
self._crafting = nil
|
||||
self._disenchant = nil
|
||||
self._item = nil
|
||||
end
|
||||
|
||||
function Filter._ItemQualityToIndex(self, str)
|
||||
for i = 0, 7 do
|
||||
local text = _G["ITEM_QUALITY"..i.."_DESC"]
|
||||
if strlower(str) == strlower(text) then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Filter.ParseStr(self, str)
|
||||
self:_Reset()
|
||||
local errMsg = nil
|
||||
local numLevelParts, numItemLevelParts, numPriceParts, numQualityParts = 0, 0, 0, 0
|
||||
self._isValid = nil
|
||||
local hasNonCraftingPart = false
|
||||
for i, part in Vararg.Iterator(strsplit("/", strtrim(str))) do
|
||||
part = strtrim(part)
|
||||
if self._isValid ~= nil then
|
||||
-- already done iterating, but can't break / return out of a VarargIterator
|
||||
if strmatch(part, "^[ip]:[0-9]+") then
|
||||
-- request item info in case we failed due to not having it (for next time)
|
||||
ItemInfo.FetchInfo(part)
|
||||
end
|
||||
elseif i == 1 then
|
||||
-- first part must be a filter string or an item
|
||||
if strmatch(part, "^[ip]:[0-9]+") then
|
||||
local name = ItemInfo.GetName(part)
|
||||
local level = ItemInfo.GetMinLevel(part)
|
||||
local quality = ItemInfo.GetQuality(part)
|
||||
if not name or not level or not quality then
|
||||
errMsg = L["The specified item was not found."]
|
||||
self._isValid = false
|
||||
else
|
||||
self._exactOnly = true
|
||||
self._item = part
|
||||
self._str = strlower(name)
|
||||
self._escapedStr = String.Escape(self._str)
|
||||
self._minQuality = quality
|
||||
self._maxQuality = quality
|
||||
self._minLevel = level
|
||||
self._maxLevel = level
|
||||
self._class = ItemInfo.GetClassId(self._item) or 0
|
||||
self._subClass = ItemInfo.GetSubClassId(self._item) or 0
|
||||
end
|
||||
else
|
||||
self._str = strlower(part)
|
||||
self._escapedStr = String.Escape(self._str)
|
||||
end
|
||||
elseif part == "" then
|
||||
-- ignore an empty part
|
||||
elseif tonumber(part) then
|
||||
if numLevelParts == 0 then
|
||||
self._minLevel = tonumber(part)
|
||||
elseif numLevelParts == 1 then
|
||||
self._maxLevel = tonumber(part)
|
||||
else
|
||||
-- already have min / max level
|
||||
errMsg = L["The same filter was specified multiple times."]
|
||||
self._isValid = false
|
||||
end
|
||||
numLevelParts = numLevelParts + 1
|
||||
hasNonCraftingPart = true
|
||||
elseif tonumber(strmatch(part, "^i(%d+)$")) then
|
||||
if numItemLevelParts == 0 then
|
||||
self._minItemLevel = tonumber(strmatch(part, "^i(%d+)$"))
|
||||
elseif numItemLevelParts == 1 then
|
||||
self._maxItemLevel = tonumber(strmatch(part, "^i(%d+)$"))
|
||||
else
|
||||
-- already have min / max item level
|
||||
errMsg = L["The same filter was specified multiple times."]
|
||||
self._isValid = false
|
||||
end
|
||||
numItemLevelParts = numItemLevelParts + 1
|
||||
hasNonCraftingPart = true
|
||||
elseif ItemClass.GetClassIdFromClassString(part) then
|
||||
self._class = ItemClass.GetClassIdFromClassString(part)
|
||||
hasNonCraftingPart = true
|
||||
elseif self._class and ItemClass.GetSubClassIdFromSubClassString(part, self._class) then
|
||||
self._subClass = ItemClass.GetSubClassIdFromSubClassString(part, self._class)
|
||||
hasNonCraftingPart = true
|
||||
elseif ItemClass.GetInventorySlotIdFromInventorySlotString(part) then
|
||||
self._invSlotId = ItemClass.GetInventorySlotIdFromInventorySlotString(part)
|
||||
hasNonCraftingPart = true
|
||||
elseif self:_ItemQualityToIndex(part) then
|
||||
if numQualityParts == 0 then
|
||||
self._minQuality = self:_ItemQualityToIndex(part)
|
||||
elseif numQualityParts == 1 then
|
||||
self._maxQuality = self:_ItemQualityToIndex(part)
|
||||
else
|
||||
-- already have min / max quality
|
||||
errMsg = L["The same filter was specified multiple times."]
|
||||
self._isValid = false
|
||||
end
|
||||
numQualityParts = numQualityParts + 1
|
||||
hasNonCraftingPart = true
|
||||
elseif Money.FromString(part) then
|
||||
if numPriceParts == 0 then
|
||||
self._maxPrice = Money.FromString(part)
|
||||
elseif numPriceParts == 1 then
|
||||
self._minPrice = self._maxPrice
|
||||
self._maxPrice = Money.FromString(part)
|
||||
else
|
||||
-- already have min / max price
|
||||
errMsg = L["The same filter was specified multiple times."]
|
||||
self._isValid = false
|
||||
end
|
||||
numPriceParts = numPriceParts + 1
|
||||
hasNonCraftingPart = true
|
||||
elseif not TSM.IsWowClassic() and strlower(part) == "uncollected" then
|
||||
if self._uncollected then
|
||||
errMsg = L["The same filter was specified multiple times."]
|
||||
self._isValid = false
|
||||
end
|
||||
self._uncollected = true
|
||||
hasNonCraftingPart = true
|
||||
elseif strlower(part) == "usable" then
|
||||
if self._usable then
|
||||
errMsg = L["The same filter was specified multiple times."]
|
||||
self._isValid = false
|
||||
end
|
||||
self._usable = true
|
||||
hasNonCraftingPart = true
|
||||
elseif not TSM.IsWowClassic() and strlower(part) == "upgrades" then
|
||||
if self._upgrades then
|
||||
errMsg = L["The same filter was specified multiple times."]
|
||||
self._isValid = false
|
||||
end
|
||||
self._upgrades = true
|
||||
hasNonCraftingPart = true
|
||||
elseif strlower(part) == "unlearned" then
|
||||
if self._unlearned then
|
||||
errMsg = L["The same filter was specified multiple times."]
|
||||
self._isValid = false
|
||||
end
|
||||
if CanIMogIt and CanIMogIt.PlayerKnowsTransmog then
|
||||
self._unlearned = true
|
||||
else
|
||||
Log.PrintUser(L["The unlearned filter was ignored because the CanIMogIt addon was not found."])
|
||||
end
|
||||
hasNonCraftingPart = true
|
||||
elseif strlower(part) == "canlearn" then
|
||||
if self._canlearn then
|
||||
errMsg = L["The same filter was specified multiple times."]
|
||||
self._isValid = false
|
||||
end
|
||||
if CanIMogIt and CanIMogIt.CharacterCanLearnTransmog then
|
||||
self._canlearn = true
|
||||
else
|
||||
Log.PrintUser(L["The canlearn filter was ignored because the CanIMogIt addon was not found."])
|
||||
end
|
||||
hasNonCraftingPart = true
|
||||
elseif strlower(part) == "exact" then
|
||||
if self._exactOnly then
|
||||
errMsg = L["The same filter was specified multiple times."]
|
||||
self._isValid = false
|
||||
end
|
||||
self._exactOnly = true
|
||||
hasNonCraftingPart = true
|
||||
elseif tonumber(strmatch(part, "^x(%d+)$")) then
|
||||
self._maxQuantity = tonumber(strmatch(part, "^x(%d+)$"))
|
||||
if self._maxQuantity == 0 then
|
||||
errMsg = L["The max quantity cannot be zero."]
|
||||
self._isValid = false
|
||||
end
|
||||
elseif strlower(part) == "crafting" then
|
||||
if self._crafting or self._disenchant then
|
||||
errMsg = L["The same filter was specified multiple times."]
|
||||
self._isValid = false
|
||||
end
|
||||
self._crafting = true
|
||||
elseif strlower(part) == "disenchant" then
|
||||
if self._disenchant or self._crafting then
|
||||
errMsg = L["The same filter was specified multiple times."]
|
||||
self._isValid = false
|
||||
end
|
||||
self._disenchant = true
|
||||
else
|
||||
-- invalid part
|
||||
errMsg = format(L["Unknown word (%s)."], part)
|
||||
self._isValid = false
|
||||
end
|
||||
end
|
||||
|
||||
if (self._crafting or self._disenchant) and hasNonCraftingPart then
|
||||
errMsg = L["Cannot use additional filters with /crafting or /disenchant."]
|
||||
self._isValid = false
|
||||
end
|
||||
|
||||
if self._isValid == nil then
|
||||
self._isValid = true
|
||||
end
|
||||
return self._isValid, errMsg
|
||||
end
|
||||
|
||||
function Filter.GetStr(self)
|
||||
return self._str ~= "" and self._str or nil
|
||||
end
|
||||
|
||||
function Filter.GetItem(self)
|
||||
return self._item
|
||||
end
|
||||
|
||||
function Filter.GetMinQuality(self)
|
||||
return self._minQuality ~= -math.huge and self._minQuality or nil
|
||||
end
|
||||
|
||||
function Filter.GetMaxQuality(self)
|
||||
return self._maxQuality ~= math.huge and self._maxQuality or nil
|
||||
end
|
||||
|
||||
function Filter.GetClass(self)
|
||||
return self._class
|
||||
end
|
||||
|
||||
function Filter.GetSubClass(self)
|
||||
return self._subClass
|
||||
end
|
||||
|
||||
function Filter.GetInvSlotId(self)
|
||||
return self._invSlotId
|
||||
end
|
||||
|
||||
function Filter.GetMinLevel(self)
|
||||
return self._minLevel ~= 0 and self._minLevel or nil
|
||||
end
|
||||
|
||||
function Filter.GetMaxLevel(self)
|
||||
return self._maxLevel ~= math.huge and self._maxLevel or nil
|
||||
end
|
||||
|
||||
function Filter.GetMinItemLevel(self)
|
||||
return self._minItemLevel ~= 0 and self._minItemLevel or nil
|
||||
end
|
||||
|
||||
function Filter.GetMaxItemLevel(self)
|
||||
return self._maxItemLevel ~= math.huge and self._maxItemLevel or nil
|
||||
end
|
||||
|
||||
function Filter.GetUncollected(self)
|
||||
return self._uncollected
|
||||
end
|
||||
|
||||
function Filter.GetUsableOnly(self)
|
||||
return self._usable
|
||||
end
|
||||
|
||||
function Filter.GetUpgrades(self)
|
||||
return self._upgrades
|
||||
end
|
||||
|
||||
function Filter.GetUnlearned(self)
|
||||
return self._unlearned
|
||||
end
|
||||
|
||||
function Filter.GetCanLearn(self)
|
||||
return self._canlearn
|
||||
end
|
||||
|
||||
function Filter.GetExactOnly(self)
|
||||
return self._exactOnly
|
||||
end
|
||||
|
||||
function Filter.GetMaxQuantity(self)
|
||||
return self._maxQuantity ~= math.huge and self._maxQuantity or nil
|
||||
end
|
||||
|
||||
function Filter.GetMinPrice(self)
|
||||
return self._minPrice ~= 0 and self._minPrice or nil
|
||||
end
|
||||
|
||||
function Filter.GetMaxPrice(self)
|
||||
return self._maxPrice ~= math.huge and self._maxPrice or nil
|
||||
end
|
||||
|
||||
function Filter.GetCrafting(self)
|
||||
return self._crafting
|
||||
end
|
||||
|
||||
function Filter.GetDisenchant(self)
|
||||
return self._disenchant
|
||||
end
|
||||
|
||||
function Filter.Matches(self, item, price)
|
||||
if not self._isValid then
|
||||
return false
|
||||
end
|
||||
|
||||
-- check the name
|
||||
local name = ItemInfo.GetName(item)
|
||||
name = name and strlower(name)
|
||||
if not name or not strfind(name, self._escapedStr) or (self._exactOnly and name ~= self._str) then
|
||||
return false
|
||||
end
|
||||
|
||||
-- check the quality
|
||||
local quality = ItemInfo.GetQuality(item)
|
||||
if not quality or quality < self._minQuality or quality > self._maxQuality then
|
||||
return false
|
||||
end
|
||||
|
||||
-- check the item level
|
||||
local itemLevel = ItemInfo.GetItemLevel(item)
|
||||
if not itemLevel or itemLevel < self._minItemLevel or itemLevel > self._maxItemLevel then
|
||||
return false
|
||||
end
|
||||
|
||||
-- check the required level
|
||||
local level = ItemInfo.GetMinLevel(item)
|
||||
if not level or level < self._minLevel or level > self._maxLevel then
|
||||
return false
|
||||
end
|
||||
|
||||
-- check the item class
|
||||
if self._class and ItemInfo.GetClassId(item) ~= self._class then
|
||||
return false
|
||||
end
|
||||
|
||||
-- check the item subclass
|
||||
if self._subClass and ItemInfo.GetSubClassId(item) ~= self._subClass then
|
||||
return false
|
||||
end
|
||||
|
||||
-- check the inventory slot
|
||||
if self._invSlotId and ItemInfo.GetInvSlotId(item) ~= self._invSlotId then
|
||||
return false
|
||||
end
|
||||
|
||||
-- check unlearned
|
||||
if self._unlearned and CanIMogIt:PlayerKnowsTransmog(ItemInfo.GetLink(item)) then
|
||||
return false
|
||||
end
|
||||
|
||||
-- check canlearn
|
||||
if self._canlearn and CanIMogIt:CharacterCanLearnTransmog(ItemInfo.GetLink(item)) then
|
||||
return false
|
||||
end
|
||||
|
||||
-- check the price
|
||||
price = price or 0
|
||||
if price < self._minPrice or price > self._maxPrice then
|
||||
return false
|
||||
end
|
||||
|
||||
-- it passed!
|
||||
return true
|
||||
end
|
||||
1368
LibTSM/Service/ItemInfo.lua
Normal file
1368
LibTSM/Service/ItemInfo.lua
Normal file
File diff suppressed because it is too large
Load Diff
73
LibTSM/Service/ItemLinked.lua
Normal file
73
LibTSM/Service/ItemLinked.lua
Normal file
@@ -0,0 +1,73 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- ItemLinked Functions.
|
||||
-- @module ItemLinked
|
||||
|
||||
local _, TSM = ...
|
||||
local ItemLinked = TSM.Init("Service.ItemLinked")
|
||||
local Table = TSM.Include("Util.Table")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local private = {
|
||||
callbacks = {},
|
||||
priorityLookup = {}
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
ItemLinked:OnModuleLoad(function()
|
||||
local origHandleModifiedItemClick = HandleModifiedItemClick
|
||||
HandleModifiedItemClick = function(link)
|
||||
return private.ItemLinkedHook(origHandleModifiedItemClick, link)
|
||||
end
|
||||
local origChatEdit_InsertLink = ChatEdit_InsertLink
|
||||
ChatEdit_InsertLink = function(link)
|
||||
return private.ItemLinkedHook(origChatEdit_InsertLink, link)
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function ItemLinked.RegisterCallback(callback, priority)
|
||||
assert(type(callback) == "function")
|
||||
tinsert(private.callbacks, callback)
|
||||
private.priorityLookup[callback] = (priority or 0) + #private.callbacks * 0.01
|
||||
Table.SortWithValueLookup(private.callbacks, private.priorityLookup)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.ItemLinkedHook(origFunc, itemLink)
|
||||
local putIntoChat = origFunc(itemLink)
|
||||
if putIntoChat then
|
||||
return putIntoChat
|
||||
end
|
||||
local name = ItemInfo.GetName(itemLink)
|
||||
if not name or not private.HandleItemLinked(name, itemLink) then
|
||||
return putIntoChat
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function private.HandleItemLinked(name, itemLink)
|
||||
for _, callback in ipairs(private.callbacks) do
|
||||
if callback(name, itemLink) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
24
LibTSM/Service/ItemTooltip.lua
Normal file
24
LibTSM/Service/ItemTooltip.lua
Normal file
@@ -0,0 +1,24 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local ItemTooltip = TSM.Init("Service.ItemTooltip")
|
||||
local Builder = TSM.Include("Service.ItemTooltipClasses.Builder")
|
||||
local Wrapper = TSM.Include("Service.ItemTooltipClasses.Wrapper")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function ItemTooltip.CreateBuilder()
|
||||
return Builder.Create()
|
||||
end
|
||||
|
||||
function ItemTooltip.SetWrapperPopulateFunction(func)
|
||||
Wrapper.SetPopulateFunction(func)
|
||||
end
|
||||
270
LibTSM/Service/ItemTooltipClasses/Builder.lua
Normal file
270
LibTSM/Service/ItemTooltipClasses/Builder.lua
Normal file
@@ -0,0 +1,270 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Builder = TSM.Init("Service.ItemTooltipClasses.Builder")
|
||||
local L = TSM.Include("Locale").GetTable()
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
local Money = TSM.Include("Util.Money")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local Theme = TSM.Include("Util.Theme")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local LibTSMClass = TSM.Include("LibTSMClass")
|
||||
local TooltipBuilder = LibTSMClass.DefineClass("TooltipBuilder")
|
||||
local private = {}
|
||||
local TOOLTIP_CACHE_TIME = 5
|
||||
local LINE_PART_SEP = "\t"
|
||||
local DISABLED_TINT = -30
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- TooltipBuilder - Class Meta Methods
|
||||
-- ============================================================================
|
||||
|
||||
function Builder.Create()
|
||||
return TooltipBuilder()
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- TooltipBuilder - Class Meta Methods
|
||||
-- ============================================================================
|
||||
|
||||
function TooltipBuilder.__init(self)
|
||||
self._lines = {}
|
||||
self._lineColors = {}
|
||||
self._level = 0
|
||||
self._levelNumLines = {}
|
||||
self._levelHasHeading = {}
|
||||
self._lastUpdate = 0
|
||||
self._modifier = nil
|
||||
self._inCombat = false
|
||||
self._itemString = nil
|
||||
self._quantity = nil
|
||||
self._disabled = nil
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- TooltipBuilder - Public Methods
|
||||
-- ============================================================================
|
||||
|
||||
function TooltipBuilder.ApplyValueColor(self, text)
|
||||
local color = Theme.GetColor("TEXT")
|
||||
if self._disabled then
|
||||
color = color:GetTint(DISABLED_TINT)
|
||||
end
|
||||
return color:ColorText(text)
|
||||
end
|
||||
|
||||
function TooltipBuilder.FormatMoney(self, value, color)
|
||||
color = color or Theme.GetColor("TEXT")
|
||||
if self._disabled then
|
||||
color = color:GetTint(DISABLED_TINT)
|
||||
end
|
||||
if not value then
|
||||
return self:ApplyValueColor("---")
|
||||
end
|
||||
return Money.ToString(value, color:GetTextColorPrefix(), TSM.db.global.tooltipOptions.tooltipPriceFormat == "icon" and "OPT_ICON" or nil, self._disabled and "OPT_DISABLE" or nil)
|
||||
end
|
||||
|
||||
function TooltipBuilder.AddLine(self, lineLeft, lineRight, color)
|
||||
if lineRight then
|
||||
lineLeft = strjoin(LINE_PART_SEP, lineLeft, lineRight)
|
||||
end
|
||||
local line = strrep(" ", self._level)..lineLeft
|
||||
color = color or Theme.GetColor("INDICATOR_ALT")
|
||||
if self._disabled then
|
||||
color = color:GetTint(DISABLED_TINT)
|
||||
end
|
||||
assert(not self._lineColors[line] or self._lineColors[line] == color)
|
||||
self._lineColors[line] = color
|
||||
tinsert(self._lines, line)
|
||||
self._levelNumLines[self._level] = self._levelNumLines[self._level] + 1
|
||||
end
|
||||
|
||||
function TooltipBuilder.AddTextLine(self, label, text, ...)
|
||||
if select("#", ...) == 0 then
|
||||
self:AddLine(label, self:ApplyValueColor(text))
|
||||
else
|
||||
self:AddLine(label, self:ApplyValueColor(text).." ("..self:_TextsToStr(...)..")")
|
||||
end
|
||||
end
|
||||
|
||||
function TooltipBuilder.AddValueLine(self, label, ...)
|
||||
self:AddLine(label, self:_ValuesToStr(...))
|
||||
end
|
||||
|
||||
function TooltipBuilder.AddItemValueLine(self, label, value)
|
||||
if self._quantity > 1 then
|
||||
label = label.." x"..self._quantity
|
||||
value = value * self._quantity
|
||||
end
|
||||
self:AddLine(label, self:FormatMoney(value))
|
||||
end
|
||||
|
||||
function TooltipBuilder.AddQuantityValueLine(self, label, quantity, ...)
|
||||
self:AddLine(label, self:ApplyValueColor(quantity).." ("..self:_ValuesToStr(...)..")")
|
||||
end
|
||||
|
||||
function TooltipBuilder.AddSubItemValueLine(self, itemString, value, multiplier, matRate, minAmount, maxAmount)
|
||||
local name = ItemInfo.GetName(itemString)
|
||||
local color = ItemInfo.GetQualityColor(itemString)
|
||||
if not name or not color then
|
||||
return
|
||||
end
|
||||
multiplier = Math.Round(multiplier * self._quantity, 0.001)
|
||||
matRate = matRate and matRate * 100
|
||||
matRate = matRate and matRate.."% " or ""
|
||||
local range = (minAmount and maxAmount) and (minAmount ~= maxAmount and "|cffffff00 ["..minAmount.."-"..maxAmount.."]|r" or "|cffffff00 ["..minAmount.."]|r") or ""
|
||||
value = value and (value * multiplier) or nil
|
||||
self:AddLine(color..matRate..name.." x"..multiplier.."|r"..range, self:FormatMoney(value))
|
||||
end
|
||||
|
||||
function TooltipBuilder.StartSection(self, headingTextLeft, headingTextRight)
|
||||
if self._level == 0 then
|
||||
headingTextLeft = Theme.GetColor("INDICATOR"):ColorText(headingTextLeft)
|
||||
end
|
||||
if headingTextRight then
|
||||
self:AddLine(headingTextLeft, headingTextRight)
|
||||
elseif headingTextLeft then
|
||||
self:AddLine(headingTextLeft)
|
||||
end
|
||||
self._level = self._level + 1
|
||||
self._levelHasHeading[self._level] = headingTextLeft and true or false
|
||||
self._levelNumLines[self._level] = 0
|
||||
end
|
||||
|
||||
function TooltipBuilder.EndSection(self)
|
||||
if self._levelNumLines[self._level] == 0 and self._levelHasHeading[self._level] then
|
||||
-- remove the previous heading line
|
||||
local headingLine = tremove(self._lines)
|
||||
assert(headingLine)
|
||||
self._lineColors[headingLine] = nil
|
||||
self._levelNumLines[self._level - 1] = self._levelNumLines[self._level - 1] - 1
|
||||
end
|
||||
self._level = self._level - 1
|
||||
assert(self._level >= 0)
|
||||
end
|
||||
|
||||
function TooltipBuilder.SetDisabled(self, disabled)
|
||||
self._disabled = disabled
|
||||
end
|
||||
|
||||
function TooltipBuilder.GetNumLines(self)
|
||||
return #self._lines
|
||||
end
|
||||
|
||||
function TooltipBuilder.GetLine(self, index)
|
||||
local line = self._lines[index]
|
||||
local left, right = strsplit(LINE_PART_SEP, line)
|
||||
return left, right, self._lineColors[line]
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- TooltipBuilder - Private Methods
|
||||
-- ============================================================================
|
||||
|
||||
function TooltipBuilder._GetModifierHash(self)
|
||||
return (IsShiftKeyDown() and 4 or 0) + (IsAltKeyDown() and 2 or 0) + (IsControlKeyDown() and 1 or 0)
|
||||
end
|
||||
|
||||
function TooltipBuilder._IsCached(self, itemString, quantity)
|
||||
if itemString == ItemString.GetPlaceholder() then
|
||||
return false
|
||||
end
|
||||
if self:_GetModifierHash() ~= self._modifier then
|
||||
return false
|
||||
end
|
||||
if self._itemString ~= itemString or self._quantity ~= quantity then
|
||||
return false
|
||||
end
|
||||
if GetTime() - self._lastUpdate >= TOOLTIP_CACHE_TIME then
|
||||
return false
|
||||
end
|
||||
if InCombatLockdown() ~= self._inCombat then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function TooltipBuilder._Prepare(self, itemString, quantity)
|
||||
if self:_IsCached(itemString, quantity) then
|
||||
-- have the lines cached already
|
||||
return true
|
||||
end
|
||||
|
||||
wipe(self._lines)
|
||||
wipe(self._lineColors)
|
||||
wipe(self._levelNumLines)
|
||||
wipe(self._levelHasHeading)
|
||||
self._level = 0
|
||||
self._levelNumLines[self._level] = 0
|
||||
self._lastUpdate = GetTime()
|
||||
self._modifier = self:_GetModifierHash()
|
||||
self._inCombat = InCombatLockdown()
|
||||
self._itemString = itemString
|
||||
self._quantity = quantity
|
||||
self._disabled = false
|
||||
|
||||
if InCombatLockdown() and itemString ~= ItemString.GetPlaceholder() then
|
||||
self:AddLine(L["Can't load TSM tooltip while in combat"])
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function TooltipBuilder._IsEmpty(self)
|
||||
assert(self._level == 0)
|
||||
return #self._lines == 0
|
||||
end
|
||||
|
||||
function TooltipBuilder._LineIterator(self)
|
||||
return private.LineIteratorHelper, self, 0
|
||||
end
|
||||
|
||||
function TooltipBuilder._TextsToStr(self, ...)
|
||||
local valueParts = TempTable.Acquire(...)
|
||||
for i = 1, #valueParts do
|
||||
valueParts[i] = self:ApplyValueColor(valueParts[i])
|
||||
end
|
||||
local result = table.concat(valueParts, " / ")
|
||||
TempTable.Release(valueParts)
|
||||
return result
|
||||
end
|
||||
|
||||
function TooltipBuilder._ValuesToStr(self, ...)
|
||||
-- can't just build the table with the vararg as there may be nils
|
||||
local valueParts = TempTable.Acquire()
|
||||
for i = 1, select("#", ...) do
|
||||
local value = select(i, ...)
|
||||
valueParts[i] = self:FormatMoney(value)
|
||||
end
|
||||
local result = table.concat(valueParts, " / ")
|
||||
TempTable.Release(valueParts)
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.LineIteratorHelper(self, index)
|
||||
index = index + 1
|
||||
if index > #self._lines then
|
||||
return
|
||||
end
|
||||
return index, self:GetLine(index)
|
||||
end
|
||||
114
LibTSM/Service/ItemTooltipClasses/ExtraTip.lua
Normal file
114
LibTSM/Service/ItemTooltipClasses/ExtraTip.lua
Normal file
@@ -0,0 +1,114 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local ExtraTip = TSM.Init("Service.ItemTooltipClasses.ExtraTip")
|
||||
local private = {
|
||||
numExtraTips = 0,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function ExtraTip.Create(tooltip)
|
||||
return private.CreateExtraTip(tooltip)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Extra Tip Methods
|
||||
-- ============================================================================
|
||||
|
||||
local EXTRA_TIP_METHODS = {
|
||||
Attach = function(self, tooltip)
|
||||
self:SetOwner(tooltip, "ANCHOR_PRESERVE")
|
||||
self.anchorFrame = nil
|
||||
self:OnUpdate()
|
||||
end,
|
||||
|
||||
Show = function(self)
|
||||
self.anchorFrame = nil
|
||||
GameTooltip.Show(self)
|
||||
local bottom = self:GetBottom()
|
||||
if bottom and self:GetParent():IsShown() then
|
||||
self.isTop = bottom <= 0
|
||||
end
|
||||
self:OnUpdate()
|
||||
local numLines = self:NumLines()
|
||||
local changedLines = self.changedLines or 0
|
||||
if changedLines >= numLines then return end
|
||||
for i = changedLines + 1, numLines do
|
||||
local left, right = self.Left[i], self.Right[i]
|
||||
local font = i == 1 and GameTooltipHeader or GameFontNormal
|
||||
|
||||
local r, g, b, a = left:GetTextColor()
|
||||
left:SetFontObject(font)
|
||||
left:SetTextColor(r, g, b, a)
|
||||
|
||||
r, g, b, a = right:GetTextColor()
|
||||
right:SetFontObject(font)
|
||||
right:SetTextColor(r, g, b, a)
|
||||
end
|
||||
self.changedLines = numLines
|
||||
GameTooltip.Show(self)
|
||||
self:OnUpdate()
|
||||
end,
|
||||
|
||||
OnUpdate = function(self)
|
||||
local tooltip = self:GetParent()
|
||||
local anchorFrame = private.GetExtraTipFrame(tooltip, tooltip:GetChildren()) or tooltip
|
||||
if anchorFrame ~= self.anchorFrame then
|
||||
self:ClearAllPoints()
|
||||
if self.isTop then
|
||||
self:SetPoint("BOTTOM", anchorFrame, "TOP")
|
||||
else
|
||||
self:SetPoint("TOP", anchorFrame, "BOTTOM")
|
||||
end
|
||||
self.anchorFrame = anchorFrame
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
local LINE_METATABLE = {
|
||||
__index = function(t, k)
|
||||
local v = _G[t.name..k]
|
||||
rawset(t, k, v)
|
||||
return v
|
||||
end,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.CreateExtraTip(tooltip)
|
||||
private.numExtraTips = private.numExtraTips + 1
|
||||
local extraTip = CreateFrame("GameTooltip", "TSMExtraTip"..private.numExtraTips, tooltip, "GameTooltipTemplate")
|
||||
extraTip:SetClampedToScreen(true)
|
||||
|
||||
for name, func in pairs(EXTRA_TIP_METHODS) do
|
||||
extraTip[name] = func
|
||||
end
|
||||
|
||||
extraTip.Left = setmetatable({name = extraTip:GetName().."TextLeft"}, LINE_METATABLE)
|
||||
extraTip.Right = setmetatable({name = extraTip:GetName().."TextRight"}, LINE_METATABLE)
|
||||
return extraTip
|
||||
end
|
||||
|
||||
function private.GetExtraTipFrame(tooltip, ...)
|
||||
for i = 1, select('#', ...) do
|
||||
local frame = select(i, ...)
|
||||
if frame.InitLines and frame:GetParent() == tooltip and frame:IsVisible() then
|
||||
return frame
|
||||
end
|
||||
end
|
||||
end
|
||||
320
LibTSM/Service/ItemTooltipClasses/Wrapper.lua
Normal file
320
LibTSM/Service/ItemTooltipClasses/Wrapper.lua
Normal file
@@ -0,0 +1,320 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Wrapper = TSM.Init("Service.ItemTooltipClasses.Wrapper")
|
||||
local ExtraTip = TSM.Include("Service.ItemTooltipClasses.ExtraTip")
|
||||
local Builder = TSM.Include("Service.ItemTooltipClasses.Builder")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local private = {
|
||||
builder = nil,
|
||||
settings = nil,
|
||||
tooltipRegistry = {},
|
||||
hookedBattlepetGlobal = nil,
|
||||
tooltipMethodPrehooks = nil,
|
||||
tooltipMethodPosthooks = {},
|
||||
lastMailTooltipUpdate = nil,
|
||||
lastMailTooltipIndex = nil,
|
||||
populateFunc = nil,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Wrapper:OnSettingsLoad(function()
|
||||
private.builder = Builder.Create()
|
||||
private.settings = Settings.NewView()
|
||||
:AddKey("global", "tooltipOptions", "embeddedTooltip")
|
||||
:AddKey("global", "tooltipOptions", "enabled")
|
||||
:AddKey("global", "tooltipOptions", "tooltipShowModifier")
|
||||
private.RegisterTooltip(GameTooltip)
|
||||
private.RegisterTooltip(ItemRefTooltip)
|
||||
if not TSM.IsWowClassic() then
|
||||
private.RegisterTooltip(BattlePetTooltip)
|
||||
private.RegisterTooltip(FloatingBattlePetTooltip)
|
||||
end
|
||||
local orig = OpenMailAttachment_OnEnter
|
||||
OpenMailAttachment_OnEnter = function(self, index)
|
||||
private.lastMailTooltipUpdate = private.lastMailTooltipUpdate or 0
|
||||
if private.lastMailTooltipIndex ~= index or private.lastMailTooltipUpdate + 0.1 < GetTime() then
|
||||
private.lastMailTooltipUpdate = GetTime()
|
||||
private.lastMailTooltipIndex = index
|
||||
orig(self, index)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Wrapper.SetPopulateFunction(func)
|
||||
assert(type(func) == "function" and not private.populateFunc)
|
||||
private.populateFunc = func
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.RegisterTooltip(tooltip)
|
||||
local reg = {}
|
||||
reg.extraTip = ExtraTip.Create(tooltip)
|
||||
private.tooltipRegistry[tooltip] = reg
|
||||
|
||||
if private.IsBattlePetTooltip(tooltip) then
|
||||
if not private.hookedBattlepetGlobal then
|
||||
private.hookedBattlepetGlobal = true
|
||||
hooksecurefunc("BattlePetTooltipTemplate_SetBattlePet", private.OnTooltipSetBattlePet)
|
||||
hooksecurefunc("BattlePetToolTip_Show", private.OnBattlePetTooltipShow)
|
||||
end
|
||||
tooltip:HookScript("OnHide", private.OnTooltipCleared)
|
||||
else
|
||||
local scriptHooks = {
|
||||
OnTooltipSetItem = private.OnTooltipSetItem,
|
||||
OnTooltipCleared = private.OnTooltipCleared
|
||||
}
|
||||
for script, prehook in pairs(scriptHooks) do
|
||||
tooltip:HookScript(script, prehook)
|
||||
end
|
||||
|
||||
for method, prehook in pairs(private.tooltipMethodPrehooks) do
|
||||
local posthook = private.tooltipMethodPosthooks[method]
|
||||
local orig = tooltip[method]
|
||||
tooltip[method] = function(...)
|
||||
prehook(...)
|
||||
local a, b, c, d, e, f, g, h, i, j, k = orig(...)
|
||||
posthook(...)
|
||||
return a, b, c, d, e, f, g, h, i, j, k
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function private.IsBattlePetTooltip(tooltip)
|
||||
if TSM.IsWowClassic() then
|
||||
return false
|
||||
end
|
||||
return tooltip == BattlePetTooltip or tooltip == FloatingBattlePetTooltip
|
||||
end
|
||||
|
||||
function private.OnTooltipSetItem(tooltip)
|
||||
local reg = private.tooltipRegistry[tooltip]
|
||||
if reg.hasItem then
|
||||
return
|
||||
end
|
||||
|
||||
tooltip:Show()
|
||||
local testName, item = tooltip:GetItem()
|
||||
if not item then
|
||||
item = reg.item
|
||||
elseif testName == "" then
|
||||
-- this is likely a case where :GetItem() is broken for recipes - detect and try to fix it
|
||||
if strmatch(item, "item:([0-9]*):") == "" then
|
||||
item = reg.item
|
||||
end
|
||||
end
|
||||
if not item then
|
||||
return
|
||||
end
|
||||
|
||||
private.SetTooltipItem(tooltip, item)
|
||||
end
|
||||
|
||||
function private.OnTooltipSetBattlePet(tooltip, data)
|
||||
local reg = private.tooltipRegistry[tooltip]
|
||||
if reg.hasItem then
|
||||
private.OnTooltipCleared(tooltip)
|
||||
end
|
||||
|
||||
local link = reg.item
|
||||
if not link then
|
||||
-- extract values from data
|
||||
local speciesID = data.speciesID
|
||||
local level = data.level
|
||||
local maxHealth = data.maxHealth
|
||||
local power = data.power
|
||||
local speed = data.speed
|
||||
local battlePetID = data.battlePetID or "0x0000000000000000"
|
||||
local name = data.name
|
||||
local customName = data.customName
|
||||
local breedQuality = data.breedQuality
|
||||
local colorCode = breedQuality == -1 and NORMAL_FONT_COLOR_CODE or (ITEM_QUALITY_COLORS[breedQuality] or ITEM_QUALITY_COLORS[0]).hex
|
||||
link = format("%s|Hbattlepet:%d:%d:%d:%d:%d:%d:%s|h[%s]|h|r", colorCode, speciesID, level, breedQuality, maxHealth, power, speed, battlePetID, customName or name)
|
||||
end
|
||||
|
||||
private.SetTooltipItem(tooltip, link)
|
||||
end
|
||||
|
||||
function private.OnTooltipCleared(tooltip)
|
||||
local reg = private.tooltipRegistry[tooltip]
|
||||
if reg.ignoreOnCleared then return end
|
||||
|
||||
reg.extraTipUsed = nil
|
||||
reg.minWidth = 0
|
||||
reg.quantity = nil
|
||||
reg.hasItem = nil
|
||||
reg.item = nil
|
||||
reg.extraTip:Hide()
|
||||
reg.extraTip.minWidth = 0
|
||||
reg.extraTip.isTop = nil
|
||||
end
|
||||
|
||||
function private.OnBattlePetTooltipShow()
|
||||
local reg = private.tooltipRegistry[BattlePetTooltip]
|
||||
reg.extraTip:Show()
|
||||
end
|
||||
|
||||
function private.SetTooltipItem(tooltip, link)
|
||||
local itemString = ItemString.Get(link)
|
||||
if not private.IsEnabled() or not itemString then
|
||||
return
|
||||
end
|
||||
|
||||
local reg = private.tooltipRegistry[tooltip]
|
||||
local quantity = max(IsShiftKeyDown() and reg.quantity or 1, 1)
|
||||
local isCached = private.builder:_Prepare(itemString, quantity)
|
||||
if not isCached then
|
||||
-- populate all the lines
|
||||
private.populateFunc(private.builder, itemString)
|
||||
end
|
||||
if private.builder:_IsEmpty() then
|
||||
return
|
||||
end
|
||||
reg.hasItem = true
|
||||
local useExtraTip = private.IsBattlePetTooltip(tooltip) or not private.settings.embeddedTooltip
|
||||
|
||||
-- setup the extra tip if necessary
|
||||
if useExtraTip then
|
||||
reg.extraTip:Attach(tooltip)
|
||||
local r, g, b = GetItemQualityColor(ItemInfo.GetQuality(link) or 0)
|
||||
reg.extraTip:AddLine(ItemInfo.GetName(link), r, g, b)
|
||||
end
|
||||
|
||||
-- add all the lines
|
||||
local targetTip = useExtraTip and reg.extraTip or tooltip
|
||||
targetTip:AddLine(" ")
|
||||
for _, left, right, lineColor in private.builder:_LineIterator() do
|
||||
local r, g, b = lineColor:GetFractionalRGBA()
|
||||
if right then
|
||||
targetTip:AddDoubleLine(left, right, r, g, b, r, g, b)
|
||||
else
|
||||
targetTip:AddLine(left, r, g, b)
|
||||
end
|
||||
end
|
||||
|
||||
-- show the tooltip / extra tip as necessary
|
||||
if not private.IsBattlePetTooltip(tooltip) then
|
||||
tooltip:Show()
|
||||
end
|
||||
if useExtraTip then
|
||||
reg.extraTip:Show()
|
||||
end
|
||||
end
|
||||
|
||||
function private.IsEnabled()
|
||||
if not private.settings.enabled then
|
||||
return false
|
||||
elseif private.settings.tooltipShowModifier == "alt" and not IsAltKeyDown() then
|
||||
return false
|
||||
elseif private.settings.tooltipShowModifier == "ctrl" and not IsControlKeyDown() then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Hook Setup Code
|
||||
-- ============================================================================
|
||||
|
||||
do
|
||||
local function PreHookHelper(self, quantityFunc, quantityOffset, ...)
|
||||
private.OnTooltipCleared(self)
|
||||
local reg = private.tooltipRegistry[self]
|
||||
reg.ignoreOnCleared = true
|
||||
if type(quantityFunc) == "number" then
|
||||
reg.quantity = quantityFunc
|
||||
else
|
||||
reg.quantity = select(quantityOffset, quantityFunc(...))
|
||||
end
|
||||
return reg
|
||||
end
|
||||
private.tooltipMethodPrehooks = {
|
||||
SetQuestItem = function(self, ...) PreHookHelper(self, GetQuestItemInfo, 3, ...) end,
|
||||
SetQuestLogItem = function(self, type, ...)
|
||||
local quantityFunc = type == "choice" and GetQuestLogChoiceInfo or GetQuestLogRewardInfo
|
||||
PreHookHelper(self, quantityFunc, 3, ...)
|
||||
end,
|
||||
SetRecipeReagentItem = function(self, ...)
|
||||
local reg = PreHookHelper(self, C_TradeSkillUI.GetRecipeReagentInfo, 3, ...)
|
||||
reg.item = C_TradeSkillUI.GetRecipeReagentItemLink(...)
|
||||
end,
|
||||
SetRecipeResultItem = function(self, ...)
|
||||
private.OnTooltipCleared(self)
|
||||
local reg = private.tooltipRegistry[self]
|
||||
reg.ignoreOnCleared = true
|
||||
local lNum, hNum = C_TradeSkillUI.GetRecipeNumItemsProduced(...)
|
||||
-- the quantity can be a range, so use a quantity of 1 if so
|
||||
reg.quantity = lNum == hNum and lNum or 1
|
||||
end,
|
||||
SetBagItem = function(self, ...) PreHookHelper(self, GetContainerItemInfo, 2, ...) end,
|
||||
SetGuildBankItem = function(self, ...)
|
||||
local reg = PreHookHelper(self, GetGuildBankItemInfo, 2, ...)
|
||||
reg.item = GetGuildBankItemLink(...)
|
||||
end,
|
||||
SetVoidItem = function(self, ...) PreHookHelper(self, 1) end,
|
||||
SetVoidDepositItem = function(self, ...) PreHookHelper(self, 1) end,
|
||||
SetVoidWithdrawalItem = function(self, ...) PreHookHelper(self, 1) end,
|
||||
SetInventoryItem = function(self, ...) PreHookHelper(self, GetInventoryItemCount, 1, ...) end,
|
||||
SetMerchantItem = function(self, ...)
|
||||
local reg = PreHookHelper(self, GetMerchantItemInfo, 4, ...)
|
||||
reg.item = GetMerchantItemLink(...)
|
||||
end,
|
||||
SetMerchantCostItem = function(self, ...) PreHookHelper(self, GetMerchantItemCostItem, 2, ...) end,
|
||||
SetBuybackItem = function(self, ...) PreHookHelper(self, GetBuybackItemInfo, 4, ...) end,
|
||||
SetAuctionItem = function(self, ...)
|
||||
local reg = PreHookHelper(self, GetAuctionItemInfo, 3, ...)
|
||||
reg.item = GetAuctionItemLink(...)
|
||||
end,
|
||||
SetAuctionSellItem = function(self, ...) PreHookHelper(self, GetAuctionSellItemInfo, 3, ...) end,
|
||||
SetInboxItem = function(self, index) PreHookHelper(self, GetInboxItem, 4, index, 1) end,
|
||||
SetSendMailItem = function(self, ...) PreHookHelper(self, GetSendMailItem, 4, ...) end,
|
||||
SetLootItem = function(self, ...) PreHookHelper(self, GetLootSlotInfo, 3, ...) end,
|
||||
SetLootRollItem = function(self, ...) PreHookHelper(self, GetLootRollItemInfo, 3, ...) end,
|
||||
SetTradePlayerItem = function(self, ...) PreHookHelper(self, GetTradePlayerItemInfo, 3, ...) end,
|
||||
SetTradeTargetItem = function(self, ...) PreHookHelper(self, GetTradeTargetItemInfo, 3, ...) end,
|
||||
SetHyperlink = function(self, link)
|
||||
local reg = private.tooltipRegistry[self]
|
||||
private.OnTooltipCleared(self)
|
||||
reg.ignoreOnCleared = true
|
||||
reg.item = link
|
||||
end,
|
||||
}
|
||||
|
||||
-- populate all the posthooks
|
||||
local function TooltipMethodPostHook(self)
|
||||
private.tooltipRegistry[self].ignoreOnCleared = nil
|
||||
end
|
||||
for funcName in pairs(private.tooltipMethodPrehooks) do
|
||||
private.tooltipMethodPosthooks[funcName] = TooltipMethodPostHook
|
||||
end
|
||||
-- SetHyperlink is special
|
||||
private.tooltipMethodPosthooks.SetHyperlink = function(self)
|
||||
local reg = private.tooltipRegistry[self]
|
||||
reg.ignoreOnCleared = nil
|
||||
end
|
||||
end
|
||||
398
LibTSM/Service/MailTracking.lua
Normal file
398
LibTSM/Service/MailTracking.lua
Normal file
@@ -0,0 +1,398 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local MailTracking = TSM.Init("Service.MailTracking")
|
||||
local Database = TSM.Include("Util.Database")
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local AuctionTracking = TSM.Include("Service.AuctionTracking")
|
||||
local private = {
|
||||
settings = nil,
|
||||
mailDB = nil,
|
||||
itemDB = nil,
|
||||
quantityDB = nil,
|
||||
isOpen = false,
|
||||
tooltip = nil,
|
||||
callbacks = {},
|
||||
expiresCallbacks = {},
|
||||
cancelAuctionQuery = nil,
|
||||
}
|
||||
local PLAYER_NAME = UnitName("player")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
MailTracking:OnSettingsLoad(function()
|
||||
private.settings = Settings.NewView()
|
||||
:AddKey("factionrealm", "internalData", "pendingMail")
|
||||
:AddKey("factionrealm", "internalData", "expiringMail")
|
||||
:AddKey("sync", "internalData", "mailQuantity")
|
||||
|
||||
-- update the structure of TSM.db.factionrealm.internalData.pendingMail
|
||||
local toUpdate = TempTable.Acquire()
|
||||
for character, pendingMailData in pairs(private.settings.pendingMail) do
|
||||
if pendingMailData.items then
|
||||
Log.Info("Converting pending mail data for %s", character)
|
||||
toUpdate[character] = pendingMailData.items
|
||||
end
|
||||
end
|
||||
for character, items in pairs(toUpdate) do
|
||||
private.settings.pendingMail[character] = items
|
||||
end
|
||||
TempTable.Release(toUpdate)
|
||||
|
||||
-- remove data for characters we don't own
|
||||
local toRemove = TempTable.Acquire()
|
||||
for character in pairs(private.settings.pendingMail) do
|
||||
if Settings.GetCharacterSyncAccountKey(character) ~= Settings.GetCurrentSyncAccountKey() then
|
||||
Log.Info("Removed pending mail data for %s", character)
|
||||
tinsert(toRemove, character)
|
||||
end
|
||||
end
|
||||
for _, character in ipairs(toRemove) do
|
||||
private.settings.pendingMail[character] = nil
|
||||
end
|
||||
TempTable.Release(toRemove)
|
||||
|
||||
private.mailDB = Database.NewSchema("MAIL_TRACKING_INBOX_INFO")
|
||||
:AddUniqueNumberField("index")
|
||||
:AddStringField("icon")
|
||||
:AddStringField("sender")
|
||||
:AddStringField("subject")
|
||||
:AddStringField("itemString")
|
||||
:AddNumberField("itemCount")
|
||||
:AddNumberField("money")
|
||||
:AddNumberField("cod")
|
||||
:AddNumberField("expires")
|
||||
:AddIndex("index")
|
||||
:Commit()
|
||||
private.itemDB = Database.NewSchema("MAIL_TRACKING_INBOX_ITEMS")
|
||||
:AddNumberField("index")
|
||||
:AddNumberField("itemIndex")
|
||||
:AddStringField("itemLink")
|
||||
:AddNumberField("quantity")
|
||||
:Commit()
|
||||
private.quantityDB = Database.NewSchema("MAIL_TRACKING_QUANTITY")
|
||||
:AddUniqueStringField("itemString")
|
||||
:AddNumberField("quantity")
|
||||
:Commit()
|
||||
|
||||
private.settings.pendingMail[PLAYER_NAME] = private.settings.pendingMail[PLAYER_NAME] or {}
|
||||
Event.Register("MAIL_SHOW", private.MailShowHandler)
|
||||
Event.Register("MAIL_CLOSED", private.MailClosedHandler)
|
||||
Event.Register("MAIL_INBOX_UPDATE", private.MailInboxUpdateHandler)
|
||||
|
||||
if TSM.IsWowClassic() then
|
||||
-- handle auction buying
|
||||
hooksecurefunc("PlaceAuctionBid", function(listType, index, bidPlaced)
|
||||
local itemString = ItemString.GetBase(GetAuctionItemLink(listType, index))
|
||||
local _, _, stackSize, _, _, _, _, _, _, buyout = GetAuctionItemInfo(listType, index)
|
||||
if not itemString or bidPlaced ~= buyout then
|
||||
return
|
||||
end
|
||||
private.ChangePendingMailQuantity(itemString, stackSize)
|
||||
end)
|
||||
|
||||
-- handle auction canceling
|
||||
hooksecurefunc("CancelAuction", function(index)
|
||||
local itemString = ItemString.GetBase(GetAuctionItemLink("owner", index))
|
||||
local _, _, stackSize = GetAuctionItemInfo("owner", index)
|
||||
-- for some reason, these APIs don't always work properly, so check the return values
|
||||
if not itemString or not stackSize or stackSize == 0 then
|
||||
return
|
||||
end
|
||||
private.ChangePendingMailQuantity(itemString, stackSize)
|
||||
end)
|
||||
else
|
||||
private.cancelAuctionQuery = AuctionTracking.CreateQuery()
|
||||
:Equal("auctionId", Database.BoundQueryParam())
|
||||
:Select("itemString", "stackSize")
|
||||
|
||||
-- handle auction canceling
|
||||
hooksecurefunc(C_AuctionHouse, "CancelAuction", function(auctionId)
|
||||
private.cancelAuctionQuery:BindParams(auctionId)
|
||||
for _, itemString, stackSize in private.cancelAuctionQuery:Iterator() do
|
||||
private.ChangePendingMailQuantity(itemString, stackSize)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- handle sending mail to alts
|
||||
hooksecurefunc("SendMail", function(target)
|
||||
local character = private.ValidateCharacter(target)
|
||||
if not character then
|
||||
return
|
||||
end
|
||||
private.settings.pendingMail[character] = private.settings.pendingMail[character] or {}
|
||||
local altPendingMail = private.settings.pendingMail[character]
|
||||
for i = 1, ATTACHMENTS_MAX_SEND do
|
||||
local itemString = ItemString.GetBase(GetSendMailItemLink(i))
|
||||
local _, _, _, quantity = GetSendMailItem(i)
|
||||
if itemString and quantity then
|
||||
altPendingMail[itemString] = (altPendingMail[itemString] or 0) + quantity
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- handle returning mail to alts
|
||||
hooksecurefunc("ReturnInboxItem", function(index)
|
||||
local character = private.ValidateCharacter(select(3, GetInboxHeaderInfo(index)))
|
||||
if not character then
|
||||
return
|
||||
end
|
||||
private.settings.pendingMail[character] = private.settings.pendingMail[character] or {}
|
||||
local altPendingMail = private.settings.pendingMail[character]
|
||||
for i = 1, ATTACHMENTS_MAX_SEND do
|
||||
local _, _, _, quantity = GetInboxItem(index, i)
|
||||
local itemLink = quantity and quantity > 0 and private.GetInboxItemLink(index, i) or nil
|
||||
local itemString = itemLink and ItemString.GetBase(itemLink) or nil
|
||||
if itemString then
|
||||
altPendingMail[itemString] = (altPendingMail[itemString] or 0) + quantity
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
private.quantityDB:BulkInsertStart()
|
||||
for itemString, quantity in pairs(private.settings.pendingMail[PLAYER_NAME]) do
|
||||
if quantity > 0 then
|
||||
private.quantityDB:BulkInsertNewRow(itemString, quantity)
|
||||
else
|
||||
private.settings.pendingMail[PLAYER_NAME][itemString] = nil
|
||||
end
|
||||
end
|
||||
private.quantityDB:BulkInsertEnd()
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function MailTracking.RegisterCallback(callback)
|
||||
tinsert(private.callbacks, callback)
|
||||
end
|
||||
|
||||
function MailTracking.RegisterExpiresCallback(callback)
|
||||
tinsert(private.expiresCallbacks, callback)
|
||||
end
|
||||
|
||||
function MailTracking.BaseItemIterator()
|
||||
return private.quantityDB:NewQuery()
|
||||
:Select("itemString")
|
||||
:IteratorAndRelease()
|
||||
end
|
||||
|
||||
function MailTracking.CreateMailInboxQuery()
|
||||
return private.mailDB:NewQuery()
|
||||
end
|
||||
|
||||
function MailTracking.CreateMailItemQuery()
|
||||
return private.itemDB:NewQuery()
|
||||
end
|
||||
|
||||
function MailTracking.GetInboxItemLink(index)
|
||||
return private.GetInboxItemLink(index, 1)
|
||||
end
|
||||
|
||||
function MailTracking.GetMailType(index)
|
||||
return private.GetMailType(index)
|
||||
end
|
||||
|
||||
function MailTracking.GetQuantityByBaseItemString(baseItemString)
|
||||
return private.quantityDB:GetUniqueRowField("itemString", baseItemString, "quantity") or 0
|
||||
end
|
||||
|
||||
function MailTracking.RecordAuctionBuyout(baseItemString, stackSize)
|
||||
if TSM.IsWowClassic() then
|
||||
-- on classic, we'll handle auction buys via a direct hook
|
||||
return
|
||||
end
|
||||
private.ChangePendingMailQuantity(baseItemString, stackSize)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Event Handlers
|
||||
-- ============================================================================
|
||||
|
||||
function private.MailShowHandler()
|
||||
private.isOpen = true
|
||||
end
|
||||
|
||||
function private.MailClosedHandler()
|
||||
private.isOpen = false
|
||||
end
|
||||
|
||||
function private.MailInboxUpdateHandler()
|
||||
if not private.isOpen then
|
||||
return
|
||||
end
|
||||
|
||||
Delay.AfterFrame("mailInboxScan", 1, private.MailInboxUpdateDelayed)
|
||||
end
|
||||
|
||||
function private.MailInboxUpdateDelayed()
|
||||
if not private.isOpen then
|
||||
return
|
||||
end
|
||||
|
||||
wipe(private.settings.mailQuantity)
|
||||
wipe(private.settings.pendingMail[PLAYER_NAME])
|
||||
private.mailDB:TruncateAndBulkInsertStart()
|
||||
private.itemDB:TruncateAndBulkInsertStart()
|
||||
local expiration = math.huge
|
||||
for i = 1, GetInboxNumItems() do
|
||||
local _, _, sender, subject, money, cod, daysLeft, itemCount = GetInboxHeaderInfo(i)
|
||||
if itemCount and itemCount > 0 and money and money > 0 then
|
||||
expiration = min(expiration, time() + (daysLeft * 24 * 60 * 60))
|
||||
end
|
||||
|
||||
if money and money > 0 then
|
||||
private.itemDB:BulkInsertNewRow(i, 0, tostring(money), 0)
|
||||
end
|
||||
|
||||
local firstItemString = nil
|
||||
for j = 1, ATTACHMENTS_MAX do
|
||||
local _, _, _, quantity = GetInboxItem(i, j)
|
||||
local itemLink = quantity and quantity > 0 and private.GetInboxItemLink(i, j) or nil
|
||||
local itemString = itemLink and ItemString.Get(itemLink) or nil
|
||||
if itemString then
|
||||
firstItemString = firstItemString or itemString
|
||||
local baseItemString = ItemString.GetBaseFast(itemString)
|
||||
private.settings.mailQuantity[baseItemString] = (private.settings.mailQuantity[baseItemString] or 0) + quantity
|
||||
private.itemDB:BulkInsertNewRow(i, j, itemLink, quantity)
|
||||
end
|
||||
end
|
||||
|
||||
local mailType = private.GetMailType(i, firstItemString) or ""
|
||||
if mailType == "BUY" then
|
||||
local _, _, _, bid = GetInboxInvoiceInfo(i)
|
||||
cod = bid
|
||||
end
|
||||
|
||||
private.mailDB:BulkInsertNewRow(i, mailType, sender or UNKNOWN, subject or "--", firstItemString or "", itemCount or 0, money or 0, cod or 0, daysLeft)
|
||||
end
|
||||
private.quantityDB:TruncateAndBulkInsertStart()
|
||||
for itemString, quantity in pairs(private.settings.mailQuantity) do
|
||||
private.quantityDB:BulkInsertNewRow(itemString, quantity)
|
||||
end
|
||||
private.quantityDB:BulkInsertEnd()
|
||||
private.itemDB:BulkInsertEnd()
|
||||
private.mailDB:BulkInsertEnd()
|
||||
|
||||
private.settings.expiringMail[PLAYER_NAME] = expiration ~= math.huge and expiration or nil
|
||||
for _, callback in ipairs(private.expiresCallbacks) do
|
||||
callback()
|
||||
end
|
||||
for _, callback in ipairs(private.callbacks) do
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.ChangePendingMailQuantity(itemString, quantity)
|
||||
assert(quantity ~= 0)
|
||||
private.settings.pendingMail[PLAYER_NAME][itemString] = (private.settings.pendingMail[PLAYER_NAME][itemString] or 0) + quantity
|
||||
if not private.quantityDB:HasUniqueRow("itemString", itemString) then
|
||||
-- create a new row
|
||||
private.quantityDB:NewRow()
|
||||
:SetField("itemString", itemString)
|
||||
:SetField("quantity", quantity)
|
||||
:Create()
|
||||
else
|
||||
local row = private.quantityDB:GetUniqueRow("itemString", itemString)
|
||||
local newValue = row:GetField("quantity") + quantity
|
||||
assert(newValue >= 0)
|
||||
if newValue == 0 then
|
||||
-- remove this row
|
||||
private.quantityDB:DeleteRow(row)
|
||||
else
|
||||
-- update this row
|
||||
row:SetField("quantity", newValue)
|
||||
:Update()
|
||||
end
|
||||
row:Release()
|
||||
end
|
||||
for _, callback in ipairs(private.callbacks) do
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
function private.ValidateCharacter(character)
|
||||
if not character then
|
||||
return
|
||||
end
|
||||
local characterName, realm = strsplit("-", strlower(character))
|
||||
-- we only care to track mails with characters on this realm
|
||||
if realm and realm ~= strlower(GetRealmName()) then
|
||||
return
|
||||
end
|
||||
-- we only care to track mails with characters on this account
|
||||
local result = nil
|
||||
for _, name in Settings.CharacterByAccountFactionrealmIterator() do
|
||||
if strlower(name) == characterName then
|
||||
result = name
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function private.GetInboxItemLink(index, num)
|
||||
local link = GetInboxItemLink(index, num)
|
||||
if ItemString.GetBase(link) ~= ItemString.GetPetCage() then
|
||||
return link
|
||||
end
|
||||
|
||||
-- need to do tooltip scanning to get battlepet links
|
||||
private.tooltip = private.tooltip or CreateFrame("GameTooltip", "TSM4MailingInboxTooltip", UIParent, "GameTooltipTemplate")
|
||||
private.tooltip:SetOwner(UIParent, "ANCHOR_NONE")
|
||||
private.tooltip:ClearLines()
|
||||
|
||||
local _, speciesId, level, breedQuality = private.tooltip:SetInboxItem(index, num)
|
||||
assert(speciesId and speciesId > 0)
|
||||
private.tooltip:Hide()
|
||||
return ItemInfo.GetLink(strjoin(":", "p", speciesId, level, breedQuality))
|
||||
end
|
||||
|
||||
function private.GetMailType(index, firstItemString)
|
||||
local _, _, _, subject, money, cod, _, numItems, _, _, _, _, isGM = GetInboxHeaderInfo(index)
|
||||
if isGM or (cod and cod > 0) or (money == 0 and (not numItems or numItems == 0)) then
|
||||
return nil
|
||||
end
|
||||
|
||||
local info = GetInboxInvoiceInfo(index)
|
||||
if money and money > 0 and info == "seller" then
|
||||
return "SALE"
|
||||
elseif numItems and numItems > 0 and info == "buyer" then
|
||||
return "BUY"
|
||||
elseif not info then
|
||||
if strfind(subject, string.gsub("^"..AUCTION_REMOVED_MAIL_SUBJECT, "%%s", "")) then
|
||||
return "CANCEL"
|
||||
end
|
||||
|
||||
if strfind(subject, string.gsub("^"..AUCTION_EXPIRED_MAIL_SUBJECT, "%%s", "")) then
|
||||
return "EXPIRE"
|
||||
end
|
||||
end
|
||||
|
||||
return "OTHER"
|
||||
end
|
||||
157
LibTSM/Service/PlayerInfo.lua
Normal file
157
LibTSM/Service/PlayerInfo.lua
Normal file
@@ -0,0 +1,157 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- PlayerInfo Functions
|
||||
-- @module PlayerInfo
|
||||
|
||||
local _, TSM = ...
|
||||
local PlayerInfo = TSM.Init("Service.PlayerInfo")
|
||||
local String = TSM.Include("Util.String")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local private = {
|
||||
connectedAlts = {},
|
||||
settings = nil,
|
||||
isPlayerCache = {},
|
||||
}
|
||||
local PLAYER_NAME = UnitName("player")
|
||||
local PLAYER_LOWER = strlower(PLAYER_NAME)
|
||||
local FACTION_LOWER = strlower(UnitFactionGroup("player"))
|
||||
local REALM_LOWER = strlower(GetRealmName())
|
||||
local PLAYER_REALM_LOWER = PLAYER_LOWER.." - "..REALM_LOWER
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
PlayerInfo:OnSettingsLoad(function()
|
||||
private.settings = Settings.NewView()
|
||||
:AddKey("factionrealm", "internalData", "guildVaults")
|
||||
:AddKey("factionrealm", "coreOptions", "ignoreGuilds")
|
||||
:AddKey("factionrealm", "internalData", "characterGuilds")
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Return all connected realm alternative characters.
|
||||
-- @return table The populated alternative characters.
|
||||
function PlayerInfo.GetConnectedAlts()
|
||||
wipe(private.connectedAlts)
|
||||
for factionrealm in TSM.db:GetConnectedRealmIterator("factionrealm") do
|
||||
for _, character in TSM.db:FactionrealmCharacterIterator(factionrealm) do
|
||||
local realm = strmatch(factionrealm, ".+ %- (.+)")
|
||||
character = Ambiguate(gsub(strmatch(character, "(.*) ?"..String.Escape("-").."?").."-"..gsub(realm, String.Escape("-"), ""), " ", ""), "none")
|
||||
if character ~= UnitName("player") then
|
||||
tinsert(private.connectedAlts, character)
|
||||
end
|
||||
end
|
||||
end
|
||||
sort(private.connectedAlts)
|
||||
return private.connectedAlts
|
||||
end
|
||||
|
||||
--- Iterate over all characters on this factionrealm.
|
||||
-- @tparam[opt=false] boolean currentAccountOnly If true, will only include the current account
|
||||
-- @return An iterator with the following fields: `index, name`
|
||||
function PlayerInfo.CharacterIterator(currentAccountOnly)
|
||||
if currentAccountOnly then
|
||||
return Settings.CharacterByAccountFactionrealmIterator()
|
||||
else
|
||||
return Settings.FactionrealmCharacterIterator()
|
||||
end
|
||||
end
|
||||
|
||||
--- Iterate over all guilds on this factionrealm.
|
||||
-- @tparam[opt=false] boolean includeIgnored If true, will include guilds which have been set to be ignored
|
||||
-- @return An iterator with the following fields: `index, guildName`
|
||||
function PlayerInfo.GuildIterator(includeIgnored)
|
||||
if includeIgnored then
|
||||
return private.GuildIteratorIgnoreIncluded, private.settings.guildVaults
|
||||
else
|
||||
return private.GuildIterator, private.settings.guildVaults
|
||||
end
|
||||
end
|
||||
|
||||
--- Get the player's guild.
|
||||
-- @tparam string player The name of the player
|
||||
-- @treturn ?string The name of the player's guilde or nil if it's not in one
|
||||
function PlayerInfo.GetPlayerGuild(player)
|
||||
return player and private.settings.characterGuilds[player] or nil
|
||||
end
|
||||
|
||||
--- Check whether or not a player belongs to the user.
|
||||
-- @tparam string target The name of the player
|
||||
-- @tparam boolean includeAlts Whether or not to include alts
|
||||
-- @tparam boolean includeOtherFaction Whether or not to include players on the other faction
|
||||
-- @tparam boolean includeOtherAccounts Whether or not to include connected accounts
|
||||
-- @treturn boolean Whether or not the player belongs to the user
|
||||
function PlayerInfo.IsPlayer(target, includeAlts, includeOtherFaction, includeOtherAccounts)
|
||||
local cacheKey = strjoin("%", target, includeAlts and "1" or "0", includeOtherFaction and "1" or "0", includeOtherAccounts and "1" or "0")
|
||||
if private.isPlayerCache.lastUpdate ~= GetTime() then
|
||||
wipe(private.isPlayerCache)
|
||||
private.isPlayerCache.lastUpdate = GetTime()
|
||||
end
|
||||
if private.isPlayerCache[cacheKey] == nil then
|
||||
private.isPlayerCache[cacheKey] = private.IsPlayerHelper(target, includeAlts, includeOtherFaction, includeOtherAccounts)
|
||||
end
|
||||
return private.isPlayerCache[cacheKey]
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.IsPlayerHelper(target, includeAlts, includeOtherFaction, includeOtherAccounts)
|
||||
target = strlower(target)
|
||||
if not strfind(target, " %- ") then
|
||||
target = gsub(target, "%-", " - ", 1)
|
||||
end
|
||||
if target == PLAYER_LOWER then
|
||||
return true
|
||||
elseif strfind(target, " %- ") and target == PLAYER_REALM_LOWER then
|
||||
return true
|
||||
end
|
||||
if not strfind(target, " %- ") then
|
||||
target = target.." - "..REALM_LOWER
|
||||
end
|
||||
if includeAlts then
|
||||
local result = false
|
||||
for _, factionrealm, character in Settings.ConnectedFactionrealmAltCharacterIterator() do
|
||||
local factionKey, realm = strmatch(factionrealm, "(.+) %- (.+)")
|
||||
factionKey = strlower(factionKey)
|
||||
if not result and target == strlower(character).." - "..strlower(realm) and (includeOtherFaction or factionKey == FACTION_LOWER) and (includeOtherAccounts or Settings.IsCurrentAccountOwner(character)) then
|
||||
result = true
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function private.GuildIterator(tbl, prevName)
|
||||
while true do
|
||||
local name = next(tbl, prevName)
|
||||
if not name then
|
||||
return nil
|
||||
end
|
||||
if not private.settings.ignoreGuilds[name] then
|
||||
return name
|
||||
end
|
||||
prevName = name
|
||||
end
|
||||
end
|
||||
|
||||
function private.GuildIteratorIgnoreIncluded(tbl, prevName)
|
||||
local name = next(tbl, prevName)
|
||||
return name
|
||||
end
|
||||
1630
LibTSM/Service/Settings.lua
Normal file
1630
LibTSM/Service/Settings.lua
Normal file
File diff suppressed because it is too large
Load Diff
83
LibTSM/Service/SlashCommands.lua
Normal file
83
LibTSM/Service/SlashCommands.lua
Normal file
@@ -0,0 +1,83 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local SlashCommands = TSM.Init("Service.SlashCommands")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local L = TSM.Include("Locale").GetTable()
|
||||
local private = {
|
||||
commandInfo = {},
|
||||
commandOrder = {},
|
||||
}
|
||||
local CALLBACK_TIME_WARNING_MS = 20
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
SlashCommands:OnModuleLoad(function()
|
||||
-- register the TSM slash commands
|
||||
SlashCmdList["TSM"] = private.OnChatCommand
|
||||
SlashCmdList["TRADESKILLMASTER"] = private.OnChatCommand
|
||||
_G["SLASH_TSM1"] = "/tsm"
|
||||
_G["SLASH_TRADESKILLMASTER1"] = "/tradeskillmaster"
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function SlashCommands.Register(key, callback, label)
|
||||
assert(key and callback)
|
||||
local keyLower = strlower(key)
|
||||
private.commandInfo[keyLower] = {
|
||||
key = key,
|
||||
label = label,
|
||||
callback = callback,
|
||||
}
|
||||
tinsert(private.commandOrder, keyLower)
|
||||
end
|
||||
|
||||
function SlashCommands.PrintHelp()
|
||||
Log.PrintUser(L["Slash Commands:"])
|
||||
for _, key in ipairs(private.commandOrder) do
|
||||
local info = private.commandInfo[key]
|
||||
if info.label then
|
||||
if info.key == "" then
|
||||
Log.PrintfUserRaw("|cffffaa00/tsm|r - %s", info.label)
|
||||
else
|
||||
Log.PrintfUserRaw("|cffffaa00/tsm %s|r - %s", info.key, info.label)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.OnChatCommand(input)
|
||||
input = strtrim(input)
|
||||
local cmd, args = strmatch(input, "^([^ ]*) ?(.*)$")
|
||||
cmd = strlower(cmd)
|
||||
if private.commandInfo[cmd] then
|
||||
local startTime = debugprofilestop()
|
||||
private.commandInfo[cmd].callback(args)
|
||||
local timeTaken = debugprofilestop() - startTime
|
||||
if timeTaken > CALLBACK_TIME_WARNING_MS then
|
||||
Log.Warn("Handler for slash command (/tsm%s) took %0.2fms", input ~= "" and " "..input or input, timeTaken)
|
||||
end
|
||||
else
|
||||
-- We weren't able to handle this command so print out the help
|
||||
SlashCommands.PrintHelp()
|
||||
end
|
||||
end
|
||||
57
LibTSM/Service/Sync.lua
Normal file
57
LibTSM/Service/Sync.lua
Normal file
@@ -0,0 +1,57 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Sync = TSM.Init("Service.Sync")
|
||||
local Connection = TSM.Include("Service.SyncClasses.Connection")
|
||||
local RPC = TSM.Include("Service.SyncClasses.RPC")
|
||||
local Mirror = TSM.Include("Service.SyncClasses.Mirror")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Sync.RegisterConnectionChangedCallback(callback)
|
||||
Connection.RegisterConnectionChangedCallback(callback)
|
||||
end
|
||||
|
||||
function Sync.RegisterRPC(name, func)
|
||||
RPC.Register(name, func)
|
||||
end
|
||||
|
||||
function Sync.CallRPC(name, targetPlayer, handler, ...)
|
||||
return RPC.Call(name, targetPlayer, handler, ...)
|
||||
end
|
||||
|
||||
function Sync.GetConnectionStatus(account)
|
||||
return Connection.GetStatus(account)
|
||||
end
|
||||
|
||||
function Sync.GetConnectedCharacterByAccount(account)
|
||||
return Connection.GetConnectedCharacterByAccount(account)
|
||||
end
|
||||
|
||||
function Sync.GetMirrorStatus(account)
|
||||
return Mirror.GetStatus(account)
|
||||
end
|
||||
|
||||
function Sync.RegisterMirrorCallback(callback)
|
||||
Mirror.RegisterCallback(callback)
|
||||
end
|
||||
|
||||
function Sync.EstablishConnection(character)
|
||||
return Connection.Establish(character)
|
||||
end
|
||||
|
||||
function Sync.GetNewAccountStatus()
|
||||
return Connection.GetNewAccountStatus()
|
||||
end
|
||||
|
||||
function Sync.RemoveAccount(account)
|
||||
Connection.Remove(account)
|
||||
end
|
||||
134
LibTSM/Service/SyncClasses/Comm.lua
Normal file
134
LibTSM/Service/SyncClasses/Comm.lua
Normal file
@@ -0,0 +1,134 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Comm = TSM.Init("Service.SyncClasses.Comm")
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local Table = TSM.Include("Util.Table")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local Constants = TSM.Include("Service.SyncClasses.Constants")
|
||||
local private = {
|
||||
handler = {},
|
||||
queuedPacket = {},
|
||||
queuedSourceCharacter = {},
|
||||
}
|
||||
-- load libraries
|
||||
LibStub("AceComm-3.0"):Embed(Comm)
|
||||
local LibSerialize = LibStub("LibSerialize")
|
||||
local LibDeflate = LibStub("LibDeflate")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Comm:OnModuleLoad(function()
|
||||
Comm:RegisterComm("TSMSyncData", private.OnCommReceived)
|
||||
end)
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Comm.RegisterHandler(dataType, handler)
|
||||
assert(Table.KeyByValue(Constants.DATA_TYPES, dataType) ~= nil)
|
||||
assert(not private.handler[dataType])
|
||||
private.handler[dataType] = handler
|
||||
end
|
||||
|
||||
function Comm.SendData(dataType, targetCharacter, data)
|
||||
assert(type(dataType) == "string" and #dataType == 1)
|
||||
local packet = TempTable.Acquire()
|
||||
packet.dt = dataType
|
||||
packet.sa = Settings.GetCurrentSyncAccountKey()
|
||||
packet.v = Constants.VERSION
|
||||
packet.d = data
|
||||
local serialized = LibSerialize:Serialize(packet)
|
||||
TempTable.Release(packet)
|
||||
local compressed = LibDeflate:EncodeForWoWAddonChannel(LibDeflate:CompressDeflate(serialized))
|
||||
assert(LibDeflate:DecompressDeflate(LibDeflate:DecodeForWoWAddonChannel(compressed)) == serialized)
|
||||
|
||||
-- give heartbeats and rpc preambles a higher priority
|
||||
local priority = (dataType == Constants.DATA_TYPES.HEARTBEAT or dataType == Constants.DATA_TYPES.RPC_PREAMBLE) and "ALERT" or nil
|
||||
-- send the message
|
||||
Comm:SendCommMessage("TSMSyncData", compressed, "WHISPER", targetCharacter, priority)
|
||||
return #compressed
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.OnCommReceived(_, packet, _, sourceCharacter)
|
||||
-- delay the processing to make sure it happens within a debuggable context (this function is called via pcall)
|
||||
tinsert(private.queuedPacket, packet)
|
||||
tinsert(private.queuedSourceCharacter, sourceCharacter)
|
||||
Delay.AfterFrame("commReceiveQueue", 0, private.ProcessReceiveQueue)
|
||||
end
|
||||
|
||||
function private.ProcessReceiveQueue()
|
||||
assert(#private.queuedPacket == #private.queuedSourceCharacter)
|
||||
while #private.queuedPacket > 0 do
|
||||
local packet = tremove(private.queuedPacket, 1)
|
||||
local sourceCharacter = tremove(private.queuedSourceCharacter, 1)
|
||||
private.ProcessReceivedPacket(packet, sourceCharacter)
|
||||
end
|
||||
end
|
||||
|
||||
function private.ProcessReceivedPacket(msg, sourceCharacter)
|
||||
-- remove realm name from source player
|
||||
sourceCharacter = strsplit("-", sourceCharacter)
|
||||
sourceCharacter = strtrim(sourceCharacter)
|
||||
local sourceCharacterAccountKey = Settings.GetCharacterSyncAccountKey(sourceCharacter)
|
||||
if sourceCharacterAccountKey and sourceCharacterAccountKey == Settings.GetCurrentSyncAccountKey() then
|
||||
Log.Err("We own the source character")
|
||||
Settings.ShowSyncSVCopyError()
|
||||
return
|
||||
end
|
||||
|
||||
-- decode and decompress
|
||||
msg = LibDeflate:DecompressDeflate(LibDeflate:DecodeForWoWAddonChannel(msg))
|
||||
if not msg then
|
||||
Log.Err("Invalid packet")
|
||||
return
|
||||
end
|
||||
local success, packet = LibSerialize:Deserialize(msg)
|
||||
if not success then
|
||||
Log.Err("Invalid packet")
|
||||
return
|
||||
end
|
||||
|
||||
-- validate the packet
|
||||
local dataType = packet.dt
|
||||
local sourceAccount = packet.sa
|
||||
local version = packet.v
|
||||
local data = packet.d
|
||||
if type(dataType) ~= "string" or #dataType > 1 or not sourceAccount or version ~= Constants.VERSION then
|
||||
Log.Info("Invalid message received")
|
||||
return
|
||||
elseif sourceAccount == Settings.GetCurrentSyncAccountKey() then
|
||||
Log.Err("We are the source account (SV copy)")
|
||||
Settings.ShowSyncSVCopyError()
|
||||
return
|
||||
elseif sourceCharacterAccountKey and sourceCharacterAccountKey ~= sourceAccount then
|
||||
-- the source player now belongs to a different account than what we expect
|
||||
Log.Err("Unexpected source account")
|
||||
Settings.ShowSyncSVCopyError()
|
||||
return
|
||||
end
|
||||
|
||||
if private.handler[dataType] then
|
||||
private.handler[dataType](dataType, sourceAccount, sourceCharacter, data)
|
||||
else
|
||||
Log.Info("Received unhandled message of type: "..strbyte(dataType))
|
||||
end
|
||||
end
|
||||
443
LibTSM/Service/SyncClasses/Connection.lua
Normal file
443
LibTSM/Service/SyncClasses/Connection.lua
Normal file
@@ -0,0 +1,443 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Connection = TSM.Init("Service.SyncClasses.Connection")
|
||||
local L = TSM.Include("Locale").GetTable()
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local Threading = TSM.Include("Service.Threading")
|
||||
local Constants = TSM.Include("Service.SyncClasses.Constants")
|
||||
local Comm = TSM.Include("Service.SyncClasses.Comm")
|
||||
local private = {
|
||||
isActive = false,
|
||||
hasFriendsInfo = false,
|
||||
newCharacter = nil,
|
||||
newAccount = nil,
|
||||
newSyncAcked = nil,
|
||||
connectionChangedCallbacks = {},
|
||||
threadId = {},
|
||||
threadRunning = {},
|
||||
connectedCharacter = {},
|
||||
lastHeartbeat = {},
|
||||
suppressThreadTime = {},
|
||||
connectionRequestReceived = {},
|
||||
addedFriends = {},
|
||||
invalidCharacters = {},
|
||||
}
|
||||
local RECEIVE_TIMEOUT = 5
|
||||
local HEARTBEAT_TIMEOUT = 10
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Connection:OnSettingsLoad(function()
|
||||
Event.Register("CHAT_MSG_SYSTEM", private.ChatMsgSystemEventHandler)
|
||||
Event.Register("FRIENDLIST_UPDATE", private.PrepareFriendsInfo)
|
||||
for _ in Settings.SyncAccountIterator() do
|
||||
private.isActive = true
|
||||
end
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.WHOAMI_ACCOUNT, private.WhoAmIAccountHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.WHOAMI_ACK, private.WhoAmIAckHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.CONNECTION_REQUEST, private.ConnectionHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.CONNECTION_REQUEST_ACK, private.ConnectionHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.DISCONNECT, private.DisconnectHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.HEARTBEAT, private.HeartbeatHandler)
|
||||
|
||||
private.PrepareFriendsInfo()
|
||||
end)
|
||||
|
||||
Connection:OnModuleUnload(function()
|
||||
for _, player in pairs(private.connectedCharacter) do
|
||||
Comm.SendData(Constants.DATA_TYPES.DISCONNECT, player)
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Connection.RegisterConnectionChangedCallback(handler)
|
||||
tinsert(private.connectionChangedCallbacks, handler)
|
||||
end
|
||||
|
||||
function Connection.IsCharacterConnected(targetCharacter)
|
||||
for _, player in pairs(private.connectedCharacter) do
|
||||
if player == targetCharacter then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function Connection.ConnectedAccountIterator()
|
||||
return pairs(private.connectedCharacter)
|
||||
end
|
||||
|
||||
function Connection.Establish(targetCharacter)
|
||||
if not private.hasFriendsInfo then
|
||||
Log.PrintUser(L["TSM is not yet ready to establish a new sync connection. Please try again later."])
|
||||
return false
|
||||
end
|
||||
local wasFriend = C_FriendList.GetFriendInfo(targetCharacter) and true or false
|
||||
if strlower(targetCharacter) == strlower(UnitName("player")) then
|
||||
Log.PrintUser(L["Sync Setup Error: You entered the name of the current character and not the character on the other account."])
|
||||
return false
|
||||
elseif not private.IsOnline(targetCharacter) and wasFriend then
|
||||
Log.PrintUser(L["Sync Setup Error: The specified player on the other account is not currently online."])
|
||||
return false
|
||||
end
|
||||
local invalidCharacter = false
|
||||
for _, player in Settings.CharacterByFactionrealmIterator() do
|
||||
if strlower(player) == strlower(targetCharacter) then
|
||||
invalidCharacter = true
|
||||
end
|
||||
end
|
||||
if invalidCharacter then
|
||||
Log.PrintUser(L["Sync Setup Error: This character is already part of a known account."])
|
||||
return false
|
||||
end
|
||||
if not private.isActive then
|
||||
private.isActive = true
|
||||
Delay.AfterTime("SYNC_CONNECTION_MANAGEMENT", 1, private.ManagementLoop, 1)
|
||||
end
|
||||
private.newCharacter = targetCharacter
|
||||
private.newAccount = nil
|
||||
private.newSyncAcked = nil
|
||||
Delay.Cancel("syncNewAccount")
|
||||
Delay.AfterTime("syncNewAccount", 0, private.SendNewAccountWhoAmI, 1)
|
||||
return true
|
||||
end
|
||||
|
||||
function Connection.GetNewAccountStatus()
|
||||
if not private.newCharacter then
|
||||
return nil
|
||||
end
|
||||
return format(L["Connecting to %s"], private.newCharacter)
|
||||
end
|
||||
|
||||
function Connection.GetStatus(account)
|
||||
if private.connectedCharacter[account] then
|
||||
return true, private.connectedCharacter[account]
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function Connection.Remove(account)
|
||||
if private.threadRunning[account] then
|
||||
Threading.Kill(private.threadId[account])
|
||||
private.ConnectionThreadDone(account)
|
||||
end
|
||||
Settings.RemoveSyncAccount(account)
|
||||
end
|
||||
|
||||
function Connection.GetConnectedCharacterByAccount(account)
|
||||
return private.connectedCharacter[account]
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Message Handlers
|
||||
-- ============================================================================
|
||||
|
||||
function private.WhoAmIAckHandler(dataType, sourceAccount, sourceCharacter, data)
|
||||
assert(dataType == Constants.DATA_TYPES.WHOAMI_ACK)
|
||||
if not private.newCharacter or strlower(private.newCharacter) ~= strlower(sourceCharacter) then
|
||||
-- we aren't trying to connect with a new account
|
||||
return
|
||||
end
|
||||
Log.Info("WHOAMI_ACK '%s'", tostring(private.newCharacter))
|
||||
private.newSyncAcked = true
|
||||
private.CheckNewAccountStatus()
|
||||
end
|
||||
|
||||
function private.WhoAmIAccountHandler(dataType, sourceAccount, sourceCharacter, data)
|
||||
assert(dataType == Constants.DATA_TYPES.WHOAMI_ACCOUNT)
|
||||
if not private.newCharacter then
|
||||
-- we aren't trying to connect with a new account
|
||||
return
|
||||
elseif strlower(private.newCharacter) ~= strlower(sourceCharacter) then
|
||||
Log.Info("WHOAMI_ACCOUNT from unknown player \"%s\", expected \"%s\"", private.newCharacter, sourceCharacter)
|
||||
return
|
||||
end
|
||||
private.newCharacter = sourceCharacter -- get correct capatilization
|
||||
private.newAccount = sourceAccount
|
||||
Log.Info("WHOAMI_ACCOUNT '%s' '%s'", private.newCharacter, private.newAccount)
|
||||
Comm.SendData(Constants.DATA_TYPES.WHOAMI_ACK, private.newCharacter)
|
||||
private.CheckNewAccountStatus()
|
||||
end
|
||||
|
||||
function private.ConnectionHandler(dataType, sourceAccount, sourceCharacter, data)
|
||||
if not private.threadRunning[sourceAccount] then
|
||||
return
|
||||
end
|
||||
private.connectionRequestReceived[sourceAccount] = true
|
||||
end
|
||||
|
||||
function private.DisconnectHandler(dataType, sourceAccount, sourceCharacter, data)
|
||||
assert(dataType == Constants.DATA_TYPES.DISCONNECT)
|
||||
if not private.threadRunning[sourceAccount] then
|
||||
return
|
||||
end
|
||||
|
||||
-- kill the thread and prevent it from running again for 2 seconds
|
||||
Threading.Kill(private.threadId[sourceAccount])
|
||||
private.ConnectionThreadDone(sourceAccount)
|
||||
private.suppressThreadTime[sourceAccount] = time() + 2
|
||||
end
|
||||
|
||||
function private.HeartbeatHandler(dataType, sourceAccount, sourceCharacter)
|
||||
assert(dataType == Constants.DATA_TYPES.HEARTBEAT)
|
||||
if not Connection.IsCharacterConnected(sourceCharacter) then
|
||||
-- we're not connected to this player
|
||||
return
|
||||
end
|
||||
private.lastHeartbeat[sourceAccount] = time()
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Management Loop / Sync Thread
|
||||
-- ============================================================================
|
||||
|
||||
function private.RequestFriendsInfo()
|
||||
C_FriendList.ShowFriends()
|
||||
end
|
||||
|
||||
function private.PrepareFriendsInfo()
|
||||
-- wait for friend info to populate
|
||||
local isValid
|
||||
local num = C_FriendList.GetNumFriends()
|
||||
if not num then
|
||||
isValid = false
|
||||
else
|
||||
isValid = true
|
||||
end
|
||||
for i = 1, num or 0 do
|
||||
if not C_FriendList.GetFriendInfoByIndex(i) then
|
||||
isValid = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if isValid then
|
||||
if not private.hasFriendsInfo and private.isActive then
|
||||
-- start the management loop
|
||||
Delay.AfterTime("SYNC_CONNECTION_MANAGEMENT", 1, private.ManagementLoop, 1)
|
||||
end
|
||||
private.hasFriendsInfo = true
|
||||
else
|
||||
-- try again
|
||||
Log.Err("Missing friends info - will try again")
|
||||
Delay.AfterTime("SYNC_PREPARE_FRIENDS_INFO", 0.5, private.RequestFriendsInfo)
|
||||
end
|
||||
end
|
||||
|
||||
function private.ManagementLoop()
|
||||
-- continuously spawn connection threads with online players as necessary
|
||||
private.RequestFriendsInfo()
|
||||
local hasAccount = false
|
||||
for _, account in Settings.SyncAccountIterator() do
|
||||
hasAccount = true
|
||||
local targetCharacter = private.GetTargetCharacter(account)
|
||||
if targetCharacter then
|
||||
if not private.threadId[account] then
|
||||
private.threadId[account] = Threading.New("SYNC_"..strmatch(account, "(%d+)$"), private.ConnectionThread)
|
||||
end
|
||||
if not private.threadRunning[account] and (private.suppressThreadTime[account] or 0) < time() then
|
||||
private.threadRunning[account] = true
|
||||
Threading.Start(private.threadId[account], account, targetCharacter)
|
||||
end
|
||||
end
|
||||
end
|
||||
if not hasAccount then
|
||||
Log.Info("No more sync accounts.")
|
||||
private.isActive = false
|
||||
if not private.newCharacter then
|
||||
Delay.Cancel("SYNC_CONNECTION_MANAGEMENT")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function private.ConnectionThreadInner(account, targetCharacter)
|
||||
-- for the initial handshake, the lower account key is the server, other is the client - after this it doesn't matter
|
||||
-- add some randomness to the timeout so we don't get stuck in a race condition
|
||||
local timeout = GetTime() + RECEIVE_TIMEOUT + random(0, 1000) / 1000
|
||||
if account < Settings.GetCurrentSyncAccountKey() then
|
||||
-- wait for the connection request from the client
|
||||
while not private.connectionRequestReceived[account] do
|
||||
if GetTime() > timeout then
|
||||
-- timed out on the connection - don't try again for a bit
|
||||
Log.Warn("Timed out")
|
||||
return
|
||||
end
|
||||
Threading.Yield(true)
|
||||
end
|
||||
-- send an connection request ACK back to the client
|
||||
Comm.SendData(Constants.DATA_TYPES.CONNECTION_REQUEST_ACK, targetCharacter)
|
||||
else
|
||||
-- send a connection request to the server
|
||||
Comm.SendData(Constants.DATA_TYPES.CONNECTION_REQUEST, targetCharacter)
|
||||
-- wait for the connection request ACK
|
||||
while not private.connectionRequestReceived[account] do
|
||||
if GetTime() > timeout then
|
||||
-- timed out on the connection - don't try again for a bit
|
||||
Log.Warn("Timed out")
|
||||
private.suppressThreadTime[account] = time() + RECEIVE_TIMEOUT
|
||||
return
|
||||
end
|
||||
Threading.Yield(true)
|
||||
end
|
||||
end
|
||||
|
||||
-- we are now connected
|
||||
Log.Info("Connected to: %s %s", account, targetCharacter)
|
||||
private.connectedCharacter[account] = targetCharacter
|
||||
private.lastHeartbeat[account] = time()
|
||||
for _, callback in ipairs(private.connectionChangedCallbacks) do
|
||||
callback(account, targetCharacter, true)
|
||||
end
|
||||
|
||||
-- now that we are connected, data can flow in both directions freely
|
||||
local lastHeartbeatSend = time()
|
||||
while true do
|
||||
-- check if they either logged off or the heartbeats have timed-out
|
||||
if not private.IsOnline(targetCharacter, true) or time() - private.lastHeartbeat[account] > HEARTBEAT_TIMEOUT then
|
||||
return
|
||||
end
|
||||
|
||||
-- check if we should send a heartbeat
|
||||
if time() - lastHeartbeatSend > floor(HEARTBEAT_TIMEOUT / 2) then
|
||||
Comm.SendData(Constants.DATA_TYPES.HEARTBEAT, targetCharacter)
|
||||
lastHeartbeatSend = time()
|
||||
end
|
||||
|
||||
Threading.Yield(true)
|
||||
end
|
||||
end
|
||||
|
||||
function private.ConnectionThread(account, targetCharacter)
|
||||
private.ConnectionThreadInner(account, targetCharacter)
|
||||
private.ConnectionThreadDone(account)
|
||||
end
|
||||
|
||||
function private.ConnectionThreadDone(account)
|
||||
Log.Info("Connection ended to %s", account)
|
||||
local player = private.connectedCharacter[account]
|
||||
private.connectedCharacter[account] = nil
|
||||
if player then
|
||||
for _, callback in ipairs(private.connectionChangedCallbacks) do
|
||||
callback(account, player, false)
|
||||
end
|
||||
end
|
||||
private.threadRunning[account] = nil
|
||||
private.connectionRequestReceived[account] = nil
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.SendNewAccountWhoAmI()
|
||||
if not private.newCharacter then
|
||||
Delay.Cancel("syncNewAccount")
|
||||
elseif not C_FriendList.GetFriendInfo(private.newCharacter) then
|
||||
Log.Info("Waiting for friends list to update")
|
||||
elseif not private.IsOnline(private.newCharacter) then
|
||||
Delay.Cancel("syncNewAccount")
|
||||
private.newCharacter = nil
|
||||
private.newAccount = nil
|
||||
private.newSyncAcked = nil
|
||||
Log.Err("New player went offline")
|
||||
else
|
||||
Comm.SendData(Constants.DATA_TYPES.WHOAMI_ACCOUNT, private.newCharacter)
|
||||
Log.Info("Sent WHOAMI_ACCOUNT")
|
||||
end
|
||||
end
|
||||
|
||||
function private.CheckNewAccountStatus()
|
||||
if not private.newCharacter or not private.newAccount or not private.newSyncAcked then
|
||||
return
|
||||
end
|
||||
Log.Info("New sync character: '%s' '%s'", private.newCharacter, private.newAccount)
|
||||
-- the other account ACK'd so setup a connection
|
||||
Settings.NewSyncCharacter(private.newAccount, private.newCharacter)
|
||||
|
||||
-- call the callbacks for this new account
|
||||
for _, callback in ipairs(private.connectionChangedCallbacks) do
|
||||
callback(private.newAccount, private.newCharacter, nil)
|
||||
end
|
||||
|
||||
private.newCharacter = nil
|
||||
private.newAccount = nil
|
||||
private.newSyncAcked = nil
|
||||
end
|
||||
|
||||
function private.GetTargetCharacter(account)
|
||||
local tempTbl = TempTable.Acquire()
|
||||
for _, character in Settings.CharacterByAccountFactionrealmIterator(account) do
|
||||
tinsert(tempTbl, character)
|
||||
end
|
||||
|
||||
-- find the player to connect to without adding to the friends list
|
||||
for _, player in ipairs(tempTbl) do
|
||||
if private.IsOnline(player, true) then
|
||||
TempTable.Release(tempTbl)
|
||||
return player
|
||||
end
|
||||
end
|
||||
-- if we failed, try again with adding to friends list
|
||||
for _, player in ipairs(tempTbl) do
|
||||
if private.IsOnline(player) then
|
||||
TempTable.Release(tempTbl)
|
||||
return player
|
||||
end
|
||||
end
|
||||
TempTable.Release(tempTbl)
|
||||
end
|
||||
|
||||
function private.IsOnline(target, noAdd)
|
||||
local info = C_FriendList.GetFriendInfo(target)
|
||||
if not info and not noAdd and not private.invalidCharacters[strlower(target)] and C_FriendList.GetNumFriends() ~= 50 then
|
||||
-- add them as a friend
|
||||
C_FriendList.AddFriend(target)
|
||||
private.RequestFriendsInfo()
|
||||
tinsert(private.addedFriends, target)
|
||||
info = C_FriendList.GetFriendInfo(target)
|
||||
end
|
||||
return info and info.connected or false
|
||||
end
|
||||
|
||||
function private.ChatMsgSystemEventHandler(_, msg)
|
||||
if #private.addedFriends == 0 then
|
||||
return
|
||||
end
|
||||
if msg == ERR_FRIEND_NOT_FOUND then
|
||||
if #private.addedFriends > 0 then
|
||||
private.invalidCharacters[strlower(tremove(private.addedFriends, 1))] = true
|
||||
end
|
||||
else
|
||||
for i, v in ipairs(private.addedFriends) do
|
||||
if format(ERR_FRIEND_ADDED_S, v) == msg then
|
||||
tremove(private.addedFriends, i)
|
||||
private.invalidCharacters[strlower(v)] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
29
LibTSM/Service/SyncClasses/Constants.lua
Normal file
29
LibTSM/Service/SyncClasses/Constants.lua
Normal file
@@ -0,0 +1,29 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Constants = TSM.Init("Service.SyncClasses.Constants")
|
||||
Constants.VERSION = 11
|
||||
Constants.DATA_TYPES = {
|
||||
-- new connection types (40-49)
|
||||
WHOAMI_ACCOUNT = strchar(40),
|
||||
WHOAMI_ACK = strchar(41),
|
||||
-- connection status types (50-69)
|
||||
CONNECTION_REQUEST = strchar(50),
|
||||
CONNECTION_REQUEST_ACK = strchar(51),
|
||||
DISCONNECT = strchar(52),
|
||||
HEARTBEAT = strchar(53),
|
||||
-- data mirroring types (70-99)
|
||||
CHARACTER_HASHES_BROADCAST = strchar(70),
|
||||
CHARACTER_SETTING_HASHES_REQUEST = strchar(71),
|
||||
CHARACTER_SETTING_HASHES_RESPONSE = strchar(72),
|
||||
CHARACTER_SETTING_DATA_REQUEST = strchar(73),
|
||||
CHARACTER_SETTING_DATA_RESPONSE = strchar(74),
|
||||
-- RPC types (100-109)
|
||||
RPC_CALL = strchar(100),
|
||||
RPC_RETURN = strchar(101),
|
||||
RPC_PREAMBLE = strchar(102),
|
||||
}
|
||||
268
LibTSM/Service/SyncClasses/Mirror.lua
Normal file
268
LibTSM/Service/SyncClasses/Mirror.lua
Normal file
@@ -0,0 +1,268 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Mirror = TSM.Init("Service.SyncClasses.Mirror")
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local Constants = TSM.Include("Service.SyncClasses.Constants")
|
||||
local Comm = TSM.Include("Service.SyncClasses.Comm")
|
||||
local Connection = TSM.Include("Service.SyncClasses.Connection")
|
||||
local private = {
|
||||
numConnected = 0,
|
||||
accountStatus = {},
|
||||
callbacks = {},
|
||||
}
|
||||
local BROADCAST_INTERVAL = 3
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Mirror:OnModuleLoad(function()
|
||||
Connection.RegisterConnectionChangedCallback(private.ConnectionChangedHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.CHARACTER_HASHES_BROADCAST, private.CharacterHashesBroadcastHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.CHARACTER_SETTING_HASHES_REQUEST, private.CharacterSettingHashesRequestHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.CHARACTER_SETTING_HASHES_RESPONSE, private.CharacterSettingHashesResponseHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.CHARACTER_SETTING_DATA_REQUEST, private.CharacterSettingDataRequestHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.CHARACTER_SETTING_DATA_RESPONSE, private.CharacterSettingDataResponseHandler)
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Mirror.GetStatus(account)
|
||||
local status = private.accountStatus[account]
|
||||
if not status then
|
||||
return false, false
|
||||
elseif status == "UPDATING" then
|
||||
return true, false
|
||||
elseif status == "SYNCED" then
|
||||
return true, true
|
||||
else
|
||||
error("Invalid status: "..tostring(status))
|
||||
end
|
||||
end
|
||||
|
||||
function Mirror.RegisterCallback(callback)
|
||||
tinsert(private.callbacks, callback)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Connection Callback Handlers
|
||||
-- ============================================================================
|
||||
|
||||
function private.ConnectionChangedHandler(account, player, connected)
|
||||
if connected == nil then
|
||||
-- new account, but not yet connected
|
||||
return
|
||||
end
|
||||
if connected then
|
||||
Log.Info("Connected to %s (%s)", account, player)
|
||||
else
|
||||
Log.Info("Disconnected from %s (%s)", account, player)
|
||||
end
|
||||
private.numConnected = private.numConnected + (connected and 1 or -1)
|
||||
assert(private.numConnected >= 0)
|
||||
if connected then
|
||||
private.accountStatus[account] = "UPDATING"
|
||||
Delay.AfterTime("mirrorCharacterHashes", 0, private.SendCharacterHashes, BROADCAST_INTERVAL)
|
||||
else
|
||||
private.accountStatus[account] = nil
|
||||
if private.numConnected == 0 then
|
||||
Delay.Cancel("mirrorCharacterHashes")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Delay-Based Last Update Send Function
|
||||
-- ============================================================================
|
||||
|
||||
function private.SendCharacterHashes()
|
||||
assert(private.numConnected > 0)
|
||||
|
||||
-- calculate the hashes of the sync settings for all characters on this account
|
||||
local hashes = TempTable.Acquire()
|
||||
for _, character in Settings.CharacterByAccountFactionrealmIterator() do
|
||||
hashes[character] = private.CalculateCharacterHash(character)
|
||||
end
|
||||
|
||||
-- send the hashes to all connected accounts
|
||||
for _, character in Connection.ConnectedAccountIterator() do
|
||||
Comm.SendData(Constants.DATA_TYPES.CHARACTER_HASHES_BROADCAST, character, hashes)
|
||||
end
|
||||
TempTable.Release(hashes)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Message Handlers
|
||||
-- ============================================================================
|
||||
|
||||
function private.CharacterHashesBroadcastHandler(dataType, sourceAccount, sourcePlayer, data)
|
||||
assert(dataType == Constants.DATA_TYPES.CHARACTER_HASHES_BROADCAST)
|
||||
if not Connection.IsCharacterConnected(sourcePlayer) then
|
||||
-- we're not connected to this player
|
||||
Log.Warn("Got CHARACTER_HASHES_BROADCAST for player which isn't connected")
|
||||
return
|
||||
end
|
||||
local didChange = false
|
||||
for _, character in Settings.CharacterByAccountFactionrealmIterator(sourceAccount) do
|
||||
if not data[character] then
|
||||
-- this character doesn't exist anymore, so remove it
|
||||
Log.Info("Removed character: '%s'", character)
|
||||
Settings.RemoveSyncCharacter(character)
|
||||
didChange = true
|
||||
end
|
||||
end
|
||||
for character, hash in pairs(data) do
|
||||
if not Settings.GetCharacterSyncAccountKey(character) then
|
||||
-- this is a new character, so add it to our DB
|
||||
Log.Info("New character: '%s' '%s'", character, sourceAccount)
|
||||
Settings.NewSyncCharacter(sourceAccount, character)
|
||||
didChange = true
|
||||
end
|
||||
if hash ~= private.CalculateCharacterHash(character) then
|
||||
-- this character's data has changed so request a hash of each of the keys
|
||||
Log.Info("Character data has changed: '%s'", character)
|
||||
Comm.SendData(Constants.DATA_TYPES.CHARACTER_SETTING_HASHES_REQUEST, sourcePlayer, character)
|
||||
didChange = true
|
||||
end
|
||||
end
|
||||
if didChange then
|
||||
private.accountStatus[sourceAccount] = "UPDATING"
|
||||
else
|
||||
private.accountStatus[sourceAccount] = "SYNCED"
|
||||
end
|
||||
end
|
||||
|
||||
function private.CharacterSettingHashesRequestHandler(dataType, sourceAccount, sourcePlayer, data)
|
||||
assert(dataType == Constants.DATA_TYPES.CHARACTER_SETTING_HASHES_REQUEST)
|
||||
if not Connection.IsCharacterConnected(sourcePlayer) then
|
||||
-- we're not connected to this player
|
||||
Log.Warn("Got CHARACTER_HASHES_BROADCAST for player which isn't connected")
|
||||
return
|
||||
elseif Settings.GetCharacterSyncAccountKey(data) ~= Settings.GetCurrentSyncAccountKey() then
|
||||
-- we don't own this character
|
||||
Log.Err("Request for character we don't own ('%s', '%s')", tostring(data), tostring(Settings.GetCharacterSyncAccountKey(data)))
|
||||
return
|
||||
end
|
||||
Log.Info("CHARACTER_SETTING_HASHES_REQUEST (%s)", data)
|
||||
local responseData = TempTable.Acquire()
|
||||
responseData._character = data
|
||||
for _, namespace, settingKey in Settings.SyncSettingIterator() do
|
||||
responseData[namespace.."."..settingKey] = private.CalculateCharacterSettingHash(data, namespace, settingKey)
|
||||
end
|
||||
Comm.SendData(Constants.DATA_TYPES.CHARACTER_SETTING_HASHES_RESPONSE, sourcePlayer, responseData)
|
||||
TempTable.Release(responseData)
|
||||
end
|
||||
|
||||
function private.CharacterSettingHashesResponseHandler(dataType, sourceAccount, sourcePlayer, data)
|
||||
assert(dataType == Constants.DATA_TYPES.CHARACTER_SETTING_HASHES_RESPONSE)
|
||||
if not Connection.IsCharacterConnected(sourcePlayer) then
|
||||
-- we're not connected to this player
|
||||
Log.Warn("Got CHARACTER_HASHES_BROADCAST for player which isn't connected")
|
||||
return
|
||||
end
|
||||
local character = data._character
|
||||
data._character = nil
|
||||
Log.Info("CHARACTER_SETTING_HASHES_RESPONSE (%s)", character)
|
||||
for key, hash in pairs(data) do
|
||||
local namespace, settingKey = strsplit(".", key)
|
||||
if private.CalculateCharacterSettingHash(character, namespace, settingKey) ~= hash then
|
||||
-- the settings data for key changed, so request the latest data for it
|
||||
Log.Info("Setting data has changed: '%s', '%s'", character, key)
|
||||
Comm.SendData(Constants.DATA_TYPES.CHARACTER_SETTING_DATA_REQUEST, sourcePlayer, character.."."..key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function private.CharacterSettingDataRequestHandler(dataType, sourceAccount, sourcePlayer, data)
|
||||
assert(dataType == Constants.DATA_TYPES.CHARACTER_SETTING_DATA_REQUEST)
|
||||
local character, namespace, settingKey = strsplit(".", data)
|
||||
if not Connection.IsCharacterConnected(sourcePlayer) then
|
||||
-- we're not connected to this player
|
||||
Log.Warn("Got CHARACTER_HASHES_BROADCAST for player which isn't connected")
|
||||
return
|
||||
elseif Settings.GetCharacterSyncAccountKey(character) ~= Settings.GetCurrentSyncAccountKey() then
|
||||
-- we don't own this character
|
||||
Log.Err("Request for character we don't own ('%s', '%s')", tostring(character), tostring(Settings.GetCharacterSyncAccountKey(character)))
|
||||
return
|
||||
end
|
||||
Log.Info("CHARACTER_SETTING_DATA_REQUEST (%s,%s,%s)", character, namespace, settingKey)
|
||||
local responseData = TempTable.Acquire()
|
||||
responseData.character = character
|
||||
responseData.namespace = namespace
|
||||
responseData.settingKey = settingKey
|
||||
responseData.data = Settings.Get("sync", Settings.GetSyncScopeKeyByCharacter(character), namespace, settingKey)
|
||||
Comm.SendData(Constants.DATA_TYPES.CHARACTER_SETTING_DATA_RESPONSE, sourcePlayer, responseData)
|
||||
TempTable.Release(responseData)
|
||||
end
|
||||
|
||||
function private.CharacterSettingDataResponseHandler(dataType, sourceAccount, sourcePlayer, data)
|
||||
assert(dataType == Constants.DATA_TYPES.CHARACTER_SETTING_DATA_RESPONSE)
|
||||
if not Connection.IsCharacterConnected(sourcePlayer) then
|
||||
-- we're not connected to this player
|
||||
Log.Warn("Got CHARACTER_HASHES_BROADCAST for player which isn't connected")
|
||||
return
|
||||
end
|
||||
local dataValueType = type(data.data)
|
||||
Log.Info("CHARACTER_SETTING_DATA_RESPONSE (%s,%s,%s,%s,%s)", data.character, data.namespace, data.settingKey, dataValueType, (dataValueType == "string" or dataValueType == "table") and #dataValueType or "-")
|
||||
if dataValueType == "table" then
|
||||
local tbl = Settings.Get("sync", Settings.GetSyncScopeKeyByCharacter(data.character), data.namespace, data.settingKey)
|
||||
wipe(tbl)
|
||||
for i, v in pairs(data.data) do
|
||||
tbl[i] = v
|
||||
end
|
||||
else
|
||||
Settings.Set("sync", Settings.GetSyncScopeKeyByCharacter(data.character), data.namespace, data.settingKey, data.data)
|
||||
end
|
||||
for _, callback in ipairs(private.callbacks) do
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.CalculateCharacterHash(character)
|
||||
local hash = nil
|
||||
local settingKeys = TempTable.Acquire()
|
||||
for _, namespace, settingKey in Settings.SyncSettingIterator() do
|
||||
tinsert(settingKeys, strjoin(".", namespace, settingKey))
|
||||
end
|
||||
sort(settingKeys)
|
||||
for _, key in ipairs(settingKeys) do
|
||||
hash = Math.CalculateHash(key, hash)
|
||||
local namespace, settingKey = strsplit(".", key)
|
||||
local settingValue = Settings.Get("sync", Settings.GetSyncScopeKeyByCharacter(character), namespace, settingKey)
|
||||
hash = Math.CalculateHash(settingValue, hash)
|
||||
end
|
||||
assert(hash)
|
||||
TempTable.Release(settingKeys)
|
||||
return hash
|
||||
end
|
||||
|
||||
function private.CalculateCharacterSettingHash(character, namespace, settingKey)
|
||||
return Math.CalculateHash(Settings.Get("sync", Settings.GetSyncScopeKeyByCharacter(character), namespace, settingKey))
|
||||
end
|
||||
177
LibTSM/Service/SyncClasses/RPC.lua
Normal file
177
LibTSM/Service/SyncClasses/RPC.lua
Normal file
@@ -0,0 +1,177 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local RPC = TSM.Init("Service.SyncClasses.RPC")
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local Constants = TSM.Include("Service.SyncClasses.Constants")
|
||||
local Comm = TSM.Include("Service.SyncClasses.Comm")
|
||||
local Connection = TSM.Include("Service.SyncClasses.Connection")
|
||||
local private = {
|
||||
rpcFunctions = {},
|
||||
pendingRPC = {},
|
||||
rpcSeqNum = 0,
|
||||
}
|
||||
local RPC_EXTRA_TIMEOUT = 15
|
||||
local CALLBACK_TIME_WARNING_THRESHOLD_MS = 20
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
RPC:OnModuleLoad(function()
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.RPC_CALL, private.HandleCall)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.RPC_RETURN, private.HandleReturn)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.RPC_PREAMBLE, private.HandlePreamble)
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function RPC.Register(name, func)
|
||||
assert(name)
|
||||
private.rpcFunctions[name] = func
|
||||
end
|
||||
|
||||
function RPC.Call(name, targetPlayer, handler, ...)
|
||||
assert(targetPlayer)
|
||||
if not Connection.IsCharacterConnected(targetPlayer) then
|
||||
return false
|
||||
end
|
||||
|
||||
assert(private.rpcFunctions[name], "Cannot call an RPC which is not also registered locally.")
|
||||
private.rpcSeqNum = private.rpcSeqNum + 1
|
||||
|
||||
local requestData = TempTable.Acquire()
|
||||
requestData.name = name
|
||||
requestData.args = TempTable.Acquire(...)
|
||||
requestData.seq = private.rpcSeqNum
|
||||
local numBytes = Comm.SendData(Constants.DATA_TYPES.RPC_CALL, targetPlayer, requestData)
|
||||
TempTable.Release(requestData.args)
|
||||
TempTable.Release(requestData)
|
||||
|
||||
local context = TempTable.Acquire()
|
||||
context.name = name
|
||||
context.handler = handler
|
||||
context.timeoutTime = time() + RPC_EXTRA_TIMEOUT + private.EstimateTransferTime(numBytes)
|
||||
private.pendingRPC[private.rpcSeqNum] = context
|
||||
Delay.AfterTime("SYNC_PENDING_RPC", 1, private.HandlePendingRPC)
|
||||
|
||||
return true, (context.timeoutTime - time()) * 2 / 3
|
||||
end
|
||||
|
||||
function RPC.Cancel(name, handler)
|
||||
for seq, info in pairs(private.pendingRPC) do
|
||||
if info.name == name and info.handler == handler then
|
||||
TempTable.Release(info)
|
||||
private.pendingRPC[seq] = nil
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Message Handlers
|
||||
-- ============================================================================
|
||||
|
||||
function private.HandleCall(dataType, _, sourcePlayer, data)
|
||||
assert(dataType == Constants.DATA_TYPES.RPC_CALL)
|
||||
if type(data) ~= "table" or type(data.name) ~= "string" or type(data.seq) ~= "number" or type(data.args) ~= "table" then
|
||||
return
|
||||
end
|
||||
if not private.rpcFunctions[data.name] then
|
||||
return
|
||||
end
|
||||
local responseData = TempTable.Acquire()
|
||||
|
||||
local startTime = debugprofilestop()
|
||||
responseData.result = TempTable.Acquire(private.rpcFunctions[data.name](unpack(data.args)))
|
||||
local timeTaken = debugprofilestop() - startTime
|
||||
if timeTaken > CALLBACK_TIME_WARNING_THRESHOLD_MS then
|
||||
Log.Warn("RPC (%s) took %0.2fms", tostring(data.name), timeTaken)
|
||||
end
|
||||
responseData.seq = data.seq
|
||||
local numBytes = Comm.SendData(Constants.DATA_TYPES.RPC_RETURN, sourcePlayer, responseData)
|
||||
TempTable.Release(responseData.result)
|
||||
TempTable.Release(responseData)
|
||||
|
||||
local transferTime = private.EstimateTransferTime(numBytes)
|
||||
if transferTime > 1 then
|
||||
-- We sent more than 1 second worth of data back, so send a preamble to allow the source to adjust its timeout accordingly.
|
||||
local preambleData = TempTable.Acquire()
|
||||
preambleData.transferTime = transferTime
|
||||
preambleData.seq = data.seq
|
||||
Comm.SendData(Constants.DATA_TYPES.RPC_PREAMBLE, sourcePlayer, preambleData)
|
||||
TempTable.Release(preambleData)
|
||||
end
|
||||
end
|
||||
|
||||
function private.HandleReturn(dataType, _, _, data)
|
||||
assert(dataType == Constants.DATA_TYPES.RPC_RETURN)
|
||||
if type(data.seq) ~= "number" or type(data.result) ~= "table" then
|
||||
return
|
||||
elseif not private.pendingRPC[data.seq] then
|
||||
return
|
||||
end
|
||||
local startTime = debugprofilestop()
|
||||
private.pendingRPC[data.seq].handler(unpack(data.result))
|
||||
local timeTaken = debugprofilestop() - startTime
|
||||
if timeTaken > CALLBACK_TIME_WARNING_THRESHOLD_MS then
|
||||
Log.Warn("RPC (%s) result handler took %0.2fms", tostring(private.pendingRPC[data.seq].name), timeTaken)
|
||||
end
|
||||
TempTable.Release(private.pendingRPC[data.seq])
|
||||
private.pendingRPC[data.seq] = nil
|
||||
end
|
||||
|
||||
function private.HandlePreamble(dataType, _, _, data)
|
||||
assert(dataType == Constants.DATA_TYPES.RPC_PREAMBLE)
|
||||
if type(data.seq) ~= "number" or type(data.transferTime) ~= "number" then
|
||||
return
|
||||
elseif not private.pendingRPC[data.seq] then
|
||||
return
|
||||
end
|
||||
-- extend the timeout
|
||||
private.pendingRPC[data.seq].timeoutTime = time() + RPC_EXTRA_TIMEOUT + data.transferTime
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.EstimateTransferTime(numBytes)
|
||||
return ceil(numBytes / (ChatThrottleLib.MAX_CPS / 2))
|
||||
end
|
||||
|
||||
function private.HandlePendingRPC()
|
||||
if not next(private.pendingRPC) then
|
||||
return
|
||||
end
|
||||
local timedOut = TempTable.Acquire()
|
||||
for seq, info in pairs(private.pendingRPC) do
|
||||
if time() > info.timeoutTime then
|
||||
tinsert(timedOut, seq)
|
||||
end
|
||||
end
|
||||
for _, seq in ipairs(timedOut) do
|
||||
local info = private.pendingRPC[seq]
|
||||
Log.Warn("RPC timed out (%s)", info.name)
|
||||
info.handler()
|
||||
TempTable.Release(info)
|
||||
private.pendingRPC[seq] = nil
|
||||
end
|
||||
TempTable.Release(timedOut)
|
||||
end
|
||||
839
LibTSM/Service/Threading.lua
Normal file
839
LibTSM/Service/Threading.lua
Normal file
@@ -0,0 +1,839 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Threading Functions
|
||||
-- @module Threading
|
||||
|
||||
local _, TSM = ...
|
||||
local Threading = TSM.Init("Service.Threading")
|
||||
local Debug = TSM.Include("Util.Debug")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Vararg = TSM.Include("Util.Vararg")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local ErrorHandler = TSM.Include("Service.ErrorHandler")
|
||||
local Thread = TSM.Include("LibTSMClass").DefineClass("Thread")
|
||||
local private = {
|
||||
threads = {},
|
||||
queue = {},
|
||||
runningThread = nil,
|
||||
schedulerFrame = nil,
|
||||
}
|
||||
local MAX_TIME_USAGE_RATIO = 0.25
|
||||
local EXCESSIVE_TIME_USED_RATIO = 1.2
|
||||
local EXCESSIVE_TIME_LOG_THRESHOLD_MS = 100
|
||||
local MAX_QUANTUM_MS = 10
|
||||
local SEND_MSG_SYNC_TIMEOUT_MS = 3000
|
||||
local YIELD_VALUE_START = {}
|
||||
local YIELD_VALUE = {}
|
||||
local SCHEDULER_TIME_WARNING_THRESHOLD_MS = 100
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Create a new thread.
|
||||
-- @tparam string name The name of the thread (for debugging purposes)
|
||||
-- @tparam function func The thread's main function
|
||||
-- @treturn string The thread id
|
||||
function Threading.New(name, func)
|
||||
assert(name and func)
|
||||
local thread = Thread(name, func)
|
||||
local threadId = strjoin("-", tostringall(thread, func))
|
||||
private.threads[threadId] = thread
|
||||
return threadId
|
||||
end
|
||||
|
||||
--- Set the callback for a thread.
|
||||
-- The callback is called when a thread finishes running and is passed all values returned by the thread's main function.
|
||||
-- @tparam string threadId The thread id
|
||||
-- @tparam function callback The callback function
|
||||
function Threading.SetCallback(threadId, callback)
|
||||
private.threads[threadId]:_SetCallback(callback)
|
||||
end
|
||||
|
||||
--- Start a thread.
|
||||
-- The thread will not actually be run until the next run of the scheduler (next frame).
|
||||
-- @tparam string threadId The thread id
|
||||
-- @tparam vararg ... Arguments to pass to the thread's main function
|
||||
function Threading.Start(threadId, ...)
|
||||
local thread = private.threads[threadId]
|
||||
assert(not thread:_IsAlive())
|
||||
-- make sure the scheduler is running
|
||||
private.StartScheduler()
|
||||
thread:_Start(...)
|
||||
end
|
||||
|
||||
--- Send a message to a thread.
|
||||
-- @tparam string threadId The thread id
|
||||
-- @tparam vararg ... The contents of the message
|
||||
function Threading.SendMessage(threadId, ...)
|
||||
local thread = private.threads[threadId]
|
||||
assert(thread:_IsAlive())
|
||||
tinsert(thread._messages, TempTable.Acquire(...))
|
||||
end
|
||||
|
||||
--- Send a synchronous message to a thread.
|
||||
-- The current execution context will be blocked until the message is delivered.
|
||||
-- @tparam string threadId The thread id
|
||||
-- @tparam vararg ... The contents of the message
|
||||
function Threading.SendSyncMessage(threadId, ...)
|
||||
if Threading.IsThreadContext() then
|
||||
-- we can't (sanely) run a thread from within a thread context, so we'll yield from the current thread first
|
||||
private.runningThread:_SendSyncMessage(threadId, ...)
|
||||
else
|
||||
local errMsg = private.threads[threadId]:_HandleSyncMessage(...)
|
||||
if errMsg then
|
||||
error(errMsg)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Kill a thread.
|
||||
-- @tparam string threadId The thread id
|
||||
function Threading.Kill(threadId)
|
||||
local thread = private.threads[threadId]
|
||||
if thread:_IsAlive() then
|
||||
thread:_Exit()
|
||||
end
|
||||
end
|
||||
|
||||
--- Check if we're currently in a thread context.
|
||||
-- @treturn boolean Whether or not we're currently in a thread context
|
||||
function Threading.IsThreadContext()
|
||||
return private.runningThread ~= nil
|
||||
end
|
||||
|
||||
--- Get the name of the current thread.
|
||||
-- @treturn ?string The name of the currently-running thread or nil if no thread is running
|
||||
function Threading.GetCurrentThreadName()
|
||||
return private.runningThread and private.runningThread._name or nil
|
||||
end
|
||||
|
||||
--- Check if the current thread has any pending messages.
|
||||
-- This must be called from a thread context.
|
||||
-- @treturn boolean Whether or not this thread has any pending messages
|
||||
function Threading.HasPendingMessage()
|
||||
return private.runningThread:_HasPendingMessage()
|
||||
end
|
||||
|
||||
--- Receive the next message.
|
||||
-- This must be called from a thread context.
|
||||
-- @return The context of the message
|
||||
function Threading.ReceiveMessage()
|
||||
return private.runningThread:_ReceiveMessage()
|
||||
end
|
||||
|
||||
--- Performs a yield.
|
||||
-- This must be called from a thread context. This function should be called regularly by threads to allow them the
|
||||
-- scheduler to switch to another thread if this thread's quantum is up. If this thread has not exceeded its quantum,
|
||||
-- this function will return right away.
|
||||
-- @tparam[opt=false] boolean force If true, forces this thread to yield, regardless of whether or not it needs to
|
||||
function Threading.Yield(force)
|
||||
private.runningThread:_Yield(force)
|
||||
end
|
||||
|
||||
--- Sleep the thread.
|
||||
-- This must be called from a thread context.
|
||||
-- @tparam number seconds The number of seconds to sleep for (may be a deciaml number)
|
||||
function Threading.Sleep(seconds)
|
||||
private.runningThread:_Sleep(seconds)
|
||||
end
|
||||
|
||||
--- Wait for a WoW event.
|
||||
-- This must be called from a thread context.
|
||||
-- @tparam string event The WoW event to wait for
|
||||
function Threading.WaitForEvent(...)
|
||||
return private.runningThread:_WaitForEvent(...)
|
||||
end
|
||||
|
||||
--- Wait for a function.
|
||||
-- This must be called from a thread context. It will block the thread until the specified function returns a true value.
|
||||
-- @tparam function func The function to wait for
|
||||
-- @tparam vararg ... Additional arguments to pass to the function
|
||||
function Threading.WaitForFunction(func, ...)
|
||||
return private.runningThread:_WaitForFunction(func, ...)
|
||||
end
|
||||
|
||||
--- Wait for a future.
|
||||
-- This must be called from a thread context. It will block the thread until the specified future is done.
|
||||
-- @tparam Future future The future to wait for
|
||||
function Threading.WaitForFuture(future)
|
||||
return private.runningThread:_WaitForFuture(future)
|
||||
end
|
||||
|
||||
--- Acquire a temp table.
|
||||
-- This must be called from a thread context. Any time a thread needs to maintain a temp table across a potential yield,
|
||||
-- it should use this API. This API will release the temp tables in the case that the thread gets killed.
|
||||
-- @see TempTable.Acquire
|
||||
-- @tparam vararg ... Values to insert into the temp table
|
||||
function Threading.AcquireSafeTempTable(...)
|
||||
return private.runningThread:_AcquireSafeTempTable(...)
|
||||
end
|
||||
|
||||
--- Release a temp table.
|
||||
-- This must be called from a thread context. This is used to release temp tables acquired with
|
||||
-- @{Threading.AcquireSafeTempTable}.
|
||||
-- @see TempTable.Release
|
||||
-- @tparam table tbl The temp table to release
|
||||
function Threading.ReleaseSafeTempTable(tbl)
|
||||
return private.runningThread:_ReleaseSafeTempTable(tbl)
|
||||
end
|
||||
|
||||
--- Release a temp table and returns its contents.
|
||||
-- This must be called from a thread context. This is used to release and unpack temp tables acquired with
|
||||
-- @{Threading.AcquireSafeTempTable}.
|
||||
-- @see TempTable.UnpackAndRelease
|
||||
-- @tparam table tbl The temp table to release and unpack
|
||||
-- @return The contents of the temp table
|
||||
function Threading.UnpackAndReleaseSafeTempTable(tbl)
|
||||
return private.runningThread:_UnpackAndReleaseSafeTempTable(tbl)
|
||||
end
|
||||
|
||||
--- Assign ownership of a database query to the thread so that if the thread is killed it'll get released safely.
|
||||
-- This must be called from a thread context.
|
||||
-- @tparam DatabaseQuery query The query object
|
||||
function Threading.GuardDatabaseQuery(query)
|
||||
return private.runningThread:_GuardDatabaseQuery(query)
|
||||
end
|
||||
|
||||
--- Removes ownership of a database query from the thread.
|
||||
-- This must be called from a thread context.
|
||||
-- @tparam DatabaseQuery query The query object
|
||||
function Threading.UnguardDatabaseQuery(query)
|
||||
return private.runningThread:_UnguardDatabaseQuery(query)
|
||||
end
|
||||
|
||||
function Threading.GetDebugStr()
|
||||
local lines = {}
|
||||
for _, thread in pairs(private.threads) do
|
||||
if thread:_IsAlive() then
|
||||
local name, stateStr, timeStr, createStr, startStr, backtrace = thread:_GetDebugInfo()
|
||||
tinsert(lines, " "..name)
|
||||
tinsert(lines, " "..stateStr)
|
||||
tinsert(lines, " "..timeStr)
|
||||
tinsert(lines, " "..createStr)
|
||||
tinsert(lines, " "..startStr)
|
||||
if #backtrace > 0 then
|
||||
tinsert(lines, " Backtrace:")
|
||||
for _, line in ipairs(backtrace) do
|
||||
tinsert(lines, " "..line)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Thread Class - General Methods
|
||||
-- ============================================================================
|
||||
|
||||
function Thread.__init(self, name, func)
|
||||
-- core fields
|
||||
self._func = func
|
||||
self._co = nil
|
||||
self._state = "DEAD"
|
||||
self._endTime = nil
|
||||
self._sleepTime = nil
|
||||
self._eventNames = {}
|
||||
self._eventArgs = nil
|
||||
self._waitFunction = nil
|
||||
self._waitFunctionArgs = nil
|
||||
self._waitFunctionResult = nil
|
||||
self._waitFuture = nil
|
||||
self._waitFutureResult = nil
|
||||
self._waitFutureDone = false
|
||||
self._syncMessage = nil
|
||||
self._syncMessageDest = nil
|
||||
self._messages = {}
|
||||
self._callback = nil
|
||||
self._returnValue = nil
|
||||
self._safeTempTables = {}
|
||||
self._guardedQueries = {}
|
||||
|
||||
-- debug fields
|
||||
self._startTime = 0
|
||||
self._cpuTimeUsed = 0
|
||||
self._realTimeUsed = 0
|
||||
self._name = name or tostring(self)
|
||||
self._createCaller = Debug.GetStackLevelLocation(3) or "?"
|
||||
self._startCaller = nil
|
||||
end
|
||||
|
||||
function Thread._Start(self, ...)
|
||||
self._co = coroutine.create(self._Main)
|
||||
self._state = "READY"
|
||||
self._endTime = 0
|
||||
self._sleepTime = nil
|
||||
wipe(self._eventNames)
|
||||
self._eventArgs = nil
|
||||
self._waitFunction = nil
|
||||
self._waitFunctionArgs = nil
|
||||
self._waitFunctionResult = nil
|
||||
self._waitFuture = nil
|
||||
self._waitFutureResult = nil
|
||||
self._waitFutureDone = false
|
||||
self._syncMessage = nil
|
||||
self._syncMessageDest = nil
|
||||
assert(not next(self._messages))
|
||||
assert(not next(self._safeTempTables))
|
||||
assert(not next(self._guardedQueries))
|
||||
self._startTime = 0
|
||||
self._cpuTimeUsed = 0
|
||||
self._realTimeUsed = 0
|
||||
self._startCaller = self._startCaller or Debug.GetStackLevelLocation(3)
|
||||
|
||||
-- run the thread once (will yield right away) to pass in self and the arguments
|
||||
local noErr, retValue = coroutine.resume(self._co, self, ...)
|
||||
assert(noErr and retValue == YIELD_VALUE_START)
|
||||
end
|
||||
|
||||
function Thread._SetCallback(self, callback)
|
||||
self._callback = callback
|
||||
end
|
||||
|
||||
function Thread._IsAlive(self)
|
||||
return self._state ~= "DEAD"
|
||||
end
|
||||
|
||||
function Thread._ToLogStr(self)
|
||||
if self._startTime then
|
||||
self._realTimeUsed = debugprofilestop() - self._startTime
|
||||
local pctStr = format("%.1f%%", Math.Round(self._cpuTimeUsed / self._realTimeUsed, 0.001) * 100)
|
||||
return format("%s [%s,%s]", self._name, self._state, pctStr)
|
||||
else
|
||||
return format("%s [%s]", self._name, self._state)
|
||||
end
|
||||
end
|
||||
|
||||
function Thread._Cleanup(self)
|
||||
for _, msg in ipairs(self._messages) do
|
||||
TempTable.Release(msg)
|
||||
end
|
||||
wipe(self._messages)
|
||||
for tbl in pairs(self._safeTempTables) do
|
||||
TempTable.Release(tbl)
|
||||
end
|
||||
wipe(self._safeTempTables)
|
||||
for query in pairs(self._guardedQueries) do
|
||||
query:Release(true)
|
||||
end
|
||||
wipe(self._guardedQueries)
|
||||
self._waitFunction = nil
|
||||
if self._waitFunctionArgs then
|
||||
TempTable.Release(self._waitFunctionArgs)
|
||||
self._waitFunctionArgs = nil
|
||||
end
|
||||
if self._waitFuture then
|
||||
self._waitFuture:Cancel()
|
||||
self._waitFuture = nil
|
||||
self._waitFutureResult = nil
|
||||
self._waitFutureDone = false
|
||||
end
|
||||
if self._waitFunctionResult then
|
||||
TempTable.Release(self._waitFunctionResult)
|
||||
self._waitFunctionResult = nil
|
||||
end
|
||||
if self._syncMessage then
|
||||
TempTable.Release(self._syncMessage)
|
||||
self._syncMessage = nil
|
||||
self._syncMessageDest = nil
|
||||
end
|
||||
end
|
||||
|
||||
function Thread._GetDebugInfo(self)
|
||||
local stateStr = nil
|
||||
if self._state == "SLEEPING" then
|
||||
stateStr = format("Sleeping for %d seconds", Math.Round(self._sleepTime, 0.001))
|
||||
elseif self._state == "WAITING_FOR_MSG" then
|
||||
if #self._messages > 0 then
|
||||
stateStr = "Got message"
|
||||
else
|
||||
stateStr = "Waiting for message"
|
||||
end
|
||||
elseif self._state == "WAITING_FOR_EVENT" then
|
||||
if next(self._eventNames) then
|
||||
local eventList = {}
|
||||
for event in pairs(self._eventNames) do
|
||||
tinsert(eventList, event)
|
||||
end
|
||||
stateStr = format("Waiting for %s", table.concat(eventList, "|"))
|
||||
else
|
||||
stateStr = format("Got %s", self._eventArgs[1])
|
||||
end
|
||||
elseif self._state == "WAITING_FOR_FUNCTION" then
|
||||
local functionName = nil
|
||||
-- look up to 2 levels deep in the globals table for the name of this function
|
||||
for k, v in pairs(_G) do
|
||||
if type(v) == "table" then
|
||||
for k2, v2 in pairs(v) do
|
||||
if v2 == self._waitFunction then
|
||||
functionName = tostring(k).."."..tostring(k2)
|
||||
break
|
||||
end
|
||||
end
|
||||
if functionName then
|
||||
break
|
||||
end
|
||||
elseif v == self._waitFunction then
|
||||
functionName = tostring(k)
|
||||
break
|
||||
end
|
||||
end
|
||||
stateStr = format("Waiting for %s", functionName or tostring(self._waitFunction))
|
||||
elseif self._state == "WAITING_FOR_FUTURE" then
|
||||
stateStr = format("Waiting for future (%s)", self._waitFuture:GetName())
|
||||
elseif self._state == "FORCED_YIELD" then
|
||||
stateStr = "Forced yield"
|
||||
elseif self._state == "SENDING_SYNC_MESSAGE" then
|
||||
stateStr = format("Sending sync message to %s", self._syncMessageDest and private.threads[self._syncMessageDest]._name or "?")
|
||||
elseif self._state == "RUNNING" then
|
||||
stateStr = "Running"
|
||||
elseif self._state == "DEAD" then
|
||||
stateStr = "Dead"
|
||||
elseif self._state == "READY" then
|
||||
stateStr = "Ready"
|
||||
else
|
||||
error("Invalid thread state: "..tostring(self._state))
|
||||
end
|
||||
if #self._messages > 0 then
|
||||
stateStr = format("%s (%d messages)", stateStr, #self._messages)
|
||||
end
|
||||
|
||||
local timeStr = "<Not Started>"
|
||||
if self._startTime then
|
||||
local wallTime = debugprofilestop() - self._startTime
|
||||
local cpuTime = self._cpuTimeUsed
|
||||
timeStr = format("Running for %.1f seconds (CPU: %dms, %.2f%%)", wallTime / 1000, cpuTime, (cpuTime / wallTime) * 100)
|
||||
end
|
||||
|
||||
local createStr = "Created @"..self._createCaller
|
||||
local startStr = "Started @"..(self._startCaller or "<Not Started>")
|
||||
|
||||
local backtrace = {}
|
||||
local level = 2
|
||||
local line = Debug.GetStackLevelLocation(level, self._co)
|
||||
while line do
|
||||
tinsert(backtrace, line)
|
||||
level = level + 1
|
||||
line = Debug.GetStackLevelLocation(level, self._co)
|
||||
end
|
||||
|
||||
|
||||
return self._name, stateStr, timeStr, createStr, startStr, backtrace
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Thread Class - Scheduler Helper Methods
|
||||
-- ============================================================================
|
||||
|
||||
function Thread._CanRun(self)
|
||||
return self._state == "READY"
|
||||
end
|
||||
|
||||
function Thread._Run(self, quantum)
|
||||
assert(not Threading.IsThreadContext())
|
||||
if not self:_CanRun() then
|
||||
return 0
|
||||
end
|
||||
private.runningThread = self
|
||||
self._state = "RUNNING"
|
||||
local startTime = debugprofilestop()
|
||||
self._endTime = startTime + quantum
|
||||
local noErr, returnVal = coroutine.resume(self._co)
|
||||
local elapsedTime = debugprofilestop() - startTime
|
||||
private.runningThread = nil
|
||||
|
||||
assert(not noErr or returnVal == YIELD_VALUE)
|
||||
if noErr and self._state == "SENDING_SYNC_MESSAGE" then
|
||||
-- yielded to send a sync message to another thread
|
||||
local destThread = private.threads[self._syncMessageDest]
|
||||
local msg = self._syncMessage
|
||||
self._syncMessage = nil
|
||||
self._syncMessageDest = nil
|
||||
local errMsg = destThread:_HandleSyncMessage(TempTable.UnpackAndRelease(msg))
|
||||
if errMsg then
|
||||
noErr = false
|
||||
returnVal = errMsg
|
||||
elseif self._state == "SENDING_SYNC_MESSAGE" then
|
||||
self._state = "READY"
|
||||
end
|
||||
end
|
||||
if not noErr then
|
||||
returnVal = returnVal or "UNKNOWN ERROR"
|
||||
ErrorHandler.ShowForThread(returnVal, self._co)
|
||||
self._state = "DEAD"
|
||||
end
|
||||
if self._state == "DEAD" then
|
||||
self:_Cleanup()
|
||||
local returnValue = self._returnValue
|
||||
self._returnValue = nil
|
||||
if self._callback and returnValue then
|
||||
self._callback(TempTable.UnpackAndRelease(returnValue))
|
||||
elseif returnValue then
|
||||
TempTable.Release(returnValue)
|
||||
end
|
||||
end
|
||||
return elapsedTime
|
||||
end
|
||||
|
||||
function Thread._UpdateState(self, elapsed)
|
||||
-- check what the thread state is
|
||||
if self._state == "SLEEPING" then
|
||||
self._sleepTime = self._sleepTime - elapsed
|
||||
if self._sleepTime <= 0 then
|
||||
self._sleepTime = nil
|
||||
self._state = "READY"
|
||||
end
|
||||
elseif self._state == "WAITING_FOR_MSG" then
|
||||
if #self._messages > 0 then
|
||||
self._state = "READY"
|
||||
end
|
||||
elseif self._state == "WAITING_FOR_EVENT" then
|
||||
assert(self._eventNames or self._eventArgs)
|
||||
if self._eventArgs then
|
||||
self._state = "READY"
|
||||
end
|
||||
elseif self._state == "WAITING_FOR_FUNCTION" then
|
||||
assert(self._waitFunction, "Waiting for function without waitFunction set")
|
||||
local result = TempTable.Acquire(self._waitFunction(unpack(self._waitFunctionArgs)))
|
||||
if result[1] then
|
||||
self._waitFunctionResult = result
|
||||
self._state = "READY"
|
||||
else
|
||||
TempTable.Release(result)
|
||||
end
|
||||
elseif self._state == "WAITING_FOR_FUTURE" then
|
||||
if self._waitFutureDone then
|
||||
assert(not self._waitFuture)
|
||||
self._waitFutureDone = false
|
||||
self._state = "READY"
|
||||
else
|
||||
assert(self._waitFuture)
|
||||
end
|
||||
elseif self._state == "FORCED_YIELD" then
|
||||
self._state = "READY"
|
||||
elseif self._state == "RUNNING" then
|
||||
-- this shouldn't happen, so just kill this thread
|
||||
self:_Exit()
|
||||
elseif self._state == "DEAD" then
|
||||
-- pass
|
||||
elseif self._state == "READY" then
|
||||
-- pass
|
||||
else
|
||||
error("Invalid thread state: "..tostring(self._state))
|
||||
end
|
||||
end
|
||||
|
||||
function Thread._ProcessEvent(self, event, ...)
|
||||
if self._state ~= "WAITING_FOR_EVENT" then
|
||||
return
|
||||
end
|
||||
assert(next(self._eventNames) or self._eventArgs)
|
||||
if not self._eventNames[event] then
|
||||
return
|
||||
end
|
||||
wipe(self._eventNames) -- only trigger the event once then clear all
|
||||
self._eventArgs = TempTable.Acquire(event, ...)
|
||||
end
|
||||
|
||||
function Thread._OnFutureDone(self, future)
|
||||
if self._state ~= "WAITING_FOR_FUTURE" then
|
||||
return
|
||||
end
|
||||
assert(self._waitFuture or self._waitFutureDone)
|
||||
if future ~= self._waitFuture then
|
||||
return
|
||||
end
|
||||
self._waitFutureDone = true
|
||||
self._waitFuture = nil
|
||||
self._waitFutureResult = future:GetValue()
|
||||
self:_UpdateState(0)
|
||||
self:_Run(0)
|
||||
end
|
||||
|
||||
function Thread._HandleSyncMessage(self, ...)
|
||||
assert(not Threading.IsThreadContext())
|
||||
local msg = TempTable.Acquire(...)
|
||||
tinsert(self._messages, 1, msg) -- this message should be received first
|
||||
-- run the thread for up to 3 seconds to get it to process the sync message
|
||||
local startTime = debugprofilestop()
|
||||
while self._messages[1] == msg do
|
||||
if debugprofilestop() - startTime > SEND_MSG_SYNC_TIMEOUT_MS or not self:_IsAlive() then
|
||||
-- want to error from the sending context, so just return the error
|
||||
return format("ERROR: A sync message was not able to be delivered! (%s)", self._name)
|
||||
end
|
||||
assert(self._state ~= "SENDING_SYNC_MESSAGE", "Circular sync message detected")
|
||||
if self._state == "WAITING_FOR_MSG" then
|
||||
self._state = "READY"
|
||||
end
|
||||
self:_Run(0)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Thread Class - Thread Context Methods
|
||||
-- ============================================================================
|
||||
|
||||
function Thread._Main(self, ...)
|
||||
self._startTime = debugprofilestop()
|
||||
coroutine.yield(YIELD_VALUE_START)
|
||||
self._returnValue = TempTable.Acquire(self._func(...))
|
||||
self:_Exit()
|
||||
end
|
||||
|
||||
function Thread._Yield(self, force)
|
||||
if force or self._state ~= "RUNNING" or debugprofilestop() > self._endTime then
|
||||
-- only change the state if it's currently set to RUNNING
|
||||
if self._state == "RUNNING" then
|
||||
self._state = force and "FORCED_YIELD" or "READY"
|
||||
end
|
||||
coroutine.yield(YIELD_VALUE)
|
||||
end
|
||||
end
|
||||
|
||||
function Thread._Sleep(self, seconds)
|
||||
self._state = "SLEEPING"
|
||||
self._sleepTime = seconds
|
||||
self:_Yield()
|
||||
end
|
||||
|
||||
function Thread._HasPendingMessage(self)
|
||||
return #self._messages > 0
|
||||
end
|
||||
|
||||
function Thread._ReceiveMessage(self)
|
||||
if #self._messages == 0 then
|
||||
-- change the state if there's no messages ready
|
||||
self._state = "WAITING_FOR_MSG"
|
||||
elseif debugprofilestop() > self._endTime then
|
||||
-- If we're about to yield, set the state to WAITING_FOR_MSG even if we have messages in the queue
|
||||
-- to allow sync messages to be sent to us.
|
||||
self._state = "WAITING_FOR_MSG"
|
||||
end
|
||||
self:_Yield()
|
||||
return TempTable.UnpackAndRelease(tremove(self._messages, 1))
|
||||
end
|
||||
|
||||
function Thread._SendSyncMessage(self, destThread, ...)
|
||||
assert(destThread ~= self)
|
||||
self._state = "SENDING_SYNC_MESSAGE"
|
||||
self._syncMessageDest = destThread
|
||||
self._syncMessage = TempTable.Acquire(...)
|
||||
self:_Yield()
|
||||
end
|
||||
|
||||
function Thread._WaitForEvent(self, ...)
|
||||
self._state = "WAITING_FOR_EVENT"
|
||||
self._eventArgs = nil
|
||||
for _, event in Vararg.Iterator(...) do
|
||||
self._eventNames[event] = true
|
||||
private.schedulerFrame:RegisterEvent(event)
|
||||
end
|
||||
local st = GetTime()
|
||||
self:_Yield()
|
||||
local yieldTime = GetTime() - st
|
||||
if yieldTime > 5 then
|
||||
local firstEvent = ...
|
||||
Log.Warn("Waited %d seconds for %s", yieldTime, firstEvent)
|
||||
end
|
||||
local result = self._eventArgs
|
||||
self._eventArgs = nil
|
||||
return TempTable.UnpackAndRelease(result)
|
||||
end
|
||||
|
||||
function Thread._WaitForFunction(self, func, ...)
|
||||
-- try the function once before yielding
|
||||
local result = TempTable.Acquire(func(...))
|
||||
if result[1] then
|
||||
return TempTable.UnpackAndRelease(result)
|
||||
end
|
||||
TempTable.Release(result)
|
||||
-- do the yield
|
||||
self._state = "WAITING_FOR_FUNCTION"
|
||||
self._waitFunction = func
|
||||
self._waitFunctionArgs = TempTable.Acquire(...)
|
||||
self:_Yield()
|
||||
result = self._waitFunctionResult
|
||||
self._waitFunction = nil
|
||||
TempTable.Release(self._waitFunctionArgs)
|
||||
self._waitFunctionArgs = nil
|
||||
self._waitFunctionResult = nil
|
||||
return TempTable.UnpackAndRelease(result)
|
||||
end
|
||||
|
||||
function Thread._WaitForFuture(self, future)
|
||||
-- try the future once before yielding
|
||||
if future:IsDone() then
|
||||
return future:GetValue()
|
||||
end
|
||||
-- register our OnDone handler and do the yield
|
||||
future:SetScript("OnDone", private.OnFutureDone)
|
||||
self._state = "WAITING_FOR_FUTURE"
|
||||
self._waitFuture = future
|
||||
self:_Yield()
|
||||
local result = self._waitFutureResult
|
||||
self._waitFutureResult = nil
|
||||
return result
|
||||
end
|
||||
|
||||
function Thread._AcquireSafeTempTable(self, ...)
|
||||
local tbl = TempTable.Acquire(...)
|
||||
assert(not self._safeTempTables[tbl])
|
||||
self._safeTempTables[tbl] = true
|
||||
return tbl
|
||||
end
|
||||
|
||||
function Thread._ReleaseSafeTempTable(self, tbl)
|
||||
assert(self._safeTempTables[tbl])
|
||||
self._safeTempTables[tbl] = nil
|
||||
return TempTable.Release(tbl)
|
||||
end
|
||||
|
||||
function Thread._UnpackAndReleaseSafeTempTable(self, tbl)
|
||||
assert(self._safeTempTables[tbl])
|
||||
self._safeTempTables[tbl] = nil
|
||||
return TempTable.UnpackAndRelease(tbl)
|
||||
end
|
||||
|
||||
function Thread._GuardDatabaseQuery(self, query)
|
||||
assert(not self._guardedQueries[query])
|
||||
self._guardedQueries[query] = true
|
||||
end
|
||||
|
||||
function Thread._UnguardDatabaseQuery(self, query)
|
||||
assert(self._guardedQueries[query])
|
||||
self._guardedQueries[query] = nil
|
||||
end
|
||||
|
||||
function Thread._Exit(self)
|
||||
assert(self:_IsAlive())
|
||||
self._state = "DEAD"
|
||||
self:_Cleanup()
|
||||
Log.Info("Thread finished: %s", self:_ToLogStr())
|
||||
if self == private.runningThread then
|
||||
coroutine.yield(YIELD_VALUE)
|
||||
error("Shouldn't get here")
|
||||
elseif self._returnValue then
|
||||
TempTable.Release(self._returnValue)
|
||||
self._returnValue = nil
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.StartScheduler()
|
||||
if private.schedulerFrame:IsVisible() then
|
||||
return
|
||||
end
|
||||
Log.Info("Starting scheduler")
|
||||
private.schedulerFrame:Show()
|
||||
end
|
||||
|
||||
function private.RunScheduler(_, elapsed)
|
||||
-- don't run any threads while in combat
|
||||
if InCombatLockdown() then
|
||||
return
|
||||
end
|
||||
local startTime = debugprofilestop()
|
||||
local numReadyThreads = 0
|
||||
wipe(private.queue)
|
||||
|
||||
-- go through all the threads, update their state, and add the ready ones into the queue
|
||||
for _, thread in pairs(private.threads) do
|
||||
thread:_UpdateState(elapsed)
|
||||
if thread:_CanRun() then
|
||||
numReadyThreads = numReadyThreads + 1
|
||||
tinsert(private.queue, thread)
|
||||
end
|
||||
end
|
||||
|
||||
local remainingTime = min(elapsed * 1000 * MAX_TIME_USAGE_RATIO, MAX_QUANTUM_MS)
|
||||
while remainingTime > 0.01 do
|
||||
local ranThread = false
|
||||
for i = #private.queue, 1, -1 do
|
||||
local thread = private.queue[i]
|
||||
if thread:_CanRun() then
|
||||
local quantum = remainingTime / numReadyThreads
|
||||
local elapsedTime = thread:_Run(quantum)
|
||||
thread._cpuTimeUsed = thread._cpuTimeUsed + elapsedTime
|
||||
remainingTime = remainingTime - min(elapsedTime, quantum)
|
||||
-- any thread which ran excessively long should be ignored for future loops
|
||||
if elapsedTime > EXCESSIVE_TIME_USED_RATIO * quantum and elapsedTime > quantum + 1 then
|
||||
if elapsedTime > EXCESSIVE_TIME_LOG_THRESHOLD_MS then
|
||||
local line = Debug.GetStackLevelLocation(2, thread._co)
|
||||
Log.Warn("Thread %s ran too long (%.1f/%.1f): %s", thread._name, elapsedTime, quantum, line or "?")
|
||||
end
|
||||
tremove(private.queue, i)
|
||||
end
|
||||
ranThread = true
|
||||
end
|
||||
end
|
||||
if not ranThread then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local hasAliveThread = false
|
||||
for _, thread in pairs(private.threads) do
|
||||
if thread:_IsAlive() then
|
||||
hasAliveThread = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not hasAliveThread then
|
||||
Log.Info("Stopping the scheduler")
|
||||
private.schedulerFrame:Hide()
|
||||
end
|
||||
|
||||
local timeTaken = debugprofilestop() - startTime
|
||||
if timeTaken > SCHEDULER_TIME_WARNING_THRESHOLD_MS then
|
||||
Log.Warn("Scheduler took %.2fms", timeTaken)
|
||||
end
|
||||
end
|
||||
|
||||
function private.ProcessEvent(self, event, ...)
|
||||
local startTime = debugprofilestop()
|
||||
for _, thread in pairs(private.threads) do
|
||||
thread:_ProcessEvent(event, ...)
|
||||
end
|
||||
local timeTaken = debugprofilestop() - startTime
|
||||
if timeTaken > SCHEDULER_TIME_WARNING_THRESHOLD_MS then
|
||||
Log.Warn("Scheduler took %.2fms to process %s", timeTaken, tostring(event))
|
||||
end
|
||||
end
|
||||
|
||||
function private.OnFutureDone(future)
|
||||
for _, thread in pairs(private.threads) do
|
||||
thread:_OnFutureDone(future)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Driver Frame
|
||||
-- ============================================================================
|
||||
|
||||
do
|
||||
private.schedulerFrame = CreateFrame("Frame")
|
||||
private.schedulerFrame:Hide()
|
||||
private.schedulerFrame:SetScript("OnUpdate", private.RunScheduler)
|
||||
private.schedulerFrame:SetScript("OnEvent", private.ProcessEvent)
|
||||
end
|
||||
126
LibTSM/UI/Tooltip.lua
Normal file
126
LibTSM/UI/Tooltip.lua
Normal file
@@ -0,0 +1,126 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- UI tooltip functions.
|
||||
-- @module Tooltip
|
||||
|
||||
local _, TSM = ...
|
||||
local Tooltip = TSM.Init("UI.Tooltip")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local Vararg = TSM.Include("Util.Vararg")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local private = {
|
||||
currentParent = nil,
|
||||
registeredEvent = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Shows a tooltip which is anchored to the frame.
|
||||
-- @tparam Frame parent The parent and anchor frame for the tooltip
|
||||
-- @tparam ?number|string|function data The tooltip information which can be either an itemId,
|
||||
-- itemString, raw text string, or function which returns one of the other options
|
||||
-- @tparam ?boolean noWrapping Disables wrapping of text lines
|
||||
-- @tparam[opt=0] number xOffset An extra x offset to apply to the anchor of the tooltip
|
||||
function Tooltip.Show(parent, data, noWrapping, xOffset)
|
||||
if not data then
|
||||
return
|
||||
elseif type(data) == "function" then
|
||||
local funcNoWrapping, funcXOffset = false, 0
|
||||
data, funcNoWrapping, funcXOffset = data()
|
||||
noWrapping = noWrapping or funcNoWrapping
|
||||
xOffset = xOffset or funcXOffset
|
||||
end
|
||||
local showCompare = false
|
||||
GameTooltip:SetOwner(parent, "ANCHOR_PRESERVE")
|
||||
GameTooltip:ClearAllPoints()
|
||||
GameTooltip:SetPoint("LEFT", parent, "RIGHT", xOffset or 0, 0)
|
||||
if type(data) == "number" then
|
||||
GameTooltip:SetHyperlink("item:"..data)
|
||||
showCompare = true
|
||||
elseif type(data) == "string" and strfind(data, "^craft:") then
|
||||
data = strmatch(data, "craft:(%d+)")
|
||||
GameTooltip:SetCraftSpell(tonumber(data))
|
||||
elseif type(data) == "string" and strfind(data, "^enchant:") then
|
||||
GameTooltip:SetHyperlink(data)
|
||||
elseif type(data) == "string" and strfind(data, "^currency:") then
|
||||
GameTooltip:SetCurrencyByID(strmatch(data, "currency:(%d+)"))
|
||||
elseif type(data) == "string" and (strfind(data, "^\124c.+\124Hitem:") or ItemString.IsItem(data)) then
|
||||
GameTooltip:SetHyperlink(ItemInfo.GetLink(data))
|
||||
showCompare = true
|
||||
elseif type(data) == "string" and (strfind(data, "^\124c.+\124Hbattlepet:") or ItemString.IsPet(data)) then
|
||||
if strmatch(data, "p:") then
|
||||
data = ItemInfo.GetLink(data)
|
||||
end
|
||||
local _, speciesID, level, breedQuality, maxHealth, power, speed = strsplit(":", data)
|
||||
BattlePetToolTip_Show(tonumber(speciesID), tonumber(level) or 0, tonumber(breedQuality) or 0, tonumber(maxHealth) or 0, tonumber(power) or 0, tonumber(speed) or 0, gsub(gsub(data, "^(.*)%[", ""), "%](.*)$", ""))
|
||||
else
|
||||
for _, line in Vararg.Iterator(strsplit("\n", data)) do
|
||||
local textLeft, textRight = strsplit(TSM.CONST.TOOLTIP_SEP, line)
|
||||
if textRight then
|
||||
GameTooltip:AddDoubleLine(textLeft, textRight, 1, 1, 1, 1, 1, 1)
|
||||
else
|
||||
GameTooltip:AddLine(textLeft, 1, 1, 1, not noWrapping)
|
||||
end
|
||||
end
|
||||
end
|
||||
GameTooltip:Show()
|
||||
private.currentParent = parent
|
||||
if showCompare then
|
||||
assert(not private.registeredEvent)
|
||||
private.registeredEvent = true
|
||||
Event.Register("MODIFIER_STATE_CHANGED", private.UpdateCompareState)
|
||||
private.UpdateCompareState()
|
||||
end
|
||||
end
|
||||
|
||||
--- Hides the current tooltip.
|
||||
function Tooltip.Hide()
|
||||
if private.registeredEvent then
|
||||
Event.Unregister("MODIFIER_STATE_CHANGED", private.UpdateCompareState)
|
||||
private.registeredEvent = false
|
||||
end
|
||||
private.currentParent = nil
|
||||
GameTooltip:SetOwner(UIParent, "ANCHOR_PRESERVE")
|
||||
GameTooltip:ClearAllPoints()
|
||||
GameTooltip:SetPoint("CENTER")
|
||||
GameTooltip:Hide()
|
||||
if not TSM.IsWowClassic() then
|
||||
BattlePetTooltip:ClearAllPoints()
|
||||
BattlePetTooltip:SetPoint("CENTER")
|
||||
BattlePetTooltip:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
--- Checks if the tooltip is visible.
|
||||
-- @tparam[opt=nil] table frame An optional parent frame to check against
|
||||
-- @treturn boolean Whether or not the tooltip is visible
|
||||
function Tooltip.IsVisible(frame)
|
||||
if frame then
|
||||
return private.currentParent == frame
|
||||
else
|
||||
return private.currentParent and true or false
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.UpdateCompareState()
|
||||
if private.currentParent and GameTooltip:IsVisible() and IsShiftKeyDown() and not GameTooltip:IsEquippedItem() then
|
||||
GameTooltip_ShowCompareItem(GameTooltip)
|
||||
else
|
||||
GameTooltip_HideShoppingTooltips(GameTooltip)
|
||||
end
|
||||
end
|
||||
139
LibTSM/UI/UIElements.lua
Normal file
139
LibTSM/UI/UIElements.lua
Normal file
@@ -0,0 +1,139 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- UI element functions.
|
||||
-- @module UIElements
|
||||
|
||||
local _, TSM = ...
|
||||
local UIElements = TSM.Init("UI.UIElements")
|
||||
local ObjectPool = TSM.Include("Util.ObjectPool")
|
||||
local Table = TSM.Include("Util.Table")
|
||||
local private = {
|
||||
elementClasses = {},
|
||||
objectPools = {},
|
||||
namedElements = {},
|
||||
activeFrameElementMap = {},
|
||||
debugNameElementLookup = {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
|
||||
--- Registers a UI Element subclass.
|
||||
-- @tparam Element class The element subclass
|
||||
function UIElements.Register(class)
|
||||
private.elementClasses[class.__name] = class
|
||||
end
|
||||
|
||||
--- Creates a new UI element.
|
||||
-- @tparam string elementType The name of the element class
|
||||
-- @tparam string id The id to assign to the element
|
||||
-- @param ... Tags to set on the element
|
||||
-- @treturn Element The created UI element object
|
||||
function UIElements.New(elementType, id, ...)
|
||||
return private.NewElementHelper(elementType, id, nil, ...)
|
||||
end
|
||||
|
||||
--- Creates a new named UI element.
|
||||
-- @tparam string elementType The name of the element class
|
||||
-- @tparam string id The id to assign to the element
|
||||
-- @tparam string name The global name of the element
|
||||
-- @treturn Element The created UI element object
|
||||
function UIElements.NewNamed(elementType, id, name, ...)
|
||||
assert(name)
|
||||
return private.NewElementHelper(elementType, id, name, ...)
|
||||
end
|
||||
|
||||
--- Recycles a UI element.
|
||||
-- @tparam Element element The UI element object
|
||||
function UIElements.Recycle(element)
|
||||
private.activeFrameElementMap[element:_GetBaseFrame()] = nil
|
||||
if not Table.KeyByValue(private.namedElements, element) then
|
||||
private.objectPools[element.__class]:Recycle(element)
|
||||
end
|
||||
end
|
||||
|
||||
--- Gets a UI element by its frame (for TSM's frame stack).
|
||||
-- @tparam table frame The frame
|
||||
-- @treturn ?Element The element or nil if the frame doesn't correspond to an element
|
||||
function UIElements.GetByFrame(frame)
|
||||
return private.activeFrameElementMap[frame]
|
||||
end
|
||||
|
||||
--- Creates a WoW UI object and tracks it for easier debugging.
|
||||
-- @tparam Element element The TSM UI element object
|
||||
-- @tparam string frameType The type of the WoW UI object
|
||||
-- @tparam ?string frameName The global name
|
||||
-- @tparam ?table parentFrame The parent WoW UI object
|
||||
-- @tparam ?string inheritsFrame A WoW UI template to inherit from
|
||||
-- @treturn table The WoW UI object
|
||||
function UIElements.CreateFrame(element, frameType, frameName, parentFrame, inheritsFrame)
|
||||
local isNamed = frameName and true or false
|
||||
if not frameName then
|
||||
-- generate a debug name to aid in later lookup
|
||||
frameName = private.GetDebugName(element)
|
||||
end
|
||||
private.debugNameElementLookup[frameName] = element
|
||||
local frame = CreateFrame(frameType, frameName, parentFrame, inheritsFrame)
|
||||
if not isNamed then
|
||||
_G[frameName] = nil
|
||||
end
|
||||
return frame
|
||||
end
|
||||
|
||||
--- Creates a WoW UI font string and tracks it for easier debugging.
|
||||
-- @tparam Element element The TSM UI element object
|
||||
-- @tparam table parentFrame The parent WoW UI frame
|
||||
-- @treturn table The WoW UI font string
|
||||
function UIElements.CreateFontString(element, parentFrame)
|
||||
local name = private.GetDebugName(element)
|
||||
private.debugNameElementLookup[name] = element
|
||||
local fontString = parentFrame:CreateFontString(name)
|
||||
_G[name] = nil
|
||||
return fontString
|
||||
end
|
||||
|
||||
--- Gets the debug name translations.
|
||||
-- @tparam table result The result table
|
||||
function UIElements.GetDebugNameTranslation(result)
|
||||
for name, element in pairs(private.debugNameElementLookup) do
|
||||
result[name] = "<"..tostring(element)..">"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.NewElementHelper(elementType, id, name, ...)
|
||||
local class = private.elementClasses[elementType]
|
||||
local element = nil
|
||||
if name then
|
||||
private.namedElements[name] = private.namedElements[name] or class(name)
|
||||
element = private.namedElements[name]
|
||||
assert(_G[name] == element:_GetBaseFrame())
|
||||
else
|
||||
if not private.objectPools[class] then
|
||||
private.objectPools[class] = ObjectPool.New("UI_"..class.__name, class, 1)
|
||||
end
|
||||
element = private.objectPools[class]:Get()
|
||||
end
|
||||
private.activeFrameElementMap[element:_GetBaseFrame()] = element
|
||||
element:SetId(id)
|
||||
element:SetTags(...)
|
||||
element:Acquire()
|
||||
return element
|
||||
end
|
||||
|
||||
function private.GetDebugName(element)
|
||||
return "TSM_UI_ELEMENT:"..element.__class.__name..":"..format("%06x", random(0, 2 ^ 24 - 1))
|
||||
end
|
||||
90
LibTSM/Util/Analytics.lua
Normal file
90
LibTSM/Util/Analytics.lua
Normal file
@@ -0,0 +1,90 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Analytics = TSM.Init("Util.Analytics")
|
||||
local Debug = TSM.Include("Util.Debug")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local private = {
|
||||
events = {},
|
||||
lastEventTime = nil,
|
||||
argsTemp = {},
|
||||
session = time(),
|
||||
sequenceNumber = 1,
|
||||
}
|
||||
local MAX_ANALYTICS_AGE = 14 * 24 * 60 * 60 -- 2 weeks
|
||||
local HIT_TYPE_IS_VALID = {
|
||||
AC = true,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Analytics.Action(name, ...)
|
||||
private.InsertHit("AC", name, ...)
|
||||
end
|
||||
|
||||
function Analytics.Save(appDB)
|
||||
appDB.analytics = appDB.analytics or {updateTime=0, data={}}
|
||||
if private.lastEventTime then
|
||||
appDB.analytics.updateTime = private.lastEventTime
|
||||
end
|
||||
-- remove any events which are too old
|
||||
for i = #appDB.analytics.data, 1, -1 do
|
||||
local _, _, timeStr = strsplit(",", appDB.analytics.data[i])
|
||||
local eventTime = (tonumber(timeStr) or 0) / 1000
|
||||
if eventTime < time() - MAX_ANALYTICS_AGE then
|
||||
tremove(appDB.analytics.data, i)
|
||||
end
|
||||
end
|
||||
for _, event in ipairs(private.events) do
|
||||
tinsert(appDB.analytics.data, event)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.InsertHit(hitType, ...)
|
||||
assert(HIT_TYPE_IS_VALID[hitType])
|
||||
wipe(private.argsTemp)
|
||||
for i = 1, select("#", ...) do
|
||||
local arg = select(i, ...)
|
||||
local argType = type(arg)
|
||||
if argType == "string" then
|
||||
-- remove non-printable and non-ascii characters
|
||||
arg = gsub(arg, "[^ -~]", "")
|
||||
-- remove characters we don't want in the JSON
|
||||
arg = gsub(arg, "[\\\"]", "")
|
||||
arg = private.AddQuotes(arg)
|
||||
elseif argType == "number" then
|
||||
-- pass
|
||||
elseif argType == "boolean" then
|
||||
arg = tostring(arg)
|
||||
else
|
||||
error("Invalid arg type: "..argType)
|
||||
end
|
||||
tinsert(private.argsTemp, arg)
|
||||
end
|
||||
Log.Info("%s %s", hitType, strjoin(" ", tostringall(...)))
|
||||
hitType = private.AddQuotes(hitType)
|
||||
local version = private.AddQuotes(TSM.GetVersion() or "???")
|
||||
local timeMs = Debug.GetTimeMilliseconds()
|
||||
local jsonStr = strjoin(",", hitType, version, timeMs, private.session, private.sequenceNumber, unpack(private.argsTemp))
|
||||
tinsert(private.events, "["..jsonStr.."]")
|
||||
private.sequenceNumber = private.sequenceNumber + 1
|
||||
private.lastEventTime = time()
|
||||
end
|
||||
|
||||
function private.AddQuotes(str)
|
||||
return "\""..str.."\""
|
||||
end
|
||||
99
LibTSM/Util/CSV.lua
Normal file
99
LibTSM/Util/CSV.lua
Normal file
@@ -0,0 +1,99 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- CSV Functions
|
||||
-- @module CSV
|
||||
|
||||
local _, TSM = ...
|
||||
local CSV = TSM.Init("Util.CSV")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local private = {}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function CSV.EncodeStart(keys)
|
||||
local context = TempTable.Acquire()
|
||||
context.keys = keys
|
||||
context.lines = TempTable.Acquire()
|
||||
context.lineParts = TempTable.Acquire()
|
||||
tinsert(context.lines, table.concat(keys, ","))
|
||||
return context
|
||||
end
|
||||
|
||||
function CSV.EncodeAddRowData(context, data)
|
||||
wipe(context.lineParts)
|
||||
for _, key in ipairs(context.keys) do
|
||||
tinsert(context.lineParts, data[key] or "")
|
||||
end
|
||||
tinsert(context.lines, table.concat(context.lineParts, ","))
|
||||
end
|
||||
|
||||
function CSV.EncodeAddRowDataRaw(context, ...)
|
||||
tinsert(context.lines, strjoin(",", ...))
|
||||
end
|
||||
|
||||
function CSV.EncodeEnd(context)
|
||||
local result = table.concat(context.lines, "\n")
|
||||
TempTable.Release(context.lineParts)
|
||||
TempTable.Release(context.lines)
|
||||
TempTable.Release(context)
|
||||
return result
|
||||
end
|
||||
|
||||
function CSV.Encode(keys, data)
|
||||
local context = CSV.EncodeStart(keys)
|
||||
for _, row in ipairs(data) do
|
||||
CSV.EncodeAddRowData(context, row)
|
||||
end
|
||||
return CSV.EncodeEnd(context)
|
||||
end
|
||||
|
||||
function CSV.DecodeStart(str, fields)
|
||||
local func = gmatch(str, strrep("([^\n,]+),", #fields - 1).."([^\n,]+)(,?[^\n,]*)")
|
||||
if strjoin(",", func()) ~= table.concat(fields, ",").."," then
|
||||
return
|
||||
end
|
||||
local context = TempTable.Acquire()
|
||||
context.func = func
|
||||
context.extraArgPos = #fields + 1
|
||||
context.result = true
|
||||
return context
|
||||
end
|
||||
|
||||
function CSV.DecodeIterator(context)
|
||||
return private.DecodeIteratorHelper, context
|
||||
end
|
||||
|
||||
function CSV.DecodeEnd(context)
|
||||
local result = context.result
|
||||
TempTable.Release(context)
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.DecodeIteratorHelper(context)
|
||||
return private.DecodeIteratorHelper2(context, context.func())
|
||||
end
|
||||
|
||||
function private.DecodeIteratorHelper2(context, v1, ...)
|
||||
if not v1 then
|
||||
return
|
||||
end
|
||||
if select(context.extraArgPos, v1, ...) ~= "" then
|
||||
context.result = false
|
||||
return
|
||||
end
|
||||
return v1, ...
|
||||
end
|
||||
209
LibTSM/Util/Color.lua
Normal file
209
LibTSM/Util/Color.lua
Normal file
@@ -0,0 +1,209 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Color Functions.
|
||||
-- @module Color
|
||||
|
||||
local _, TSM = ...
|
||||
local Color = TSM.Init("Util.Color")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
local HSLuv = TSM.Include("Util.HSLuv")
|
||||
local private = {
|
||||
context = {},
|
||||
transparent = nil,
|
||||
fullWhite = nil,
|
||||
fullBlack = nil,
|
||||
}
|
||||
local TINT_VALUES = {
|
||||
SELECTED = 15,
|
||||
HOVER = 12,
|
||||
SELECTED_HOVER = 20,
|
||||
DISABLED = -40,
|
||||
}
|
||||
local OPACITY_VALUES = {
|
||||
HIGHLIGHT = 50,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Metatable
|
||||
-- ============================================================================
|
||||
|
||||
local COLOR_MT = {
|
||||
__index = {
|
||||
GetTint = function(self, tintPct)
|
||||
local context = private.context[self]
|
||||
assert(context.hex)
|
||||
if type(tintPct) == "string" then
|
||||
local sign, tintKey = strmatch(tintPct, "^([%+%-])([A-Z_]+)$")
|
||||
assert(TINT_VALUES[tintKey])
|
||||
tintPct = tonumber(TINT_VALUES[tintKey]) * (sign == "+" and 1 or -1)
|
||||
end
|
||||
assert(type(tintPct) == "number")
|
||||
if tintPct == 0 then
|
||||
return self
|
||||
end
|
||||
if not context.tints[tintPct] then
|
||||
local l = context.l + tintPct
|
||||
l = min(l, 100)
|
||||
assert(private.IsValidValue(l, 100))
|
||||
local r, g, b = HSLuv.ToRGB(context.h, context.s, l)
|
||||
context.tints[tintPct] = private.NewColorHelper(r, g, b, context.a)
|
||||
end
|
||||
return context.tints[tintPct]
|
||||
end,
|
||||
GetOpacity = function(self, opacityPct)
|
||||
local context = private.context[self]
|
||||
assert(context.hex)
|
||||
if type(opacityPct) == "string" then
|
||||
assert(OPACITY_VALUES[opacityPct])
|
||||
opacityPct = tonumber(OPACITY_VALUES[opacityPct])
|
||||
end
|
||||
assert(private.IsValidValue(opacityPct, 100))
|
||||
if opacityPct == 100 then
|
||||
return self
|
||||
end
|
||||
if not context.opacities[opacityPct] then
|
||||
assert(context.a == 255)
|
||||
local a = Math.Round(255 * opacityPct / 100)
|
||||
assert(private.IsValidValue(a, 255))
|
||||
context.opacities[opacityPct] = private.NewColorHelper(context.r, context.g, context.b, a)
|
||||
end
|
||||
return context.opacities[opacityPct]
|
||||
end,
|
||||
GetRGBA = function(self)
|
||||
local context = private.context[self]
|
||||
assert(context.hex)
|
||||
return context.r, context.g, context.b, context.a
|
||||
end,
|
||||
GetFractionalRGBA = function(self)
|
||||
local context = private.context[self]
|
||||
assert(context.hex)
|
||||
return context.r / 255, context.g / 255, context.b / 255, context.a / 255
|
||||
end,
|
||||
IsLight = function(self)
|
||||
local context = private.context[self]
|
||||
assert(context.hex)
|
||||
return context.l >= 50
|
||||
end,
|
||||
GetHex = function(self)
|
||||
local context = private.context[self]
|
||||
assert(context.hex)
|
||||
return context.hex
|
||||
end,
|
||||
ColorText = function(self, text)
|
||||
return self:GetTextColorPrefix()..text.."|r"
|
||||
end,
|
||||
GetTextColorPrefix = function(self)
|
||||
local context = private.context[self]
|
||||
assert(context.hex)
|
||||
return format("|c%02x%02x%02x%02x", context.a, context.r, context.g, context.b)
|
||||
end,
|
||||
Equals = function(self, other)
|
||||
return self:GetHex() == other:GetHex()
|
||||
end,
|
||||
},
|
||||
__newindex = function(self, key, value) error("Color cannot be modified") end,
|
||||
__metatable = false,
|
||||
__tostring = function(self)
|
||||
local context = private.context[self]
|
||||
return "Color:"..context.hex
|
||||
end,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Color:OnModuleLoad(function()
|
||||
private.transparent = private.NewColorHelper(0, 0, 0, 0)
|
||||
private.fullWhite = private.NewColorHelper(255, 255, 255, 255)
|
||||
private.fullBlack = private.NewColorHelper(0, 0, 0, 255)
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Create a new color object from a hex string.
|
||||
-- @tparam string hex The hex string which represents the color (in either "#AARRGGBB" or "#RRGGBB" format)
|
||||
-- @treturn Color The color object
|
||||
function Color.NewFromHex(hex)
|
||||
return private.NewColorHelper(private.HexToRGBA(hex))
|
||||
end
|
||||
|
||||
--- Returns whether or not the argument is a color object.
|
||||
-- @param arg The argument to check
|
||||
-- @treturn boolean Whether or not the argument is a color object.
|
||||
function Color.IsInstance(arg)
|
||||
return type(arg) == "table" and private.context[arg] and true or false
|
||||
end
|
||||
|
||||
--- Gets a predefined fully-transparent color.
|
||||
-- @treturn Color The color object
|
||||
function Color.GetTransparent()
|
||||
return private.transparent
|
||||
end
|
||||
|
||||
--- Gets a predefined fully-opaque white color.
|
||||
-- @treturn Color The color object
|
||||
function Color.GetFullWhite()
|
||||
return private.fullWhite
|
||||
end
|
||||
|
||||
--- Gets a predefined fully-opaque black color.
|
||||
-- @treturn Color The color object
|
||||
function Color.GetFullBlack()
|
||||
return private.fullBlack
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.NewColorHelper(r, g, b, a)
|
||||
assert(private.IsValidValue(r, 255) and private.IsValidValue(g, 255) and private.IsValidValue(b, 255) and private.IsValidValue(a, 255))
|
||||
if a == 0 then
|
||||
assert(r == 0 and g == 0 and b == 0, "Invalid color with alpha of 0")
|
||||
end
|
||||
local context = {
|
||||
tints = {},
|
||||
opacities = {},
|
||||
r = r,
|
||||
g = g,
|
||||
b = b,
|
||||
a = a,
|
||||
h = nil,
|
||||
s = nil,
|
||||
l = nil,
|
||||
hex = private.RGBAToHex(r, g, b, a),
|
||||
}
|
||||
context.h, context.s, context.l = HSLuv.FromRGB(r, g, b)
|
||||
context.hex = private.RGBAToHex(r, g, b, a)
|
||||
local color = setmetatable({}, COLOR_MT)
|
||||
private.context[color] = context
|
||||
return color
|
||||
end
|
||||
|
||||
function private.IsValidValue(value, maxValue)
|
||||
return type(value) == "number" and value >= 0 and value <= maxValue and value == floor(value)
|
||||
end
|
||||
|
||||
function private.HexToRGBA(hex)
|
||||
local a, r, g, b = strmatch(strlower(hex), "^#([0-9a-f]?[0-9a-f]?)([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])$")
|
||||
return tonumber(r, 16), tonumber(g, 16), tonumber(b, 16), tonumber(a ~= "" and a or "ff", 16)
|
||||
end
|
||||
|
||||
function private.RGBAToHex(r, g, b, a)
|
||||
return format("#%02x%02x%02x%02x", Math.Round(a), Math.Round(r), Math.Round(g), Math.Round(b))
|
||||
end
|
||||
127
LibTSM/Util/Database.lua
Normal file
127
LibTSM/Util/Database.lua
Normal file
@@ -0,0 +1,127 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Database Functions.
|
||||
-- @module Database
|
||||
|
||||
local _, TSM = ...
|
||||
local Database = TSM.Init("Util.Database")
|
||||
local Constants = TSM.Include("Util.DatabaseClasses.Constants")
|
||||
local Schema = TSM.Include("Util.DatabaseClasses.Schema")
|
||||
local Table = TSM.Include("Util.DatabaseClasses.DBTable")
|
||||
local private = {
|
||||
dbByNameLookup = {},
|
||||
infoNameDB = nil,
|
||||
infoFieldDB = nil,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Database:OnModuleLoad(function()
|
||||
-- create our info database tables - don't use :Commit() to create these since that'll insert into these tables
|
||||
private.infoNameDB = Database.NewSchema("DEBUG_INFO_NAME")
|
||||
:AddUniqueStringField("name")
|
||||
:AddIndex("name")
|
||||
:Commit()
|
||||
private.infoFieldDB = Database.NewSchema("DEBUG_INFO_FIELD")
|
||||
:AddStringField("dbName")
|
||||
:AddStringField("field")
|
||||
:AddStringField("type")
|
||||
:AddStringField("attributes")
|
||||
:AddNumberField("order")
|
||||
:AddIndex("dbName")
|
||||
:Commit()
|
||||
|
||||
Table.SetCreateCallback(private.OnTableCreate)
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Database.NewSchema(name)
|
||||
return Schema.Get(name)
|
||||
end
|
||||
|
||||
function Database.OtherFieldQueryParam(otherFieldName)
|
||||
return Constants.OTHER_FIELD_QUERY_PARAM, otherFieldName
|
||||
end
|
||||
|
||||
function Database.BoundQueryParam()
|
||||
return Constants.BOUND_QUERY_PARAM
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Debug Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Database.InfoNameIterator()
|
||||
return private.infoNameDB:NewQuery()
|
||||
:Select("name")
|
||||
:OrderBy("name", true)
|
||||
:IteratorAndRelease()
|
||||
end
|
||||
|
||||
function Database.CreateInfoFieldQuery(dbName)
|
||||
return private.infoFieldDB:NewQuery()
|
||||
:Equal("dbName", dbName)
|
||||
end
|
||||
|
||||
function Database.GetNumRows(dbName)
|
||||
return private.dbByNameLookup[dbName]:GetNumRows()
|
||||
end
|
||||
|
||||
function Database.GetNumActiveQueries(dbName)
|
||||
return #private.dbByNameLookup[dbName]._queries
|
||||
end
|
||||
|
||||
function Database.CreateDBQuery(dbName)
|
||||
return private.dbByNameLookup[dbName]:NewQuery()
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.OnTableCreate(tbl, schema)
|
||||
local name = schema:_GetName()
|
||||
assert(not private.dbByNameLookup[name], "A database with this name already exists")
|
||||
private.dbByNameLookup[name] = tbl
|
||||
|
||||
private.infoNameDB:NewRow()
|
||||
:SetField("name", name)
|
||||
:Create()
|
||||
|
||||
for index, fieldName, fieldType, isIndex, isUnique in schema:_FieldIterator() do
|
||||
local fieldAttributes = (isIndex and isUnique and "index,unique") or (isIndex and "index") or (isUnique and "unique") or ""
|
||||
private.infoFieldDB:NewRow()
|
||||
:SetField("dbName", name)
|
||||
:SetField("field", fieldName)
|
||||
:SetField("type", fieldType)
|
||||
:SetField("attributes", fieldAttributes)
|
||||
:SetField("order", index)
|
||||
:Create()
|
||||
end
|
||||
for fieldName in schema:_MultiFieldIndexIterator() do
|
||||
private.infoFieldDB:NewRow()
|
||||
:SetField("dbName", name)
|
||||
:SetField("field", fieldName)
|
||||
:SetField("type", "-")
|
||||
:SetField("attributes", "multi-field index")
|
||||
:SetField("order", -1)
|
||||
:Create()
|
||||
end
|
||||
end
|
||||
12
LibTSM/Util/DatabaseClasses/Constants.lua
Normal file
12
LibTSM/Util/DatabaseClasses/Constants.lua
Normal file
@@ -0,0 +1,12 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Constants = TSM.Init("Util.DatabaseClasses.Constants")
|
||||
Constants.DB_INDEX_FIELD_SEP = "~"
|
||||
Constants.DB_INDEX_VALUE_SEP = "\001"
|
||||
Constants.OTHER_FIELD_QUERY_PARAM = newproxy()
|
||||
Constants.BOUND_QUERY_PARAM = newproxy()
|
||||
1247
LibTSM/Util/DatabaseClasses/DBTable.lua
Normal file
1247
LibTSM/Util/DatabaseClasses/DBTable.lua
Normal file
File diff suppressed because it is too large
Load Diff
1619
LibTSM/Util/DatabaseClasses/Query.lua
Normal file
1619
LibTSM/Util/DatabaseClasses/Query.lua
Normal file
File diff suppressed because it is too large
Load Diff
447
LibTSM/Util/DatabaseClasses/QueryClause.lua
Normal file
447
LibTSM/Util/DatabaseClasses/QueryClause.lua
Normal file
@@ -0,0 +1,447 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local QueryClause = TSM.Init("Util.DatabaseClasses.QueryClause")
|
||||
local Constants = TSM.Include("Util.DatabaseClasses.Constants")
|
||||
local Util = TSM.Include("Util.DatabaseClasses.Util")
|
||||
local ObjectPool = TSM.Include("Util.ObjectPool")
|
||||
local LibTSMClass = TSM.Include("LibTSMClass")
|
||||
local DatabaseQueryClause = LibTSMClass.DefineClass("DatabaseQueryClause")
|
||||
local private = {
|
||||
objectPool = nil,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
QueryClause:OnModuleLoad(function()
|
||||
private.objectPool = ObjectPool.New("DATABASE_QUERY_CLAUSES", DatabaseQueryClause, 1)
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function QueryClause.Get(query, parent)
|
||||
local clause = private.objectPool:Get()
|
||||
clause:_Acquire(query, parent)
|
||||
return clause
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Class Method Methods
|
||||
-- ============================================================================
|
||||
|
||||
function DatabaseQueryClause.__init(self)
|
||||
self._query = nil
|
||||
self._operation = nil
|
||||
self._parent = nil
|
||||
-- comparison
|
||||
self._field = nil
|
||||
self._value = nil
|
||||
self._boundValue = nil
|
||||
self._otherField = nil
|
||||
-- or / and
|
||||
self._subClauses = {}
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._Acquire(self, query, parent)
|
||||
self._query = query
|
||||
self._parent = parent
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._Release(self)
|
||||
self._query = nil
|
||||
self._operation = nil
|
||||
self._parent = nil
|
||||
self._field = nil
|
||||
self._value = nil
|
||||
self._boundValue = nil
|
||||
self._otherField = nil
|
||||
for _, clause in ipairs(self._subClauses) do
|
||||
clause:_Release()
|
||||
end
|
||||
wipe(self._subClauses)
|
||||
private.objectPool:Recycle(self)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Public Class Method
|
||||
-- ============================================================================
|
||||
|
||||
function DatabaseQueryClause.Equal(self, field, value, otherField)
|
||||
return self:_SetComparisonOperation("EQUAL", field, value, otherField)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.NotEqual(self, field, value, otherField)
|
||||
return self:_SetComparisonOperation("NOT_EQUAL", field, value, otherField)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.LessThan(self, field, value, otherField)
|
||||
return self:_SetComparisonOperation("LESS", field, value, otherField)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.LessThanOrEqual(self, field, value, otherField)
|
||||
return self:_SetComparisonOperation("LESS_OR_EQUAL", field, value, otherField)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.GreaterThan(self, field, value, otherField)
|
||||
return self:_SetComparisonOperation("GREATER", field, value, otherField)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.GreaterThanOrEqual(self, field, value, otherField)
|
||||
return self:_SetComparisonOperation("GREATER_OR_EQUAL", field, value, otherField)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.Matches(self, field, value)
|
||||
return self:_SetComparisonOperation("MATCHES", field, value)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.Contains(self, field, value)
|
||||
return self:_SetComparisonOperation("CONTAINS", field, value)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.StartsWith(self, field, value)
|
||||
return self:_SetComparisonOperation("STARTS_WITH", field, value)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.IsNil(self, field)
|
||||
return self:_SetComparisonOperation("IS_NIL", field)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.IsNotNil(self, field)
|
||||
return self:_SetComparisonOperation("IS_NOT_NIL", field)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.Custom(self, func, arg)
|
||||
return self:_SetComparisonOperation("CUSTOM", func, arg)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.HashEqual(self, fields, value)
|
||||
return self:_SetComparisonOperation("HASH_EQUAL", fields, value)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.InTable(self, field, value)
|
||||
return self:_SetComparisonOperation("IN_TABLE", field, value)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.NotInTable(self, field, value)
|
||||
return self:_SetComparisonOperation("NOT_IN_TABLE", field, value)
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.Or(self)
|
||||
return self:_SetSubClauseOperation("OR")
|
||||
end
|
||||
|
||||
function DatabaseQueryClause.And(self)
|
||||
return self:_SetSubClauseOperation("AND")
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Class Method
|
||||
-- ============================================================================
|
||||
|
||||
function DatabaseQueryClause._GetParent(self)
|
||||
return self._parent
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._IsTrue(self, row)
|
||||
local value = self._value
|
||||
if value == Constants.BOUND_QUERY_PARAM then
|
||||
value = self._boundValue
|
||||
elseif value == Constants.OTHER_FIELD_QUERY_PARAM then
|
||||
value = row:GetField(self._otherField)
|
||||
end
|
||||
local operation = self._operation
|
||||
if operation == "EQUAL" then
|
||||
return row[self._field] == value
|
||||
elseif operation == "NOT_EQUAL" then
|
||||
return row[self._field] ~= value
|
||||
elseif operation == "LESS" then
|
||||
return row[self._field] < value
|
||||
elseif operation == "LESS_OR_EQUAL" then
|
||||
return row[self._field] <= value
|
||||
elseif operation == "GREATER" then
|
||||
return row[self._field] > value
|
||||
elseif operation == "GREATER_OR_EQUAL" then
|
||||
return row[self._field] >= value
|
||||
elseif operation == "MATCHES" then
|
||||
return strfind(strlower(row[self._field]), value) and true or false
|
||||
elseif operation == "CONTAINS" then
|
||||
return strfind(strlower(row[self._field]), value, 1, true) and true or false
|
||||
elseif operation == "STARTS_WITH" then
|
||||
return strsub(strlower(row[self._field]), 1, #value) == value
|
||||
elseif operation == "IS_NIL" then
|
||||
return row[self._field] == nil
|
||||
elseif operation == "IS_NOT_NIL" then
|
||||
return row[self._field] ~= nil
|
||||
elseif operation == "CUSTOM" then
|
||||
return self._field(row, value) and true or false
|
||||
elseif operation == "HASH_EQUAL" then
|
||||
return row:CalculateHash(self._field) == value
|
||||
elseif operation == "IN_TABLE" then
|
||||
return value[row[self._field]] ~= nil
|
||||
elseif operation == "NOT_IN_TABLE" then
|
||||
return value[row[self._field]] == nil
|
||||
elseif operation == "OR" then
|
||||
for i = 1, #self._subClauses do
|
||||
if self._subClauses[i]:_IsTrue(row) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
elseif operation == "AND" then
|
||||
for i = 1, #self._subClauses do
|
||||
if not self._subClauses[i]:_IsTrue(row) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
else
|
||||
error("Invalid operation: " .. tostring(operation))
|
||||
end
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._GetIndexValue(self, indexField)
|
||||
if self._operation == "EQUAL" then
|
||||
if self._field ~= indexField then
|
||||
return
|
||||
end
|
||||
if self._value == Constants.OTHER_FIELD_QUERY_PARAM then
|
||||
return
|
||||
elseif self._value == Constants.BOUND_QUERY_PARAM then
|
||||
local result = Util.ToIndexValue(self._boundValue)
|
||||
return result, result
|
||||
else
|
||||
local result = Util.ToIndexValue(self._value)
|
||||
return result, result
|
||||
end
|
||||
elseif self._operation == "LESS_OR_EQUAL" then
|
||||
if self._field ~= indexField then
|
||||
return
|
||||
end
|
||||
if self._value == Constants.OTHER_FIELD_QUERY_PARAM then
|
||||
return
|
||||
elseif self._value == Constants.BOUND_QUERY_PARAM then
|
||||
return nil, Util.ToIndexValue(self._boundValue)
|
||||
else
|
||||
return nil, Util.ToIndexValue(self._value)
|
||||
end
|
||||
elseif self._operation == "GREATER_OR_EQUAL" then
|
||||
if self._field ~= indexField then
|
||||
return
|
||||
end
|
||||
if self._value == Constants.OTHER_FIELD_QUERY_PARAM then
|
||||
return
|
||||
elseif self._value == Constants.BOUND_QUERY_PARAM then
|
||||
return Util.ToIndexValue(self._boundValue), nil
|
||||
else
|
||||
return Util.ToIndexValue(self._value), nil
|
||||
end
|
||||
elseif self._operation == "STARTS_WITH" then
|
||||
if self._field ~= indexField then
|
||||
return
|
||||
end
|
||||
local minValue = nil
|
||||
if self._value == Constants.OTHER_FIELD_QUERY_PARAM then
|
||||
return
|
||||
elseif self._value == Constants.BOUND_QUERY_PARAM then
|
||||
minValue = Util.ToIndexValue(self._boundValue)
|
||||
else
|
||||
minValue = Util.ToIndexValue(self._value)
|
||||
end
|
||||
-- calculate the max value
|
||||
assert(gsub(minValue, "\255", "") ~= "")
|
||||
local maxValue = nil
|
||||
for i = #minValue, 1, -1 do
|
||||
if strsub(minValue, i, i) ~= "\255" then
|
||||
maxValue = strsub(minValue, 1, i - 1)..strrep("\255", #minValue - i + 1)
|
||||
break
|
||||
end
|
||||
end
|
||||
return minValue, maxValue
|
||||
elseif self._operation == "OR" then
|
||||
local numSubClauses = #self._subClauses
|
||||
if numSubClauses == 0 then
|
||||
return
|
||||
end
|
||||
-- all of the subclauses need to support the same index
|
||||
local valueMin, valueMax = self._subClauses[1]:_GetIndexValue(indexField)
|
||||
for i = 2, numSubClauses do
|
||||
local subClauseValueMin, subClauseValueMax = self._subClauses[i]:_GetIndexValue(indexField)
|
||||
if subClauseValueMin ~= valueMin or subClauseValueMax ~= valueMax then
|
||||
return
|
||||
end
|
||||
end
|
||||
return valueMin, valueMax
|
||||
elseif self._operation == "AND" then
|
||||
-- get the most constrained range of index values from the subclauses
|
||||
local valueMin, valueMax = nil, nil
|
||||
for _, subClause in ipairs(self._subClauses) do
|
||||
local subClauseValueMin, subClauseValueMax = subClause:_GetIndexValue(indexField)
|
||||
if subClauseValueMin ~= nil and (valueMin == nil or subClauseValueMin > valueMin) then
|
||||
valueMin = subClauseValueMin
|
||||
end
|
||||
if subClauseValueMax ~= nil and (valueMax == nil or subClauseValueMax < valueMax) then
|
||||
valueMax = subClauseValueMax
|
||||
end
|
||||
end
|
||||
return valueMin, valueMax
|
||||
end
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._GetTrigramIndexValue(self, indexField)
|
||||
if self._operation == "EQUAL" then
|
||||
if self._field ~= indexField then
|
||||
return
|
||||
end
|
||||
if self._value == Constants.OTHER_FIELD_QUERY_PARAM then
|
||||
return
|
||||
elseif self._value == Constants.BOUND_QUERY_PARAM then
|
||||
return self._boundValue
|
||||
else
|
||||
return self._value
|
||||
end
|
||||
elseif self._operation == "CONTAINS" then
|
||||
if self._field ~= indexField then
|
||||
return
|
||||
end
|
||||
if self._value == Constants.OTHER_FIELD_QUERY_PARAM then
|
||||
return
|
||||
elseif self._value == Constants.BOUND_QUERY_PARAM then
|
||||
return self._boundValue
|
||||
else
|
||||
return self._value
|
||||
end
|
||||
elseif self._operation == "OR" then
|
||||
-- all of the subclauses need to support the same trigram value
|
||||
local value = nil
|
||||
for i = 1, #self._subClauses do
|
||||
local subClause = self._subClauses[i]
|
||||
local subClauseValue = subClause:_GetTrigramIndexValue(indexField)
|
||||
if not subClauseValue then
|
||||
return
|
||||
end
|
||||
if i == 1 then
|
||||
value = subClauseValue
|
||||
elseif subClauseValue ~= value then
|
||||
return
|
||||
end
|
||||
end
|
||||
return value
|
||||
elseif self._operation == "AND" then
|
||||
-- at least one of the subclauses need to support the trigram
|
||||
for _, subClause in ipairs(self._subClauses) do
|
||||
local value = subClause:_GetTrigramIndexValue(indexField)
|
||||
if value then
|
||||
return value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._IsStrictIndex(self, indexField, indexValueMin, indexValueMax)
|
||||
if self._value == Constants.OTHER_FIELD_QUERY_PARAM then
|
||||
return false
|
||||
end
|
||||
if self._operation == "EQUAL" and self._field == indexField and indexValueMin == indexValueMax then
|
||||
if self._value == Constants.BOUND_QUERY_PARAM then
|
||||
return Util.ToIndexValue(self._boundValue) == indexValueMin
|
||||
else
|
||||
return Util.ToIndexValue(self._value) == indexValueMin
|
||||
end
|
||||
elseif self._operation == "GREATER_OR_EQUAL" and self._field == indexField then
|
||||
if self._value == Constants.BOUND_QUERY_PARAM then
|
||||
return Util.ToIndexValue(self._boundValue) == indexValueMin
|
||||
else
|
||||
return Util.ToIndexValue(self._value) == indexValueMin
|
||||
end
|
||||
elseif self._operation == "LESS_OR_EQUAL" and self._field == indexField then
|
||||
if self._value == Constants.BOUND_QUERY_PARAM then
|
||||
return Util.ToIndexValue(self._boundValue) == indexValueMax
|
||||
else
|
||||
return Util.ToIndexValue(self._value) == indexValueMax
|
||||
end
|
||||
elseif self._operation == "OR" and #self._subClauses == 1 then
|
||||
return self._subClauses[1]:_IsStrictIndex(indexField, indexValueMin, indexValueMax)
|
||||
elseif self._operation == "AND" then
|
||||
-- must be strict for all subclauses
|
||||
for _, subClause in ipairs(self._subClauses) do
|
||||
if not subClause:_IsStrictIndex(indexField, indexValueMin, indexValueMax) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._UsesField(self, field)
|
||||
if field == self._field or self._operation == "CUSTOM" then
|
||||
return true
|
||||
end
|
||||
if self._operation == "OR" or self._operation == "AND" then
|
||||
for i = 1, #self._subClauses do
|
||||
if self._subClauses[i]:_UsesField(field) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._InsertSubClause(self, subClause)
|
||||
assert(self._operation == "OR" or self._operation == "AND")
|
||||
tinsert(self._subClauses, subClause)
|
||||
self._query:_MarkResultStale()
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._SetComparisonOperation(self, operation, field, value, otherField)
|
||||
assert(not self._operation)
|
||||
assert(value == Constants.OTHER_FIELD_QUERY_PARAM or not otherField)
|
||||
self._operation = operation
|
||||
self._field = field
|
||||
self._value = value
|
||||
self._otherField = otherField
|
||||
self._query:_MarkResultStale()
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._SetSubClauseOperation(self, operation)
|
||||
assert(not self._operation)
|
||||
self._operation = operation
|
||||
assert(#self._subClauses == 0)
|
||||
self._query:_MarkResultStale()
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseQueryClause._BindParams(self, ...)
|
||||
if self._value == Constants.BOUND_QUERY_PARAM then
|
||||
self._boundValue = ...
|
||||
self._query:_MarkResultStale()
|
||||
return 1
|
||||
end
|
||||
local valuesUsed = 0
|
||||
for _, clause in ipairs(self._subClauses) do
|
||||
valuesUsed = valuesUsed + clause:_BindParams(select(valuesUsed + 1, ...))
|
||||
end
|
||||
self._query:_MarkResultStale()
|
||||
return valuesUsed
|
||||
end
|
||||
297
LibTSM/Util/DatabaseClasses/QueryResultRow.lua
Normal file
297
LibTSM/Util/DatabaseClasses/QueryResultRow.lua
Normal file
@@ -0,0 +1,297 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local QueryResultRow = TSM.Init("Util.DatabaseClasses.QueryResultRow")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local ObjectPool = TSM.Include("Util.ObjectPool")
|
||||
local private = {
|
||||
context = {},
|
||||
objectPool = nil,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Metatable
|
||||
-- ============================================================================
|
||||
|
||||
local ROW_PROTOTYPE = {
|
||||
_Acquire = function(self, db, query, newRowUUID)
|
||||
local context = private.context[self]
|
||||
context.db = db
|
||||
context.query = query
|
||||
context.isNewRow = newRowUUID and true or false
|
||||
if newRowUUID then
|
||||
context.uuid = newRowUUID
|
||||
end
|
||||
end,
|
||||
|
||||
_Release = function(self)
|
||||
local context = private.context[self]
|
||||
context.db = nil
|
||||
context.query = nil
|
||||
context.isNewRow = nil
|
||||
context.uuid = nil
|
||||
assert(not context.pendingChanges)
|
||||
wipe(self)
|
||||
end,
|
||||
|
||||
Release = function(self)
|
||||
self:_Release()
|
||||
private.objectPool:Recycle(self)
|
||||
end,
|
||||
|
||||
_SetUUID = function(self, uuid)
|
||||
local context = private.context[self]
|
||||
context.uuid = uuid
|
||||
wipe(self)
|
||||
end,
|
||||
|
||||
GetUUID = function(self)
|
||||
local uuid = private.context[self].uuid
|
||||
assert(uuid)
|
||||
return uuid
|
||||
end,
|
||||
|
||||
GetQuery = function(self)
|
||||
local query = private.context[self].query
|
||||
assert(query)
|
||||
return query
|
||||
end,
|
||||
|
||||
GetField = function(self, field, ...)
|
||||
if ... then
|
||||
error("GetField() only supports 1 field")
|
||||
end
|
||||
return self[field]
|
||||
end,
|
||||
|
||||
GetFields = function(self, ...)
|
||||
local numFields = select("#", ...)
|
||||
local field1, field2, field3, field4, field5, field6, field7, field8, field9, field10 = ...
|
||||
if numFields == 0 then
|
||||
return
|
||||
elseif numFields == 1 then
|
||||
return self[field1]
|
||||
elseif numFields == 2 then
|
||||
return self[field1], self[field2]
|
||||
elseif numFields == 3 then
|
||||
return self[field1], self[field2], self[field3]
|
||||
elseif numFields == 4 then
|
||||
return self[field1], self[field2], self[field3], self[field4]
|
||||
elseif numFields == 5 then
|
||||
return self[field1], self[field2], self[field3], self[field4], self[field5]
|
||||
elseif numFields == 6 then
|
||||
return self[field1], self[field2], self[field3], self[field4], self[field5], self[field6]
|
||||
elseif numFields == 7 then
|
||||
return self[field1], self[field2], self[field3], self[field4], self[field5], self[field6], self[field7]
|
||||
elseif numFields == 8 then
|
||||
return self[field1], self[field2], self[field3], self[field4], self[field5], self[field6], self[field7], self[field8]
|
||||
elseif numFields == 9 then
|
||||
return self[field1], self[field2], self[field3], self[field4], self[field5], self[field6], self[field7], self[field8], self[field9]
|
||||
elseif numFields == 10 then
|
||||
return self[field1], self[field2], self[field3], self[field4], self[field5], self[field6], self[field7], self[field8], self[field9], self[field10]
|
||||
else
|
||||
error("GetFields() only supports up to 10 fields")
|
||||
end
|
||||
end,
|
||||
|
||||
CalculateHash = function(self, fields)
|
||||
local hash = nil
|
||||
for _, field in ipairs(fields) do
|
||||
hash = Math.CalculateHash(self[field], hash)
|
||||
end
|
||||
return hash
|
||||
end,
|
||||
|
||||
SetField = function(self, field, value)
|
||||
local context = private.context[self]
|
||||
local isSameValue = not context.isNewRow and value == self[field]
|
||||
if isSameValue and not context.pendingChanges then
|
||||
-- setting to the same value, so ignore this call
|
||||
return self
|
||||
end
|
||||
if context.db:_IsSmartMapField(field) then
|
||||
error(format("Cannot set smart map field (%s)", tostring(field)), 3)
|
||||
end
|
||||
local fieldType = context.db:_GetFieldType(field)
|
||||
if not fieldType then
|
||||
error(format("Field %s doesn't exist", tostring(field)), 3)
|
||||
elseif fieldType ~= type(value) then
|
||||
error(format("Field %s should be a %s, got %s", tostring(field), tostring(fieldType), type(value)), 2)
|
||||
end
|
||||
if isSameValue then
|
||||
-- setting the field to its original value, so clear any pending change
|
||||
context.pendingChanges[field] = nil
|
||||
if not next(context.pendingChanges) then
|
||||
TempTable.Release(context.pendingChanges)
|
||||
context.pendingChanges = nil
|
||||
end
|
||||
else
|
||||
context.pendingChanges = context.pendingChanges or TempTable.Acquire()
|
||||
context.pendingChanges[field] = value
|
||||
end
|
||||
return self
|
||||
end,
|
||||
|
||||
_CreateHelper = function(self)
|
||||
local context = private.context[self]
|
||||
assert(context.isNewRow and context.pendingChanges)
|
||||
|
||||
-- make sure all the fields are set
|
||||
for field in context.db:FieldIterator() do
|
||||
assert(context.pendingChanges[field] ~= nil)
|
||||
end
|
||||
|
||||
-- apply all the pending changes
|
||||
for field, value in pairs(context.pendingChanges) do
|
||||
-- cache this new value
|
||||
rawset(self, field, value)
|
||||
end
|
||||
|
||||
TempTable.Release(context.pendingChanges)
|
||||
context.pendingChanges = nil
|
||||
context.isNewRow = nil
|
||||
end,
|
||||
|
||||
Create = function(self)
|
||||
self:_CreateHelper()
|
||||
private.context[self].db:_InsertRow(self)
|
||||
end,
|
||||
|
||||
CreateAndClone = function(self)
|
||||
self:_CreateHelper()
|
||||
local clonedRow = self:Clone()
|
||||
private.context[self].db:_InsertRow(self)
|
||||
return clonedRow
|
||||
end,
|
||||
|
||||
Update = function(self)
|
||||
local context = private.context[self]
|
||||
assert(not context.isNewRow)
|
||||
if not context.pendingChanges then
|
||||
return
|
||||
end
|
||||
|
||||
-- apply all the pending changes
|
||||
local oldValues = TempTable.Acquire()
|
||||
for field, value in pairs(context.pendingChanges) do
|
||||
oldValues[field] = self[field]
|
||||
-- cache this new value
|
||||
rawset(self, field, value)
|
||||
end
|
||||
|
||||
TempTable.Release(context.pendingChanges)
|
||||
context.pendingChanges = nil
|
||||
context.db:_UpdateRow(self, oldValues)
|
||||
TempTable.Release(oldValues)
|
||||
return self
|
||||
end,
|
||||
|
||||
CreateOrUpdateAndRelease = function(self)
|
||||
local context = private.context[self]
|
||||
if context.isNewRow then
|
||||
self:Create()
|
||||
else
|
||||
self:Update()
|
||||
self:Release()
|
||||
end
|
||||
end,
|
||||
|
||||
Clone = function(self)
|
||||
local context = private.context[self]
|
||||
assert(not context.isNewRow and not context.pendingChanges)
|
||||
local newRow = QueryResultRow.Get()
|
||||
newRow:_Acquire(context.db)
|
||||
newRow:_SetUUID(context.uuid)
|
||||
return newRow
|
||||
end,
|
||||
}
|
||||
|
||||
local ROW_MT = {
|
||||
-- getter
|
||||
__index = function(self, key)
|
||||
if key == nil then
|
||||
error("Attempt to get nil key")
|
||||
end
|
||||
if ROW_PROTOTYPE[key] then
|
||||
return ROW_PROTOTYPE[key]
|
||||
end
|
||||
-- cache the value
|
||||
local context = private.context[self]
|
||||
if context.isNewRow then
|
||||
error("Getting value on a new row: "..tostring(key))
|
||||
end
|
||||
local result = nil
|
||||
if context.query then
|
||||
-- use the query to lookup the result
|
||||
result = context.query:_GetResultRowData(context.uuid, key)
|
||||
else
|
||||
-- we're not tied to a query so this should be a local DB field
|
||||
if not context.db:_GetFieldType(key) then
|
||||
error("Invalid field: "..tostring(key), 2)
|
||||
end
|
||||
result = context.db:GetRowFieldByUUID(context.uuid, key)
|
||||
end
|
||||
if result ~= nil then
|
||||
rawset(self, key, result)
|
||||
end
|
||||
return result
|
||||
end,
|
||||
-- setter
|
||||
__newindex = function(self, key, value)
|
||||
error("Table is read-only", 2)
|
||||
end,
|
||||
__eq = function(self, other)
|
||||
local uuid = private.context[self].uuid
|
||||
local uuidOther = private.context[other].uuid
|
||||
return uuid and uuidOther and uuid == uuidOther
|
||||
end,
|
||||
__tostring = function(self)
|
||||
local context = private.context[self]
|
||||
return "QueryResultRow:"..strmatch(tostring(context), "table:[^0-9a-fA-F]*([0-9a-fA-F]+)")..":"..self:GetUUID()
|
||||
end,
|
||||
__metatable = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
QueryResultRow:OnModuleLoad(function()
|
||||
private.objectPool = ObjectPool.New("DATABASE_QUERY_RESULT_ROWS", private.CreateNew, 2)
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function QueryResultRow.Get()
|
||||
return private.objectPool:Get()
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.CreateNew()
|
||||
local row = setmetatable({}, ROW_MT)
|
||||
private.context[row] = {
|
||||
db = nil,
|
||||
query = nil,
|
||||
isNewRow = nil,
|
||||
uuid = nil,
|
||||
}
|
||||
return row
|
||||
end
|
||||
198
LibTSM/Util/DatabaseClasses/Schema.lua
Normal file
198
LibTSM/Util/DatabaseClasses/Schema.lua
Normal file
@@ -0,0 +1,198 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Schema = TSM.Init("Util.DatabaseClasses.Schema")
|
||||
local Constants = TSM.Include("Util.DatabaseClasses.Constants")
|
||||
local DBTable = TSM.Include("Util.DatabaseClasses.DBTable")
|
||||
local ObjectPool = TSM.Include("Util.ObjectPool")
|
||||
local LibTSMClass = TSM.Include("LibTSMClass")
|
||||
local DatabaseSchema = LibTSMClass.DefineClass("DatabaseSchema")
|
||||
local private = {
|
||||
objectPool = nil,
|
||||
}
|
||||
local FIELD_TYPE_IS_VALID = {
|
||||
string = true,
|
||||
number = true,
|
||||
boolean = true,
|
||||
}
|
||||
local MAX_MULTI_FIELD_INDEX_PARTS = 2
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Modules Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Schema.Get(name)
|
||||
if not private.objectPool then
|
||||
private.objectPool = ObjectPool.New("DATABASE_SCHEMAS", DatabaseSchema, 2)
|
||||
end
|
||||
local schema = private.objectPool:Get()
|
||||
schema:_Acquire(name)
|
||||
return schema
|
||||
end
|
||||
|
||||
function Schema.IsClass(obj)
|
||||
return obj:__isa(DatabaseSchema)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Class Method Methods
|
||||
-- ============================================================================
|
||||
|
||||
function DatabaseSchema.__init(self)
|
||||
self._name = nil
|
||||
self._fieldList = {}
|
||||
self._fieldTypeLookup = {}
|
||||
self._isIndex = {}
|
||||
self._isUnique = {}
|
||||
self._smartMapLookup = {}
|
||||
self._smartMapInputLookup = {}
|
||||
self._trigramIndexField = nil
|
||||
end
|
||||
|
||||
function DatabaseSchema._Acquire(self, name)
|
||||
assert(type(name) == "string")
|
||||
self._name = name
|
||||
end
|
||||
|
||||
function DatabaseSchema._Release(self)
|
||||
self._name = nil
|
||||
wipe(self._fieldList)
|
||||
wipe(self._fieldTypeLookup)
|
||||
wipe(self._isIndex)
|
||||
wipe(self._isUnique)
|
||||
wipe(self._smartMapLookup)
|
||||
wipe(self._smartMapInputLookup)
|
||||
self._trigramIndexField = nil
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Public Class Method
|
||||
-- ============================================================================
|
||||
|
||||
function DatabaseSchema.Release(self)
|
||||
self:_Release()
|
||||
private.objectPool:Recycle(self)
|
||||
end
|
||||
|
||||
function DatabaseSchema.AddStringField(self, fieldName)
|
||||
self:_AddField("string", fieldName)
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseSchema.AddNumberField(self, fieldName)
|
||||
self:_AddField("number", fieldName)
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseSchema.AddBooleanField(self, fieldName)
|
||||
self:_AddField("boolean", fieldName)
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseSchema.AddUniqueStringField(self, fieldName)
|
||||
self:_AddField("string", fieldName, true)
|
||||
self._isUnique[fieldName] = true
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseSchema.AddUniqueNumberField(self, fieldName)
|
||||
self:_AddField("number", fieldName, true)
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseSchema.AddSmartMapField(self, fieldName, map, inputFieldName)
|
||||
assert(self._fieldTypeLookup[inputFieldName] == map:GetKeyType())
|
||||
self:_AddField(map:GetValueType(), fieldName)
|
||||
self._smartMapLookup[fieldName] = map
|
||||
self._smartMapInputLookup[fieldName] = inputFieldName
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseSchema.AddIndex(self, ...)
|
||||
local numFields = select("#", ...)
|
||||
assert(numFields > 0)
|
||||
assert(numFields <= MAX_MULTI_FIELD_INDEX_PARTS, "Unsupported number of fields in index")
|
||||
for i = 1, numFields do
|
||||
local fieldName = select(i, ...)
|
||||
assert(self._fieldTypeLookup[fieldName])
|
||||
end
|
||||
self._isIndex[strjoin(Constants.DB_INDEX_FIELD_SEP, ...)] = true
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseSchema.AddTrigramIndex(self, fieldName)
|
||||
assert(not self._trigramIndexField)
|
||||
self._trigramIndexField = fieldName
|
||||
return self
|
||||
end
|
||||
|
||||
function DatabaseSchema.Commit(self)
|
||||
local db = DBTable.Create(self)
|
||||
self:Release()
|
||||
return db
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Class Method
|
||||
-- ============================================================================
|
||||
|
||||
function DatabaseSchema._GetName(self)
|
||||
return self._name
|
||||
end
|
||||
|
||||
function DatabaseSchema._AddField(self, fieldType, fieldName, isUnique)
|
||||
assert(FIELD_TYPE_IS_VALID[fieldType])
|
||||
assert(type(fieldName) == "string" and strsub(fieldName, 1, 1) ~= "_" and not strmatch(fieldName, Constants.DB_INDEX_FIELD_SEP))
|
||||
assert(not self._fieldTypeLookup[fieldName])
|
||||
tinsert(self._fieldList, fieldName)
|
||||
self._fieldTypeLookup[fieldName] = fieldType
|
||||
if isUnique then
|
||||
self._isUnique[fieldName] = true
|
||||
end
|
||||
end
|
||||
|
||||
function DatabaseSchema._FieldIterator(self)
|
||||
return private.FieldIterator, self, 0
|
||||
end
|
||||
|
||||
function DatabaseSchema._MultiFieldIndexIterator(self)
|
||||
return private.MultiFieldIndexIterator, self, nil
|
||||
end
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.FieldIterator(self, index)
|
||||
index = index + 1
|
||||
if index > #self._fieldList then
|
||||
return
|
||||
end
|
||||
local fieldName = self._fieldList[index]
|
||||
return index, fieldName, self._fieldTypeLookup[fieldName], self._isIndex[fieldName], self._isUnique[fieldName], self._smartMapLookup[fieldName], self._smartMapInputLookup[fieldName]
|
||||
end
|
||||
|
||||
function private.MultiFieldIndexIterator(self, fieldName)
|
||||
while true do
|
||||
fieldName = next(self._isIndex, fieldName)
|
||||
if not fieldName then
|
||||
return
|
||||
end
|
||||
if strmatch(fieldName, Constants.DB_INDEX_FIELD_SEP) then
|
||||
return fieldName
|
||||
end
|
||||
end
|
||||
end
|
||||
31
LibTSM/Util/DatabaseClasses/Util.lua
Normal file
31
LibTSM/Util/DatabaseClasses/Util.lua
Normal file
@@ -0,0 +1,31 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Util = TSM.Init("Util.DatabaseClasses.Util")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Util.ToIndexValue(value)
|
||||
if value == nil then
|
||||
return nil
|
||||
end
|
||||
local valueType = type(value)
|
||||
if valueType == "string" then
|
||||
return strlower(value)
|
||||
elseif valueType == "boolean" then
|
||||
return value and 1 or 0
|
||||
elseif valueType == "number" and Math.IsNan(value) then
|
||||
return nil
|
||||
else
|
||||
return value
|
||||
end
|
||||
end
|
||||
85
LibTSM/Util/Debug.lua
Normal file
85
LibTSM/Util/Debug.lua
Normal file
@@ -0,0 +1,85 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Debug Functions
|
||||
-- @module Debug
|
||||
|
||||
local _, TSM = ...
|
||||
local Debug = TSM.Init("Util.Debug")
|
||||
local private = {
|
||||
startSystemTimeMs = floor(GetTime() * 1000),
|
||||
startTimeMs = time() * 1000 + (floor(GetTime() * 1000) % 1000),
|
||||
}
|
||||
local ADDON_NAME_SHORTEN_PATTERN = {
|
||||
-- shorten "TradeSkillMaster" to "TSM"
|
||||
[".-lMaster\\"] = "TSM\\",
|
||||
[".-r\\LibTSM"] = "TSM\\LibTSM",
|
||||
}
|
||||
local IGNORED_STACK_LEVEL_MATCHERS = {
|
||||
-- ignore wrapper code from LibTSMClass
|
||||
"LibTSMClass%.lua:",
|
||||
}
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Gets the current time in milliseconds since epoch
|
||||
-- The time returned could be up to a second off absolutely, but relative times are guarenteed to be accurate.
|
||||
-- @treturn number The current time in milliseconds since epoch
|
||||
function Debug.GetTimeMilliseconds()
|
||||
local systemTimeMs = floor(GetTime() * 1000)
|
||||
return private.startTimeMs + (systemTimeMs - private.startSystemTimeMs)
|
||||
end
|
||||
|
||||
--- Gets the location string for the specified stack level
|
||||
-- @tparam number targetLevel The stack level to get the location for
|
||||
-- @tparam[opt] thread thread The thread to get the location for
|
||||
-- @treturn string The location string
|
||||
function Debug.GetStackLevelLocation(targetLevel, thread)
|
||||
targetLevel = targetLevel + 1
|
||||
assert(targetLevel > 0)
|
||||
local level = 1
|
||||
while true do
|
||||
local stackLine = nil
|
||||
if thread then
|
||||
stackLine = debugstack(thread, level, 1, 0)
|
||||
else
|
||||
stackLine = debugstack(level, 1, 0)
|
||||
end
|
||||
if not stackLine or stackLine == "" then
|
||||
return
|
||||
end
|
||||
if TSM.IsWowClassic() or TSM.__IS_TEST_ENV then
|
||||
stackLine = strmatch(stackLine, "^%.*([^:]+:%d+):")
|
||||
else
|
||||
local numSubs = nil
|
||||
stackLine, numSubs = gsub(stackLine, "^%[string \"@([^%.]+%.lua)\"%](:%d+).*$", "%1%2")
|
||||
stackLine = numSubs > 0 and stackLine or nil
|
||||
end
|
||||
if stackLine then
|
||||
local ignored = false
|
||||
for _, matchStr in ipairs(IGNORED_STACK_LEVEL_MATCHERS) do
|
||||
if strmatch(stackLine, matchStr) then
|
||||
ignored = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not ignored then
|
||||
targetLevel = targetLevel - 1
|
||||
if targetLevel == 0 then
|
||||
stackLine = gsub(stackLine, "/", "\\")
|
||||
for matchStr, replaceStr in pairs(ADDON_NAME_SHORTEN_PATTERN) do
|
||||
stackLine = gsub(stackLine, matchStr, replaceStr)
|
||||
end
|
||||
return stackLine
|
||||
end
|
||||
end
|
||||
end
|
||||
level = level + 1
|
||||
end
|
||||
end
|
||||
177
LibTSM/Util/Delay.lua
Normal file
177
LibTSM/Util/Delay.lua
Normal file
@@ -0,0 +1,177 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Delay Functions
|
||||
-- @module Delay
|
||||
|
||||
local _, TSM = ...
|
||||
local Delay = TSM.Init("Util.Delay")
|
||||
local Debug = TSM.Include("Util.Debug")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local private = {
|
||||
delays = {},
|
||||
frameNumber = 0,
|
||||
frame = nil,
|
||||
}
|
||||
local CALLBACK_TIME_WARNING_THRESHOLD_MS = 20
|
||||
local MIN_TIME_DURATION = 0.0001
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Delay:OnModuleLoad(function()
|
||||
private.frame = CreateFrame("Frame")
|
||||
private.frame:SetScript("OnUpdate", private.ProcessDelays)
|
||||
private.frame:Show()
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Call a callback after a set amount of time.
|
||||
-- Note that the delay may be up to 1 frame time longer than requested.
|
||||
-- @tparam[opt] string label A label for the delay (to allow it to be cancelled)
|
||||
-- @tparam number duration The amount of time to delay for
|
||||
-- @tparam function callback The function called when the delay is finished
|
||||
-- @tparam[opt] number repeatDelay The amount of time to set this delay for once it completes
|
||||
-- @param[opt=nil] context A context value to pass along to the callback (ignored if the delay was previously started)
|
||||
function Delay.AfterTime(label, duration, callback, repeatDelay, context)
|
||||
if type(label) == "number" then
|
||||
-- no label specified
|
||||
assert(not repeatDelay)
|
||||
duration, callback, repeatDelay, context = label, duration, callback, repeatDelay
|
||||
label = nil
|
||||
end
|
||||
assert(type(duration) == "number" and type(callback) == "function" and (not repeatDelay or type(repeatDelay) == "number"))
|
||||
repeatDelay = repeatDelay and max(repeatDelay, MIN_TIME_DURATION) or nil
|
||||
duration = max(duration, MIN_TIME_DURATION)
|
||||
|
||||
if label then
|
||||
for _, delay in ipairs(private.delays) do
|
||||
if delay.label == label then
|
||||
-- delay is already running, so just return
|
||||
return
|
||||
end
|
||||
end
|
||||
else
|
||||
label = Debug.GetStackLevelLocation(2)
|
||||
end
|
||||
|
||||
local delayTbl = TempTable.Acquire()
|
||||
delayTbl.endTime = GetTime() + duration
|
||||
delayTbl.callback = callback
|
||||
delayTbl.label = label
|
||||
delayTbl.repeatDelay = repeatDelay
|
||||
delayTbl.context = context
|
||||
tinsert(private.delays, delayTbl)
|
||||
end
|
||||
|
||||
--- Call a callback after a set number of frames.
|
||||
-- Note that the delay may be up to 1 frame time longer than requested.
|
||||
-- @tparam[opt] string label A label for the delay (to allow it to be cancelled)
|
||||
-- @tparam number duration The number of frames to delay for
|
||||
-- @tparam function callback The function called when the delay is finished
|
||||
-- @tparam[opt] number repeatDelay The number of frames to set this delay for once it completes
|
||||
-- @param[opt=nil] context A context value to pass along to the callback (ignored if the delay was previously started)
|
||||
function Delay.AfterFrame(label, duration, callback, repeatDelay, context)
|
||||
if type(label) == "number" then
|
||||
-- no label specified
|
||||
assert(not repeatDelay)
|
||||
duration, callback, repeatDelay, context = label, duration, callback, repeatDelay
|
||||
label = nil
|
||||
end
|
||||
assert(type(duration) == "number" and type(callback) == "function" and (not repeatDelay or type(repeatDelay) == "number"))
|
||||
repeatDelay = repeatDelay and max(repeatDelay, 1) or nil
|
||||
duration = max(duration, 1)
|
||||
|
||||
if label then
|
||||
for _, delay in ipairs(private.delays) do
|
||||
if delay.label == label then
|
||||
-- delay is already running, so just return
|
||||
return
|
||||
end
|
||||
end
|
||||
else
|
||||
label = Debug.GetStackLevelLocation(2)
|
||||
assert(label)
|
||||
end
|
||||
|
||||
local delayTbl = TempTable.Acquire()
|
||||
delayTbl.endFrame = private.frameNumber + duration
|
||||
delayTbl.callback = callback
|
||||
delayTbl.label = label
|
||||
delayTbl.repeatDelay = repeatDelay
|
||||
delayTbl.context = context
|
||||
tinsert(private.delays, delayTbl)
|
||||
end
|
||||
|
||||
--- Cancel a delay.
|
||||
-- This works for both time and frame delays.
|
||||
-- @tparam string label The label the delay was created with
|
||||
function Delay.Cancel(label)
|
||||
for i, delay in ipairs(private.delays) do
|
||||
if delay.label == label then
|
||||
TempTable.Release(tremove(private.delays, i))
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Main Delay Callback
|
||||
-- ============================================================================
|
||||
|
||||
function private.ProcessDelays()
|
||||
private.frameNumber = private.frameNumber + 1
|
||||
-- the delays can change as we do our callbacks, so keep looping through them until there are no more pending
|
||||
while true do
|
||||
local pendingLabel, pendingCallback, pendingContext = nil, nil, nil
|
||||
for i, delay in ipairs(private.delays) do
|
||||
assert(delay.endFrame or delay.endTime)
|
||||
if delay.endFrame and delay.endFrame <= private.frameNumber then
|
||||
pendingLabel = delay.label
|
||||
pendingCallback = delay.callback
|
||||
pendingContext = delay.context
|
||||
if delay.repeatDelay then
|
||||
delay.endFrame = private.frameNumber + delay.repeatDelay
|
||||
else
|
||||
TempTable.Release(tremove(private.delays, i))
|
||||
end
|
||||
break
|
||||
elseif delay.endTime and delay.endTime <= GetTime() then
|
||||
pendingLabel = delay.label
|
||||
pendingCallback = delay.callback
|
||||
pendingContext = delay.context
|
||||
if delay.repeatDelay then
|
||||
delay.endTime = GetTime() + delay.repeatDelay
|
||||
else
|
||||
TempTable.Release(tremove(private.delays, i))
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
if not pendingLabel then
|
||||
-- no more pending delays to process
|
||||
assert(not pendingCallback)
|
||||
break
|
||||
end
|
||||
local startTime = debugprofilestop()
|
||||
pendingCallback(pendingContext)
|
||||
local timeTaken = debugprofilestop() - startTime
|
||||
if timeTaken > CALLBACK_TIME_WARNING_THRESHOLD_MS then
|
||||
Log.Warn("Delay callback (%s) took %0.2fms", pendingLabel, timeTaken)
|
||||
end
|
||||
end
|
||||
end
|
||||
112
LibTSM/Util/Event.lua
Normal file
112
LibTSM/Util/Event.lua
Normal file
@@ -0,0 +1,112 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Event Functions
|
||||
-- @module Event
|
||||
|
||||
local _, TSM = ...
|
||||
local Event = TSM.Init("Util.Event")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local private = {
|
||||
registry = {
|
||||
event = {},
|
||||
callback = {},
|
||||
},
|
||||
eventFrame = nil,
|
||||
temp = {},
|
||||
eventQueue = {},
|
||||
processingEvent = false,
|
||||
}
|
||||
local CALLBACK_TIME_WARNING_THRESHOLD_MS = 20
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Registers an event callback.
|
||||
-- @tparam string event The WoW event to register for (i.e. BAG_UPDATE)
|
||||
-- @tparam function callback The function to be called from the event handler
|
||||
function Event.Register(event, callback)
|
||||
assert(type(event) == "string" and event == strupper(event) and type(callback) == "function")
|
||||
-- make sure this event/callback isn't already registered
|
||||
for i = 1, #private.registry.event do
|
||||
assert(private.registry.event[i] ~= event or private.registry.callback[i] ~= callback)
|
||||
end
|
||||
private.eventFrame:RegisterEvent(event)
|
||||
tinsert(private.registry.event, event)
|
||||
tinsert(private.registry.callback, callback)
|
||||
end
|
||||
|
||||
--- Unregisters an event callback.
|
||||
-- @tparam string event The WoW event which the callback was registered for
|
||||
-- @tparam function callback The function which was passed to @{Event.Register} for this event
|
||||
function Event.Unregister(event, callback)
|
||||
assert(type(event) == "string" and event == strupper(event) and type(callback) == "function")
|
||||
local index = nil
|
||||
local shouldUnregister = true
|
||||
for i = 1, #private.registry.event do
|
||||
if private.registry.event[i] == event and private.registry.callback[i] == callback then
|
||||
assert(not index)
|
||||
index = i
|
||||
elseif private.registry.event[i] == event then
|
||||
shouldUnregister = false
|
||||
end
|
||||
end
|
||||
assert(index)
|
||||
tremove(private.registry.event, index)
|
||||
tremove(private.registry.callback, index)
|
||||
if shouldUnregister then
|
||||
private.eventFrame:UnregisterEvent(event)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Event Frame
|
||||
-- ============================================================================
|
||||
|
||||
function private.ProcessEvent(event, ...)
|
||||
-- NOTE: the registered events may change within the callback, so copy them to a temp table
|
||||
wipe(private.temp)
|
||||
for i = 1, #private.registry.event do
|
||||
if private.registry.event[i] == event then
|
||||
tinsert(private.temp, private.registry.callback[i])
|
||||
end
|
||||
end
|
||||
for _, callback in ipairs(private.temp) do
|
||||
local startTime = debugprofilestop()
|
||||
callback(event, ...)
|
||||
local timeTaken = debugprofilestop() - startTime
|
||||
if timeTaken > CALLBACK_TIME_WARNING_THRESHOLD_MS then
|
||||
Log.Warn("Event (%s) callback took %.2fms", event, timeTaken)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function private.EventHandler(_, event, ...)
|
||||
if private.processingEvent then
|
||||
-- we are already in the middle of processing another event, so queue this one up
|
||||
tinsert(private.eventQueue, TempTable.Acquire(event, ...))
|
||||
assert(#private.eventQueue < 50)
|
||||
return
|
||||
end
|
||||
private.processingEvent = true
|
||||
private.ProcessEvent(event, ...)
|
||||
-- process queued events
|
||||
while #private.eventQueue > 0 do
|
||||
local tbl = tremove(private.eventQueue, 1)
|
||||
private.ProcessEvent(TempTable.UnpackAndRelease(tbl))
|
||||
end
|
||||
private.processingEvent = false
|
||||
end
|
||||
|
||||
private.eventFrame = CreateFrame("Frame")
|
||||
private.eventFrame:SetScript("OnEvent", private.EventHandler)
|
||||
private.eventFrame:Show()
|
||||
33
LibTSM/Util/FSM.lua
Normal file
33
LibTSM/Util/FSM.lua
Normal file
@@ -0,0 +1,33 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- FSM Functions.
|
||||
-- @module FSM
|
||||
|
||||
local _, TSM = ...
|
||||
local FSM = TSM.Init("Util.FSM")
|
||||
local Machine = TSM.Include("Util.FSMClasses.Machine")
|
||||
local State = TSM.Include("Util.FSMClasses.State")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Create a new FSM.
|
||||
-- @tparam string name The name of the FSM (for debugging purposes)
|
||||
-- @treturn Machine The FSM object
|
||||
function FSM.New(name)
|
||||
return Machine.Create(name)
|
||||
end
|
||||
|
||||
--- Create a new FSM state.
|
||||
-- @tparam string state The name of the state
|
||||
-- @treturn State The State object
|
||||
function FSM.NewState(state)
|
||||
return State.Create(state)
|
||||
end
|
||||
182
LibTSM/Util/FSMClasses/Machine.lua
Normal file
182
LibTSM/Util/FSMClasses/Machine.lua
Normal file
@@ -0,0 +1,182 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- FSMMachine Class.
|
||||
-- This class allows implementing event-driving finite state machines.
|
||||
-- @classmod FSMMachine
|
||||
|
||||
local _, TSM = ...
|
||||
local Machine = TSM.Init("Util.FSMClasses.Machine")
|
||||
local State = TSM.Include("Util.FSMClasses.State")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local LibTSMClass = TSM.Include("LibTSMClass")
|
||||
local FSMMachine = LibTSMClass.DefineClass("FSMMachine")
|
||||
local private = {
|
||||
eventTransitionHandlerCache = {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Machine.Create(name)
|
||||
return FSMMachine(name)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Class Meta Methods
|
||||
-- ============================================================================
|
||||
|
||||
function FSMMachine.__init(self, name)
|
||||
self._name = name
|
||||
self._currentState = nil
|
||||
self._context = nil
|
||||
self._loggingDisabledCount = 0
|
||||
self._stateObjs = {}
|
||||
self._defaultEvents = {}
|
||||
self._handlingEvent = nil
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Public Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
--- Add an FSM state.
|
||||
-- @tparam FSM self The FSM object
|
||||
-- @tparam FSMState stateObj The FSM state object to add
|
||||
-- @treturn FSM The FSM object
|
||||
function FSMMachine.AddState(self, stateObj)
|
||||
assert(State.IsInstance(stateObj))
|
||||
local name = stateObj:_GetName()
|
||||
assert(not self._stateObjs[name], "state already exists")
|
||||
self._stateObjs[stateObj:_GetName()] = stateObj
|
||||
return self
|
||||
end
|
||||
|
||||
--- Add a default event handler.
|
||||
-- @tparam FSM self The FSM object
|
||||
-- @tparam string event The event name
|
||||
-- @tparam function handler The default event handler
|
||||
-- @treturn FSM The FSM object
|
||||
function FSMMachine.AddDefaultEvent(self, event, handler)
|
||||
assert(not self._defaultEvents[event], "event already exists")
|
||||
self._defaultEvents[event] = handler
|
||||
return self
|
||||
end
|
||||
|
||||
--- Add a simple default event-based transition.
|
||||
-- @tparam FSMMachine self The FSMMachine object
|
||||
-- @tparam string event The event name
|
||||
-- @tparam string toState The state to transition to
|
||||
-- @treturn FSMMachine The FSMMachine object
|
||||
function FSMMachine.AddDefaultEventTransition(self, event, toState)
|
||||
if not private.eventTransitionHandlerCache[toState] then
|
||||
private.eventTransitionHandlerCache[toState] = function(context, ...)
|
||||
return toState, ...
|
||||
end
|
||||
end
|
||||
return self:AddDefaultEvent(event, private.eventTransitionHandlerCache[toState])
|
||||
end
|
||||
|
||||
--- Initialize the FSM.
|
||||
-- @tparam FSM self The FSM object
|
||||
-- @tparam string initialState The name of the initial state
|
||||
-- @param[opt={}] context The FSM context table which gets passed to all state and event handlers
|
||||
-- @treturn FSM The FSM object
|
||||
function FSMMachine.Init(self, initialState, context)
|
||||
assert(self._stateObjs[initialState], "invalid initial state")
|
||||
self._currentState = initialState
|
||||
self._context = context or {}
|
||||
-- validate all the transitions
|
||||
for name, obj in pairs(self._stateObjs) do
|
||||
for _, toState in obj:_ToStateIterator() do
|
||||
assert(self._stateObjs[toState], format("toState doesn't exist (%s -> %s)", name, toState))
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Process an event.
|
||||
-- @tparam FSM self The FSM object
|
||||
-- @tparam string event The name of the event
|
||||
-- @tparam[opt] vararg ... Additional arguments to pass to the handler function
|
||||
-- @treturn FSM The FSM object
|
||||
function FSMMachine.ProcessEvent(self, event, ...)
|
||||
assert(self._currentState, "FSM not initialized")
|
||||
if self._handlingEvent then
|
||||
Log.RaiseStackLevel()
|
||||
Log.Warn("[%s] %s (ignored - handling event - %s)", self._name, event, self._handlingEvent)
|
||||
Log.LowerStackLevel()
|
||||
return self
|
||||
elseif self._inTransition then
|
||||
Log.RaiseStackLevel()
|
||||
Log.Warn("[%s] %s (ignored - in transition)", self._name, event)
|
||||
Log.LowerStackLevel()
|
||||
return self
|
||||
end
|
||||
|
||||
if self._loggingDisabledCount == 0 then
|
||||
Log.RaiseStackLevel()
|
||||
Log.Info("[%s] %s", self._name, event)
|
||||
Log.LowerStackLevel()
|
||||
end
|
||||
self._handlingEvent = event
|
||||
local currentStateObj = self._stateObjs[self._currentState]
|
||||
if currentStateObj:_HasEventHandler(event) then
|
||||
self:_Transition(TempTable.Acquire(currentStateObj:_ProcessEvent(event, self._context, ...)))
|
||||
elseif self._defaultEvents[event] then
|
||||
self:_Transition(TempTable.Acquire(self._defaultEvents[event](self._context, ...)))
|
||||
end
|
||||
self._handlingEvent = nil
|
||||
return self
|
||||
end
|
||||
|
||||
--- Enable or disable event and state transition logs (can be called recursively).
|
||||
-- @tparam FSM self The FSM object
|
||||
-- @tparam boolean enabled Whether or not logging should be enabled
|
||||
-- @treturn FSM The FSM object
|
||||
function FSMMachine.SetLoggingEnabled(self, enabled)
|
||||
self._loggingDisabledCount = self._loggingDisabledCount + (enabled and -1 or 1)
|
||||
assert(self._loggingDisabledCount >= 0)
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
function FSMMachine._Transition(self, eventResult)
|
||||
local result = eventResult
|
||||
while result[1] do
|
||||
-- perform the transition
|
||||
local currentStateObj = self._stateObjs[self._currentState]
|
||||
local toState = tremove(result, 1)
|
||||
local toStateObj = self._stateObjs[toState]
|
||||
if self._loggingDisabledCount == 0 then
|
||||
Log.RaiseStackLevel()
|
||||
Log.RaiseStackLevel()
|
||||
Log.Info("[%s] %s -> %s", self._name, self._currentState, toState)
|
||||
Log.LowerStackLevel()
|
||||
Log.LowerStackLevel()
|
||||
end
|
||||
assert(toStateObj and currentStateObj:_IsTransitionValid(toState), "invalid transition")
|
||||
self._inTransition = true
|
||||
currentStateObj:_Exit(self._context)
|
||||
self._currentState = toState
|
||||
result = TempTable.Acquire(toStateObj:_Enter(self._context, TempTable.UnpackAndRelease(result)))
|
||||
self._inTransition = false
|
||||
end
|
||||
TempTable.Release(result)
|
||||
end
|
||||
157
LibTSM/Util/FSMClasses/State.lua
Normal file
157
LibTSM/Util/FSMClasses/State.lua
Normal file
@@ -0,0 +1,157 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- FSMState Class.
|
||||
-- This class represents a single state within an @{FSMMachine}.
|
||||
-- @classmod FSMState
|
||||
|
||||
local _, TSM = ...
|
||||
local State = TSM.Init("Util.FSMClasses.State")
|
||||
local LibTSMClass = TSM.Include("LibTSMClass")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local FSMState = LibTSMClass.DefineClass("FSMState")
|
||||
local private = {
|
||||
eventTransitionHandlerCache = {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Class Meta Methods
|
||||
-- ============================================================================
|
||||
|
||||
function State.Create(name)
|
||||
return FSMState(name)
|
||||
end
|
||||
|
||||
function State.IsInstance(obj)
|
||||
return obj:__isa(FSMState)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Class Meta Methods
|
||||
-- ============================================================================
|
||||
|
||||
function FSMState.__init(self, name)
|
||||
self._name = name
|
||||
self._onEnterHandler = nil
|
||||
self._onExitHandler = nil
|
||||
self._transitionValid = {}
|
||||
self._events = {}
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Public Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
--- Set the OnEnter handler.
|
||||
-- This function is called upon entering the state.
|
||||
-- @tparam FSMState self The FSM state object
|
||||
-- @tparam ?function|string handler The handler function or a method name to call on the context object
|
||||
-- @treturn FSMState The FSM state object
|
||||
function FSMState.SetOnEnter(self, handler)
|
||||
assert(type(handler) == "function" or type(handler) == "string")
|
||||
self._onEnterHandler = handler
|
||||
return self
|
||||
end
|
||||
|
||||
--- Set the OnExit handler.
|
||||
-- This function is called upon existing the state.
|
||||
-- @tparam FSMState self The FSM state object
|
||||
-- @tparam ?function|string handler The handler function or a method name to call on the context object
|
||||
-- @treturn FSMState The FSM state object
|
||||
function FSMState.SetOnExit(self, handler)
|
||||
assert(type(handler) == "function" or type(handler) == "string")
|
||||
self._onExitHandler = handler
|
||||
return self
|
||||
end
|
||||
|
||||
--- Add a transition.
|
||||
-- @tparam FSMState self The FSM state object
|
||||
-- @tparam string toState The state this transition goes to
|
||||
-- @treturn FSMState The FSM state object
|
||||
function FSMState.AddTransition(self, toState)
|
||||
assert(not self._transitionValid[toState], "transition already exists")
|
||||
self._transitionValid[toState] = true
|
||||
return self
|
||||
end
|
||||
|
||||
--- Add a handled event.
|
||||
-- @tparam FSMState self The FSM state object
|
||||
-- @tparam string event The name of the event
|
||||
-- @tparam function handler The function called when the event occurs
|
||||
-- @treturn FSMState The FSM state object
|
||||
function FSMState.AddEvent(self, event, handler)
|
||||
assert(not self._events[event], "event already exists")
|
||||
self._events[event] = handler
|
||||
return self
|
||||
end
|
||||
|
||||
--- Add a simple event-based transition.
|
||||
-- @tparam FSMState self The FSM state object
|
||||
-- @tparam string event The event name
|
||||
-- @tparam string toState The state to transition to
|
||||
-- @treturn FSMState The FSM state object
|
||||
function FSMState.AddEventTransition(self, event, toState)
|
||||
if not private.eventTransitionHandlerCache[toState] then
|
||||
private.eventTransitionHandlerCache[toState] = function(context, ...)
|
||||
return toState, ...
|
||||
end
|
||||
end
|
||||
return self:AddEvent(event, private.eventTransitionHandlerCache[toState])
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
function FSMState._GetName(self)
|
||||
return self._name
|
||||
end
|
||||
|
||||
function FSMState._ToStateIterator(self)
|
||||
local temp = TempTable.Acquire()
|
||||
for toState in pairs(self._transitionValid) do
|
||||
tinsert(temp, toState)
|
||||
end
|
||||
return TempTable.Iterator(temp)
|
||||
end
|
||||
|
||||
function FSMState._IsTransitionValid(self, toState)
|
||||
return self._transitionValid[toState]
|
||||
end
|
||||
|
||||
function FSMState._HasEventHandler(self, event)
|
||||
return self._events[event] and true or false
|
||||
end
|
||||
|
||||
function FSMState._ProcessEvent(self, event, context, ...)
|
||||
return self:_HandlerHelper(self._events[event], context, ...)
|
||||
end
|
||||
|
||||
function FSMState._Enter(self, context, ...)
|
||||
return self:_HandlerHelper(self._onEnterHandler, context, ...)
|
||||
end
|
||||
|
||||
function FSMState._Exit(self, context)
|
||||
return self:_HandlerHelper(self._onExitHandler, context)
|
||||
end
|
||||
|
||||
function FSMState._HandlerHelper(self, handler, context, ...)
|
||||
if type(handler) == "function" then
|
||||
return handler(context, ...)
|
||||
elseif type(handler) == "string" then
|
||||
return context[handler](context, ...)
|
||||
elseif handler ~= nil then
|
||||
error("Invalid handler: "..tostring(handler))
|
||||
end
|
||||
end
|
||||
94
LibTSM/Util/FontObject.lua
Normal file
94
LibTSM/Util/FontObject.lua
Normal file
@@ -0,0 +1,94 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- FontObject Functions.
|
||||
-- @module FontObject
|
||||
|
||||
local _, TSM = ...
|
||||
local FontObject = TSM.Init("Util.FontObject")
|
||||
local private = {
|
||||
context = {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Metatable
|
||||
-- ============================================================================
|
||||
|
||||
local FONT_OBJECT_MT = {
|
||||
__index = {
|
||||
SetPath = function(self, path)
|
||||
assert(type(path) == "string")
|
||||
local context = private.context[self]
|
||||
context.path = path
|
||||
return self
|
||||
end,
|
||||
SetSize = function(self, size)
|
||||
assert(type(size) == "number")
|
||||
local context = private.context[self]
|
||||
context.size = size
|
||||
return self
|
||||
end,
|
||||
SetLineHeight = function(self, lineHeight)
|
||||
assert(type(lineHeight) == "number")
|
||||
local context = private.context[self]
|
||||
context.lineHeight = lineHeight
|
||||
return self
|
||||
end,
|
||||
GetWowFont = function(self)
|
||||
local context = private.context[self]
|
||||
-- wow renders the font slightly bigger than the designs would indicate, so subtract one from the font height
|
||||
if context.path == "Fonts\\ARKai_C.ttf" then
|
||||
-- this font is a bit smaller than it should be, so increase it by 1
|
||||
return context.path, context.size + 1
|
||||
else
|
||||
-- wow renders other fonts slightly bigger than the designs would indicate, so decrease the height by 1
|
||||
return context.path, context.size - 1
|
||||
end
|
||||
end,
|
||||
GetSpacing = function(self)
|
||||
local context = private.context[self]
|
||||
assert(context.lineHeight >= context.size)
|
||||
return context.lineHeight - context.size
|
||||
end,
|
||||
},
|
||||
__newindex = function(self, key, value) error("FontObject cannot be modified") end,
|
||||
__metatable = false,
|
||||
__tostring = function(self)
|
||||
local context = private.context[self]
|
||||
local shortPath = strmatch(context.path, "([^/\\]+)%.[A-Za-z]+$")
|
||||
return "FontObject:"..tostring(shortPath)..":"..tostring(context.size)..":"..tostring(context.lineHeight)
|
||||
end,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Create an font object from a path and height.
|
||||
-- @tparam string path The path to the font file
|
||||
-- @tparam number size The size of the font in pixels
|
||||
-- @tparam number lineHeight The height of each line of text in pixels
|
||||
-- @treturn FontObject The font object
|
||||
function FontObject.New(path, size, lineHeight)
|
||||
local obj = setmetatable({}, FONT_OBJECT_MT)
|
||||
private.context[obj] = {
|
||||
path = nil,
|
||||
size = nil,
|
||||
lineHeight = nil,
|
||||
}
|
||||
obj:SetPath(path)
|
||||
obj:SetSize(size)
|
||||
obj:SetLineHeight(lineHeight)
|
||||
return obj
|
||||
end
|
||||
|
||||
function FontObject.IsInstance(obj)
|
||||
return type(obj) == "table" and private.context[obj] and true or false
|
||||
end
|
||||
110
LibTSM/Util/Future.lua
Normal file
110
LibTSM/Util/Future.lua
Normal file
@@ -0,0 +1,110 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Future Functions.
|
||||
-- @module Future
|
||||
|
||||
local _, TSM = ...
|
||||
local Future = TSM.Init("Util.Future")
|
||||
local private = {
|
||||
context = {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Metatable
|
||||
-- ============================================================================
|
||||
|
||||
local FUTURE_MT = {
|
||||
__index = {
|
||||
GetName = function(self)
|
||||
local context = private.context[self]
|
||||
return context.name
|
||||
end,
|
||||
SetScript = function(self, script, handler)
|
||||
assert(type(handler) == "function")
|
||||
local context = private.context[self]
|
||||
if script == "OnDone" then
|
||||
assert(context.state ~= "DONE")
|
||||
assert(not context.onDone)
|
||||
context.onDone = handler
|
||||
elseif script == "OnCleanup" then
|
||||
assert(not context.onCleanup)
|
||||
context.onCleanup = handler
|
||||
else
|
||||
error("Unknown script: "..tostring(script))
|
||||
end
|
||||
end,
|
||||
Start = function(self)
|
||||
local context = private.context[self]
|
||||
assert(context.state == "RESET")
|
||||
context.state = "STARTED"
|
||||
end,
|
||||
Cancel = function(self)
|
||||
local context = private.context[self]
|
||||
assert(context.state ~= "RESET")
|
||||
private.Reset(self)
|
||||
end,
|
||||
Done = function(self, value)
|
||||
local context = private.context[self]
|
||||
assert(context.state == "STARTED")
|
||||
context.state = "DONE"
|
||||
context.value = value
|
||||
if context.onDone then
|
||||
context.onDone(self)
|
||||
end
|
||||
end,
|
||||
IsDone = function(self)
|
||||
local context = private.context[self]
|
||||
assert(context.state ~= "RESET")
|
||||
return context.state == "DONE"
|
||||
end,
|
||||
GetValue = function(self)
|
||||
local context = private.context[self]
|
||||
assert(context.state == "DONE")
|
||||
local value = context.value
|
||||
private.Reset(self)
|
||||
return value
|
||||
end,
|
||||
},
|
||||
__newindex = function(self, key, value) error("Future cannot be modified") end,
|
||||
__metatable = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Create a new future.
|
||||
-- @tparam string name The name of the future for debugging purposes
|
||||
-- @treturn Future The future object
|
||||
function Future.New(name)
|
||||
local future = setmetatable({}, FUTURE_MT)
|
||||
private.context[future] = {
|
||||
name = name,
|
||||
}
|
||||
private.Reset(future)
|
||||
return future
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.Reset(future)
|
||||
local context = private.context[future]
|
||||
context.state = "RESET"
|
||||
context.value = nil
|
||||
context.onDone = nil
|
||||
if context.onCleanup then
|
||||
context.onCleanup()
|
||||
end
|
||||
end
|
||||
253
LibTSM/Util/HSLuv.lua
Normal file
253
LibTSM/Util/HSLuv.lua
Normal file
@@ -0,0 +1,253 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
-- NOTE: The following code is heavily based on https://github.com/hsluv/hsluv-lua, with some
|
||||
-- modifications to work properly with TSM. Its original license is below:
|
||||
|
||||
--[[
|
||||
Lua implementation of HSLuv and HPLuv color spaces
|
||||
Homepage: http://www.hsluv.org/
|
||||
|
||||
Copyright (C) 2019 Alexei Boronine
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
]]
|
||||
|
||||
local _, TSM = ...
|
||||
local HSLuv = TSM.Init("Util.HSLuv")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
local private = {}
|
||||
local M = {
|
||||
{ 3.240969941904521, -1.537383177570093, -0.498610760293 },
|
||||
{ -0.96924363628087, 1.87596750150772, 0.041555057407175 },
|
||||
{ 0.055630079696993, -0.20397695888897, 1.056971514242878 }
|
||||
}
|
||||
local M_INV = {
|
||||
{ 0.41239079926595, 0.35758433938387, 0.18048078840183 },
|
||||
{ 0.21263900587151, 0.71516867876775, 0.072192315360733 },
|
||||
{ 0.019330818715591, 0.11919477979462, 0.95053215224966 }
|
||||
}
|
||||
local REF_Y = 1.0
|
||||
local REF_U = 0.19783000664283
|
||||
local REF_V = 0.46831999493879
|
||||
local KAPPA = 903.2962962
|
||||
local EPSILON = 0.0088564516
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function HSLuv.ToRGB(h, s, l)
|
||||
return private.HSLuvToRGB(h, s, l)
|
||||
end
|
||||
|
||||
function HSLuv.FromRGB(r, g, b)
|
||||
return private.RGBToHSLuv(r, g, b)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.MaxSafeChromaForLH(l, h)
|
||||
local hrad = h / 360 * math.pi * 2
|
||||
local chroma = 1.7976931348623157e+308
|
||||
local sub1 = ((l + 16) ^ 3) / 1560896
|
||||
local sub2 = nil
|
||||
if sub1 > EPSILON then
|
||||
sub2 = sub1
|
||||
else
|
||||
sub2 = l / KAPPA
|
||||
end
|
||||
for i = 1, 3 do
|
||||
for t = 0, 1 do
|
||||
local top1 = (284517 * M[i][1] - 94839 * M[i][3]) * sub2
|
||||
local top2 = (838422 * M[i][3] + 769860 * M[i][2] + 731718 * M[i][1]) * l * sub2 - 769860 * t * l
|
||||
local bottom = (632260 * M[i][3] - 126452 * M[i][2]) * sub2 + 126452 * t
|
||||
if bottom ~= 0 then
|
||||
local slope = top1 / bottom
|
||||
local intercept = top2 / bottom
|
||||
if hrad ~= 0 or slope ~= 0 then
|
||||
local length = intercept / (math.sin(hrad) - slope * math.cos(hrad))
|
||||
if length >= 0 then
|
||||
chroma = min(chroma, length)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return chroma
|
||||
end
|
||||
|
||||
function private.DotProduct(a, b1, b2, b3)
|
||||
return a[1] * b1 + a[2] * b2 + a[3] * b3
|
||||
end
|
||||
|
||||
function private.FromLinear(c)
|
||||
if c <= 0.0031308 then
|
||||
return 12.92 * c
|
||||
else
|
||||
return 1.055 * (c ^ 0.416666666666666685) - 0.055
|
||||
end
|
||||
end
|
||||
|
||||
function private.ToLinear(c)
|
||||
if c > 0.04045 then
|
||||
return ((c + 0.055) / 1.055) ^ 2.4
|
||||
else
|
||||
return c / 12.92
|
||||
end
|
||||
end
|
||||
|
||||
function private.XYZToRGB(x, y, z)
|
||||
local r = private.FromLinear(private.DotProduct(M[1], x, y, z))
|
||||
local g = private.FromLinear(private.DotProduct(M[2], x, y, z))
|
||||
local b = private.FromLinear(private.DotProduct(M[3], x, y, z))
|
||||
return r, g, b
|
||||
end
|
||||
|
||||
function private.RGBToXYZ(r, g, b)
|
||||
r = private.ToLinear(r)
|
||||
g = private.ToLinear(g)
|
||||
b = private.ToLinear(b)
|
||||
local x = private.DotProduct(M_INV[1], r, g, b)
|
||||
local y = private.DotProduct(M_INV[2], r, g, b)
|
||||
local z = private.DotProduct(M_INV[3], r, g, b)
|
||||
return x, y, z
|
||||
end
|
||||
|
||||
function private.YToL(Y)
|
||||
if Y <= EPSILON then
|
||||
return Y / REF_Y * KAPPA
|
||||
else
|
||||
return 116 * ((Y / REF_Y) ^ 0.333333333333333315) - 16
|
||||
end
|
||||
end
|
||||
|
||||
function private.LToY(L)
|
||||
if L <= 8 then
|
||||
return REF_Y * L / KAPPA
|
||||
else
|
||||
return REF_Y * (((L + 16) / 116) ^ 3)
|
||||
end
|
||||
end
|
||||
|
||||
function private.XYZToLUV(x, y, z)
|
||||
local divider = x + 15 * y + 3 * z
|
||||
local varU = 4 * x
|
||||
local varV = 9 * y
|
||||
if divider ~= 0 then
|
||||
varU = varU / divider
|
||||
varV = varV / divider
|
||||
else
|
||||
varU = 0
|
||||
varV = 0
|
||||
end
|
||||
local L = private.YToL(y)
|
||||
if L == 0 then
|
||||
return 0, 0, 0
|
||||
end
|
||||
return L, 13 * L * (varU - REF_U), 13 * L * (varV - REF_V)
|
||||
end
|
||||
|
||||
function private.LUVToXYZ(l, u, v)
|
||||
if l == 0 then
|
||||
return 0, 0, 0
|
||||
end
|
||||
local varU = u / (13 * l) + REF_U
|
||||
local varV = v / (13 * l) + REF_V
|
||||
local Y = private.LToY(l)
|
||||
local X = 0 - (9 * Y * varU) / ((((varU - 4) * varV) - varU * varV))
|
||||
return X, Y, (9 * Y - 15 * varV * Y - varV * X) / (3 * varV)
|
||||
end
|
||||
|
||||
function private.LUVToLCH(l, u, v)
|
||||
local C = math.sqrt(u * u + v * v)
|
||||
local H
|
||||
if C < 0.00000001 then
|
||||
H = 0
|
||||
else
|
||||
H = math.atan2(v, u) * 180.0 / 3.1415926535897932
|
||||
if H < 0 then
|
||||
H = 360 + H
|
||||
end
|
||||
end
|
||||
return l, C, H
|
||||
end
|
||||
|
||||
function private.LCHToLUV(l, c, h)
|
||||
local hrad = h / 360.0 * 2 * math.pi
|
||||
return l, math.cos(hrad) * c, math.sin(hrad) * c
|
||||
end
|
||||
|
||||
function private.HSLuvToLCH(h, s, l)
|
||||
if l > 99.9999999 then
|
||||
return 100, 0, h
|
||||
end
|
||||
if l < 0.00000001 then
|
||||
return 0, 0, h
|
||||
end
|
||||
return l, private.MaxSafeChromaForLH(l, h) / 100 * s, h
|
||||
end
|
||||
|
||||
function private.LCHToHSLuv(l, c, h)
|
||||
local max_chroma = private.MaxSafeChromaForLH(l, h)
|
||||
if l > 99.9999999 then
|
||||
return h, 0, 100
|
||||
end
|
||||
if l < 0.00000001 then
|
||||
return h, 0, 0
|
||||
end
|
||||
|
||||
return h, c / max_chroma * 100, l
|
||||
end
|
||||
|
||||
function private.HSLuvToRGB(h, s, l)
|
||||
local v1, v2, v3 = h, s, l
|
||||
v1, v2, v3 = private.HSLuvToLCH(v1, v2, v3)
|
||||
v1, v2, v3 = private.LCHToLUV(v1, v2, v3)
|
||||
v1, v2, v3 = private.LUVToXYZ(v1, v2, v3)
|
||||
local r, g, b = private.XYZToRGB(v1, v2, v3)
|
||||
r = Math.Round(r * 255)
|
||||
g = Math.Round(g * 255)
|
||||
b = Math.Round(b * 255)
|
||||
assert(r >= 0 and r <= 255)
|
||||
assert(g >= 0 and g <= 255)
|
||||
assert(b >= 0 and b <= 255)
|
||||
return r, g, b
|
||||
end
|
||||
|
||||
function private.RGBToHSLuv(r, g, b)
|
||||
local v1, v2, v3 = r / 255, g / 255, b / 255
|
||||
v1, v2, v3 = private.RGBToXYZ(v1, v2, v3)
|
||||
v1, v2, v3 = private.XYZToLUV(v1, v2, v3)
|
||||
v1, v2, v3 = private.LUVToLCH(v1, v2, v3)
|
||||
local h, s, l = private.LCHToHSLuv(v1, v2, v3)
|
||||
h = Math.Round(h) % 360
|
||||
s = Math.Round(s)
|
||||
l = Math.Round(l)
|
||||
assert(h >= 0 and h < 360)
|
||||
assert(s >= 0 and s <= 100)
|
||||
assert(l >= 0 and l <= 100)
|
||||
return h, s, l
|
||||
end
|
||||
328
LibTSM/Util/ItemString.lua
Normal file
328
LibTSM/Util/ItemString.lua
Normal file
@@ -0,0 +1,328 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Item String functions
|
||||
-- @module ItemString
|
||||
|
||||
local _, TSM = ...
|
||||
local ItemString = TSM.Init("Util.ItemString")
|
||||
local BonusIds = TSM.Include("Data.BonusIds")
|
||||
local SmartMap = TSM.Include("Util.SmartMap")
|
||||
local private = {
|
||||
filteredItemStringCache = {},
|
||||
itemStringCache = {},
|
||||
baseItemStringMap = nil,
|
||||
baseItemStringReader = nil,
|
||||
hasNonBaseItemStrings = {},
|
||||
bonusIdsTemp = {},
|
||||
modifiersTemp = {},
|
||||
}
|
||||
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 = 152632
|
||||
local IMPORTANT_MODIFIER_TYPES = {
|
||||
[9] = true,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
ItemString:OnModuleLoad(function()
|
||||
private.baseItemStringMap = SmartMap.New("string", "string", private.ToBaseItemString)
|
||||
private.baseItemStringReader = private.baseItemStringMap:CreateReader()
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Gets the constant unknown item string for places where the itemString is not known.
|
||||
-- @treturn string The itemString
|
||||
function ItemString.GetUnknown()
|
||||
return UNKNOWN_ITEM_STRING
|
||||
end
|
||||
|
||||
--- Gets the constant placeholder item string.
|
||||
-- @treturn string The itemString
|
||||
function ItemString.GetPlaceholder()
|
||||
return PLACEHOLDER_ITEM_STRING
|
||||
end
|
||||
|
||||
--- Gets the battlepet cage item string.
|
||||
-- @treturn string The itemString
|
||||
function ItemString.GetPetCage()
|
||||
return PET_CAGE_ITEM_STRING
|
||||
end
|
||||
|
||||
--- Gets the base itemString smart map.
|
||||
-- @treturn SmartMap The smart map
|
||||
function ItemString.GetBaseMap()
|
||||
return private.baseItemStringMap
|
||||
end
|
||||
|
||||
--- Converts the parameter into an itemString.
|
||||
-- @tparam ?number|string item Either an itemId, itemLink, or itemString to be converted
|
||||
-- @treturn string The itemString
|
||||
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.
|
||||
-- @tparam string item An item to get the id of
|
||||
-- @treturn number The itemId
|
||||
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.
|
||||
-- @tparam string itemString An itemString to get the base itemString of
|
||||
-- @treturn string The base itemString
|
||||
function ItemString.GetBaseFast(itemString)
|
||||
if not itemString then
|
||||
return nil
|
||||
end
|
||||
return private.baseItemStringReader[itemString]
|
||||
end
|
||||
|
||||
--- Converts the parameter into a base itemString.
|
||||
-- @tparam string item An item to get the base itemString of
|
||||
-- @treturn string The base itemString
|
||||
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.
|
||||
-- @tparam table itemKey An itemKey to get the itemString of
|
||||
-- @treturn string The base itemString
|
||||
function ItemString.GetBaseFromItemKey(itemKey)
|
||||
if itemKey.battlePetSpeciesID > 0 then
|
||||
return "p:"..itemKey.battlePetSpeciesID
|
||||
else
|
||||
return "i:"..itemKey.itemID
|
||||
end
|
||||
end
|
||||
|
||||
function ItemString.HasNonBase(baseItemString)
|
||||
return private.hasNonBaseItemStrings[baseItemString] or false
|
||||
end
|
||||
|
||||
--- Converts the parameter into a WoW itemString.
|
||||
-- @tparam string itemString An itemString to get the WoW itemString of
|
||||
-- @treturn number The WoW itemString
|
||||
function ItemString.ToWow(itemString)
|
||||
local _, itemId, rand, extra = strsplit(":", itemString)
|
||||
local level = UnitLevel("player")
|
||||
local spec = not TSM.IsWowClassic() and GetSpecialization() or nil
|
||||
spec = spec and GetSpecializationInfo(spec) or ""
|
||||
local extraPart = extra and strmatch(itemString, "i:[0-9]+:[0-9%-]*:(.+)") or ""
|
||||
return "item:"..itemId.."::::::"..(rand or "").."::"..level..":"..spec..":::"..extraPart..":::"
|
||||
end
|
||||
|
||||
function ItemString.IsItem(itemString)
|
||||
return strmatch(itemString, "^i:[%-:0-9]+$") and true or false
|
||||
end
|
||||
|
||||
function ItemString.IsPet(itemString)
|
||||
return strmatch(itemString, "^p:[%-:0-9]+$") 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, "^[ip]:([0-9]+)$")
|
||||
if itemId then
|
||||
if tonumber(itemId) > ITEM_MAX_ID then
|
||||
return nil
|
||||
end
|
||||
-- this is already an itemString
|
||||
return item
|
||||
end
|
||||
itemId = strmatch(item, "item:(%d+)")
|
||||
if itemId and tonumber(itemId) > ITEM_MAX_ID then
|
||||
return nil
|
||||
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%-:]+)$") then
|
||||
return private.FixItemString(item)
|
||||
elseif strmatch(item, "^p:([0-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)
|
||||
local result = strmatch(itemString, "^(p:%d+:%d+:%d+)$")
|
||||
if result then
|
||||
return result
|
||||
end
|
||||
return 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)
|
||||
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)
|
||||
tinsert(private.modifiersTemp, modifier)
|
||||
end
|
||||
modifierType = nil
|
||||
end
|
||||
end
|
||||
if #private.modifiersTemp > 0 then
|
||||
assert(#private.modifiersTemp % 2 == 0)
|
||||
tinsert(private.modifiersTemp, 1, #private.modifiersTemp / 2)
|
||||
modifiersStr = table.concat(private.modifiersTemp, ":")
|
||||
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
|
||||
58
LibTSM/Util/JSON.lua
Normal file
58
LibTSM/Util/JSON.lua
Normal file
@@ -0,0 +1,58 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- JSON Functions
|
||||
-- @module JSON
|
||||
|
||||
local _, TSM = ...
|
||||
local JSON = TSM.Init("Util.JSON")
|
||||
local private = {}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function JSON.Encode(value)
|
||||
if type(value) == "string" then
|
||||
return "\""..private.SanitizeString(value).."\""
|
||||
elseif type(value) == "number" or type(value) == "boolean" then
|
||||
return tostring(value)
|
||||
elseif type(value) == "table" then
|
||||
local absCount = 0
|
||||
for _ in pairs(value) do
|
||||
absCount = absCount + 1
|
||||
end
|
||||
local tblParts = {}
|
||||
if #value == absCount then
|
||||
for _, v in ipairs(value) do
|
||||
tinsert(tblParts, JSON.Encode(v))
|
||||
end
|
||||
return "["..table.concat(tblParts, ",").."]"
|
||||
else
|
||||
for k, v in pairs(value) do
|
||||
tinsert(tblParts, "\""..private.SanitizeString(k).."\":"..JSON.Encode(v))
|
||||
end
|
||||
return "{"..table.concat(tblParts, ",").."}"
|
||||
end
|
||||
else
|
||||
error("Invalid type: "..type(value))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.SanitizeString(str)
|
||||
str = gsub(str, "\124cff[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]([^\124]+)\124r", "%1")
|
||||
str = gsub(str, "[\\]+", "/")
|
||||
str = gsub(str, "\"", "'")
|
||||
return str
|
||||
end
|
||||
189
LibTSM/Util/Log.lua
Normal file
189
LibTSM/Util/Log.lua
Normal file
@@ -0,0 +1,189 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Log = TSM.Init("Util.Log")
|
||||
local Debug = TSM.Include("Util.Debug")
|
||||
local Theme = TSM.Include("Util.Theme")
|
||||
local private = {
|
||||
severity = {},
|
||||
location = {},
|
||||
timeStr = {},
|
||||
msg = {},
|
||||
writeIndex = 1,
|
||||
len = 0,
|
||||
temp = {},
|
||||
logToChat = TSM.__IS_TEST_ENV or false,
|
||||
currentThreadNameFunc = nil,
|
||||
stackLevel = 3,
|
||||
chatFrame = nil,
|
||||
}
|
||||
local MAX_ROWS = 200
|
||||
local MAX_MSG_LEN = 150
|
||||
local CHAT_LOG_COLOR_KEYS = {
|
||||
TRACE = "BLUE",
|
||||
INFO = "GREEN",
|
||||
WARN = "YELLOW",
|
||||
ERR = "RED",
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Log.SetChatFrame(chatFrame)
|
||||
private.chatFrame = strlower(chatFrame)
|
||||
end
|
||||
|
||||
function Log.SetLoggingToChatEnabled(enabled)
|
||||
if TSM.__IS_TEST_ENV then
|
||||
enabled = true
|
||||
end
|
||||
if private.logToChat == enabled then
|
||||
return
|
||||
end
|
||||
private.logToChat = enabled
|
||||
if enabled then
|
||||
-- dump our buffer
|
||||
local len = Log.Length()
|
||||
print(format("Printing %d buffered logs:", len))
|
||||
for i = 1, len do
|
||||
private.LogToChat(Log.Get(i))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Log.SetCurrentThreadNameFunction(func)
|
||||
private.currentThreadNameFunc = func
|
||||
end
|
||||
|
||||
function Log.Length()
|
||||
return private.len
|
||||
end
|
||||
|
||||
function Log.Get(index)
|
||||
assert(index <= private.len)
|
||||
local readIndex = (private.writeIndex - private.len + index - 2) % MAX_ROWS + 1
|
||||
return private.severity[readIndex], private.location[readIndex], private.timeStr[readIndex], private.msg[readIndex]
|
||||
end
|
||||
|
||||
function Log.RaiseStackLevel()
|
||||
private.stackLevel = private.stackLevel + 1
|
||||
end
|
||||
|
||||
function Log.LowerStackLevel()
|
||||
private.stackLevel = private.stackLevel - 1
|
||||
end
|
||||
|
||||
function Log.StackTrace()
|
||||
Log.RaiseStackLevel()
|
||||
Log.Trace("Stack Trace:")
|
||||
local level = 2
|
||||
local line = Debug.GetStackLevelLocation(level)
|
||||
while line do
|
||||
Log.Trace(" " .. line)
|
||||
level = level + 1
|
||||
line = Debug.GetStackLevelLocation(level)
|
||||
end
|
||||
Log.LowerStackLevel()
|
||||
end
|
||||
|
||||
function Log.Trace(...)
|
||||
private.Log("TRACE", ...)
|
||||
end
|
||||
|
||||
function Log.Info(...)
|
||||
private.Log("INFO", ...)
|
||||
end
|
||||
|
||||
function Log.Warn(...)
|
||||
private.Log("WARN", ...)
|
||||
end
|
||||
|
||||
function Log.Err(...)
|
||||
private.Log("ERR", ...)
|
||||
end
|
||||
|
||||
function Log.PrintUserRaw(str)
|
||||
private.GetChatFrame():AddMessage(str)
|
||||
end
|
||||
|
||||
function Log.PrintfUserRaw(...)
|
||||
Log.PrintUserRaw(format(...))
|
||||
end
|
||||
|
||||
function Log.PrintUser(str)
|
||||
Log.PrintUserRaw(Theme.GetColor("INDICATOR"):ColorText("TSM")..": "..str)
|
||||
end
|
||||
|
||||
function Log.PrintfUser(...)
|
||||
Log.PrintUser(format(...))
|
||||
end
|
||||
|
||||
function Log.ColorUserAccentText(text)
|
||||
return Theme.GetColor("INDICATOR_ALT"):ColorText(text)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.GetChatFrame()
|
||||
for i = 1, NUM_CHAT_WINDOWS do
|
||||
local name = strlower(GetChatWindowInfo(i) or "")
|
||||
if name ~= "" and name == private.chatFrame then
|
||||
return _G["ChatFrame" .. i]
|
||||
end
|
||||
end
|
||||
return DEFAULT_CHAT_FRAME
|
||||
end
|
||||
|
||||
function private.Log(severity, fmtStr, ...)
|
||||
assert(type(fmtStr) == "string" and CHAT_LOG_COLOR_KEYS[severity])
|
||||
wipe(private.temp)
|
||||
for i = 1, select("#", ...) do
|
||||
local arg = select(i, ...)
|
||||
if type(arg) == "boolean" then
|
||||
arg = arg and "T" or "F"
|
||||
elseif type(arg) ~= "string" and type(arg) ~= "number" then
|
||||
arg = tostring(arg)
|
||||
end
|
||||
private.temp[i] = arg
|
||||
end
|
||||
-- ignore anything after a newline in the log message
|
||||
local msg = strsplit("\n", format(fmtStr, unpack(private.temp)))
|
||||
if #msg > MAX_MSG_LEN then
|
||||
msg = strsub(msg, 1, -4).."..."
|
||||
end
|
||||
local location = Debug.GetStackLevelLocation(private.stackLevel)
|
||||
location = location and strmatch(location, "([^\\/]+%.lua:[0-9]+)") or "?:?"
|
||||
local threadName = private.currentThreadNameFunc and private.currentThreadNameFunc() or nil
|
||||
if threadName then
|
||||
location = location.."|"..threadName
|
||||
end
|
||||
local timeMs = Debug.GetTimeMilliseconds()
|
||||
local timeStr = format("%s.%03d", date("%H:%M:%S", floor(timeMs / 1000)), timeMs % 1000)
|
||||
|
||||
-- append the log
|
||||
private.severity[private.writeIndex] = severity
|
||||
private.location[private.writeIndex] = location
|
||||
private.timeStr[private.writeIndex] = timeStr
|
||||
private.msg[private.writeIndex] = msg
|
||||
private.writeIndex = (private.writeIndex < MAX_ROWS) and (private.writeIndex + 1) or 1
|
||||
private.len = min(private.len + 1, MAX_ROWS)
|
||||
|
||||
if private.logToChat then
|
||||
private.LogToChat(severity, location, timeStr, msg)
|
||||
end
|
||||
end
|
||||
|
||||
function private.LogToChat(severity, location, timeStr, msg)
|
||||
print(strjoin(" ", timeStr, Theme.GetFeedbackColor(CHAT_LOG_COLOR_KEYS[severity]):ColorText("{"..location.."}"), msg))
|
||||
end
|
||||
162
LibTSM/Util/Math.lua
Normal file
162
LibTSM/Util/Math.lua
Normal file
@@ -0,0 +1,162 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Math Functions
|
||||
-- @module Math
|
||||
|
||||
local _, TSM = ...
|
||||
local Math = TSM.Init("Util.Math")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local NAN = math.huge * 0
|
||||
local IS_NAN_GT_INF = NAN > math.huge
|
||||
local NAN_STR = tostring(NAN)
|
||||
local private = {
|
||||
keysTemp = {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Returns NAN.
|
||||
-- @treturn number NAN
|
||||
function Math.GetNan()
|
||||
return NAN
|
||||
end
|
||||
|
||||
--- Checks if a value is NAN.
|
||||
-- @tparam number value The number to check
|
||||
-- @treturn boolean Whether or not the value is NAN
|
||||
function Math.IsNan(value)
|
||||
if IS_NAN_GT_INF then
|
||||
-- optimization if NAN > math.huge (which it is in Wow's version of lua)
|
||||
return value > math.huge and tostring(value) == NAN_STR
|
||||
else
|
||||
return tostring(value) == NAN_STR
|
||||
end
|
||||
end
|
||||
|
||||
--- Rounds a value to a specified significant value.
|
||||
-- @tparam number value The number to be rounded
|
||||
-- @tparam number sig The value to round to the nearest multiple of
|
||||
-- @treturn number The rounded value
|
||||
function Math.Round(value, sig)
|
||||
sig = sig or 1
|
||||
return floor((value / sig) + 0.5) * sig
|
||||
end
|
||||
|
||||
--- Rounds a value down to a specified significant value.
|
||||
-- @tparam number value The number to be rounded
|
||||
-- @tparam number sig The value to round down to the nearest multiple of
|
||||
-- @treturn number The rounded value
|
||||
function Math.Floor(value, sig)
|
||||
sig = sig or 1
|
||||
return floor(value / sig) * sig
|
||||
end
|
||||
|
||||
--- Rounds a value up to a specified significant value.
|
||||
-- @tparam number value The number to be rounded
|
||||
-- @tparam number sig The value to round up to the nearest multiple of
|
||||
-- @treturn number The rounded value
|
||||
function Math.Ceil(value, sig)
|
||||
sig = sig or 1
|
||||
return ceil(value / sig) * sig
|
||||
end
|
||||
|
||||
--- Scales a value from one range to another.
|
||||
-- @tparam number value The number to be scaled
|
||||
-- @tparam number fromStart The start value of the range to scale from
|
||||
-- @tparam number fromEnd The end value of the range to scale from (can be less than fromStart)
|
||||
-- @tparam number toStart The start value of the range to scale to
|
||||
-- @tparam number toEnd The end value of the range to scale to (can be less than toStart)
|
||||
-- @treturn number The scaled value
|
||||
function Math.Scale(value, fromStart, fromEnd, toStart, toEnd)
|
||||
assert(value >= min(fromStart, fromEnd) and value <= max(fromStart, fromEnd))
|
||||
return toStart + ((value - fromStart) / (fromEnd - fromStart)) * (toEnd - toStart)
|
||||
end
|
||||
|
||||
--- Bounds a number between a min and max value.
|
||||
-- @tparam number value The number to be bounded
|
||||
-- @tparam number minValue The min value
|
||||
-- @tparam number maxValue The max value
|
||||
-- @treturn number The bounded value
|
||||
function Math.Bound(value, minValue, maxValue)
|
||||
return min(max(value, minValue), maxValue)
|
||||
end
|
||||
|
||||
--- Calculates the has of the specified data
|
||||
-- This data can handle data of type string or number. It can also handle a table being passed as the data assuming
|
||||
-- all keys and values of the table are also hashable (strings, numbers, or tables with the same restriction). This
|
||||
-- function uses the [djb2 algorithm](http://www.cse.yorku.ca/~oz/hash.html).
|
||||
-- @param data The data to be hased
|
||||
-- @tparam[opt] number hash The initial value of the hash
|
||||
-- @treturn number The hash value
|
||||
function Math.CalculateHash(data, hash)
|
||||
hash = hash or 5381
|
||||
local maxValue = 2 ^ 24
|
||||
local dataType = type(data)
|
||||
if dataType == "string" then
|
||||
-- iterate through 8 bytes at a time
|
||||
for i = 1, ceil(#data / 8) do
|
||||
local b1, b2, b3, b4, b5, b6, b7, b8 = strbyte(data, (i - 1) * 8 + 1, i * 8)
|
||||
hash = (hash * 33 + b1) % maxValue
|
||||
if not b2 then break end
|
||||
hash = (hash * 33 + b2) % maxValue
|
||||
if not b3 then break end
|
||||
hash = (hash * 33 + b3) % maxValue
|
||||
if not b4 then break end
|
||||
hash = (hash * 33 + b4) % maxValue
|
||||
if not b5 then break end
|
||||
hash = (hash * 33 + b5) % maxValue
|
||||
if not b6 then break end
|
||||
hash = (hash * 33 + b6) % maxValue
|
||||
if not b7 then break end
|
||||
hash = (hash * 33 + b7) % maxValue
|
||||
if not b8 then break end
|
||||
hash = (hash * 33 + b8) % maxValue
|
||||
end
|
||||
elseif dataType == "number" then
|
||||
assert(data == floor(data), "Invalid number")
|
||||
if data < 0 then
|
||||
data = data * -1
|
||||
hash = (hash * 33 + 59) % maxValue
|
||||
end
|
||||
while data > 0 do
|
||||
hash = (hash * 33 + data % 256) % maxValue
|
||||
data = floor(data / 256)
|
||||
end
|
||||
elseif dataType == "table" then
|
||||
local keys = nil
|
||||
if private.keysTemp.inUse then
|
||||
keys = TempTable.Acquire()
|
||||
else
|
||||
keys = private.keysTemp
|
||||
private.keysTemp.inUse = true
|
||||
end
|
||||
for k in pairs(data) do
|
||||
tinsert(keys, k)
|
||||
end
|
||||
sort(keys)
|
||||
for _, key in ipairs(keys) do
|
||||
hash = Math.CalculateHash(key, hash)
|
||||
hash = Math.CalculateHash(data[key], hash)
|
||||
end
|
||||
if keys == private.keysTemp then
|
||||
wipe(private.keysTemp)
|
||||
else
|
||||
TempTable.Release(keys)
|
||||
end
|
||||
elseif dataType == "boolean" then
|
||||
hash = (hash * 33 + (data and 1 or 0)) % maxValue
|
||||
elseif dataType == "nil" then
|
||||
hash = (hash * 33 + 17) % maxValue
|
||||
else
|
||||
error("Invalid data")
|
||||
end
|
||||
return hash
|
||||
end
|
||||
177
LibTSM/Util/Money.lua
Normal file
177
LibTSM/Util/Money.lua
Normal file
@@ -0,0 +1,177 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Money Functions
|
||||
-- @module Money
|
||||
|
||||
local _, TSM = ...
|
||||
local Money = TSM.Init("Util.Money")
|
||||
local String = TSM.Include("Util.String")
|
||||
local private = {
|
||||
textMoneyParts = {},
|
||||
}
|
||||
local GOLD_ICON = "|TInterface\\MoneyFrame\\UI-GoldIcon:0|t"
|
||||
local SILVER_ICON = "|TInterface\\MoneyFrame\\UI-SilverIcon:0|t"
|
||||
local COPPER_ICON = "|TInterface\\MoneyFrame\\UI-CopperIcon:0|t"
|
||||
local GOLD_ICON_DISABLED = "|TInterface\\MoneyFrame\\UI-GoldIcon:0:0:0:0:1:1:0:1:0:1:100:100:100|t"
|
||||
local SILVER_ICON_DISABLED = "|TInterface\\MoneyFrame\\UI-SilverIcon:0:0:0:0:1:1:0:1:0:1:100:100:100|t"
|
||||
local COPPER_ICON_DISABLED = "|TInterface\\MoneyFrame\\UI-CopperIcon:0:0:0:0:1:1:0:1:0:1:100:100:100|t"
|
||||
local GOLD_TEXT = "|cffffd70ag|r"
|
||||
local SILVER_TEXT = "|cffc7c7cfs|r"
|
||||
local COPPER_TEXT = "|cffeda55fc|r"
|
||||
local GOLD_TEXT_DISABLED = "|cff5d5222g|r"
|
||||
local SILVER_TEXT_DISABLED = "|cff464646s|r"
|
||||
local COPPER_TEXT_DISABLED = "|cff402d22c|r"
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Converts a numeric money value (in copper) to a string for display in the UI.
|
||||
-- Supported options:
|
||||
--
|
||||
-- * OPT\_ICON Use texture icons instead of g/s/c letters
|
||||
-- * OPT\_TRIM Remove any non-significant 0 valued denominations (i.e. "1g" instead of "1g 0s 0c")
|
||||
-- * OPT\_83\_NO\_COPPER Remove the copper value entirely if we're patch 8.3
|
||||
-- * OPT\_DISABLE Uses a muted color from the denomination text (not allowed with "OPT\_ICON" or "OPT\_NO\_COLOR")
|
||||
-- @tparam number value The money value to be converted in copper (100 copper per silver, 100 silver per gold)
|
||||
-- @tparam[opt] string color A color prefix to use for the numbers in the result (i.e. "|cff00ff00" for red)
|
||||
-- @param[opt] ... One or more options to modify the format of the result
|
||||
-- @return The string representation of the specified money value
|
||||
function Money.ToString(value, color, ...)
|
||||
value = tonumber(value)
|
||||
if not value then
|
||||
return
|
||||
end
|
||||
assert(not color or strmatch(color, "^\124cff[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]$"))
|
||||
|
||||
-- parse the options
|
||||
local isIcon, trim, disabled, noCopper = false, false, false, false
|
||||
for i = 1, select('#', ...) do
|
||||
local opt = select(i, ...)
|
||||
if opt == nil then
|
||||
-- pass
|
||||
elseif opt == "OPT_ICON" then
|
||||
isIcon = true
|
||||
elseif opt == "OPT_TRIM" then
|
||||
trim = true
|
||||
elseif opt == "OPT_DISABLE" then
|
||||
disabled = true
|
||||
elseif opt == "OPT_83_NO_COPPER" then
|
||||
noCopper = not TSM.IsWowClassic()
|
||||
else
|
||||
error("Invalid option: "..tostring(opt))
|
||||
end
|
||||
end
|
||||
|
||||
local isNegative = value < 0
|
||||
value = abs(value)
|
||||
local gold = floor(value / COPPER_PER_GOLD)
|
||||
local silver = floor((value % COPPER_PER_GOLD) / COPPER_PER_SILVER)
|
||||
local copper = floor(value % COPPER_PER_SILVER)
|
||||
assert(not noCopper or copper == 0)
|
||||
local goldText, silverText, copperText = nil, nil, nil
|
||||
if isIcon then
|
||||
if disabled then
|
||||
goldText, silverText, copperText = GOLD_ICON_DISABLED, SILVER_ICON_DISABLED, COPPER_ICON_DISABLED
|
||||
else
|
||||
goldText, silverText, copperText = GOLD_ICON, SILVER_ICON, COPPER_ICON
|
||||
end
|
||||
else
|
||||
if disabled then
|
||||
goldText, silverText, copperText = GOLD_TEXT_DISABLED, SILVER_TEXT_DISABLED, COPPER_TEXT_DISABLED
|
||||
else
|
||||
goldText, silverText, copperText = GOLD_TEXT, SILVER_TEXT, COPPER_TEXT
|
||||
end
|
||||
end
|
||||
|
||||
if value == 0 then
|
||||
return private.FormatNumber(0, true, color)..(noCopper and silverText or copperText)
|
||||
end
|
||||
|
||||
wipe(private.textMoneyParts)
|
||||
-- add gold
|
||||
if gold > 0 then
|
||||
private.InsertMoneyPart(gold, color, goldText)
|
||||
end
|
||||
-- add silver
|
||||
if silver > 0 or (not trim and gold > 0) then
|
||||
private.InsertMoneyPart(silver, color, silverText)
|
||||
end
|
||||
-- add copper
|
||||
if copper > 0 or (not trim and not noCopper and (gold + silver) > 0) then
|
||||
private.InsertMoneyPart(copper, color, copperText)
|
||||
end
|
||||
local text = table.concat(private.textMoneyParts, " ")
|
||||
if isNegative then
|
||||
return (color and (color.."-|r") or "-")..text
|
||||
else
|
||||
return text
|
||||
end
|
||||
end
|
||||
|
||||
--- Converts a string money value to a number value (in copper).
|
||||
-- The value passed to this function can contain colored text, but must use g/s/c for the denominations and not icons.
|
||||
-- @tparam string value The money value to be converted as a string
|
||||
-- @treturn string The numeric representation of the specified money value
|
||||
function Money.FromString(value)
|
||||
-- remove any colors
|
||||
value = gsub(gsub(strtrim(value), "\124c([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])", ""), "\124r", "")
|
||||
-- remove any separators
|
||||
value = gsub(value, String.Escape(LARGE_NUMBER_SEPERATOR), "")
|
||||
|
||||
-- extract gold/silver/copper values
|
||||
local gold = tonumber(strmatch(value, "([0-9]+)g"))
|
||||
local silver = tonumber(strmatch(value, "([0-9]+)s"))
|
||||
local copper = tonumber(strmatch(value, "([0-9]+)c"))
|
||||
if not gold and not silver and not copper then return end
|
||||
|
||||
-- test that there are no extra characters (other than spaces)
|
||||
value = gsub(value, "[0-9]+g", "", 1)
|
||||
value = gsub(value, "[0-9]+s", "", 1)
|
||||
value = gsub(value, "[0-9]+c", "", 1)
|
||||
if strtrim(value) ~= "" then return end
|
||||
|
||||
return ((gold or 0) * COPPER_PER_GOLD) + ((silver or 0) * COPPER_PER_SILVER) + (copper or 0)
|
||||
end
|
||||
|
||||
--- Returns the colored gold indicator text
|
||||
-- @treturn string The colored gold indicator text
|
||||
function Money.GetGoldText()
|
||||
return GOLD_TEXT
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.InsertMoneyPart(value, color, text)
|
||||
tinsert(private.textMoneyParts, private.FormatNumber(value, #private.textMoneyParts == 0, color)..text)
|
||||
end
|
||||
|
||||
function private.FormatNumber(num, isMostSignificant, color)
|
||||
if num < 10 and not isMostSignificant then
|
||||
num = "0"..num
|
||||
elseif isMostSignificant and num >= 1000 then
|
||||
num = tostring(num)
|
||||
local result = ""
|
||||
for i = 4, #num, 3 do
|
||||
result = LARGE_NUMBER_SEPERATOR..strsub(num, -(i - 1), -(i - 3))..result
|
||||
end
|
||||
result = strsub(num, 1, (#num % 3 == 0) and 3 or (#num % 3))..result
|
||||
num = result
|
||||
end
|
||||
|
||||
if color then
|
||||
return color..num.."|r"
|
||||
else
|
||||
return num
|
||||
end
|
||||
end
|
||||
215
LibTSM/Util/NineSlice.lua
Normal file
215
LibTSM/Util/NineSlice.lua
Normal file
@@ -0,0 +1,215 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- NineSlice Functions.
|
||||
-- @module NineSlice
|
||||
|
||||
local _, TSM = ...
|
||||
local NineSlice = TSM.Init("Util.NineSlice")
|
||||
local private = {
|
||||
styles = {},
|
||||
context = {},
|
||||
}
|
||||
local PART_INFO = {
|
||||
topLeft = {
|
||||
points = {
|
||||
{ "TOPLEFT" },
|
||||
},
|
||||
},
|
||||
bottomLeft = {
|
||||
points = {
|
||||
{ "BOTTOMLEFT" },
|
||||
},
|
||||
},
|
||||
topRight = {
|
||||
points = {
|
||||
{ "TOPRIGHT" },
|
||||
},
|
||||
},
|
||||
bottomRight = {
|
||||
points = {
|
||||
{ "BOTTOMRIGHT" },
|
||||
},
|
||||
},
|
||||
left = {
|
||||
points = {
|
||||
{ "TOPLEFT", "topLeft", "BOTTOMLEFT" },
|
||||
{ "BOTTOMLEFT", "bottomLeft", "TOPLEFT" },
|
||||
},
|
||||
},
|
||||
right = {
|
||||
points = {
|
||||
{ "TOPRIGHT", "topRight", "BOTTOMRIGHT" },
|
||||
{ "BOTTOMRIGHT", "bottomRight", "TOPRIGHT" },
|
||||
},
|
||||
},
|
||||
top = {
|
||||
points = {
|
||||
{ "TOPLEFT", "topLeft", "TOPRIGHT" },
|
||||
{ "TOPRIGHT", "topRight", "TOPLEFT" },
|
||||
},
|
||||
},
|
||||
bottom = {
|
||||
points = {
|
||||
{ "BOTTOMLEFT", "bottomLeft", "BOTTOMRIGHT" },
|
||||
{ "BOTTOMRIGHT", "bottomRight", "BOTTOMLEFT" },
|
||||
},
|
||||
},
|
||||
center = {
|
||||
points = {
|
||||
{ "TOPLEFT", "topLeft", "BOTTOMRIGHT" },
|
||||
{ "BOTTOMRIGHT", "bottomRight", "TOPLEFT" },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Metatable
|
||||
-- ============================================================================
|
||||
|
||||
local NINE_SLICE_MT = {
|
||||
__index = {
|
||||
Hide = function(self)
|
||||
local context = private.context[self]
|
||||
for _, texture in pairs(context.parts) do
|
||||
texture:Hide()
|
||||
end
|
||||
end,
|
||||
SetStyle = function(self, key, inset)
|
||||
local context = private.context[self]
|
||||
local style = private.styles[key]
|
||||
for part, texture in pairs(context.parts) do
|
||||
local partStyle = style[part]
|
||||
if partStyle then
|
||||
texture:Show()
|
||||
else
|
||||
texture:Hide()
|
||||
end
|
||||
end
|
||||
if context.styleKey == key and context.inset == inset then
|
||||
return
|
||||
end
|
||||
context.styleKey = key
|
||||
context.inset = inset
|
||||
for part, texture in pairs(context.parts) do
|
||||
local partStyle = style[part]
|
||||
if partStyle then
|
||||
texture:ClearAllPoints()
|
||||
for i, point in ipairs(PART_INFO[part].points) do
|
||||
local anchor, relFrame, relAnchor, xOff, yOff = nil, nil, nil, 0, 0
|
||||
if partStyle.offset then
|
||||
xOff, yOff = unpack(partStyle.offset[i])
|
||||
end
|
||||
if #point == 1 then
|
||||
anchor = unpack(point)
|
||||
elseif #point == 3 then
|
||||
anchor, relFrame, relAnchor = unpack(point)
|
||||
relFrame = context.parts[relFrame]
|
||||
assert(relFrame)
|
||||
else
|
||||
error("Invalid point")
|
||||
end
|
||||
if relFrame then
|
||||
texture:SetPoint(anchor, relFrame, relAnchor, xOff, yOff)
|
||||
else
|
||||
if inset and xOff == 0 and strmatch(anchor, "LEFT") then
|
||||
xOff = inset
|
||||
elseif inset and xOff == 0 and strmatch(anchor, "RIGHT") then
|
||||
xOff = -inset
|
||||
end
|
||||
if inset and yOff == 0 and strmatch(anchor, "TOP") then
|
||||
yOff = -inset
|
||||
elseif inset and yOff == 0 and strmatch(anchor, "BOTTOM") then
|
||||
yOff = inset
|
||||
end
|
||||
texture:SetPoint(anchor, xOff, yOff)
|
||||
end
|
||||
end
|
||||
texture:SetSize(partStyle.width, partStyle.height)
|
||||
texture:SetTexture(partStyle.texture)
|
||||
texture:SetTexCoord(unpack(partStyle.coord))
|
||||
end
|
||||
end
|
||||
end,
|
||||
SetVertexColor = function(self, r, g, b, a)
|
||||
for part in pairs(PART_INFO) do
|
||||
self:SetPartVertexColor(part, r, g, b, a)
|
||||
end
|
||||
end,
|
||||
SetPartVertexColor = function(self, part, r, g, b, a)
|
||||
local context = private.context[self]
|
||||
context.parts[part]:SetVertexColor(r, g, b, a)
|
||||
end,
|
||||
},
|
||||
__newindex = function(self, key, value) error("NineSlice cannot be modified") end,
|
||||
__tostring = function(self) return "NineSlice:"..strmatch(tostring(private.context[self]), "table:[^0-9a-fA-F]*([0-9a-fA-F]+)") end,
|
||||
__metatable = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Registers a nine-slice style.
|
||||
-- @tparam string key The style key
|
||||
-- @tparam table info The style info table
|
||||
function NineSlice.RegisterStyle(key, info)
|
||||
assert(not private.styles[key])
|
||||
private.styles[key] = info
|
||||
for part in pairs(PART_INFO) do
|
||||
-- allowed to be missing the center part
|
||||
if part ~= "center" or info[part] ~= nil then
|
||||
assert(type(info[part].texture) == "string")
|
||||
assert(#info[part].coord == 4)
|
||||
assert(info[part].width > 0)
|
||||
assert(info[part].height > 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Create an nine-slice object.
|
||||
-- @tparam table frame The parent frame
|
||||
-- @tparam[opt=0] number subLayer The texture subLayer
|
||||
-- @treturn NineSlice The nine-slice object
|
||||
function NineSlice.New(frame, subLayer)
|
||||
local obj = setmetatable({}, NINE_SLICE_MT)
|
||||
local context = {
|
||||
frame = frame,
|
||||
parts = {},
|
||||
styleKey = nil,
|
||||
inset = nil,
|
||||
}
|
||||
private.context[obj] = context
|
||||
|
||||
-- create all the textures
|
||||
for part in pairs(PART_INFO) do
|
||||
local texture = frame:CreateTexture(nil, "BACKGROUND", nil, subLayer or 0)
|
||||
texture:SetBlendMode("BLEND")
|
||||
context.parts[part] = texture
|
||||
end
|
||||
|
||||
-- set the points for all the textures
|
||||
for part, info in pairs(PART_INFO) do
|
||||
for _, point in ipairs(info.points) do
|
||||
if #point == 1 then
|
||||
context.parts[part]:SetPoint(unpack(point))
|
||||
elseif #point == 3 then
|
||||
local anchor, relFrame, relAnchor = unpack(point)
|
||||
relFrame = context.parts[relFrame]
|
||||
assert(relFrame)
|
||||
context.parts[part]:SetPoint(anchor, relFrame, relAnchor)
|
||||
else
|
||||
error("Invalid point")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return obj
|
||||
end
|
||||
120
LibTSM/Util/ObjectPool.lua
Normal file
120
LibTSM/Util/ObjectPool.lua
Normal file
@@ -0,0 +1,120 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- ObjectPool Functions.
|
||||
-- @module ObjectPool
|
||||
|
||||
local _, TSM = ...
|
||||
local ObjectPool = TSM.Init("Util.ObjectPool")
|
||||
local Debug = TSM.Include("Util.Debug")
|
||||
local private = {
|
||||
debugLeaks = TSM.__IS_TEST_ENV or false,
|
||||
instances = {},
|
||||
context = {},
|
||||
}
|
||||
local DEBUG_STATS_MIN_COUNT = 1
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Metatable
|
||||
-- ============================================================================
|
||||
|
||||
local OBJECT_POOL_MT = {
|
||||
__index = {
|
||||
Get = function(self)
|
||||
local context = private.context[self]
|
||||
local obj = tremove(context.freeList)
|
||||
if not obj then
|
||||
context.numCreated = context.numCreated + 1
|
||||
obj = context.createFunc()
|
||||
assert(obj)
|
||||
end
|
||||
if private.debugLeaks then
|
||||
context.state[obj] = (Debug.GetStackLevelLocation(2 + context.extraStackOffset) or "?").." -> "..(Debug.GetStackLevelLocation(3 + context.extraStackOffset) or "?")
|
||||
else
|
||||
context.state[obj] = "???"
|
||||
end
|
||||
return obj
|
||||
end,
|
||||
Recycle = function(self, obj)
|
||||
local context = private.context[self]
|
||||
assert(context.state[obj])
|
||||
context.state[obj] = nil
|
||||
tinsert(context.freeList, obj)
|
||||
end,
|
||||
},
|
||||
__newindex = function(self, key, value) error("Object pool cannot be modified") end,
|
||||
__metatable = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Create a new object pool.
|
||||
-- @tparam string name The name of the object pool for debug purposes
|
||||
-- @tparam function createFunc The function which is called to create a new object
|
||||
-- @tparam[opt=0] number extraStackOffset The extra stack offset for tracking where objects are being used from or nil to disable stack info
|
||||
-- @treturn ObjectPool The object pool object
|
||||
function ObjectPool.New(name, createFunc, extraStackOffset)
|
||||
assert(createFunc)
|
||||
assert(not private.instances[name])
|
||||
local pool = setmetatable({}, OBJECT_POOL_MT)
|
||||
private.context[pool] = {
|
||||
createFunc = createFunc,
|
||||
extraStackOffset = extraStackOffset or 0,
|
||||
freeList = {},
|
||||
state = {},
|
||||
numCreated = 0,
|
||||
}
|
||||
private.instances[name] = private.context[pool]
|
||||
return pool
|
||||
end
|
||||
|
||||
function ObjectPool.EnableLeakDebug()
|
||||
private.debugLeaks = true
|
||||
end
|
||||
|
||||
function ObjectPool.GetDebugInfo()
|
||||
local debugInfo = {}
|
||||
for name, context in pairs(private.instances) do
|
||||
local numCreated, numInUse, info = private.GetDebugStats(context)
|
||||
debugInfo[name] = {
|
||||
numCreated = numCreated,
|
||||
numInUse = numInUse,
|
||||
info = info,
|
||||
}
|
||||
end
|
||||
return debugInfo
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.GetDebugStats(context)
|
||||
local counts = {}
|
||||
local totalCount = 0
|
||||
for _, caller in pairs(context.state) do
|
||||
counts[caller] = (counts[caller] or 0) + 1
|
||||
totalCount = totalCount + 1
|
||||
end
|
||||
local debugInfo = {}
|
||||
for info, count in pairs(counts) do
|
||||
if count > DEBUG_STATS_MIN_COUNT then
|
||||
tinsert(debugInfo, format("[%d] %s", count, info))
|
||||
end
|
||||
end
|
||||
if #debugInfo == 0 then
|
||||
tinsert(debugInfo, "<none>")
|
||||
end
|
||||
return context.numCreated, totalCount, debugInfo
|
||||
end
|
||||
92
LibTSM/Util/ScriptWrapper.lua
Normal file
92
LibTSM/Util/ScriptWrapper.lua
Normal file
@@ -0,0 +1,92 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local ScriptWrapper = TSM.Init("Util.ScriptWrapper")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local private = {
|
||||
handlers = {},
|
||||
objLookup = {},
|
||||
wrappers = {},
|
||||
propagateWrappers = {},
|
||||
nestedLevel = 0,
|
||||
}
|
||||
local SCRIPT_CALLBACK_TIME_WARNING_THRESHOLD_MS = 20
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Sets the handler for a script on a frame.
|
||||
-- @tparam table frame The frame to call SetScript() on
|
||||
-- @tparam string script The script to set
|
||||
-- @tparam function handler The handler to set
|
||||
-- @tparam[opt=frame] table obj The object to pass to the handler as the first parameter (instead of frame)
|
||||
function ScriptWrapper.Set(frame, script, handler, obj)
|
||||
assert(type(frame) == "table" and type(script) == "string" and type(handler) == "function")
|
||||
local key = private.GetFrameScriptKey(frame, script)
|
||||
private.handlers[key] = handler
|
||||
private.objLookup[key] = obj or frame
|
||||
if not private.wrappers[script] then
|
||||
private.wrappers[script] = function(...)
|
||||
private.ScriptHandlerCommon(script, ...)
|
||||
end
|
||||
end
|
||||
frame:SetScript(script, private.wrappers[script])
|
||||
end
|
||||
|
||||
--- Sets the script handler to simply propogate the script to the parent element.
|
||||
-- @tparam table frame The frame to call SetScript() on
|
||||
-- @tparam string script The script which should be propagated
|
||||
-- @tparam[opt=frame] table obj The object to pass to the handler as the first parameter (instead of frame)
|
||||
function ScriptWrapper.SetPropagate(frame, script, obj)
|
||||
if not private.propagateWrappers[script] then
|
||||
private.propagateWrappers[script] = function(f, ...)
|
||||
local parentFrame = f:GetParent()
|
||||
local parentScript = parentFrame:GetScript(script)
|
||||
if not parentScript then
|
||||
return
|
||||
end
|
||||
parentScript(parentFrame, ...)
|
||||
end
|
||||
end
|
||||
ScriptWrapper.Set(frame, script, private.propagateWrappers[script], obj)
|
||||
end
|
||||
|
||||
--- Clears a previously-registered script handler.
|
||||
-- @tparam table frame The frame to clear the scrip ton
|
||||
-- @tparam string script The script which should be cleared
|
||||
function ScriptWrapper.Clear(frame, script)
|
||||
local key = private.GetFrameScriptKey(frame, script)
|
||||
private.handlers[key] = nil
|
||||
private.objLookup[key] = nil
|
||||
frame:SetScript(script, nil)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.GetFrameScriptKey(frame, script)
|
||||
return tostring(frame)..":"..script
|
||||
end
|
||||
|
||||
function private.ScriptHandlerCommon(script, frame, ...)
|
||||
local key = private.GetFrameScriptKey(frame, script)
|
||||
local obj = private.objLookup[key]
|
||||
private.nestedLevel = private.nestedLevel + 1
|
||||
local startTime = debugprofilestop()
|
||||
private.handlers[key](obj, ...)
|
||||
local timeTaken = debugprofilestop() - startTime
|
||||
private.nestedLevel = private.nestedLevel - 1
|
||||
if private.nestedLevel == 0 and timeTaken > SCRIPT_CALLBACK_TIME_WARNING_THRESHOLD_MS then
|
||||
Log.Warn("Script handler (%s) for frame (%s) took %0.2fms", script, tostring(obj), timeTaken)
|
||||
end
|
||||
end
|
||||
36
LibTSM/Util/SlotId.lua
Normal file
36
LibTSM/Util/SlotId.lua
Normal file
@@ -0,0 +1,36 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- SlotId Functions
|
||||
-- @module SlotId
|
||||
|
||||
local _, TSM = ...
|
||||
local SlotId = TSM.Init("Util.SlotId")
|
||||
local SLOT_ID_MULTIPLIER = 1000
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Combines a container and slot into a slotId.
|
||||
-- @tparam number container The container
|
||||
-- @tparam number slot The slot
|
||||
-- @treturn number The slotId
|
||||
function SlotId.Join(container, slot)
|
||||
return container * SLOT_ID_MULTIPLIER + slot
|
||||
end
|
||||
|
||||
--- Splits a slotId into a container and slot
|
||||
-- @tparam number slotId The slotId
|
||||
-- @treturn number container The container
|
||||
-- @treturn number slot The slot
|
||||
function SlotId.Split(slotId)
|
||||
local container = floor(slotId / SLOT_ID_MULTIPLIER)
|
||||
local slot = slotId % SLOT_ID_MULTIPLIER
|
||||
return container, slot
|
||||
end
|
||||
217
LibTSM/Util/SmartMap.lua
Normal file
217
LibTSM/Util/SmartMap.lua
Normal file
@@ -0,0 +1,217 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Smart Map.
|
||||
-- @module SmartMap
|
||||
|
||||
local _, TSM = ...
|
||||
local SmartMap = TSM.Init("Util.SmartMap")
|
||||
local private = {
|
||||
mapContext = {},
|
||||
readerContext = {},
|
||||
}
|
||||
local VALID_FIELD_TYPES = {
|
||||
string = true,
|
||||
number = true,
|
||||
boolean = true,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Metatable Methods
|
||||
-- ============================================================================
|
||||
|
||||
local SMART_MAP_MT = {
|
||||
-- getter
|
||||
__index = function(self, key)
|
||||
if key == nil then
|
||||
error("Attempt to get nil key")
|
||||
end
|
||||
if key == "ValueChanged" then
|
||||
return private.MapValueChanged
|
||||
elseif key == "SetCallbacksPaused" then
|
||||
return private.MapSetCallbacksPaused
|
||||
elseif key == "CreateReader" then
|
||||
return private.MapCreateReader
|
||||
elseif key == "GetKeyType" then
|
||||
return private.MapGetKeyType
|
||||
elseif key == "GetValueType" then
|
||||
return private.MapGetValueType
|
||||
elseif key == "Iterator" then
|
||||
return private.MapIterator
|
||||
else
|
||||
error("Invalid map method: "..tostring(key), 2)
|
||||
end
|
||||
end,
|
||||
|
||||
-- setter
|
||||
__newindex = function(self, key, value)
|
||||
error("Map cannot be written to directly", 2)
|
||||
end,
|
||||
|
||||
__tostring = function(self)
|
||||
return "SmartMap:"..strmatch(tostring(private.mapContext[self]), "table:[^0-9a-fA-F]*([0-9a-fA-F]+)")
|
||||
end,
|
||||
|
||||
__metatable = false,
|
||||
}
|
||||
|
||||
local READER_MT = {
|
||||
-- getter
|
||||
__index = function(self, key)
|
||||
-- check if the map already has the value for this key cached
|
||||
local readerContext = private.readerContext[self]
|
||||
local map = readerContext.map
|
||||
local mapContext = private.mapContext[map]
|
||||
if mapContext.data[key] ~= nil then
|
||||
return mapContext.data[key]
|
||||
end
|
||||
|
||||
-- get the value for this key
|
||||
local value = mapContext.func(key)
|
||||
if value == nil then
|
||||
error(format("No value for key (%s)", tostring(key)))
|
||||
elseif type(value) ~= mapContext.valueType then
|
||||
error(format("Invalid type of value (got %s, expected %s): %s", type(value), mapContext.valueType, tostring(value)))
|
||||
end
|
||||
|
||||
-- cache the value both on the map and on this reader
|
||||
mapContext.data[key] = value
|
||||
rawset(self, key, value)
|
||||
|
||||
return value
|
||||
end,
|
||||
|
||||
-- setter
|
||||
__newindex = function(self, key, value)
|
||||
error("Reader is read-only", 2)
|
||||
end,
|
||||
|
||||
__tostring = function(self)
|
||||
return "SmartMapReader:"..strmatch(tostring(private.readerContext[self]), "table:[^0-9a-fA-F]*([0-9a-fA-F]+)")
|
||||
end,
|
||||
|
||||
__metatable = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function SmartMap.New(keyType, valueType, callable)
|
||||
assert(VALID_FIELD_TYPES[keyType] and VALID_FIELD_TYPES[valueType])
|
||||
local map = setmetatable({}, SMART_MAP_MT)
|
||||
private.mapContext[map] = {
|
||||
keyType = keyType,
|
||||
valueType = valueType,
|
||||
func = callable,
|
||||
data = {},
|
||||
readers = {},
|
||||
callbacksPaused = 0,
|
||||
hasReaderCallback = false,
|
||||
}
|
||||
return map
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.MapValueChanged(self, key)
|
||||
local mapContext = private.mapContext[self]
|
||||
local oldValue = mapContext.data[key]
|
||||
if oldValue == nil then
|
||||
-- nobody cares about this value
|
||||
return
|
||||
end
|
||||
|
||||
if not mapContext.hasReaderCallback then
|
||||
-- no reader has registered a callback, so just clear the value
|
||||
mapContext.data[key] = nil
|
||||
for _, reader in ipairs(mapContext.readers) do
|
||||
rawset(reader, key, nil)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- get the new value
|
||||
local newValue = mapContext.func(key)
|
||||
if type(newValue) ~= mapContext.valueType then
|
||||
error(format("Invalid type (got %s, expected %s)", type(newValue), mapContext.valueType))
|
||||
end
|
||||
if oldValue == newValue then
|
||||
-- the value didn't change
|
||||
return
|
||||
end
|
||||
|
||||
-- update the data
|
||||
mapContext.data[key] = newValue
|
||||
|
||||
for _, reader in ipairs(mapContext.readers) do
|
||||
local readerContext = private.readerContext[reader]
|
||||
local prevValue = rawget(reader, key)
|
||||
if prevValue ~= nil then
|
||||
rawset(reader, key, newValue)
|
||||
if readerContext.callback then
|
||||
readerContext.pendingChanges[key] = prevValue
|
||||
if mapContext.callbacksPaused == 0 then
|
||||
readerContext.callback(reader, readerContext.pendingChanges)
|
||||
wipe(readerContext.pendingChanges)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function private.MapSetCallbacksPaused(self, paused)
|
||||
local mapContext = private.mapContext[self]
|
||||
if paused then
|
||||
mapContext.callbacksPaused = mapContext.callbacksPaused + 1
|
||||
else
|
||||
mapContext.callbacksPaused = mapContext.callbacksPaused - 1
|
||||
assert(mapContext.callbacksPaused >= 0)
|
||||
if mapContext.callbacksPaused == 0 then
|
||||
for _, reader in ipairs(mapContext.readers) do
|
||||
local readerContext = private.readerContext[reader]
|
||||
if readerContext.callback and next(readerContext.pendingChanges) then
|
||||
readerContext.callback(reader, readerContext.pendingChanges)
|
||||
wipe(readerContext.pendingChanges)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function private.MapCreateReader(self, callback)
|
||||
assert(callback == nil or type(callback) == "function")
|
||||
local reader = setmetatable({}, READER_MT)
|
||||
local mapContext = private.mapContext[self]
|
||||
tinsert(mapContext.readers, reader)
|
||||
mapContext.hasReaderCallback = mapContext.hasReaderCallback or (callback and true or false)
|
||||
private.readerContext[reader] = {
|
||||
map = self,
|
||||
callback = callback,
|
||||
pendingChanges = {},
|
||||
}
|
||||
return reader
|
||||
end
|
||||
|
||||
function private.MapGetKeyType(self)
|
||||
return private.mapContext[self].keyType
|
||||
end
|
||||
|
||||
function private.MapGetValueType(self)
|
||||
return private.mapContext[self].valueType
|
||||
end
|
||||
|
||||
function private.MapIterator(self)
|
||||
return pairs(private.mapContext[self].data)
|
||||
end
|
||||
84
LibTSM/Util/Sound.lua
Normal file
84
LibTSM/Util/Sound.lua
Normal file
@@ -0,0 +1,84 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Sound Functions
|
||||
-- @module Sound
|
||||
|
||||
local _, TSM = ...
|
||||
local Sound = TSM.Init("Util.Sound")
|
||||
local L = TSM.Include("Locale").GetTable()
|
||||
local NO_SOUND_KEY = "TSM_NO_SOUND" -- this can never change
|
||||
local SOUNDS = {
|
||||
[NO_SOUND_KEY] = "<"..L["No Sound"]..">",
|
||||
["AuctionWindowOpen"] = L["Auction Window Open"],
|
||||
["AuctionWindowClose"] = L["Auction Window Close"],
|
||||
["alarmclockwarning3"] = L["Alarm Clock"],
|
||||
["UI_AutoQuestComplete"] = L["Auto Quest Complete"],
|
||||
["TSM_CASH_REGISTER"] = L["Cash Register"],
|
||||
["HumanExploration"] = L["Exploration"],
|
||||
["Fishing Reel in"] = L["Fishing Reel In"],
|
||||
["LevelUp"] = L["Level Up"],
|
||||
["MapPing"] = L["Map Ping"],
|
||||
["MONEYFRAMEOPEN"] = L["Money Frame Open"],
|
||||
["IgPlayerInviteAccept"] = L["Player Invite Accept"],
|
||||
["QUESTADDED"] = L["Quest Added"],
|
||||
["QUESTCOMPLETED"] = L["Quest Completed"],
|
||||
["UI_QuestObjectivesComplete"] = L["Quest Objectives Complete"],
|
||||
["RaidWarning"] = L["Raid Warning"],
|
||||
["ReadyCheck"] = L["Ready Check"],
|
||||
["UnwrapGift"] = L["Unwrap Gift"],
|
||||
}
|
||||
local SOUNDKITIDS = {
|
||||
["AuctionWindowOpen"] = 5274,
|
||||
["AuctionWindowClose"] = 5275,
|
||||
["alarmclockwarning3"] = 12889,
|
||||
["UI_AutoQuestComplete"] = 23404,
|
||||
["HumanExploration"] = 4140,
|
||||
["Fishing Reel in"] = 3407,
|
||||
["LevelUp"] = 888,
|
||||
["MapPing"] = 3175,
|
||||
["MONEYFRAMEOPEN"] = 891,
|
||||
["IgPlayerInviteAccept"] = 880,
|
||||
["QUESTADDED"] = 618,
|
||||
["QUESTCOMPLETED"] = 878,
|
||||
["UI_QuestObjectivesComplete"] = 26905,
|
||||
["RaidWarning"] = 8959,
|
||||
["ReadyCheck"] = 8960,
|
||||
["UnwrapGift"] = 64329,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Gets the key used to represent no sound.
|
||||
-- @return The key used to represent no sound
|
||||
function Sound.GetNoSoundKey()
|
||||
return NO_SOUND_KEY
|
||||
end
|
||||
|
||||
--- Gets the key-value table containing all supported sounds.
|
||||
-- The key is the what gets passed to @{Sound.PlaySound} and the value is a localized string describing the sound.
|
||||
-- @return The sounds table
|
||||
function Sound.GetSounds()
|
||||
return SOUNDS
|
||||
end
|
||||
|
||||
--- Plays a sound and flashes the client icon.
|
||||
-- @param soundKey The key of the sound (from @{Sound.GetSounds}) to play
|
||||
function Sound.PlaySound(soundKey)
|
||||
if soundKey == NO_SOUND_KEY then
|
||||
-- do nothing
|
||||
elseif soundKey == "TSM_CASH_REGISTER" then
|
||||
PlaySoundFile("Interface\\Addons\\TradeSkillMaster\\Media\\register.mp3", "Master")
|
||||
FlashClientIcon()
|
||||
else
|
||||
PlaySound(SOUNDKITIDS[soundKey], "Master")
|
||||
FlashClientIcon()
|
||||
end
|
||||
end
|
||||
93
LibTSM/Util/String.lua
Normal file
93
LibTSM/Util/String.lua
Normal file
@@ -0,0 +1,93 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- String Functions
|
||||
-- @module String
|
||||
|
||||
local _, TSM = ...
|
||||
local String = TSM.Init("Util.String")
|
||||
local MAGIC_CHARACTERS = {
|
||||
["["] = true,
|
||||
["]"] = true,
|
||||
["("] = true,
|
||||
[")"] = true,
|
||||
["."] = true,
|
||||
["+"] = true,
|
||||
["-"] = true,
|
||||
["*"] = true,
|
||||
["?"] = true,
|
||||
["^"] = true,
|
||||
["$"] = true,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Splits a string in a way which won't cause stack overflows for large inputs.
|
||||
-- The lua strsplit function causes a stack overflow if passed large inputs. This API fixes that issue and also supports
|
||||
-- separators which are more than one character in length.
|
||||
-- @tparam string str The string to be split
|
||||
-- @tparam string sep The separator to use to split the string
|
||||
-- @tparam[opt=nil] table resultTbl An optional table to store the result in
|
||||
-- @treturn table The result as a list of substrings
|
||||
function String.SafeSplit(str, sep, resultTbl)
|
||||
resultTbl = resultTbl or {}
|
||||
local s = 1
|
||||
local sepLength = #sep
|
||||
if sepLength == 0 then
|
||||
tinsert(resultTbl, str)
|
||||
return resultTbl
|
||||
end
|
||||
while true do
|
||||
local e = strfind(str, sep, s)
|
||||
if not e then
|
||||
tinsert(resultTbl, strsub(str, s))
|
||||
break
|
||||
end
|
||||
tinsert(resultTbl, strsub(str, s, e - 1))
|
||||
s = e + sepLength
|
||||
end
|
||||
return resultTbl
|
||||
end
|
||||
|
||||
--- Escapes any magic characters used by lua's pattern matching.
|
||||
-- @tparam string str The string to be escaped
|
||||
-- @treturn string The escaped string
|
||||
function String.Escape(str)
|
||||
assert(not strmatch(str, "\001"), "Input string must not contain '\\001' characters")
|
||||
str = gsub(str, "%%", "\001")
|
||||
for char in pairs(MAGIC_CHARACTERS) do
|
||||
str = gsub(str, "%"..char, "%%"..char)
|
||||
end
|
||||
str = gsub(str, "\001", "%%%%")
|
||||
return str
|
||||
end
|
||||
|
||||
--- Check if a string which contains multiple values separated by a specific string contains the value.
|
||||
-- @tparam string str The string to be searched
|
||||
-- @tparam string sep The separating string
|
||||
-- @tparam string value The value to search for
|
||||
-- @treturn boolean Whether or not the value was found
|
||||
-- @within String
|
||||
function String.SeparatedContains(str, sep, value)
|
||||
return str == value or strmatch(str, "^"..value..sep) or strmatch(str, sep..value..sep) or strmatch(str, sep..value.."$")
|
||||
end
|
||||
|
||||
--- Iterates over the parts of a string which are separated by a character.
|
||||
-- @tparam string str The string to be split
|
||||
-- @tparam string sep The separator to use to split the string
|
||||
-- @return An iterator with fields: `part`
|
||||
-- @within String
|
||||
function String.SplitIterator(str, sep)
|
||||
assert(#sep == 1)
|
||||
if MAGIC_CHARACTERS[sep] then
|
||||
sep = "%"..sep
|
||||
end
|
||||
return gmatch(str, "([^"..sep.."]+)")
|
||||
end
|
||||
484
LibTSM/Util/Table.lua
Normal file
484
LibTSM/Util/Table.lua
Normal file
@@ -0,0 +1,484 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Table Functions
|
||||
-- @module Table
|
||||
|
||||
local _, TSM = ...
|
||||
local Table = TSM.Init("Util.Table")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local private = {
|
||||
filterTemp = {},
|
||||
sortValueLookup = nil,
|
||||
sortValueReverse = false,
|
||||
sortValueUnstable = false,
|
||||
iterContext = { arg = {}, index = {}, helperFunc = {}, cleanupFunc = {} },
|
||||
inwardIteratorContext = {},
|
||||
}
|
||||
setmetatable(private.iterContext.arg, { __mode = "k" })
|
||||
setmetatable(private.iterContext.index, { __mode = "k" })
|
||||
setmetatable(private.iterContext.helperFunc, { __mode = "k" })
|
||||
setmetatable(private.iterContext.cleanupFunc, { __mode = "k" })
|
||||
local READ_ONLY_TABLE_MT = {
|
||||
__index = function(_, key) error(format("Key (%s) does not exist in read-only table", tostring(key)), 2) end,
|
||||
__newindex = function(_, key) error(format("Writing (%s) to read-only table", tostring(key)), 2) end,
|
||||
__metatable = false,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Creates an iterator from a table.
|
||||
-- NOTE: This iterator must be run to completion and not interrupted (i.e. with a `break` or `return`).
|
||||
-- @tparam table tbl The table (numerically-indexed) to iterate over
|
||||
-- @tparam[opt] function helperFunc A helper function which gets passed the current index, value, and user-specified arg
|
||||
-- and returns nothing if an entry in the table should be skipped or the result of an iteration loop
|
||||
-- @param[opt] arg A value to be passed to the helper function
|
||||
-- @tparam[opt] function cleanupFunc A function to be called (passed `tbl`) to cleanup at the end of iterator
|
||||
-- @return An iterator with fields: `index, value` or the return of `helperFunc`
|
||||
function Table.Iterator(tbl, helperFunc, arg, cleanupFunc)
|
||||
local iterContext = TempTable.Acquire()
|
||||
iterContext.data = tbl
|
||||
iterContext.arg = arg
|
||||
iterContext.index = 0
|
||||
iterContext.helperFunc = helperFunc
|
||||
iterContext.cleanupFunc = cleanupFunc
|
||||
return private.TableIterator, iterContext
|
||||
end
|
||||
|
||||
--- Creates an iterator from the keys of a table.
|
||||
-- @tparam table tbl The table to iterate over the keys of
|
||||
-- @return An iterator with fields: `key`
|
||||
function Table.KeyIterator(tbl)
|
||||
return private.TableKeyIterator, tbl, nil
|
||||
end
|
||||
|
||||
--- Uses a function to filter the entries in a table.
|
||||
-- @tparam table tbl The table to be filtered
|
||||
-- @tparam function func The filter function which gets passed `key, value, ...` and returns true if that entry should
|
||||
-- be removed from the table
|
||||
-- @param[opt] ... Optional arguments to be passed to the filter function
|
||||
function Table.Filter(tbl, func, ...)
|
||||
assert(not next(private.filterTemp))
|
||||
for k, v in pairs(tbl) do
|
||||
if func(k, v, ...) then
|
||||
tinsert(private.filterTemp, k)
|
||||
end
|
||||
end
|
||||
for _, k in ipairs(private.filterTemp) do
|
||||
tbl[k] = nil
|
||||
end
|
||||
wipe(private.filterTemp)
|
||||
end
|
||||
|
||||
--- Removes all occurences of the value in the table.
|
||||
-- Only the numerically-indexed entries are checked.
|
||||
-- @tparam table tbl The table to remove the value from
|
||||
-- @param value The value to remove
|
||||
-- @treturn number The number of values removed
|
||||
function Table.RemoveByValue(tbl, value)
|
||||
local numRemoved = 0
|
||||
for i = #tbl, 1, -1 do
|
||||
if tbl[i] == value then
|
||||
tremove(tbl, i)
|
||||
numRemoved = numRemoved + 1
|
||||
end
|
||||
end
|
||||
return numRemoved
|
||||
end
|
||||
|
||||
--- Gets the table key by value.
|
||||
-- @tparam table tbl The table to look through
|
||||
-- @param value The value to get the key of
|
||||
-- @return The key for the specified value or `nil`
|
||||
function Table.KeyByValue(tbl, value)
|
||||
for k, v in pairs(tbl) do
|
||||
if v == value then
|
||||
return k
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Gets the number of entries in the table.
|
||||
-- This can be used when the count of a non-numerically-indexed table is desired (i.e. `#tbl` wouldn't work).
|
||||
-- @tparam table tbl The table to get the number of entries in
|
||||
-- @treturn number The number of entries
|
||||
function Table.Count(tbl)
|
||||
local count = 0
|
||||
for _ in pairs(tbl) do
|
||||
count = count + 1
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
--- Gets the distinct table key by value.
|
||||
-- This function will assert if the value is not found in the table or if more than one key is found.
|
||||
-- @tparam table tbl The table to look through
|
||||
-- @param value The value to get the key of
|
||||
-- @return The key for the specified value
|
||||
function Table.GetDistinctKey(tbl, value)
|
||||
local key = nil
|
||||
for k, v in pairs(tbl) do
|
||||
if v == value then
|
||||
assert(not key)
|
||||
key = k
|
||||
end
|
||||
end
|
||||
assert(key)
|
||||
return key
|
||||
end
|
||||
|
||||
--- Checks if two tables have the same entries (non-recursively).
|
||||
-- @tparam table tbl1 The first table to check
|
||||
-- @tparam table tbl2 The second table to check
|
||||
-- @treturn boolean Whether or not the tables are equal
|
||||
function Table.Equal(tbl1, tbl2)
|
||||
if Table.Count(tbl1) ~= Table.Count(tbl2) then
|
||||
return false
|
||||
end
|
||||
for k, v in pairs(tbl1) do
|
||||
if tbl2[k] ~= v then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- Returns whether or not the table is currently sorted.
|
||||
-- @tparam table tbl The table to check
|
||||
-- @tparam[opt] function sortFunc The helper function to use to determine sort order (same prototype as is used for `sort()`)
|
||||
-- @tparam[opt=1] number firstIndex The first index to check
|
||||
-- @tparam[opt=#tbl] number lastIndex The last index to check
|
||||
-- @treturn boolean Whether or not the table is sorted
|
||||
function Table.IsSorted(tbl, sortFunc, firstIndex, lastIndex)
|
||||
sortFunc = sortFunc or private.DefaultSortFunc
|
||||
firstIndex = firstIndex or 1
|
||||
lastIndex = lastIndex or #tbl
|
||||
local prevValue = tbl[firstIndex]
|
||||
for i = firstIndex + 1, lastIndex do
|
||||
local value = tbl[i]
|
||||
if sortFunc(value, prevValue) then
|
||||
return false
|
||||
end
|
||||
prevValue = value
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- Sorts a table with some optimizations over lua's sort().
|
||||
-- @tparam table tbl The table to sort
|
||||
-- @tparam[opt] function sortFunc The helper function to use to determine sort order (same prototype as is used for `sort()`)
|
||||
function Table.Sort(tbl, sortFunc)
|
||||
if Table.IsSorted(tbl, sortFunc) then
|
||||
return
|
||||
end
|
||||
sort(tbl, sortFunc)
|
||||
end
|
||||
|
||||
--- Returns whether or not the table is currently sorted with a value lookup table.
|
||||
-- @tparam table tbl The table to sort
|
||||
-- @tparam table valueLookup The sort value lookup table
|
||||
-- @tparam[opt=1] number firstIndex The first index to check
|
||||
-- @tparam[opt=#tbl] number lastIndex The last index to check
|
||||
function Table.IsSortedWithValueLookup(tbl, valueLookup, firstIndex, lastIndex)
|
||||
assert(not private.sortValueLookup and valueLookup)
|
||||
private.sortValueLookup = valueLookup
|
||||
private.sortValueReverse = false
|
||||
private.sortValueUnstable = false
|
||||
local result = Table.IsSorted(tbl, private.TableSortWithValueLookupHelper, firstIndex, lastIndex)
|
||||
private.sortValueLookup = nil
|
||||
return result
|
||||
end
|
||||
|
||||
--- Merges two sorted tables with a value lookup table.
|
||||
-- @tparam table tbl1 The first table to merge
|
||||
-- @tparam table tbl2 The second table to merge
|
||||
-- @tparam table result The result table
|
||||
-- @tparam table valueLookup The sort value lookup table
|
||||
function Table.MergeSortedWithValueLookup(tbl1, tbl2, result, valueLookup)
|
||||
assert(not private.sortValueLookup and valueLookup)
|
||||
private.sortValueLookup = valueLookup
|
||||
private.sortValueReverse = false
|
||||
private.sortValueUnstable = false
|
||||
|
||||
local index1, index2, resultIndex = 1, 1, 1
|
||||
while true do
|
||||
local value1 = tbl1[index1]
|
||||
local value2 = tbl2[index2]
|
||||
if value1 == nil and value2 == nil then
|
||||
-- we're done
|
||||
break
|
||||
elseif value1 == nil then
|
||||
result[resultIndex] = value2
|
||||
index2 = index2 + 1
|
||||
elseif value2 == nil then
|
||||
result[resultIndex] = value1
|
||||
index1 = index1 + 1
|
||||
elseif private.TableSortWithValueLookupHelper(value1, value2) then
|
||||
result[resultIndex] = value1
|
||||
index1 = index1 + 1
|
||||
else
|
||||
result[resultIndex] = value2
|
||||
index2 = index2 + 1
|
||||
end
|
||||
resultIndex = resultIndex + 1
|
||||
end
|
||||
private.sortValueLookup = nil
|
||||
end
|
||||
|
||||
--- Does a table sort with an extra value lookup step.
|
||||
-- @tparam table tbl The table to sort
|
||||
-- @tparam table valueLookup The sort value lookup table
|
||||
-- @tparam[opt=false] boolean reverse Reverse the sort order
|
||||
-- @tparam[opt=false] boolean unstable Don't try to make the sort stable
|
||||
function Table.SortWithValueLookup(tbl, valueLookup, reverse, unstable)
|
||||
assert(not private.sortValueLookup and valueLookup)
|
||||
private.sortValueLookup = valueLookup
|
||||
private.sortValueReverse = reverse
|
||||
private.sortValueUnstable = unstable
|
||||
Table.Sort(tbl, private.TableSortWithValueLookupHelper)
|
||||
private.sortValueLookup = nil
|
||||
end
|
||||
|
||||
--- Creates an iterator which iterates through a numerically-indexed table (list) from the ends inward.
|
||||
-- @tparam table tbl The table to iterate over
|
||||
-- @return An iterator with fields: `index`, `value`, `isAscending`
|
||||
function Table.InwardIterator(tbl)
|
||||
assert(not private.inwardIteratorContext[tbl])
|
||||
local context = TempTable.Acquire()
|
||||
private.inwardIteratorContext[tbl] = context
|
||||
context.inUse = true
|
||||
context.tbl = tbl
|
||||
context.leftIndex = 1
|
||||
context.rightIndex = #tbl
|
||||
context.isAscending = true
|
||||
return private.InwardIteratorHelper, context, 0
|
||||
end
|
||||
|
||||
--- Reverses the direction of the current inward iterator.
|
||||
-- @tparam table tbl The table being iterated over
|
||||
function Table.InwardIteratorReverse(tbl)
|
||||
local context = private.inwardIteratorContext[tbl]
|
||||
assert(context and context.tbl == tbl)
|
||||
context.isAscending = not context.isAscending
|
||||
end
|
||||
|
||||
--- Sets a table as read-only (modifications aren't checked).
|
||||
-- @tparam table tbl The table to make read-only
|
||||
function Table.SetReadOnly(tbl)
|
||||
setmetatable(tbl, READ_ONLY_TABLE_MT)
|
||||
end
|
||||
|
||||
--- Appends all values passed in to the end of the table.
|
||||
-- @tparam table tbl The table to insert the data into
|
||||
-- @param ... The values to insert
|
||||
function Table.Append(tbl, ...)
|
||||
local len = #tbl
|
||||
for i = 1, select("#", ...) do
|
||||
tbl[len + i] = select(i, ...)
|
||||
end
|
||||
end
|
||||
|
||||
--- Performs a binary search on a sorted table and returns the index of the search value.
|
||||
-- @tparam table tbl The table to search
|
||||
-- @tparam number|string searchValue The value to search for
|
||||
-- @tparam[opt=nil] function valueFunc A function to call to get the value to compare
|
||||
-- @param ... Extra values to pass to valueFunc
|
||||
-- @treturn ?number The index of the value or nil if it wasn't found
|
||||
-- @treturn ?number The insert index
|
||||
function Table.BinarySearch(tbl, searchValue, valueFunc, ...)
|
||||
if valueFunc then
|
||||
searchValue = valueFunc(searchValue, ...)
|
||||
end
|
||||
local insertIndex = 1
|
||||
local low, mid, high = 1, 0, #tbl
|
||||
while low <= high do
|
||||
mid = floor((low + high) / 2)
|
||||
local value = tbl[mid]
|
||||
if valueFunc then
|
||||
value = valueFunc(tbl[mid], ...)
|
||||
end
|
||||
if value == searchValue then
|
||||
return mid, mid
|
||||
elseif value < searchValue then
|
||||
-- we're too low
|
||||
low = mid + 1
|
||||
else
|
||||
-- we're too high
|
||||
high = mid - 1
|
||||
end
|
||||
insertIndex = low
|
||||
end
|
||||
return nil, insertIndex
|
||||
end
|
||||
|
||||
--- Inserts a value into a sorted table by using the insertIndex returned by Table.BinarySearch().
|
||||
-- @see Table.BinarySearch
|
||||
-- @tparam table tbl The table
|
||||
-- @tparam number|string value The value to insert
|
||||
-- @tparam[opt=nil] function valueFunc A function to call to get the value to compare
|
||||
-- @param ... Extra values to pass to valueFunc
|
||||
function Table.InsertSorted(tbl, value, valueFunc, ...)
|
||||
local _, insertIndex = Table.BinarySearch(tbl, value, valueFunc, ...)
|
||||
tinsert(tbl, insertIndex, value)
|
||||
end
|
||||
|
||||
--- Gets the common values from two or more sorted tables.
|
||||
-- @tparam table tbls The tables to compare
|
||||
-- @tparam table result The result table
|
||||
-- @tparam[opt=nil] function valueFunc A function to call to get the value to compare
|
||||
-- @param ... Extra values to pass to valueFunc
|
||||
function Table.GetCommonValuesSorted(tbls, result, valueFunc, ...)
|
||||
local numTbls = #tbls
|
||||
if numTbls == 0 then
|
||||
return
|
||||
elseif numTbls == 1 then
|
||||
for i = 1, #tbls[1] do
|
||||
result[i] = tbls[1][i]
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- initialize our iterator indexes
|
||||
for i = 1, numTbls do
|
||||
local t = tbls[i]
|
||||
assert(t._index == nil)
|
||||
t._index = 1
|
||||
end
|
||||
|
||||
while true do
|
||||
-- go through each list and check if the current values are equal and get the max value
|
||||
local isDone, isEqual = false, true
|
||||
local equalValue, maxValue = nil, nil
|
||||
for i = 1, numTbls do
|
||||
local t = tbls[i]
|
||||
local value = t[t._index]
|
||||
value = value and valueFunc and valueFunc(value, ...)
|
||||
if not value then
|
||||
isDone = true
|
||||
break
|
||||
elseif i == 1 then
|
||||
equalValue = value
|
||||
maxValue = value
|
||||
else
|
||||
if value ~= equalValue then
|
||||
isEqual = false
|
||||
end
|
||||
if value > maxValue then
|
||||
maxValue = value
|
||||
end
|
||||
end
|
||||
end
|
||||
if isDone then
|
||||
break
|
||||
end
|
||||
if isEqual then
|
||||
-- all lists contained the same value, so insert it into our result and advance all the indexes
|
||||
tinsert(result, tbls[1][tbls[1]._index])
|
||||
for i = 1, numTbls do
|
||||
local t = tbls[i]
|
||||
t._index = t._index + 1
|
||||
end
|
||||
else
|
||||
-- all lists aren't on the same value, so advanced each one to at least the current max value
|
||||
for i = 1, numTbls do
|
||||
local t = tbls[i]
|
||||
local value = t[t._index]
|
||||
value = value and valueFunc and valueFunc(value, ...)
|
||||
while value and value < maxValue do
|
||||
t._index = t._index + 1
|
||||
value = t[t._index]
|
||||
value = value and valueFunc and valueFunc(value, ...)
|
||||
end
|
||||
end
|
||||
if isDone then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- clear all our iterator indexes
|
||||
for i = 1, numTbls do
|
||||
tbls[i]._index = nil
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.TableKeyIterator(tbl, prevKey)
|
||||
local key = next(tbl, prevKey)
|
||||
return key
|
||||
end
|
||||
|
||||
function private.TableIterator(iterContext)
|
||||
iterContext.index = iterContext.index + 1
|
||||
if iterContext.index > #iterContext.data then
|
||||
local data = iterContext.data
|
||||
local cleanupFunc = iterContext.cleanupFunc
|
||||
TempTable.Release(iterContext)
|
||||
if cleanupFunc then
|
||||
cleanupFunc(data)
|
||||
end
|
||||
return
|
||||
end
|
||||
if iterContext.helperFunc then
|
||||
local result = TempTable.Acquire(iterContext.helperFunc(iterContext.index, iterContext.data[iterContext.index], iterContext.arg))
|
||||
if #result == 0 then
|
||||
TempTable.Release(result)
|
||||
return private.TableIterator(iterContext)
|
||||
end
|
||||
return TempTable.UnpackAndRelease(result)
|
||||
else
|
||||
return iterContext.index, iterContext.data[iterContext.index]
|
||||
end
|
||||
end
|
||||
|
||||
function private.TableSortWithValueLookupHelper(a, b)
|
||||
local aValue = private.sortValueLookup[a]
|
||||
local bValue = private.sortValueLookup[b]
|
||||
if aValue == bValue then
|
||||
if private.sortValueUnstable then
|
||||
return false
|
||||
else
|
||||
return a > b
|
||||
end
|
||||
end
|
||||
if private.sortValueReverse then
|
||||
return aValue > bValue
|
||||
else
|
||||
return aValue < bValue
|
||||
end
|
||||
end
|
||||
|
||||
function private.InwardIteratorHelper(context)
|
||||
if context.leftIndex > context.rightIndex then
|
||||
private.inwardIteratorContext[context.tbl] = nil
|
||||
TempTable.Release(context)
|
||||
return
|
||||
end
|
||||
local index = nil
|
||||
if context.isAscending then
|
||||
-- iterating in ascending order
|
||||
index = context.leftIndex
|
||||
context.leftIndex = context.leftIndex + 1
|
||||
else
|
||||
-- iterating in descending order
|
||||
index = context.rightIndex
|
||||
context.rightIndex = context.rightIndex - 1
|
||||
end
|
||||
return index, context.tbl[index], context.isAscending
|
||||
end
|
||||
|
||||
function private.DefaultSortFunc(a, b)
|
||||
return a < b
|
||||
end
|
||||
141
LibTSM/Util/TempTable.lua
Normal file
141
LibTSM/Util/TempTable.lua
Normal file
@@ -0,0 +1,141 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- TempTable Functions
|
||||
-- @module TempTable
|
||||
|
||||
local _, TSM = ...
|
||||
local TempTable = TSM.Init("Util.TempTable")
|
||||
local Debug = TSM.Include("Util.Debug")
|
||||
local private = {
|
||||
debugLeaks = TSM.__IS_TEST_ENV or false,
|
||||
freeTempTables = {},
|
||||
tempTableState = {},
|
||||
}
|
||||
local NUM_TEMP_TABLES = 100
|
||||
local RELEASED_TEMP_TABLE_MT = {
|
||||
__newindex = function(self, key, value)
|
||||
error("Attempt to access temp table after release")
|
||||
end,
|
||||
__index = function(self, key)
|
||||
error("Attempt to access temp table after release")
|
||||
end,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Acquires a temporary table.
|
||||
-- Temporary tables are recycled tables which can be used instead of creating a new table every time one is needed for a
|
||||
-- defined lifecycle. This avoids relying on the garbage collector and improves overall performance.
|
||||
-- @param ... Any number of valuse to insert into the table initially
|
||||
-- @treturn table The temporary table
|
||||
function TempTable.Acquire(...)
|
||||
local tbl = tremove(private.freeTempTables, 1)
|
||||
assert(tbl, "Could not acquire temp table")
|
||||
setmetatable(tbl, nil)
|
||||
if private.debugLeaks then
|
||||
private.tempTableState[tbl] = (Debug.GetStackLevelLocation(2) or "?").." -> "..(Debug.GetStackLevelLocation(3) or "?")
|
||||
else
|
||||
private.tempTableState[tbl] = true
|
||||
end
|
||||
for i = 1, select("#", ...) do
|
||||
tbl[i] = select(i, ...)
|
||||
end
|
||||
return tbl
|
||||
end
|
||||
|
||||
--- Iterators over a temporary table, releasing it when done.
|
||||
-- NOTE: This iterator must be run to completion and not interrupted (i.e. with a `break` or `return`).
|
||||
-- @tparam table tbl The temporary table to iterator over
|
||||
-- @tparam[opt=1] number numFields The number of fields to unpack with each iteration
|
||||
-- @return An iterator with fields: `index, {numFields...}`
|
||||
function TempTable.Iterator(tbl, numFields)
|
||||
numFields = numFields or 1
|
||||
assert(numFields >= 1 and #tbl % numFields == 0)
|
||||
assert(private.tempTableState[tbl])
|
||||
tbl.__iterNumFields = numFields
|
||||
return private.TempTableIteratorHelper, tbl, 1 - numFields
|
||||
end
|
||||
|
||||
--- Releases a temporary table.
|
||||
-- The temporary table will be returned to the pool and must not be accessed after being released.
|
||||
-- @tparam table tbl The temporary table to release
|
||||
function TempTable.Release(tbl)
|
||||
private.TempTableReleaseHelper(tbl)
|
||||
end
|
||||
|
||||
--- Releases a temporary table and returns its values.
|
||||
-- Releases the temporary table (see @{TempTable.Release}) and returns its unpacked values.
|
||||
-- @tparam table tbl The temporary table to release and unpack
|
||||
-- @return The result of calling `unpack` on the table
|
||||
function TempTable.UnpackAndRelease(tbl)
|
||||
return private.TempTableReleaseHelper(tbl, unpack(tbl))
|
||||
end
|
||||
|
||||
function TempTable.EnableLeakDebug()
|
||||
private.debugLeaks = true
|
||||
end
|
||||
|
||||
function TempTable.GetDebugInfo()
|
||||
local debugInfo = {}
|
||||
local counts = {}
|
||||
for _, info in pairs(private.tempTableState) do
|
||||
counts[info] = (counts[info] or 0) + 1
|
||||
end
|
||||
for info, count in pairs(counts) do
|
||||
tinsert(debugInfo, format("[%d] %s", count, type(info) == "string" and info or "?"))
|
||||
end
|
||||
if #debugInfo == 0 then
|
||||
tinsert(debugInfo, "<none>")
|
||||
end
|
||||
return debugInfo
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.TempTableIteratorHelper(tbl, index)
|
||||
local numFields = tbl.__iterNumFields
|
||||
index = index + numFields
|
||||
if index > #tbl then
|
||||
TempTable.Release(tbl)
|
||||
return
|
||||
end
|
||||
if numFields == 1 then
|
||||
return index, tbl[index]
|
||||
else
|
||||
return index, unpack(tbl, index, index + numFields - 1)
|
||||
end
|
||||
end
|
||||
|
||||
function private.TempTableReleaseHelper(tbl, ...)
|
||||
assert(private.tempTableState[tbl])
|
||||
wipe(tbl)
|
||||
tinsert(private.freeTempTables, tbl)
|
||||
private.tempTableState[tbl] = nil
|
||||
setmetatable(tbl, RELEASED_TEMP_TABLE_MT)
|
||||
return ...
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Temp Table Setup
|
||||
-- ============================================================================
|
||||
|
||||
do
|
||||
for _ = 1, NUM_TEMP_TABLES do
|
||||
local tempTbl = setmetatable({}, RELEASED_TEMP_TABLE_MT)
|
||||
tinsert(private.freeTempTables, tempTbl)
|
||||
end
|
||||
end
|
||||
318
LibTSM/Util/Theme.lua
Normal file
318
LibTSM/Util/Theme.lua
Normal file
@@ -0,0 +1,318 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Theme Functions.
|
||||
-- @module Theme
|
||||
|
||||
local _, TSM = ...
|
||||
local Theme = TSM.Init("Util.Theme")
|
||||
local FontPaths = TSM.Include("Data.FontPaths")
|
||||
local Table = TSM.Include("Util.Table")
|
||||
local Color = TSM.Include("Util.Color")
|
||||
local FontObject = TSM.Include("Util.FontObject")
|
||||
local private = {
|
||||
callbacks = {},
|
||||
names = {},
|
||||
colorSets = {},
|
||||
currentColorSet = nil,
|
||||
fontFrame = nil,
|
||||
currentFontSet = nil,
|
||||
}
|
||||
local THEME_COLOR_KEYS = {
|
||||
FRAME_BG = true,
|
||||
PRIMARY_BG = true,
|
||||
PRIMARY_BG_ALT = true,
|
||||
ACTIVE_BG = true,
|
||||
ACTIVE_BG_ALT = true,
|
||||
}
|
||||
local STATIC_COLORS = {
|
||||
INDICATOR = Color.NewFromHex("#ffd839"),
|
||||
INDICATOR_ALT = Color.NewFromHex("#79a2ff"),
|
||||
INDICATOR_DISABLED = Color.NewFromHex("#6f5819"),
|
||||
|
||||
TEXT = Color.NewFromHex("#ffffff"),
|
||||
TEXT_ALT = Color.NewFromHex("#e2e2e2"),
|
||||
TEXT_DISABLED = Color.NewFromHex("#424242"),
|
||||
}
|
||||
local FEEDBACK_COLORS = {
|
||||
RED = Color.NewFromHex("#f72d20"),
|
||||
YELLOW = Color.NewFromHex("#e1f720"),
|
||||
GREEN = Color.NewFromHex("#4ff720"),
|
||||
BLUE = Color.NewFromHex("#2076f7"),
|
||||
ORANGE = Color.NewFromHex("#f77a20"),
|
||||
}
|
||||
local BLIZZARD_COLOR = Color.NewFromHex("#00b4ff")
|
||||
local GROUP_COLORS = {
|
||||
Color.NewFromHex("#fcf141"),
|
||||
Color.NewFromHex("#bdaec6"),
|
||||
Color.NewFromHex("#06a2cb"),
|
||||
Color.NewFromHex("#ffb85c"),
|
||||
Color.NewFromHex("#51b599"),
|
||||
}
|
||||
local PROFESSION_DIFFICULTY_COLORS = {
|
||||
optimal = Color.NewFromHex("#ff8040"),
|
||||
medium = Color.NewFromHex("#ffff00"),
|
||||
easy = Color.NewFromHex("#40c040"),
|
||||
trivial = Color.NewFromHex("#808080"),
|
||||
header = Color.NewFromHex("#ffd100"),
|
||||
subheader = Color.NewFromHex("#ffd100"),
|
||||
nodifficulty = Color.NewFromHex("#f5f5f5"),
|
||||
}
|
||||
-- NOTE: there is a global ITEM_QUALITY_COLORS so we need to use another name
|
||||
local TSM_ITEM_QUALITY_COLORS = {
|
||||
[0] = Color.NewFromHex("#9d9d9d"),
|
||||
[1] = Color.NewFromHex("#ffffff"),
|
||||
[2] = Color.NewFromHex("#1eff00"),
|
||||
[3] = Color.NewFromHex("#0070dd"),
|
||||
[4] = Color.NewFromHex("#a334ee"),
|
||||
[5] = Color.NewFromHex("#ff8000"),
|
||||
[6] = Color.NewFromHex("#e6cc80"),
|
||||
[7] = Color.NewFromHex("#00ccff"),
|
||||
[8] = Color.NewFromHex("#00ccff"),
|
||||
}
|
||||
local AUCTION_PCT_COLORS = {
|
||||
{ -- blue
|
||||
color = "BLUE",
|
||||
value = 50,
|
||||
},
|
||||
{ -- green
|
||||
color = "GREEN",
|
||||
value = 80,
|
||||
},
|
||||
{ -- yellow
|
||||
color = "YELLOW",
|
||||
value = 110,
|
||||
},
|
||||
{ -- orange
|
||||
color = "ORANGE",
|
||||
value = 135,
|
||||
},
|
||||
{ -- red
|
||||
color = "RED",
|
||||
value = math.huge,
|
||||
},
|
||||
default = "TEXT",
|
||||
bid = "TEXT_ALT",
|
||||
}
|
||||
local CONSTANTS = {
|
||||
COL_SPACING = 8,
|
||||
SCROLLBAR_MARGIN = 4,
|
||||
SCROLLBAR_WIDTH = 4,
|
||||
MOUSE_WHEEL_SCROLL_AMOUNT = 60,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Theme:OnModuleLoad(function()
|
||||
Table.SetReadOnly(STATIC_COLORS)
|
||||
Table.SetReadOnly(FEEDBACK_COLORS)
|
||||
Table.SetReadOnly(GROUP_COLORS)
|
||||
Table.SetReadOnly(PROFESSION_DIFFICULTY_COLORS)
|
||||
Table.SetReadOnly(TSM_ITEM_QUALITY_COLORS)
|
||||
Table.SetReadOnly(CONSTANTS)
|
||||
|
||||
-- create a frame to load fonts
|
||||
private.fontFrame = CreateFrame("Frame", nil, UIParent)
|
||||
private.fontFrame.texts = {}
|
||||
private.fontFrame:SetAllPoints()
|
||||
private.fontFrame:SetScript("OnUpdate", private.FontFrameOnUpdate)
|
||||
|
||||
-- TODO: eventually allow for different font sets?
|
||||
private.currentFontSet = {
|
||||
HEADING_H5 = FontObject.New(FontPaths.GetBodyRegular(), 20, 28),
|
||||
BODY_BODY1 = FontObject.New(FontPaths.GetBodyRegular(), 16, 24),
|
||||
BODY_BODY1_BOLD = FontObject.New(FontPaths.GetBodyBold(), 16, 24),
|
||||
BODY_BODY2 = FontObject.New(FontPaths.GetBodyRegular(), 14, 20),
|
||||
BODY_BODY2_MEDIUM = FontObject.New(FontPaths.GetBodyMedium(), 14, 20),
|
||||
BODY_BODY2_BOLD = FontObject.New(FontPaths.GetBodyBold(), 14, 20),
|
||||
BODY_BODY3 = FontObject.New(FontPaths.GetBodyRegular(), 12, 20),
|
||||
BODY_BODY3_MEDIUM = FontObject.New(FontPaths.GetBodyMedium(), 12, 20),
|
||||
ITEM_BODY1 = FontObject.New(FontPaths.GetItem(), 16, 24),
|
||||
ITEM_BODY2 = FontObject.New(FontPaths.GetItem(), 14, 20),
|
||||
ITEM_BODY3 = FontObject.New(FontPaths.GetItem(), 12, 20),
|
||||
TABLE_TABLE1 = FontObject.New(FontPaths.GetTable(), 12, 20),
|
||||
}
|
||||
|
||||
-- load the fonts
|
||||
for _, obj in pairs(private.currentFontSet) do
|
||||
local fontPath = obj:GetWowFont()
|
||||
private.QueueFontLoad(fontPath)
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Registers a callback when the theme changes.
|
||||
-- @tparam function callback The callback function
|
||||
function Theme.RegisterChangeCallback(callback)
|
||||
assert(type(callback) == "function")
|
||||
tinsert(private.callbacks, callback)
|
||||
end
|
||||
|
||||
--- Registers a new color set.
|
||||
-- @tparam string key The key which represents the color set
|
||||
-- @tparam string name The name of the color set
|
||||
-- @tparam table colorSet The colors which make up the color set (with keys specified in `THEME_COLOR_KEYS`)
|
||||
function Theme.RegisterColorSet(key, name, colorSet)
|
||||
assert(not private.colorSets[key])
|
||||
for k in pairs(THEME_COLOR_KEYS) do
|
||||
assert(Color.IsInstance(colorSet[k]))
|
||||
end
|
||||
private.names[key] = name
|
||||
private.colorSets[key] = colorSet
|
||||
end
|
||||
|
||||
--- Sets the active color set.
|
||||
-- @tparam string key The key which represents the color set
|
||||
function Theme.SetActiveColorSet(key)
|
||||
assert(private.colorSets[key])
|
||||
if private.currentColorSet == private.colorSets[key] then
|
||||
return
|
||||
end
|
||||
private.currentColorSet = private.colorSets[key]
|
||||
for _, callback in ipairs(private.callbacks) do
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
function Theme.GetThemeName(key)
|
||||
return private.names[key]
|
||||
end
|
||||
|
||||
--- Gets the color object from the current active color set.
|
||||
-- @tparam string key The key of the color to get
|
||||
-- @treturn Color The color object
|
||||
function Theme.GetColor(key, themeKey)
|
||||
local colorKey, tintPct, opacityPct = strmatch(key, "^([A-Z_]+)([%-%+]?[0-9A-Z_]*)%%?([0-9A-Z_]*)$")
|
||||
tintPct = tonumber(tintPct) or (tintPct ~= "" and tintPct or nil)
|
||||
opacityPct = tonumber(opacityPct) or (opacityPct ~= "" and opacityPct or nil)
|
||||
assert(colorKey)
|
||||
local color = nil
|
||||
if THEME_COLOR_KEYS[colorKey] then
|
||||
color = themeKey and private.colorSets[themeKey][colorKey] or private.currentColorSet[colorKey]
|
||||
else
|
||||
color = STATIC_COLORS[colorKey]
|
||||
end
|
||||
assert(color)
|
||||
if tintPct then
|
||||
color = color:GetTint(tintPct)
|
||||
end
|
||||
if opacityPct then
|
||||
color = color:GetOpacity(opacityPct)
|
||||
end
|
||||
return color
|
||||
end
|
||||
|
||||
--- Gets the color object for a given feedback color key.
|
||||
-- @tparam string key The key of the feedback color to get
|
||||
-- @treturn Color The color object
|
||||
function Theme.GetFeedbackColor(key)
|
||||
return FEEDBACK_COLORS[key]
|
||||
end
|
||||
|
||||
--- Gets the color object for Blizzard GMs.
|
||||
-- @treturn Color The color object
|
||||
function Theme.GetBlizzardColor()
|
||||
return BLIZZARD_COLOR
|
||||
end
|
||||
|
||||
--- Gets the color object for a given group level.
|
||||
-- @tparam number level The level of the group (1-based)
|
||||
-- @treturn Color The color object
|
||||
function Theme.GetGroupColor(level)
|
||||
level = ((level - 1) % #GROUP_COLORS) + 1
|
||||
return GROUP_COLORS[level]
|
||||
end
|
||||
|
||||
function Theme.GetProfessionDifficultyColor(difficulty)
|
||||
return PROFESSION_DIFFICULTY_COLORS[difficulty]
|
||||
end
|
||||
|
||||
function Theme.GetItemQualityColor(quality)
|
||||
return TSM_ITEM_QUALITY_COLORS[quality]
|
||||
end
|
||||
|
||||
function Theme.GetAuctionPercentColor(pct)
|
||||
if pct == "BID" then
|
||||
return Theme.GetColor(AUCTION_PCT_COLORS.bid)
|
||||
end
|
||||
for _, info in ipairs(AUCTION_PCT_COLORS) do
|
||||
if pct < info.value then
|
||||
return Theme.GetFeedbackColor(info.color)
|
||||
end
|
||||
end
|
||||
return Theme.GetColor(AUCTION_PCT_COLORS.default)
|
||||
end
|
||||
|
||||
--- Gets the font object from the current active font set.
|
||||
-- @tparam string key The key of the font to get
|
||||
-- @treturn FontObject The font object
|
||||
function Theme.GetFont(key)
|
||||
local fontObj = private.currentFontSet[key]
|
||||
assert(fontObj)
|
||||
return fontObj
|
||||
end
|
||||
|
||||
--- Gets the column spacing constant value.
|
||||
-- @treturn number The column spacing
|
||||
function Theme.GetColSpacing()
|
||||
return CONSTANTS.COL_SPACING
|
||||
end
|
||||
|
||||
--- Gets the scrollbar margin constant value.
|
||||
-- @treturn number The scrollbar margin
|
||||
function Theme.GetScrollbarMargin()
|
||||
return CONSTANTS.SCROLLBAR_MARGIN
|
||||
end
|
||||
|
||||
--- Gets the scrollbar width constant value.
|
||||
-- @treturn number The scrollbar width
|
||||
function Theme.GetScrollbarWidth()
|
||||
return CONSTANTS.SCROLLBAR_WIDTH
|
||||
end
|
||||
|
||||
--- Gets the scrollbar width constant value.
|
||||
-- @treturn number The scrollbar width
|
||||
function Theme.GetMouseWheelScrollAmount()
|
||||
return CONSTANTS.MOUSE_WHEEL_SCROLL_AMOUNT
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.QueueFontLoad(path)
|
||||
if private.fontFrame.texts[path] then
|
||||
return
|
||||
end
|
||||
local fontString = private.fontFrame:CreateFontString()
|
||||
fontString:SetPoint("CENTER")
|
||||
fontString:SetWidth(10000)
|
||||
fontString:SetHeight(6)
|
||||
fontString:SetFont(path, 6)
|
||||
fontString:SetText("1")
|
||||
private.fontFrame.texts[path] = fontString
|
||||
private.fontFrame:Show()
|
||||
end
|
||||
|
||||
function private.FontFrameOnUpdate(frame)
|
||||
for _, fontString in pairs(frame.texts) do
|
||||
if fontString:IsVisible() then
|
||||
assert(fontString:GetStringWidth() > 0, "Text not loaded: "..tostring(fontString:GetFont()))
|
||||
fontString:Hide()
|
||||
end
|
||||
end
|
||||
frame:Hide()
|
||||
end
|
||||
48
LibTSM/Util/Vararg.lua
Normal file
48
LibTSM/Util/Vararg.lua
Normal file
@@ -0,0 +1,48 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Vararg Functions
|
||||
-- @module Vararg
|
||||
|
||||
local _, TSM = ...
|
||||
local Vararg = TSM.Init("Util.Vararg")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Stores a varag into a table.
|
||||
-- @tparam table tbl The table to store the values in
|
||||
-- @param ... Zero or more values to store in the table
|
||||
function Vararg.IntoTable(tbl, ...)
|
||||
for i = 1, select("#", ...) do
|
||||
tbl[i] = select(i, ...)
|
||||
end
|
||||
end
|
||||
|
||||
--- Creates an iterator from a vararg.
|
||||
-- NOTE: This iterator must be run to completion and not interrupted (i.e. with a `break` or `return`).
|
||||
-- @param ... The values to iterate over
|
||||
-- @return An iterator with fields: `index, value`
|
||||
function Vararg.Iterator(...)
|
||||
return TempTable.Iterator(TempTable.Acquire(...))
|
||||
end
|
||||
|
||||
--- Returns whether not the value exists within the vararg.
|
||||
-- @param value The value to search for
|
||||
-- @param ... Any number of values to search in
|
||||
-- @treturn boolean Whether or not the value was found in the vararg
|
||||
function Vararg.In(value, ...)
|
||||
for i = 1, select("#", ...) do
|
||||
if value == select(i, ...) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
67
LibTSM/Util/Wow.lua
Normal file
67
LibTSM/Util/Wow.lua
Normal file
@@ -0,0 +1,67 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Wow Functions
|
||||
-- @module Wow
|
||||
|
||||
local _, TSM = ...
|
||||
local Wow = TSM.Init("Util.Wow")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Shows a basic Wow message popup.
|
||||
-- @tparam string text The text to display
|
||||
function Wow.ShowBasicMessage(text)
|
||||
if BasicMessageDialog:IsShown() then
|
||||
return
|
||||
end
|
||||
BasicMessageDialog.Text:SetText(text)
|
||||
BasicMessageDialog:Show()
|
||||
end
|
||||
|
||||
--- Shows a WoW static popup dialog.
|
||||
-- @tparam string name The unique (global) name of the dialog to be shown
|
||||
function Wow.ShowStaticPopupDialog(name)
|
||||
StaticPopupDialogs[name].preferredIndex = 4
|
||||
StaticPopup_Show(name)
|
||||
for i = 1, 100 do
|
||||
if _G["StaticPopup" .. i] and _G["StaticPopup" .. i].which == name then
|
||||
_G["StaticPopup" .. i]:SetFrameStrata("TOOLTIP")
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Sets the WoW item ref frame to the specified link.
|
||||
-- @tparam string link The itemLink to show the item ref frame for
|
||||
function Wow.SafeItemRef(link)
|
||||
if type(link) ~= "string" then return end
|
||||
-- extract the Blizzard itemString for both items and pets
|
||||
local blizzItemString = strmatch(link, "^\124c[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]\124H(item:[^\124]+)\124.+$")
|
||||
blizzItemString = blizzItemString or strmatch(link, "^\124c[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]\124H(battlepet:[^\124]+)\124.+$")
|
||||
if blizzItemString then
|
||||
SetItemRef(blizzItemString, link, "LeftButton")
|
||||
end
|
||||
end
|
||||
|
||||
--- Checks if an addon is installed.
|
||||
-- This function only checks if the addon is installed, not if it's enabled.
|
||||
-- @tparam string name The name of the addon
|
||||
-- @treturn boolean Whether or not the addon is installed
|
||||
function Wow.IsAddonInstalled(name)
|
||||
return select(2, GetAddOnInfo(name)) and true or false
|
||||
end
|
||||
|
||||
--- Checks if an addon is currently enabled.
|
||||
-- @tparam string name The name of the addon
|
||||
-- @treturn boolean Whether or not the addon is enabled
|
||||
function Wow.IsAddonEnabled(name)
|
||||
return GetAddOnEnableState(UnitName("player"), name) == 2 and select(4, GetAddOnInfo(name)) and true or false
|
||||
end
|
||||
Reference in New Issue
Block a user