initial commit

This commit is contained in:
Gitea 2020-11-13 14:13:12 -05:00
commit 05df49ff60
368 changed files with 128754 additions and 0 deletions

13
ChangeLog.md Normal file
View File

@ -0,0 +1,13 @@
## v4.10.14 Changes
* Fixed tooltip error when hovering over favorite searches
* Fixed error on base group UI after removing a group
* Fixed TSM taking a long time on logout for some users
* [Retail] Added additional bonusIds
* [Retail] Added light/rich illusion dust conversion
* [Retail] Updated ink trading for Shadowlands
* [Retail] Fixed error when syncing professions between accounts
* [Retail] Fixed error when creating profession groups
* [Retail] Fixed issue with "/disenchant" search filter
[Known Issues](http://support.tradeskillmaster.com/display/KB/TSM4+Currently+Known+Issues)

420
Core/API.lua Normal file
View File

@ -0,0 +1,420 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Public TSM API functions
-- @module TSM_API
local _, TSM = ...
local Money = TSM.Include("Util.Money")
local ItemString = TSM.Include("Util.ItemString")
local ItemInfo = TSM.Include("Service.ItemInfo")
local CustomPrice = TSM.Include("Service.CustomPrice")
local Inventory = TSM.Include("Service.Inventory")
TSM_API = {}
local private = {}
-- ============================================================================
-- UI
-- ============================================================================
--- Checks if a TSM UI is currently visible.
-- @within UI
-- @tparam string uiName A string which represents the UI ("AUCTION", "CRAFTING", "MAILING", or "VENDORING")
-- @treturn boolean Whether or not the TSM UI is visible
function TSM_API.IsUIVisible(uiName)
private.CheckCallMethod(uiName)
if uiName == "AUCTION" then
return TSM.UI.AuctionUI.IsVisible()
elseif uiName == "CRAFTING" then
return TSM.UI.CraftingUI.IsVisible()
elseif uiName == "MAILING" then
return TSM.UI.MailingUI.IsVisible()
elseif uiName == "VENDORING" then
return TSM.UI.VendoringUI.IsVisible()
else
error("Invalid uiName: "..tostring(uiName), 2)
end
end
-- ============================================================================
-- Groups
-- ============================================================================
--- Gets a current list of TSM group paths.
-- @within Group
-- @tparam table result A table to store the result in
-- @treturn table The passed table, populated with group paths
function TSM_API.GetGroupPaths(result)
private.CheckCallMethod(result)
if type(result) ~= "table" then
error("Invalid 'result' argument type (must be a table): "..tostring(result), 2)
end
for _, groupPath in TSM.Groups.GroupIterator() do
tinsert(result, groupPath)
end
return result
end
--- Formats a TSM group path into a human-readable form
-- @within Group
-- @tparam string path The group path to be formatted
-- @treturn string The formatted group path
function TSM_API.FormatGroupPath(path)
private.CheckCallMethod(path)
if type(path) ~= "string" then
error("Invalid 'path' argument type (must be a string): "..tostring(path), 2)
elseif path == "" then
error("Invalid 'path' argument (empty string)", 2)
end
return TSM.Groups.Path.Format(path)
end
--- Splits a TSM group path into its parent path and group name components.
-- @within Group
-- @tparam string path The group path to be split
-- @treturn string The path of the parent group or nil if the specified path has no parent
-- @treturn string The name of the group
function TSM_API.SplitGroupPath(path)
private.CheckCallMethod(path)
if type(path) ~= "string" then
error("Invalid 'path' argument type (must be a string): "..tostring(path), 2)
elseif path == "" then
error("Invalid 'path' argument (empty string)", 2)
end
local parentPath, groupName = TSM.Groups.Path.Split(path)
if parentPath == TSM.CONST.ROOT_GROUP_PATH then
parentPath = nil
end
return parentPath, groupName
end
--- Gets the path to the group which a specific item is in.
-- @within Group
-- @tparam string itemString The TSM item string to get the group path of
-- @treturn string The path to the group which the item is in, or nil if it's not in a group
function TSM_API.GetGroupPathByItem(itemString)
private.CheckCallMethod(itemString)
itemString = private.ValidateTSMItemString(itemString)
local path = TSM.Groups.GetPathByItem(itemString)
return path ~= TSM.CONST.ROOT_GROUP_PATH and path or nil
end
-- ============================================================================
-- Profiles
-- ============================================================================
--- Gets a current list of TSM profiles.
-- @within Profile
-- @tparam table result A table to store the result in
-- @treturn table The passed table, populated with group paths
function TSM_API.GetProfiles(result)
private.CheckCallMethod(result)
for _, profileName in TSM.db:ProfileIterator() do
tinsert(result, profileName)
end
return result
end
--- Gets the active TSM profile.
-- @within Profile
-- @treturn string The name of the currently active profile
function TSM_API.GetActiveProfile()
return TSM.db:GetCurrentProfile()
end
--- Sets the active TSM profile.
-- @within Profile
-- @tparam string profile The name of the profile to make active
function TSM_API.SetActiveProfile(profile)
private.CheckCallMethod(profile)
if type(profile) ~= "string" then
error("Invalid 'profile' argument type (must be a string): "..tostring(profile), 2)
elseif not TSM.db:ProfileExists(profile) then
error("Profile does not exist: "..profile, 2)
elseif profile == TSM.db:GetCurrentProfile() then
error("Profile is already active: "..profile, 2)
end
return TSM.db:SetProfile(profile)
end
-- ============================================================================
-- Prices
-- ============================================================================
--- Gets a list of price source keys which can be used in TSM custom prices.
-- @within Price
-- @tparam table result A table to store the result in
-- @treturn table The passed table, populated with price source keys
function TSM_API.GetPriceSourceKeys(result)
private.CheckCallMethod(result)
if type(result) ~= "table" then
error("Invalid 'result' argument type (must be a table): "..tostring(result), 2)
end
for key in CustomPrice.Iterator() do
tinsert(result, key)
end
return result
end
--- Gets the localized description of a given price source key.
-- @within Price
-- @tparam string key The price source key
-- @treturn string The localized description
function TSM_API.GetPriceSourceDescription(key)
private.CheckCallMethod(key)
if type(key) ~= "string" then
error("Invalid 'key' argument type (must be a string): "..tostring(key), 2)
end
local result = CustomPrice.GetDescription(key)
if not result then
error("Unknown price source key: "..tostring(key), 2)
end
return result
end
--- Gets whether or not a custom price string is valid.
-- @within Price
-- @tparam string customPriceStr The custom price string
-- @treturn boolean Whether or not the custom price is valid
-- @treturn string The (localized) error message or nil if the custom price was valid
function TSM_API.IsCustomPriceValid(customPriceStr)
private.CheckCallMethod(customPriceStr)
if type(customPriceStr) ~= "string" then
error("Invalid 'customPriceStr' argument type (must be a string): "..tostring(customPriceStr), 2)
end
return CustomPrice.Validate(customPriceStr)
end
--- Evalulates a custom price string or price source key for a given item
-- @within Price
-- @tparam string customPriceStr The custom price string or price source key to get the value of
-- @tparam string itemString The TSM item string to get the value for
-- @treturn number The value in copper or nil if the custom price string is not valid
-- @treturn string The (localized) error message if the custom price string is not valid or nil if it is valid
function TSM_API.GetCustomPriceValue(customPriceStr, itemString)
private.CheckCallMethod(customPriceStr)
if type(customPriceStr) ~= "string" then
error("Invalid 'customPriceStr' argument type (must be a string): "..tostring(customPriceStr), 2)
end
itemString = private.ValidateTSMItemString(itemString)
return CustomPrice.GetValue(customPriceStr, itemString)
end
-- ============================================================================
-- Money
-- ============================================================================
--- Converts a money value to a formatted, human-readable string.
-- @within Money
-- @tparam number value The money value in copper to be converted
-- @treturn string The formatted money string
function TSM_API.FormatMoneyString(value)
private.CheckCallMethod(value)
if type(value) ~= "number" then
error("Invalid 'value' argument type (must be a number): "..tostring(value), 2)
end
local result = Money.ToString(value)
assert(result)
return result
end
--- Converts a formatted, human-readable money string to a value.
-- @within Money
-- @tparam string str The formatted money string
-- @treturn number The money value in copper
function TSM_API.ParseMoneyString(str)
private.CheckCallMethod(str)
if type(str) ~= "string" then
error("Invalid 'str' argument type (must be a string): "..tostring(str), 2)
end
local result = Money.FromString(str)
assert(result)
return result
end
-- ============================================================================
-- Item
-- ============================================================================
--- Converts an item to a TSM item string.
-- @within Item
-- @tparam string item Either an item link, TSM item string, or WoW item string
-- @treturn string The TSM item string or nil if the specified item could not be converted
function TSM_API.ToItemString(item)
private.CheckCallMethod(item)
if type(item) ~= "string" then
error("Invalid 'item' argument type (must be a string): "..tostring(item), 2)
end
return ItemString.Get(item)
end
--- Gets an item's name from a given TSM item string.
-- @within Item
-- @tparam string itemString The TSM item string
-- @treturn string The name of the item or nil if it couldn't be determined
function TSM_API.GetItemName(itemString)
private.CheckCallMethod(itemString)
itemString = private.ValidateTSMItemString(itemString)
return ItemInfo.GetName(itemString)
end
--- Gets an item link from a given TSM item string.
-- @within Item
-- @tparam string itemString The TSM item string
-- @treturn string The item link or an "[Unknown Item]" link
function TSM_API.GetItemLink(itemString)
private.CheckCallMethod(itemString)
itemString = private.ValidateTSMItemString(itemString)
local result = ItemInfo.GetLink(itemString)
assert(result)
return result
end
-- ============================================================================
-- Inventory
-- ============================================================================
--- Gets the quantity of an item in a character's bags.
-- @within Inventory
-- @tparam string itemString The TSM item string (note that inventory data is tracked per base item)
-- @tparam ?string character The character to get data for (defaults to the current character if not set)
-- @tparam ?string factionrealm The factionrealm to get data for (defaults to the current factionrealm if not set)
-- @treturn number The quantity of the specified item
function TSM_API.GetBagQuantity(itemString, character, factionrealm)
private.CheckCallMethod(itemString)
itemString = private.ValidateTSMItemString(itemString)
assert(character == nil or type(character) == "string")
assert(factionrealm == nil or type(factionrealm) == "string")
return Inventory.GetBagQuantity(itemString, character, factionrealm)
end
--- Gets the quantity of an item in a character's bank.
-- @within Inventory
-- @tparam string itemString The TSM item string (note that inventory data is tracked per base item)
-- @tparam ?string character The character to get data for (defaults to the current character if not set)
-- @tparam ?string factionrealm The factionrealm to get data for (defaults to the current factionrealm if not set)
-- @treturn number The quantity of the specified item
function TSM_API.GetBankQuantity(itemString, character, factionrealm)
private.CheckCallMethod(itemString)
itemString = private.ValidateTSMItemString(itemString)
assert(character == nil or type(character) == "string")
assert(factionrealm == nil or type(factionrealm) == "string")
return Inventory.GetBankQuantity(itemString, character, factionrealm)
end
--- Gets the quantity of an item in a character's reagent bank.
-- @within Inventory
-- @tparam string itemString The TSM item string (note that inventory data is tracked per base item)
-- @tparam ?string character The character to get data for (defaults to the current character if not set)
-- @tparam ?string factionrealm The factionrealm to get data for (defaults to the current factionrealm if not set)
-- @treturn number The quantity of the specified item
function TSM_API.GetReagentBankQuantity(itemString, character, factionrealm)
private.CheckCallMethod(itemString)
itemString = private.ValidateTSMItemString(itemString)
assert(character == nil or type(character) == "string")
assert(factionrealm == nil or type(factionrealm) == "string")
return Inventory.GetReagentBankQuantity(itemString, character, factionrealm)
end
--- Gets the quantity of an item posted to the auction house by a character.
-- @within Inventory
-- @tparam string itemString The TSM item string (note that inventory data is tracked per base item)
-- @tparam ?string character The character to get data for (defaults to the current character if not set)
-- @tparam ?string factionrealm The factionrealm to get data for (defaults to the current factionrealm if not set)
-- @treturn number The quantity of the specified item
function TSM_API.GetAuctionQuantity(itemString, character, factionrealm)
private.CheckCallMethod(itemString)
itemString = private.ValidateTSMItemString(itemString)
assert(character == nil or type(character) == "string")
assert(factionrealm == nil or type(factionrealm) == "string")
return Inventory.GetAuctionQuantity(itemString, character, factionrealm)
end
--- Gets the quantity of an item in a character's mailbox.
-- @within Inventory
-- @tparam string itemString The TSM item string (note that inventory data is tracked per base item)
-- @tparam ?string character The character to get data for (defaults to the current character if not set)
-- @tparam ?string factionrealm The factionrealm to get data for (defaults to the current factionrealm if not set)
-- @treturn number The quantity of the specified item
function TSM_API.GetMailQuantity(itemString, character, factionrealm)
private.CheckCallMethod(itemString)
itemString = private.ValidateTSMItemString(itemString)
assert(character == nil or type(character) == "string")
assert(factionrealm == nil or type(factionrealm) == "string")
return Inventory.GetMailQuantity(itemString, character, factionrealm)
end
--- Gets the quantity of an item in a guild's bank.
-- @within Inventory
-- @tparam string itemString The TSM item string (note that inventory data is tracked per base item)
-- @tparam ?string guild The guild to get data for (defaults to the current character's guild if not set)
-- @treturn number The quantity of the specified item
function TSM_API.GetGuildQuantity(itemString, guild)
private.CheckCallMethod(itemString)
itemString = private.ValidateTSMItemString(itemString)
assert(guild == nil or type(guild) == "string")
return Inventory.GetGuildQuantity(itemString, guild)
end
--- Get some total quantities for an item.
-- @within Inventory
-- @tparam string itemString The TSM item string (note that inventory data is tracked per base item)
-- @treturn number The total quantity the current player has (bags, bank, reagent bank, and mail)
-- @treturn number The total quantity alt characters have (bags, bank, reagent bank, and mail)
-- @treturn number The total quantity the current player has on the auction house
-- @treturn number The total quantity alt characters have on the auction house
function TSM_API.GetPlayerTotals(itemString)
private.CheckCallMethod(itemString)
itemString = private.ValidateTSMItemString(itemString)
return Inventory.GetPlayerTotals(itemString)
end
--- Get the total number of items in all tracked guild banks.
-- @within Inventory
-- @tparam string itemString The TSM item string (note that inventory data is tracked per base item)
-- @treturn number The total quantity in all tracked guild banks
function TSM_API.GetGuildTotal(itemString)
private.CheckCallMethod(itemString)
itemString = private.ValidateTSMItemString(itemString)
return Inventory.GetGuildTotal(itemString)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.ValidateTSMItemString(itemString)
if type(itemString) ~= "string" or not strmatch(itemString, "[ip]:%d+") then
error("Invalid 'itemString' argument type (must be a TSM item string): "..tostring(itemString), 3)
end
local newItemString = ItemString.Get(itemString)
if not newItemString then
error("Invalid TSM itemString: "..itemString, 3)
end
return newItemString
end
function private.CheckCallMethod(firstArg)
if firstArg == TSM_API then
error("Invalid usage of colon operator to call TSM_API function", 3)
end
end

20
Core/Const/__init.lua Normal file
View File

@ -0,0 +1,20 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
TSM.CONST = {}
-- Miscellaneous constants which should never change
TSM.CONST.OPERATION_SEP = "\001"
TSM.CONST.GROUP_SEP = "`"
TSM.CONST.ROOT_GROUP_PATH = ""
TSM.CONST.TOOLTIP_SEP = "\001"
TSM.CONST.MIN_BONUS_ID_ITEM_LEVEL = 200
TSM.CONST.AUCTION_DURATIONS = {
not TSM.IsWowClassic() and AUCTION_DURATION_ONE or gsub(AUCTION_DURATION_ONE, "12", "2"),
not TSM.IsWowClassic() and AUCTION_DURATION_TWO or gsub(AUCTION_DURATION_TWO, "24", "8"),
not TSM.IsWowClassic() and AUCTION_DURATION_THREE or gsub(AUCTION_DURATION_THREE, "48", "24"),
}

18
Core/Development/Core.lua Normal file
View File

@ -0,0 +1,18 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
-- only create the TSMDEV table if we're in a dev or test environment
local version = GetAddOnMetadata("TradeSkillMaster", "Version")
if not strmatch(version, "^@tsm%-project%-version@$") and version ~= "v4.99.99" then
return
end
TSMDEV = {}
function TSMDEV.Dump(value)
LoadAddOn("Blizzard_DebugTools")
DevTools_Dump(value)
end

View File

@ -0,0 +1,163 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
if not TSMDEV then return end
local _, TSM = ...
TSMDEV.Profiling = {}
local Profiling = TSMDEV.Profiling
local Math = TSM.Include("Util.Math")
local private = {
startTime = nil,
nodes = {},
nodeRuns = {},
nodeStart = {},
nodeTotal = {},
nodeMaxContext = {},
nodeMaxTime = {},
nodeParent = {},
nodeStack = {},
}
local NODE_PATH_SEP = "`"
-- ============================================================================
-- Module Functions
-- ============================================================================
--- Starts profiling.
function Profiling.Start()
assert(not private.startTime, "Profiling already started")
private.startTime = debugprofilestop()
end
--- Starts profiling of a node.
-- Profiling must have been started for this to have any effect.
-- @tparam string node The name of the profiling node
function Profiling.StartNode(node)
if not private.startTime then
-- profiling is not running
return
end
local nodeStackLen = #private.nodeStack
local parentNode = nodeStackLen > 0 and table.concat(private.nodeStack, NODE_PATH_SEP) or nil
private.nodeStack[nodeStackLen + 1] = node
node = table.concat(private.nodeStack, NODE_PATH_SEP)
if private.nodeStart[node] then
error("Node already started", 2)
end
if not private.nodeTotal[node] then
tinsert(private.nodes, node)
private.nodeTotal[node] = 0
private.nodeRuns[node] = 0
private.nodeMaxContext[node] = nil
private.nodeMaxTime[node] = 0
private.nodeParent[node] = parentNode
elseif private.nodeParent[node] ~= parentNode then
error("Node changed parents", 2)
end
private.nodeStart[node] = debugprofilestop()
end
--- Ends profiling of a node.
-- Profiling of this node must have been started for this to have any effect.
-- @tparam string node The name of the profiling node
-- @param[opt] arg An extra argument which is printed if this invocation represents the max duration for the node
function Profiling.EndNode(node, arg)
if not private.startTime then
-- profiling is not running
return
end
local endTime = debugprofilestop()
local nodeStackLen = #private.nodeStack
if node ~= private.nodeStack[nodeStackLen] then
error("Node isn't at the top of the stack", 2)
end
node = table.concat(private.nodeStack, NODE_PATH_SEP)
if not private.nodeStart[node] then
error("Node hasn't been started", 2)
end
private.nodeStack[nodeStackLen] = nil
local nodeTime = endTime - private.nodeStart[node]
private.nodeRuns[node] = private.nodeRuns[node] + 1
private.nodeTotal[node] = private.nodeTotal[node] + nodeTime
private.nodeStart[node] = nil
if nodeTime > private.nodeMaxTime[node] then
private.nodeMaxContext[node] = arg
private.nodeMaxTime[node] = nodeTime
end
end
--- Ends profiling and prints the results to chat.
-- @tparam[opt=0] number minTotalTime The minimum total time to print the profiling info
function Profiling.End(minTotalTime)
if not private.startTime then
-- profiling is not running
return
end
local totalTime = debugprofilestop() - private.startTime
if totalTime > (minTotalTime or 0) then
print(format("Total: %.03f", Math.Round(totalTime, 0.001)))
for _, node in ipairs(private.nodes) do
local parentNode = private.nodeParent[node]
local parentTotalTime = nil
if parentNode then
parentTotalTime = private.nodeTotal[parentNode]
else
parentTotalTime = totalTime
end
local nodeTotalTime = Math.Round(private.nodeTotal[node], 0.001)
local pctTime = Math.Round(nodeTotalTime * 100 / parentTotalTime)
local nodeRuns = private.nodeRuns[node]
local nodeMaxContext = private.nodeMaxContext[node]
local level = private.GetLevel(node)
local name = strmatch(node, NODE_PATH_SEP.."?([^"..NODE_PATH_SEP.."]+)$")
if nodeMaxContext ~= nil then
local nodeMaxTime = private.nodeMaxTime[node]
print(format("%s%s | %d%% | %.03f | %d | %.03f | %s", strrep(" ", level), name, pctTime, nodeTotalTime, nodeRuns, nodeMaxTime, tostring(nodeMaxContext)))
else
print(format("%s%s | %d%% | %.03f | %d", strrep(" ", level), name, pctTime, nodeTotalTime, nodeRuns))
end
end
end
private.startTime = nil
wipe(private.nodes)
wipe(private.nodeRuns)
wipe(private.nodeStart)
wipe(private.nodeTotal)
wipe(private.nodeMaxContext)
wipe(private.nodeMaxTime)
end
--- Checks whether or not we're currently profiling.
-- @treturn boolean Whether or not we're currently profiling.
function Profiling.IsActive()
return private.startTime and true or false
end
--- Gets the total memory used by TSM.
-- @treturn number The amount of memory being used in bytes
function Profiling.GetMemoryUsage()
collectgarbage()
UpdateAddOnMemoryUsage("TradeSkillMaster")
return GetAddOnMemoryUsage("TradeSkillMaster") * 1024
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetLevel(node)
local level = 0
while node do
level = level + 1
node = private.nodeParent[node]
end
return level
end

204
Core/Lib/Addon.lua Normal file
View File

@ -0,0 +1,204 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local TSM_NAME, TSM = ...
local Analytics = TSM.Include("Util.Analytics")
local Event = TSM.Include("Util.Event")
local Log = TSM.Include("Util.Log")
local LibTSMClass = TSM.Include("LibTSMClass")
local private = {
eventFrames = {},
initializeQueue = {},
enableQueue = {},
disableQueue = {},
totalInitializeTime = 0,
totalEnableTime = 0,
}
local TIME_WARNING_THRESHOLD_MS = 20
local MAX_TIME_PER_EVENT_MS = 12000
local NUM_EVENT_FRAMES = 10
-- ============================================================================
-- Event Handling
-- ============================================================================
function private.DoInitialize()
local eventStartTime = debugprofilestop()
while #private.initializeQueue > 0 and debugprofilestop() < (eventStartTime + MAX_TIME_PER_EVENT_MS) do
local addon = tremove(private.initializeQueue, 1)
if addon.OnInitialize then
local addonStartTime = debugprofilestop()
addon.OnInitialize()
local addonTimeTaken = debugprofilestop() - addonStartTime
if addonTimeTaken > TIME_WARNING_THRESHOLD_MS then
Log.Warn("OnInitialize (%s) took %0.2fms", addon, addonTimeTaken)
end
end
tinsert(private.enableQueue, addon)
end
if private.totalInitializeTime == 0 then
for _, path, moduleLoadTime, settingsLoadTime in TSM.ModuleInfoIterator() do
if moduleLoadTime > TIME_WARNING_THRESHOLD_MS then
Log.Warn("Loading module %s took %0.2fms", path, moduleLoadTime)
end
if settingsLoadTime > TIME_WARNING_THRESHOLD_MS then
Log.Warn("Loading settings for %s took %0.2fms", path, settingsLoadTime)
end
end
end
private.totalInitializeTime = private.totalInitializeTime + debugprofilestop() - eventStartTime
return #private.initializeQueue == 0
end
function private.DoEnable()
local eventStartTime = debugprofilestop()
while #private.enableQueue > 0 and debugprofilestop() < (eventStartTime + MAX_TIME_PER_EVENT_MS) do
local addon = tremove(private.enableQueue, 1)
if addon.OnEnable then
local addonStartTime = debugprofilestop()
addon.OnEnable()
local addonTimeTaken = debugprofilestop() - addonStartTime
if addonTimeTaken > TIME_WARNING_THRESHOLD_MS then
Log.Warn("OnEnable (%s) took %0.2fms", addon, addonTimeTaken)
end
end
tinsert(private.disableQueue, addon)
end
if private.totalEnableTime == 0 then
for _, path, _, _, gameDataLoadTime in TSM.ModuleInfoIterator() do
if (gameDataLoadTime or 0) > TIME_WARNING_THRESHOLD_MS then
Log.Warn("Loading game data for %s took %0.2fms", path, gameDataLoadTime)
end
end
end
private.totalEnableTime = private.totalEnableTime + debugprofilestop() - eventStartTime
return #private.enableQueue == 0
end
function private.PlayerLogoutHandler()
private.OnDisableHelper()
wipe(private.disableQueue)
end
function private.OnDisableHelper()
local disableStartTime = debugprofilestop()
for _, addon in ipairs(private.disableQueue) do
-- defer the main TSM.OnDisable() call to the very end
if addon.OnDisable and addon ~= TSM then
local startTime = debugprofilestop()
addon.OnDisable()
local timeTaken = debugprofilestop() - startTime
if timeTaken > TIME_WARNING_THRESHOLD_MS then
Log.Warn("OnDisable (%s) took %0.2fms", addon, timeTaken)
end
end
end
local totalDisableTime = debugprofilestop() - disableStartTime
Analytics.Action("ADDON_DISABLE", floor(totalDisableTime))
if TSM.OnDisable then
TSM.OnDisable()
end
end
do
-- Blizzard did something silly in 8.1 where scripts time throw an error after 19 seconds, but nothing prevents us
-- from just splitting the processing across multiple script handlers, so we do that here.
local function EventHandler(self, event, arg)
if event == "ADDON_LOADED" and arg == "TradeSkillMaster" then
if private.DoInitialize() then
-- we're done
for _, frame in ipairs(private.eventFrames) do
frame:UnregisterEvent(event)
end
Analytics.Action("ADDON_INITIALIZE", floor(private.totalInitializeTime))
elseif self == private.eventFrames[#private.eventFrames] then
error("Ran out of event frames to initialize TSM")
end
elseif event == "PLAYER_LOGIN" then
if private.DoEnable() then
-- we're done
for _, frame in ipairs(private.eventFrames) do
frame:UnregisterEvent(event)
end
Analytics.Action("ADDON_ENABLE", floor(private.totalEnableTime))
elseif self == private.eventFrames[#private.eventFrames] then
error("Ran out of event frames to enable TSM")
end
end
end
for _ = 1, NUM_EVENT_FRAMES do
local frame = CreateFrame("Frame")
frame:SetScript("OnEvent", EventHandler)
frame:RegisterEvent("ADDON_LOADED")
frame:RegisterEvent("PLAYER_LOGIN")
tinsert(private.eventFrames, frame)
end
Event.Register("PLAYER_LOGOUT", private.PlayerLogoutHandler)
end
-- ============================================================================
-- AddonPackage Class
-- ============================================================================
local AddonPackage = LibTSMClass.DefineClass("AddonPackage")
function AddonPackage.__init(self, name)
self.name = name
tinsert(private.initializeQueue, self)
end
function AddonPackage.__tostring(self)
return self.name
end
function AddonPackage.NewPackage(self, name)
local package = AddonPackage(name)
assert(not self[name])
self[name] = package
return package
end
-- ============================================================================
-- Addon Class
-- ============================================================================
local Addon = LibTSMClass.DefineClass("Addon", AddonPackage)
function Addon.__init(self, name)
self.__super:__init(name)
end
-- ============================================================================
-- Initialization Code
-- ============================================================================
do
LibTSMClass.ConstructWithTable(TSM, Addon, TSM_NAME)
end
-- ============================================================================
-- Module Functions (Debug Only)
-- ============================================================================
function TSM.AddonTestLogout()
private.OnDisableHelper()
TSM.DebugLogout()
for _, path, _, _, _, moduleUnloadTime in TSM.ModuleInfoIterator() do
if moduleUnloadTime > TIME_WARNING_THRESHOLD_MS then
Log.Warn("Unloading %s took %0.2fms", path, moduleUnloadTime)
end
end
end

188
Core/Lib/Exporter.lua Normal file
View File

@ -0,0 +1,188 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local ExporterModule = TSM:NewPackage("Exporter")
local TempTable = TSM.Include("Util.TempTable")
local LibAceSerializer = LibStub("AceSerializer-3.0")
local Exporter = TSM.Include("LibTSMClass").DefineClass("Exporter")
local private = {}
-- ============================================================================
-- Module Functions
-- ============================================================================
function ExporterModule.New()
return Exporter()
end
-- ============================================================================
-- Class definition
-- ============================================================================
function Exporter.__init(self)
self.options = {
["includeAttachedOperations"] = true
}
self.groups = {}
self.operations = {}
self.groupOperations = {}
self.operationsBlacklist = {}
self.groupTargets = {}
for _, module in TSM.Operations.ModuleIterator() do
self.groupOperations[module] = {}
self.operationsBlacklist[module] = {}
self.operations[module] = {}
end
end
--- Blacklist the given operation from being included with the export
-- @tparam self the exporter
-- @tparam module the operation belongs to
-- @tparam name of the operation
function Exporter.BlacklistOperation(self, module, name)
self.operationsBlacklist[module][name] = true
end
--- Reset the selected groups and drop the cached copies of the operations
-- @tparam self the exporter
function Exporter.ResetSelection(self)
wipe(self.groups)
wipe(self.groupOperations)
for _, module in TSM.Operations.ModuleIterator() do
wipe(self.operations[module])
end
end
--- Add the path to the current selected groups
-- @tparam self the exporter
-- @tparam string path the group to add
function Exporter.SelectGroup(self, path)
if path ~= TSM.CONST.ROOT_GROUP_PATH then
tinsert(self.groups, path)
for _, module in TSM.Operations.ModuleIterator() do
for _, operationName, operationSettings in TSM.Operations.GroupOperationIterator(module, path) do
if not self.operationsBlacklist[module][operationName] then
self.operations[module][operationName] = operationSettings
end
end
end
end
end
--- Finishes bookkeeping when the group selection changes
-- @tparam self the exporter
function Exporter.FinalizeGroupSelections(self)
TSM.Groups.SortGroupList(self.groups)
self:_SetupGroupTargets()
for _, path in ipairs(self.groups) do
self:_SaveGroupOperations(path)
end
end
--- gets a string that is the exported groups with the selected options
-- @tparam self the exporter
function Exporter.GetExportString(self)
local items = {}
local selectedGroups = {}
for _, group in ipairs(self.groups) do
selectedGroups[group] = true
self:_SaveGroupOperations(group)
end
self:_SaveItems(selectedGroups, items)
local groupExport = table.concat(items, ",")
if not self.options.includeAttachedOperations then
return groupExport
end
return LibAceSerializer:Serialize({groupExport=groupExport, groupOperations=self.groupOperations, operations=self.operations})
end
function Exporter._SaveGroupOperations(self, group)
if not self.options.includeAttachedOperations then
return
end
local relPath = self.groupTargets[group]
self.groupOperations[relPath] = TSM.db.profile.userData.groups[group]
for _, moduleName in TSM.Operations.ModuleIterator() do
local operationInfo = self.groupOperations[relPath][moduleName]
for _, operationName in ipairs(operationInfo) do
local data = CopyTable(TSM.Operations.GetSettings(moduleName, operationName))
data.ignorePlayer = nil
data.ignoreFactionrealm = nil
data.relationships = nil
self.operations[moduleName] = self.operations[moduleName] or {}
self.operations[moduleName][operationName] = data
end
end
end
function Exporter._SaveItems(self, selectedGroups, saveItems)
local temp = TempTable.Acquire()
for _, itemString, groupPath in TSM.Groups.ItemIterator() do
if selectedGroups[groupPath] then
tinsert(temp, itemString)
end
end
sort(temp, private.GroupsThenItemsSortFunc)
local currentPath = ""
for _, itemString in pairs(temp) do
local rawPath = TSM.Groups.GetPathByItem(itemString)
local relPath = self.groupTargets[rawPath]
if relPath ~= currentPath then
tinsert(saveItems, "group:"..relPath)
currentPath = relPath
end
tinsert(saveItems, itemString)
end
TempTable.Release(temp)
end
function Exporter._SetupGroupTargets(self)
wipe(self.groupTargets)
if #self.groups < 1 then
return
end
local knownRoots = {}
for _, groupPath in ipairs(self.groups) do
local root, leaf = TSM.Groups.Path.Split(groupPath)
leaf = gsub(leaf, ",", TSM.CONST.GROUP_SEP..TSM.CONST.GROUP_SEP)
if knownRoots[root] then
self.groupTargets[groupPath] = leaf
else
if self.groupTargets[root] then
self.groupTargets[groupPath] = TSM.Groups.Path.Join(self.groupTargets[root], leaf)
else
knownRoots[root] = true
self.groupTargets[groupPath] = leaf
end
end
end
end
-- ============================================================================
-- Private Functions
-- ============================================================================
function private.GroupsThenItemsSortFunc(a, b)
local groupA = strlower(gsub(TSM.Groups.GetPathByItem(a), TSM.CONST.GROUP_SEP, "\001"))
local groupB = strlower(gsub(TSM.Groups.GetPathByItem(b), TSM.CONST.GROUP_SEP, "\001"))
if groupA == groupB then
return a < b
end
return groupA < groupB
end

View File

@ -0,0 +1,235 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Auctions = TSM.Accounting:NewPackage("Auctions")
local Database = TSM.Include("Util.Database")
local CSV = TSM.Include("Util.CSV")
local String = TSM.Include("Util.String")
local Log = TSM.Include("Util.Log")
local ItemString = TSM.Include("Util.ItemString")
local CustomPrice = TSM.Include("Service.CustomPrice")
local private = {
db = nil,
numExpiresQuery = nil,
dataChanged = false,
statsQuery = nil,
statsTemp = {},
}
local COMBINE_TIME_THRESHOLD = 300 -- group expenses within 5 minutes together
local REMOVE_OLD_THRESHOLD = 180 * 24 * 60 * 60 -- remove records over 6 months old
local SECONDS_PER_DAY = 24 * 60 * 60
local CSV_KEYS = { "itemString", "stackSize", "quantity", "player", "time" }
-- ============================================================================
-- Module Functions
-- ============================================================================
function Auctions.OnInitialize()
private.db = Database.NewSchema("ACCOUNTING_AUCTIONS")
:AddStringField("baseItemString")
:AddStringField("type")
:AddStringField("itemString")
:AddNumberField("stackSize")
:AddNumberField("quantity")
:AddStringField("player")
:AddNumberField("time")
:AddNumberField("saveTime")
:AddIndex("baseItemString")
:AddIndex("time")
:Commit()
private.numExpiresQuery = private.db:NewQuery()
:Select("quantity")
:Equal("type", "expire")
:Equal("baseItemString", Database.BoundQueryParam())
:GreaterThanOrEqual("time", Database.BoundQueryParam())
private.statsQuery = private.db:NewQuery()
:Select("type", "quantity")
:Equal("baseItemString", Database.BoundQueryParam())
:GreaterThanOrEqual("time", Database.BoundQueryParam())
private.db:BulkInsertStart()
private.LoadData("cancel", TSM.db.realm.internalData.csvCancelled, TSM.db.realm.internalData.saveTimeCancels)
private.LoadData("expire", TSM.db.realm.internalData.csvExpired, TSM.db.realm.internalData.saveTimeExpires)
private.db:BulkInsertEnd()
CustomPrice.OnSourceChange("NumExpires")
end
function Auctions.OnDisable()
if not private.dataChanged then
-- nothing changed, so no need to save
return
end
local cancelSaveTimes, expireSaveTimes = {}, {}
local cancelEncodeContext = CSV.EncodeStart(CSV_KEYS)
local expireEncodeContext = CSV.EncodeStart(CSV_KEYS)
-- order by time to speed up loading
local query = private.db:NewQuery()
:Select("type", "itemString", "stackSize", "quantity", "player", "time", "saveTime")
:OrderBy("time", true)
for _, recordType, itemString, stackSize, quantity, player, timestamp, saveTime in query:Iterator() do
local saveTimes, encodeContext = nil, nil
if recordType == "cancel" then
saveTimes = cancelSaveTimes
encodeContext = cancelEncodeContext
elseif recordType == "expire" then
saveTimes = expireSaveTimes
encodeContext = expireEncodeContext
else
error("Invalid recordType: "..tostring(recordType))
end
-- add the save time
tinsert(saveTimes, saveTime ~= 0 and saveTime or time())
-- add to our list of CSV lines
CSV.EncodeAddRowDataRaw(encodeContext, itemString, stackSize, quantity, player, timestamp)
end
query:Release()
TSM.db.realm.internalData.csvCancelled = CSV.EncodeEnd(cancelEncodeContext)
TSM.db.realm.internalData.saveTimeCancels = table.concat(cancelSaveTimes, ",")
TSM.db.realm.internalData.csvExpired = CSV.EncodeEnd(expireEncodeContext)
TSM.db.realm.internalData.saveTimeExpires = table.concat(expireSaveTimes, ",")
end
function Auctions.InsertCancel(itemString, stackSize, timestamp)
private.InsertRecord("cancel", itemString, stackSize, timestamp)
end
function Auctions.InsertExpire(itemString, stackSize, timestamp)
private.InsertRecord("expire", itemString, stackSize, timestamp)
end
function Auctions.GetStats(itemString, minTime)
private.statsQuery:BindParams(ItemString.GetBase(itemString), minTime or 0)
wipe(private.statsTemp)
private.statsQuery:GroupedSum("type", "quantity", private.statsTemp)
local cancel = private.statsTemp.cancel or 0
local expire = private.statsTemp.expire or 0
local total = cancel + expire
return cancel, expire, total
end
function Auctions.GetNumExpires(itemString, minTime)
private.numExpiresQuery:BindParams(ItemString.GetBase(itemString), minTime or 0)
local num = 0
for _, quantity in private.numExpiresQuery:Iterator() do
num = num + quantity
end
return num
end
function Auctions.GetNumExpiresSinceSale(itemString)
return Auctions.GetNumExpires(itemString, TSM.Accounting.Transactions.GetLastSaleTime(itemString))
end
function Auctions.CreateQuery()
return private.db:NewQuery()
end
function Auctions.RemoveOldData(days)
private.dataChanged = true
private.db:SetQueryUpdatesPaused(true)
local numRecords = private.db:NewQuery()
:LessThan("time", time() - days * SECONDS_PER_DAY)
:DeleteAndRelease()
private.db:SetQueryUpdatesPaused(false)
CustomPrice.OnSourceChange("NumExpires")
return numRecords
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.LoadData(recordType, csvRecords, csvSaveTimes)
local saveTimes = String.SafeSplit(csvSaveTimes, ",")
if not saveTimes then
return
end
local decodeContext = CSV.DecodeStart(csvRecords, CSV_KEYS)
if not decodeContext then
Log.Err("Failed to decode %s records", recordType)
private.dataChanged = true
return
end
local removeTime = time() - REMOVE_OLD_THRESHOLD
local index = 1
local prevTimestamp = 0
for itemString, stackSize, quantity, player, timestamp in CSV.DecodeIterator(decodeContext) do
itemString = ItemString.Get(itemString)
local baseItemString = ItemString.GetBaseFast(itemString)
local saveTime = tonumber(saveTimes[index])
stackSize = tonumber(stackSize)
quantity = tonumber(quantity)
timestamp = tonumber(timestamp)
if itemString and baseItemString and stackSize and quantity and timestamp and saveTime and timestamp > removeTime then
local newTimestamp = floor(timestamp)
if newTimestamp ~= timestamp then
-- make sure all timestamps are stored as integers
private.dataChanged = true
timestamp = newTimestamp
end
if timestamp < prevTimestamp then
-- not ordered by timestamp
private.dataChanged = true
end
prevTimestamp = timestamp
private.db:BulkInsertNewRowFast8(baseItemString, recordType, itemString, stackSize, quantity, player, timestamp, saveTime)
else
private.dataChanged = true
end
index = index + 1
end
if not CSV.DecodeEnd(decodeContext) then
Log.Err("Failed to decode %s records", recordType)
private.dataChanged = true
end
CustomPrice.OnSourceChange("NumExpires")
end
function private.InsertRecord(recordType, itemString, stackSize, timestamp)
private.dataChanged = true
assert(itemString and stackSize and stackSize > 0 and timestamp)
timestamp = floor(timestamp)
local baseItemString = ItemString.GetBase(itemString)
local matchingRow = private.db:NewQuery()
:Equal("type", recordType)
:Equal("baseItemString", baseItemString)
:Equal("itemString", itemString)
:Equal("stackSize", stackSize)
:Equal("player", UnitName("player"))
:GreaterThan("time", timestamp - COMBINE_TIME_THRESHOLD)
:LessThan("time", timestamp + COMBINE_TIME_THRESHOLD)
:Equal("saveTime", 0)
:GetFirstResultAndRelease()
if matchingRow then
matchingRow:SetField("quantity", matchingRow:GetField("quantity") + stackSize)
matchingRow:Update()
matchingRow:Release()
else
private.db:NewRow()
:SetField("baseItemString", baseItemString)
:SetField("type", recordType)
:SetField("itemString", itemString)
:SetField("stackSize", stackSize)
:SetField("quantity", stackSize)
:SetField("player", UnitName("player"))
:SetField("time", timestamp)
:SetField("saveTime", 0)
:Create()
end
if recordType == "expire" then
CustomPrice.OnSourceChange("NumExpires", itemString)
end
end

View File

@ -0,0 +1,54 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Accounting = TSM:NewPackage("Accounting")
local Math = TSM.Include("Util.Math")
local private = {
characterGuildTemp = {},
}
local SECONDS_PER_DAY = 24 * 60 * 60
-- ============================================================================
-- Module Functions
-- ============================================================================
function Accounting.GetSummaryQuery(timeFilterStart, timeFilterEnd, ignoredCharacters)
local query = TSM.Accounting.Transactions.CreateQuery()
:Select("type", "itemString", "price", "quantity", "time")
if timeFilterStart then
query:GreaterThan("time", timeFilterStart)
end
if timeFilterEnd then
query:LessThan("time", timeFilterEnd)
end
if ignoredCharacters then
wipe(private.characterGuildTemp)
for characterGuild in pairs(ignoredCharacters) do
local character, realm = strmatch(characterGuild, "^(.+) %- .+ %- (.+)$")
if character and realm == GetRealmName() then
private.characterGuildTemp[character] = true
end
end
query:NotInTable("player", private.characterGuildTemp)
end
return query
end
function Accounting.GetSaleRate(itemString)
-- since auction data only goes back 180 days, limit the sales to that same time range
local _, totalSaleNum = TSM.Accounting.Transactions.GetSaleStats(itemString, 180 * SECONDS_PER_DAY)
if not totalSaleNum then
return nil
end
local _, _, totalFailed = TSM.Accounting.Auctions.GetStats(itemString)
if not totalFailed then
return nil
end
return Math.Round(totalSaleNum / (totalSaleNum + totalFailed), 0.01)
end

View File

@ -0,0 +1,56 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Garrison = TSM.Accounting:NewPackage("Garrison")
local Event = TSM.Include("Util.Event")
local private = {}
local GOLD_TRAIT_ID = 256 -- traitId for the treasure hunter trait which increases gold from missions
-- ============================================================================
-- Module Functions
-- ============================================================================
function Garrison.OnInitialize()
if not TSM.IsWowClassic() then
Event.Register("GARRISON_MISSION_COMPLETE_RESPONSE", private.MissionComplete)
end
end
-- ============================================================================
-- Misson Reward Tracking
-- ============================================================================
function private.MissionComplete(_, missionId)
local moneyAward = 0
local info = C_Garrison.GetBasicMissionInfo(missionId)
if not info then
return
end
local rewards = info.rewards or info.overMaxRewards
for _, reward in pairs(rewards) do
if reward.title == GARRISON_REWARD_MONEY and reward.currencyID == 0 then
moneyAward = moneyAward + reward.quantity
end
end
if moneyAward > 0 then
-- check for followers which give bonus gold
local multiplier = 1
for _, followerId in ipairs(info.followers) do
for _, trait in ipairs(C_Garrison.GetFollowerAbilities(followerId)) do
if trait.id == GOLD_TRAIT_ID then
multiplier = multiplier + 1
end
end
end
moneyAward = moneyAward * multiplier
TSM.Accounting.Money.InsertGarrisonIncome(moneyAward)
end
end

View File

@ -0,0 +1,291 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local GoldTracker = TSM.Accounting:NewPackage("GoldTracker")
local Event = TSM.Include("Util.Event")
local Delay = TSM.Include("Util.Delay")
local CSV = TSM.Include("Util.CSV")
local Math = TSM.Include("Util.Math")
local Log = TSM.Include("Util.Log")
local Table = TSM.Include("Util.Table")
local TempTable = TSM.Include("Util.TempTable")
local Settings = TSM.Include("Service.Settings")
local PlayerInfo = TSM.Include("Service.PlayerInfo")
local private = {
truncateGoldLog = {},
characterGoldLog = {},
guildGoldLog = {},
currentCharacterKey = nil,
playerLogCount = 0,
searchValueTemp = {},
}
local CSV_KEYS = { "minute", "copper" }
local CHARACTER_KEY_SEP = " - "
local SECONDS_PER_MIN = 60
local SECONDS_PER_DAY = SECONDS_PER_MIN * 60 * 24
local MAX_COPPER_VALUE = 10 * 1000 * 1000 * COPPER_PER_GOLD - 1
local ERRONEOUS_ZERO_THRESHOLD = 5 * 1000 * COPPER_PER_GOLD
-- ============================================================================
-- Module Functions
-- ============================================================================
function GoldTracker.OnInitialize()
if not TSM.IsWowClassic() then
Event.Register("GUILDBANKFRAME_OPENED", private.GuildLogGold)
Event.Register("GUILDBANK_UPDATE_MONEY", private.GuildLogGold)
end
Event.Register("PLAYER_MONEY", private.PlayerLogGold)
-- get a list of known characters / guilds
local validCharacterGuilds = TempTable.Acquire()
for _, character in Settings.CharacterByFactionrealmIterator() do
validCharacterGuilds[character..CHARACTER_KEY_SEP..UnitFactionGroup("player")..CHARACTER_KEY_SEP..GetRealmName()] = true
local guild = TSM.db.factionrealm.internalData.characterGuilds[character]
if guild then
validCharacterGuilds[guild] = true
end
end
-- load the gold log data
for realm in TSM.db:GetConnectedRealmIterator("realm") do
for factionrealm in TSM.db:FactionrealmByRealmIterator(realm) do
for _, character in TSM.db:FactionrealmCharacterIterator(factionrealm) do
local data = TSM.db:Get("sync", TSM.db:GetSyncScopeKeyByCharacter(character, factionrealm), "internalData", "goldLog")
if data then
local lastUpdate = TSM.db:Get("sync", TSM.db:GetSyncScopeKeyByCharacter(character, factionrealm), "internalData", "goldLogLastUpdate") or 0
local characterKey = character..CHARACTER_KEY_SEP..factionrealm
private.LoadCharacterGoldLog(characterKey, data, validCharacterGuilds, lastUpdate)
end
end
local guildData = TSM.db:Get("factionrealm", factionrealm, "internalData", "guildGoldLog")
if guildData then
for guild, data in pairs(guildData) do
local entries = {}
local decodeContext = CSV.DecodeStart(data, CSV_KEYS)
if decodeContext then
for minute, copper in CSV.DecodeIterator(decodeContext) do
tinsert(entries, { minute = tonumber(minute), copper = tonumber(copper) })
end
CSV.DecodeEnd(decodeContext)
end
private.guildGoldLog[guild] = entries
local lastEntryTime = #entries > 0 and entries[#entries].minute * SECONDS_PER_MIN or math.huge
local lastUpdate = TSM.db:Get("factionrealm", factionrealm, "internalData", "guildGoldLogLastUpdate")
if not validCharacterGuilds[guild] and max(lastEntryTime, lastUpdate and lastUpdate[guild] or 0) < time() - 30 * SECONDS_PER_DAY then
-- this guild may not be valid and the last entry is over 30 days old, so truncate the data
private.truncateGoldLog[guild] = lastEntryTime
end
end
end
end
end
TempTable.Release(validCharacterGuilds)
private.currentCharacterKey = UnitName("player")..CHARACTER_KEY_SEP..UnitFactionGroup("player")..CHARACTER_KEY_SEP..GetRealmName()
assert(private.characterGoldLog[private.currentCharacterKey])
end
function GoldTracker.OnEnable()
-- Log the current player gold (need to wait for OnEnable, otherwise GetMoney() returns 0 when first logging in)
private.PlayerLogGold()
end
function GoldTracker.OnDisable()
private.PlayerLogGold()
TSM.db.sync.internalData.goldLog = CSV.Encode(CSV_KEYS, private.characterGoldLog[private.currentCharacterKey])
TSM.db.sync.internalData.goldLogLastUpdate = private.characterGoldLog[private.currentCharacterKey].lastUpdate
local guild = PlayerInfo.GetPlayerGuild(UnitName("player"))
if guild and private.guildGoldLog[guild] then
TSM.db.factionrealm.internalData.guildGoldLog[guild] = CSV.Encode(CSV_KEYS, private.guildGoldLog[guild])
TSM.db.factionrealm.internalData.guildGoldLogLastUpdate[guild] = private.guildGoldLog[guild].lastUpdate
end
end
function GoldTracker.CharacterGuildIterator()
return private.CharacterGuildIteratorHelper
end
function GoldTracker.GetGoldAtTime(timestamp, ignoredCharactersGuilds)
local value = 0
for character, logEntries in pairs(private.characterGoldLog) do
if #logEntries > 0 and not ignoredCharactersGuilds[character] and (private.truncateGoldLog[character] or math.huge) > timestamp then
value = value + private.GetValueAtTime(logEntries, timestamp)
end
end
for guild, logEntries in pairs(private.guildGoldLog) do
if #logEntries > 0 and not ignoredCharactersGuilds[guild] and (private.truncateGoldLog[guild] or math.huge) > timestamp then
value = value + private.GetValueAtTime(logEntries, timestamp)
end
end
return value
end
function GoldTracker.GetGraphTimeRange(ignoredCharactersGuilds)
local minTime = Math.Floor(time(), SECONDS_PER_MIN)
for character, logEntries in pairs(private.characterGoldLog) do
if #logEntries > 0 and not ignoredCharactersGuilds[character] then
minTime = min(minTime, logEntries[1].minute * SECONDS_PER_MIN)
end
end
for guild, logEntries in pairs(private.guildGoldLog) do
if #logEntries > 0 and not ignoredCharactersGuilds[guild] then
minTime = min(minTime, logEntries[1].minute * SECONDS_PER_MIN)
end
end
return minTime, Math.Floor(time(), SECONDS_PER_MIN), SECONDS_PER_MIN
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.LoadCharacterGoldLog(characterKey, data, validCharacterGuilds, lastUpdate)
assert(not private.characterGoldLog[characterKey])
local decodeContext = CSV.DecodeStart(data, CSV_KEYS)
if not decodeContext then
Log.Err("Failed to decode (%s, %d)", characterKey, #data)
private.characterGoldLog[characterKey] = {}
return
end
local entries = {}
for minute, copper in CSV.DecodeIterator(decodeContext) do
tinsert(entries, { minute = tonumber(minute), copper = tonumber(copper) })
end
CSV.DecodeEnd(decodeContext)
-- clean up any erroneous 0 entries, entries which are too high, and duplicate entries
local didChange = true
while didChange do
didChange = false
for i = #entries - 1, 2, -1 do
local prevValue = entries[i-1].copper
local value = entries[i].copper
local nextValue = entries[i+1].copper
if prevValue > ERRONEOUS_ZERO_THRESHOLD and value == 0 and nextValue > ERRONEOUS_ZERO_THRESHOLD then
-- this is likely an erroneous 0 value
didChange = true
tremove(entries, i)
end
end
for i = #entries, 2, -1 do
local prevValue = entries[i-1].copper
local value = entries[i].copper
if prevValue == value or value > MAX_COPPER_VALUE then
-- this is either a duplicate or invalid value
didChange = true
tremove(entries, i)
end
end
end
private.characterGoldLog[characterKey] = entries
local lastEntryTime = #entries > 0 and entries[#entries].minute * SECONDS_PER_MIN or math.huge
if not validCharacterGuilds[characterKey] and max(lastEntryTime, lastUpdate) < time() - 30 * SECONDS_PER_DAY then
-- this character may not be valid and the last entry is over 30 days old, so truncate the data
private.truncateGoldLog[characterKey] = lastEntryTime
end
end
function private.UpdateGoldLog(goldLog, copper)
copper = Math.Round(copper, COPPER_PER_GOLD * (TSM.IsWowClassic() and 1 or 1000))
local currentMinute = floor(time() / SECONDS_PER_MIN)
local prevRecord = goldLog[#goldLog]
-- store the last update time
goldLog.lastUpdate = time()
if prevRecord and copper == prevRecord.copper then
-- amount of gold hasn't changed, so nothing to do
return
elseif prevRecord and prevRecord.minute == currentMinute then
-- gold has changed and the previous record is for the current minute so just modify it
prevRecord.copper = copper
else
-- amount of gold changed and we're in a new minute, so insert a new record
while prevRecord and prevRecord.minute > currentMinute - 1 do
-- their clock may have changed - just delete everything that's too recent
tremove(goldLog)
prevRecord = goldLog[#goldLog]
end
tinsert(goldLog, {
minute = currentMinute,
copper = copper
})
end
end
function private.GuildLogGold()
local guildName = GetGuildInfo("player")
local isGuildLeader = IsGuildLeader()
if guildName and not isGuildLeader then
-- check if our alt is the guild leader
for i = 1, GetNumGuildMembers() do
local name, _, rankIndex = GetGuildRosterInfo(i)
if name and rankIndex == 0 and PlayerInfo.IsPlayer(gsub(name, "%-", " - "), true) then
isGuildLeader = true
end
end
end
if guildName and isGuildLeader then
if not private.guildGoldLog[guildName] then
private.guildGoldLog[guildName] = {}
end
private.UpdateGoldLog(private.guildGoldLog[guildName], GetGuildBankMoney())
end
end
function private.PlayerLogGold()
-- GetMoney sometimes returns 0 for a while after login, so keep trying for 30 seconds before recording a 0
local money = GetMoney()
if money == 0 and private.playerLogCount < 30 then
private.playerLogCount = private.playerLogCount + 1
Delay.AfterTime(1, private.PlayerLogGold)
return
end
private.playerLogCount = 0
private.UpdateGoldLog(private.characterGoldLog[private.currentCharacterKey], money)
TSM.db.sync.internalData.money = money
end
function private.GetValueAtTime(logEntries, timestamp)
local minute = floor(timestamp / SECONDS_PER_MIN)
if logEntries[1].minute > minute then
-- timestamp is before we had any data
return 0
end
private.searchValueTemp.minute = minute
local index, insertIndex = Table.BinarySearch(logEntries, private.searchValueTemp, private.GetEntryMinute)
-- if we didn't find an exact match, the index is the previous one (compared to the insert index)
-- as that point's gold value is true up until the next point
index = index or (insertIndex - 1)
return logEntries[index].copper
end
function private.GetEntryMinute(entry)
return entry.minute
end
function private.CharacterGuildIteratorHelper(_, lastKey)
local result, isGuild = nil, nil
if not lastKey or private.characterGoldLog[lastKey] then
result = next(private.characterGoldLog, lastKey)
isGuild = false
if not result then
lastKey = nil
end
end
if not result then
result = next(private.guildGoldLog, lastKey)
isGuild = result and true or false
end
return result, isGuild
end

View File

@ -0,0 +1,387 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Mail = TSM.Accounting:NewPackage("Mail")
local Event = TSM.Include("Util.Event")
local Delay = TSM.Include("Util.Delay")
local String = TSM.Include("Util.String")
local ItemString = TSM.Include("Util.ItemString")
local ItemInfo = TSM.Include("Service.ItemInfo")
local InventoryInfo = TSM.Include("Service.InventoryInfo")
local AuctionTracking = TSM.Include("Service.AuctionTracking")
local Inventory = TSM.Include("Service.Inventory")
local private = {
hooks = {},
}
local SECONDS_PER_DAY = 24 * 60 * 60
local EXPIRED_MATCH_TEXT = AUCTION_EXPIRED_MAIL_SUBJECT:gsub("%%s", "")
local CANCELLED_MATCH_TEXT = AUCTION_REMOVED_MAIL_SUBJECT:gsub("%%s", "")
local OUTBID_MATCH_TEXT = AUCTION_OUTBID_MAIL_SUBJECT:gsub("%%s", "(.+)")
-- ============================================================================
-- Module Functions
-- ============================================================================
function Mail.OnInitialize()
Event.Register("MAIL_SHOW", function() Delay.AfterTime("ACCOUNTING_GET_SELLERS", 0.1, private.RequestSellerInfo, 0.1) end)
Event.Register("MAIL_CLOSED", function() Delay.Cancel("ACCOUNTING_GET_SELLERS") end)
-- hook certain mail functions
private.hooks.TakeInboxItem = TakeInboxItem
TakeInboxItem = function(...)
Mail:ScanCollectedMail("TakeInboxItem", 1, ...)
end
private.hooks.TakeInboxMoney = TakeInboxMoney
TakeInboxMoney = function(...)
Mail:ScanCollectedMail("TakeInboxMoney", 1, ...)
end
private.hooks.AutoLootMailItem = AutoLootMailItem
AutoLootMailItem = function(...)
Mail:ScanCollectedMail("AutoLootMailItem", 1, ...)
end
private.hooks.SendMail = SendMail
SendMail = private.CheckSendMail
end
-- ============================================================================
-- Inbox Functions
-- ============================================================================
function private.RequestSellerInfo()
local isDone = true
for i = 1, GetInboxNumItems() do
local invoiceType, _, seller = GetInboxInvoiceInfo(i)
if invoiceType and seller == "" then
isDone = false
end
end
if isDone and GetInboxNumItems() > 0 then
Delay.Cancel("ACCOUNTING_GET_SELLERS")
end
end
function private.CanLootMailIndex(index, copper)
local currentMoney = GetMoney()
assert(currentMoney <= MAXIMUM_BID_PRICE)
-- check if this would put them over the gold cap
if currentMoney + copper > MAXIMUM_BID_PRICE then return end
local _, _, _, _, _, _, _, itemCount = GetInboxHeaderInfo(index)
if not itemCount or itemCount == 0 then return true end
for j = 1, ATTACHMENTS_MAX_RECEIVE do
-- TODO: prevent items that you can't loot because of internal mail error
if CalculateTotalNumberOfFreeBagSlots() <= 0 then
return
end
local link = GetInboxItemLink(index, j)
local itemString = ItemString.Get(link)
local _, _, _, count = GetInboxItem(index, j)
local quantity = count or 0
local maxUnique = private.GetInboxMaxUnique(index, j)
-- dont record unique items that we can't loot
local playerQty = Inventory.GetBagQuantity(itemString) + Inventory.GetBankQuantity(itemString) + Inventory.GetReagentBankQuantity(itemString)
if maxUnique > 0 and maxUnique < playerQty + quantity then
return
end
if itemString then
for bag = 0, NUM_BAG_SLOTS do
if InventoryInfo.ItemWillGoInBag(link, bag) then
for slot = 1, GetContainerNumSlots(bag) do
local iString = ItemString.Get(GetContainerItemLink(bag, slot))
if iString == itemString then
local _, stackSize = GetContainerItemInfo(bag, slot)
local maxStackSize = ItemInfo.GetMaxStack(itemString) or 1
if (maxStackSize - stackSize) >= quantity then
return true
end
elseif not iString then
return true
end
end
end
end
end
end
end
function private.GetInboxMaxUnique(index, num)
if not num then
num = 1
end
if not TSMScanTooltip then
CreateFrame("GameTooltip", "TSMScanTooltip", UIParent, "GameTooltipTemplate")
end
TSMScanTooltip:SetOwner(UIParent, "ANCHOR_NONE")
TSMScanTooltip:ClearLines()
local _, speciesId = TSMScanTooltip:SetInboxItem(index, num)
if (speciesId or 0) > 0 then
return 0
else
for id = 2, TSMScanTooltip:NumLines() do
local text = private.GetTooltipText(_G["TSMScanTooltipTextLeft"..id])
if text then
if text == ITEM_UNIQUE then
return 1
else
local match = text and strmatch(text, "^"..ITEM_UNIQUE.." %((%d+)%)$")
if match then
return tonumber(match)
end
end
end
end
end
return 0
end
function private.GetTooltipText(text)
local textStr = strtrim(text and text:GetText() or "")
if textStr == "" then return end
return textStr
end
-- scans the mail that the player just attempted to collected (Pre-Hook)
function Mail:ScanCollectedMail(oFunc, attempt, index, subIndex)
local invoiceType, itemName, buyer, bid, _, _, ahcut, _, _, _, quantity = GetInboxInvoiceInfo(index)
buyer = buyer or (invoiceType == "buyer" and AUCTION_HOUSE_MAIL_MULTIPLE_SELLERS or AUCTION_HOUSE_MAIL_MULTIPLE_BUYERS)
local _, stationeryIcon, sender, subject, money, codAmount, daysLeft = GetInboxHeaderInfo(index)
if not subject then return end
if attempt > 2 then
if buyer == "" then
buyer = "?"
elseif sender == "" then
sender = "?"
end
end
local success = false
if invoiceType == "seller" and buyer and buyer ~= "" then -- AH Sales
local saleTime = (time() + (daysLeft - 30) * SECONDS_PER_DAY)
local itemString = ItemInfo.ItemNameToItemString(itemName)
if not itemString or itemString == ItemString.GetUnknown() then
itemString = AuctionTracking.GetSaleHintItemString(itemName, quantity, bid)
end
if private.CanLootMailIndex(index, (bid - ahcut)) then
if itemString then
local copper = floor((bid - ahcut) / quantity + 0.5)
TSM.Accounting.Transactions.InsertAuctionSale(itemString, quantity, copper, buyer, saleTime)
end
success = true
end
elseif invoiceType == "buyer" and buyer and buyer ~= "" then -- AH Buys
local copper = floor(bid / quantity + 0.5)
if not TSM.IsWowClassic() then
if subIndex then
quantity = select(4, GetInboxItem(index, subIndex))
else
quantity = 0
for i = 1, ATTACHMENTS_MAX do
quantity = quantity + (select(4, GetInboxItem(index, i)) or 0)
end
end
end
local link = (subIndex or 1) == 1 and private.GetFirstInboxItemLink(index) or GetInboxItemLink(index, subIndex or 1)
local itemString = ItemString.Get(link)
if itemString and private.CanLootMailIndex(index, 0) then
local buyTime = (time() + (daysLeft - 30) * SECONDS_PER_DAY)
TSM.Accounting.Transactions.InsertAuctionBuy(itemString, quantity, copper, buyer, buyTime)
success = true
end
elseif codAmount > 0 then -- COD Buys (only if all attachments are same item)
local link = (subIndex or 1) == 1 and private.GetFirstInboxItemLink(index) or GetInboxItemLink(index, subIndex or 1)
local itemString = ItemString.Get(link)
if itemString and sender then
local name = ItemInfo.GetName(link)
local total = 0
local stacks = 0
local ignore = false
for i = 1, ATTACHMENTS_MAX_RECEIVE do
local nameCheck, _, _, count = GetInboxItem(index, i)
if nameCheck and count then
if nameCheck == name then
total = total + count
stacks = stacks + 1
else
ignore = true
end
end
end
if total ~= 0 and not ignore and private.CanLootMailIndex(index, codAmount) then
local copper = floor(codAmount / total + 0.5)
local buyTime = (time() + (daysLeft - 3) * SECONDS_PER_DAY)
local maxStack = ItemInfo.GetMaxStack(link)
for _ = 1, stacks do
local stackSize = (total >= maxStack) and maxStack or total
TSM.Accounting.Transactions.InsertCODBuy(itemString, stackSize, copper, sender, buyTime)
total = total - stackSize
if total <= 0 then
break
end
end
end
success = true
end
elseif money > 0 and invoiceType ~= "seller" and not strfind(subject, OUTBID_MATCH_TEXT) then
local str = nil
if GetLocale() == "deDE" then
str = gsub(subject, gsub(COD_PAYMENT, String.Escape("%1$s"), ""), "")
else
str = gsub(subject, gsub(COD_PAYMENT, String.Escape("%s"), ""), "")
end
local saleTime = (time() + (daysLeft - 31) * SECONDS_PER_DAY)
if sender and private.CanLootMailIndex(index, money) then
if str and strfind(str, "TSM$") then -- payment for a COD the player sent
local codName = strtrim(strmatch(str, "([^%(]+)"))
local qty = strmatch(str, "%(([0-9]+)%)")
qty = tonumber(qty)
local itemString = ItemInfo.ItemNameToItemString(codName)
if itemString then
local copper = floor(money / qty + 0.5)
local maxStack = ItemInfo.GetMaxStack(itemString) or 1
local stacks = ceil(qty / maxStack)
for _ = 1, stacks do
local stackSize = (qty >= maxStack) and maxStack or qty
TSM.Accounting.Transactions.InsertCODSale(itemString, stackSize, copper, sender, saleTime)
qty = qty - stackSize
if qty <= 0 then
break
end
end
end
else -- record a money transfer
TSM.Accounting.Money.InsertMoneyTransferIncome(money, sender, saleTime)
end
success = true
end
elseif strfind(subject, EXPIRED_MATCH_TEXT) then -- expired auction
local expiredTime = (time() + (daysLeft - 30) * SECONDS_PER_DAY)
local link = (subIndex or 1) == 1 and private.GetFirstInboxItemLink(index) or GetInboxItemLink(index, subIndex or 1)
local _, _, _, count = GetInboxItem(index, subIndex or 1)
if TSM.IsWowClassic() then
quantity = count or 0
else
if subIndex then
quantity = select(4, GetInboxItem(index, subIndex))
else
quantity = 0
for i = 1, ATTACHMENTS_MAX do
quantity = quantity + (select(4, GetInboxItem(index, i)) or 0)
end
end
end
local itemString = ItemString.Get(link)
if private.CanLootMailIndex(index, 0) and itemString and quantity then
TSM.Accounting.Auctions.InsertExpire(itemString, quantity, expiredTime)
success = true
end
elseif strfind(subject, CANCELLED_MATCH_TEXT) then -- cancelled auction
local cancelledTime = (time() + (daysLeft - 30) * SECONDS_PER_DAY)
local link = (subIndex or 1) == 1 and private.GetFirstInboxItemLink(index) or GetInboxItemLink(index, subIndex or 1)
local _, _, _, count = GetInboxItem(index, subIndex or 1)
if TSM.IsWowClassic() then
quantity = count or 0
else
if subIndex then
quantity = select(4, GetInboxItem(index, subIndex))
else
quantity = 0
for i = 1, ATTACHMENTS_MAX do
quantity = quantity + (select(4, GetInboxItem(index, i)) or 0)
end
end
end
local itemString = ItemString.Get(link)
if private.CanLootMailIndex(index, 0) and itemString and quantity then
TSM.Accounting.Auctions.InsertCancel(itemString, quantity, cancelledTime)
success = true
end
end
if success then
private.hooks[oFunc](index, subIndex)
elseif (not stationeryIcon or (invoiceType and (not buyer or buyer == ""))) and attempt <= 5 then
Delay.AfterTime("accountingHookDelay", 0.2, function() Mail:ScanCollectedMail(oFunc, attempt + 1, index, subIndex) end)
elseif attempt > 5 then
private.hooks[oFunc](index, subIndex)
else
private.hooks[oFunc](index, subIndex)
end
end
-- ============================================================================
-- Sending Functions
-- ============================================================================
-- scans the mail that the player just attempted to send (Pre-Hook) to see if COD
function private.CheckSendMail(destination, currentSubject, ...)
local codAmount = GetSendMailCOD()
local moneyAmount = GetSendMailMoney()
local mailCost = GetSendMailPrice()
local subject
local total = 0
local ignore = false
if codAmount ~= 0 then
for i = 1, 12 do
local itemName, _, _, count = GetSendMailItem(i)
if itemName and count then
if not subject then
subject = itemName
end
if subject == itemName then
total = total + count
else
ignore = true
end
end
end
else
ignore = true
end
if moneyAmount > 0 then
-- add a record for the money transfer
TSM.Accounting.Money.InsertMoneyTransferExpense(moneyAmount, destination)
mailCost = mailCost - moneyAmount
end
TSM.Accounting.Money.InsertPostageExpense(mailCost, destination)
if not ignore then
private.hooks.SendMail(destination, subject .. " (" .. total .. ") TSM", ...)
else
private.hooks.SendMail(destination, currentSubject, ...)
end
end
function private.GetFirstInboxItemLink(index)
if not TSMAccountingMailTooltip then
CreateFrame("GameTooltip", "TSMAccountingMailTooltip", UIParent, "GameTooltipTemplate")
end
TSMAccountingMailTooltip:SetOwner(UIParent, "ANCHOR_NONE")
TSMAccountingMailTooltip:ClearLines()
local _, speciesId, level, breedQuality, maxHealth, power, speed = TSMAccountingMailTooltip:SetInboxItem(index)
local link = nil
if (speciesId or 0) > 0 then
link = ItemInfo.GetLink(strjoin(":", "p", speciesId, level, breedQuality, maxHealth, power, speed))
else
link = GetInboxItemLink(index, 1)
end
TSMAccountingMailTooltip:Hide()
return link
end

View File

@ -0,0 +1,133 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Merchant = TSM.Accounting:NewPackage("Merchant")
local Event = TSM.Include("Util.Event")
local Math = TSM.Include("Util.Math")
local ItemString = TSM.Include("Util.ItemString")
local ItemInfo = TSM.Include("Service.ItemInfo")
local private = {
repairMoney = 0,
couldRepair = nil,
repairCost = 0,
pendingSales = {
itemString = {},
quantity = {},
copper = {},
insertTime = {},
},
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Merchant.OnInitialize()
Event.Register("MERCHANT_SHOW", private.SetupRepairCost)
Event.Register("BAG_UPDATE_DELAYED", private.OnMerchantUpdate)
Event.Register("UPDATE_INVENTORY_DURABILITY", private.AddRepairCosts)
Event.Register("MERCHANT_CLOSED", private.OnMerchantClosed)
hooksecurefunc("UseContainerItem", private.CheckMerchantSale)
hooksecurefunc("BuyMerchantItem", private.OnMerchantBuy)
hooksecurefunc("BuybackItem", private.OnMerchantBuyback)
end
-- ============================================================================
-- Repair Cost Tracking
-- ============================================================================
function private.SetupRepairCost()
private.repairMoney = GetMoney()
private.couldRepair = CanMerchantRepair()
-- if merchant can repair set up variables so we can track repairs
if private.couldRepair then
private.repairCost = GetRepairAllCost()
end
end
function private.OnMerchantUpdate()
-- Could have bought something before or after repair
private.repairMoney = GetMoney()
-- log any pending sales
for i, insertTime in ipairs(private.pendingSales.insertTime) do
if GetTime() - insertTime < 5 then
TSM.Accounting.Transactions.InsertVendorSale(private.pendingSales.itemString[i], private.pendingSales.quantity[i], private.pendingSales.copper[i])
end
end
wipe(private.pendingSales.itemString)
wipe(private.pendingSales.quantity)
wipe(private.pendingSales.copper)
wipe(private.pendingSales.insertTime)
end
function private.AddRepairCosts()
if private.couldRepair and private.repairCost > 0 then
local cash = GetMoney()
if private.repairMoney > cash then
-- this is probably a repair bill
local cost = private.repairMoney - cash
TSM.Accounting.Money.InsertRepairBillExpense(cost)
-- reset money as this might have been a single item repair
private.repairMoney = cash
-- reset the repair cost for the next repair
private.repairCost = GetRepairAllCost()
end
end
end
function private.OnMerchantClosed()
private.couldRepair = nil
private.repairCost = 0
end
-- ============================================================================
-- Merchant Purchases / Sales Tracking
-- ============================================================================
function private.CheckMerchantSale(bag, slot, onSelf)
-- check if we are trying to sell something to a vendor
if (not MerchantFrame:IsShown() and not TSM.UI.VendoringUI.IsVisible()) or onSelf then
return
end
local itemString = ItemString.Get(GetContainerItemLink(bag, slot))
local _, quantity = GetContainerItemInfo(bag, slot)
local copper = ItemInfo.GetVendorSell(itemString)
if not itemString or not quantity or not copper then
return
end
tinsert(private.pendingSales.itemString, itemString)
tinsert(private.pendingSales.quantity, quantity)
tinsert(private.pendingSales.copper, copper)
tinsert(private.pendingSales.insertTime, GetTime())
end
function private.OnMerchantBuy(index, quantity)
local _, _, price, batchQuantity = GetMerchantItemInfo(index)
local itemString = ItemString.Get(GetMerchantItemLink(index))
if not itemString or not price or price <= 0 then
return
end
quantity = quantity or batchQuantity
local copper = Math.Round(price / batchQuantity)
TSM.Accounting.Transactions.InsertVendorBuy(itemString, quantity, copper)
end
function private.OnMerchantBuyback(index)
local _, _, price, quantity = GetBuybackItemInfo(index)
local itemString = ItemString.Get(GetBuybackItemLink(index))
if not itemString or not price or price <= 0 then
return
end
local copper = Math.Round(price / quantity)
TSM.Accounting.Transactions.InsertVendorBuy(itemString, quantity, copper)
end

View File

@ -0,0 +1,171 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Money = TSM.Accounting:NewPackage("Money")
local Database = TSM.Include("Util.Database")
local CSV = TSM.Include("Util.CSV")
local Log = TSM.Include("Util.Log")
local private = {
db = nil,
dataChanged = false,
}
local CSV_KEYS = { "type", "amount", "otherPlayer", "player", "time" }
local COMBINE_TIME_THRESHOLD = 300 -- group expenses within 5 minutes together
local SECONDS_PER_DAY = 24 * 60 * 60
-- ============================================================================
-- Module Functions
-- ============================================================================
function Money.OnInitialize()
private.db = Database.NewSchema("ACCOUNTING_MONEY")
:AddStringField("recordType")
:AddStringField("type")
:AddNumberField("amount")
:AddStringField("otherPlayer")
:AddStringField("player")
:AddNumberField("time")
:AddIndex("recordType")
:Commit()
private.db:BulkInsertStart()
private.LoadData("expense", TSM.db.realm.internalData.csvExpense)
private.LoadData("income", TSM.db.realm.internalData.csvIncome)
private.db:BulkInsertEnd()
end
function Money.OnDisable()
if not private.dataChanged then
-- nothing changed, so just keep the previous saved values
return
end
TSM.db.realm.internalData.csvExpense = private.SaveData("expense")
TSM.db.realm.internalData.csvIncome = private.SaveData("income")
end
function Money.InsertMoneyTransferExpense(amount, destination)
private.InsertRecord("expense", "Money Transfer", amount, destination, time())
end
function Money.InsertPostageExpense(amount, destination)
private.InsertRecord("expense", "Postage", amount, destination, time())
end
function Money.InsertRepairBillExpense(amount)
private.InsertRecord("expense", "Repair Bill", amount, "Merchant", time())
end
function Money.InsertMoneyTransferIncome(amount, source, timestamp)
private.InsertRecord("income", "Money Transfer", amount, source, timestamp)
end
function Money.InsertGarrisonIncome(amount)
private.InsertRecord("income", "Garrison", amount, "Mission", time())
end
function Money.CreateQuery()
return private.db:NewQuery()
end
function Money.CharacterIterator(recordType)
return private.db:NewQuery()
:Equal("recordType", recordType)
:Distinct("player")
:Select("player")
:IteratorAndRelease()
end
function Money.RemoveOldData(days)
private.dataChanged = true
local query = private.db:NewQuery()
:LessThan("time", time() - days * SECONDS_PER_DAY)
local numRecords = 0
private.db:SetQueryUpdatesPaused(true)
for _, row in query:Iterator() do
private.db:DeleteRow(row)
numRecords = numRecords + 1
end
query:Release()
private.db:SetQueryUpdatesPaused(false)
return numRecords
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.LoadData(recordType, csvRecords)
local decodeContext = CSV.DecodeStart(csvRecords, CSV_KEYS)
if not decodeContext then
Log.Err("Failed to decode %s records", recordType)
private.dataChanged = true
return
end
for type, amount, otherPlayer, player, timestamp in CSV.DecodeIterator(decodeContext) do
amount = tonumber(amount)
timestamp = tonumber(timestamp)
if amount and timestamp then
local newTimestamp = floor(timestamp)
if newTimestamp ~= timestamp then
-- make sure all timestamps are stored as integers
timestamp = newTimestamp
private.dataChanged = true
end
private.db:BulkInsertNewRowFast6(recordType, type, amount, otherPlayer, player, timestamp)
else
private.dataChanged = true
end
end
if not CSV.DecodeEnd(decodeContext) then
Log.Err("Failed to decode %s records", recordType)
private.dataChanged = true
end
end
function private.SaveData(recordType)
local query = private.db:NewQuery()
:Equal("recordType", recordType)
local encodeContext = CSV.EncodeStart(CSV_KEYS)
for _, row in query:Iterator() do
CSV.EncodeAddRowData(encodeContext, row)
end
query:Release()
return CSV.EncodeEnd(encodeContext)
end
function private.InsertRecord(recordType, type, amount, otherPlayer, timestamp)
private.dataChanged = true
assert(type and amount and amount > 0 and otherPlayer and timestamp)
timestamp = floor(timestamp)
local matchingRow = private.db:NewQuery()
:Equal("recordType", recordType)
:Equal("type", type)
:Equal("otherPlayer", otherPlayer)
:Equal("player", UnitName("player"))
:GreaterThan("time", timestamp - COMBINE_TIME_THRESHOLD)
:LessThan("time", timestamp + COMBINE_TIME_THRESHOLD)
:GetFirstResultAndRelease()
if matchingRow then
matchingRow:SetField("amount", matchingRow:GetField("amount") + amount)
matchingRow:Update()
matchingRow:Release()
else
private.db:NewRow()
:SetField("recordType", recordType)
:SetField("type", type)
:SetField("amount", amount)
:SetField("otherPlayer", otherPlayer)
:SetField("player", UnitName("player"))
:SetField("time", timestamp)
:Create()
end
end

View File

@ -0,0 +1,287 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local AccountingSync = TSM.Accounting:NewPackage("Sync")
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 Theme = TSM.Include("Util.Theme")
local Sync = TSM.Include("Service.Sync")
local private = {
accountLookup = {},
accountStatus = {},
pendingChunks = {},
dataTemp = {},
}
local CHANGE_NOTIFICATION_DELAY = 5
local RETRY_DELAY = 5
-- ============================================================================
-- Module Functions
-- ============================================================================
function AccountingSync.OnInitialize()
Sync.RegisterConnectionChangedCallback(private.ConnectionChangedHandler)
Sync.RegisterRPC("ACCOUNTING_GET_PLAYER_HASH", private.RPCGetPlayerHash)
Sync.RegisterRPC("ACCOUNTING_GET_PLAYER_CHUNKS", private.RPCGetPlayerChunks)
Sync.RegisterRPC("ACCOUNTING_GET_PLAYER_DATA", private.RPCGetData)
Sync.RegisterRPC("ACCOUNTING_CHANGE_NOTIFICATION", private.RPCChangeNotification)
end
function AccountingSync.GetStatus(account)
local status = private.accountStatus[account]
if not status then
return Theme.GetFeedbackColor("RED"):ColorText(L["Not Connected"])
elseif status == "GET_PLAYER_HASH" or status == "GET_PLAYER_CHUNKS" or status == "GET_PLAYER_DATA" or status == "RETRY" then
return Theme.GetFeedbackColor("YELLOW"):ColorText(L["Updating"])
elseif status == "SYNCED" then
return Theme.GetFeedbackColor("GREEN"):ColorText(L["Up to date"])
else
error("Invalid status: "..tostring(status))
end
end
function AccountingSync.OnTransactionsChanged()
Delay.AfterTime("ACCOUNTING_SYNC_CHANGE", CHANGE_NOTIFICATION_DELAY, private.NotifyChange)
end
-- ============================================================================
-- RPC Functions and Result Handlers
-- ============================================================================
function private.GetPlayerHash(player)
local account = private.accountLookup[player]
private.accountStatus[account] = "GET_PLAYER_HASH"
TSM.Accounting.Transactions.PrepareSyncHashes(player)
Sync.CallRPC("ACCOUNTING_GET_PLAYER_HASH", player, private.RPCGetPlayerHashResultHandler)
end
function private.RPCGetPlayerHash()
local player = UnitName("player")
return player, TSM.Accounting.Transactions.GetSyncHash(player)
end
function private.RPCGetPlayerHashResultHandler(player, hash)
local account = player and private.accountLookup[player]
if not account then
-- request timed out, so try again
Log.Warn("Getting player hash timed out")
private.QueueRetriesByStatus("GET_PLAYER_HASH")
return
elseif not hash then
-- the hash isn't ready yet, so try again
Log.Warn("Sync player hash not ready yet")
private.QueueRetryByPlayer(player)
return
end
if private.accountStatus[account] == "RETRY" then
-- There is a race condition where if we tried to issue GET_PLAYER_HASH for two players and one times out,
-- we would also queue a retry for the other one, so handle that here.
private.accountStatus[account] = "GET_PLAYER_HASH"
end
assert(private.accountStatus[account] == "GET_PLAYER_HASH")
local currentHash = TSM.Accounting.Transactions.GetSyncHash(player)
if not currentHash then
-- don't have our hash yet, so try again
Log.Warn("Current player hash not ready yet")
private.QueueRetryByPlayer(player)
return
end
if hash ~= currentHash then
Log.Info("Need updated transactions data from %s (%s, %s)", player, hash, currentHash)
private.GetPlayerChunks(player)
else
Log.Info("Transactions data for %s already up to date (%s, %s)", player, hash, currentHash)
private.accountStatus[account] = "SYNCED"
end
end
function private.GetPlayerChunks(player)
local account = private.accountLookup[player]
private.accountStatus[account] = "GET_PLAYER_CHUNKS"
Sync.CallRPC("ACCOUNTING_GET_PLAYER_CHUNKS", player, private.RPCGetPlayerChunksResultHandler)
end
function private.RPCGetPlayerChunks()
local player = UnitName("player")
return player, TSM.Accounting.Transactions.GetSyncHashByDay(player)
end
function private.RPCGetPlayerChunksResultHandler(player, chunks)
local account = player and private.accountLookup[player]
if not account then
-- request timed out, so try again from the start
Log.Warn("Getting chunks timed out")
private.QueueRetriesByStatus("GET_PLAYER_CHUNKS")
return
elseif not chunks then
-- the hashes have been invalidated, so try again from the start
Log.Warn("Sync player chunks not ready yet")
private.QueueRetryByPlayer(player)
return
end
assert(private.accountStatus[account] == "GET_PLAYER_CHUNKS")
local currentChunks = TSM.Accounting.Transactions.GetSyncHashByDay(player)
if not currentChunks then
-- our hashes have been invalidated, so try again from the start
Log.Warn("Local hashes are invalid")
private.QueueRetryByPlayer(player)
return
end
for day in pairs(currentChunks) do
if not chunks[day] then
-- remove day which no longer exists
TSM.Accounting.Transactions.RemovePlayerDay(player, day)
end
end
-- queue up all the pending chunks
private.pendingChunks[player] = private.pendingChunks[player] or TempTable.Acquire()
wipe(private.pendingChunks[player])
for day, hash in pairs(chunks) do
if currentChunks[day] ~= hash then
tinsert(private.pendingChunks[player], day)
end
end
local requestDay = private.GetNextPendingChunk(player)
if requestDay then
Log.Info("Requesting transactions data (%s, %s, %s, %s)", player, requestDay, tostring(currentChunks[requestDay]), chunks[requestDay])
private.GetPlayerData(player, requestDay)
else
Log.Info("All chunks are up to date (%s)", player)
private.accountStatus[account] = "SYNCED"
end
end
function private.GetPlayerData(player, requestDay)
local account = private.accountLookup[player]
private.accountStatus[account] = "GET_PLAYER_DATA"
Sync.CallRPC("ACCOUNTING_GET_PLAYER_DATA", player, private.RPCGetDataResultHandler, requestDay)
end
function private.RPCGetData(day)
local player = UnitName("player")
wipe(private.dataTemp)
TSM.Accounting.Transactions.GetSyncData(player, day, private.dataTemp)
return player, day, private.dataTemp
end
function private.RPCGetDataResultHandler(player, day, data)
local account = player and private.accountLookup[player]
if not account then
-- request timed out, so try again from the start
Log.Warn("Getting transactions data timed out")
private.QueueRetriesByStatus("GET_PLAYER_DATA")
return
elseif #data % 9 ~= 0 then
-- invalid data - just silently give up
Log.Warn("Got invalid transactions data")
return
end
assert(private.accountStatus[account] == "GET_PLAYER_DATA")
Log.Info("Received transactions data (%s, %s, %s)", player, day, #data)
TSM.Accounting.Transactions.HandleSyncedData(player, day, data)
local requestDay = private.GetNextPendingChunk(player)
if requestDay then
-- request the next chunk
Log.Info("Requesting transactions data (%s, %s)", player, requestDay)
private.GetPlayerData(player, requestDay)
else
-- request chunks again to check for other chunks we need to sync
private.GetPlayerChunks(player)
end
end
function private.RPCChangeNotification(player)
if private.accountStatus[private.accountLookup[player]] == "SYNCED" then
-- request the player hash
Log.Info("Got change notification - requesting player hash")
private.GetPlayerHash(player)
else
Log.Info("Got change notification - dropping (%s)", tostring(private.accountStatus[private.accountLookup[player]]))
end
end
function private.RPCChangeNotificationResultHandler()
-- nop
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.ConnectionChangedHandler(account, player, connected)
if connected then
private.accountLookup[player] = account
private.GetPlayerHash(player)
else
private.accountLookup[player] = nil
private.accountStatus[account] = nil
if private.pendingChunks[player] then
TempTable.Release(private.pendingChunks[player])
private.pendingChunks[player] = nil
end
end
end
function private.GetNextPendingChunk(player)
if not private.pendingChunks[player] then
return nil
end
local result = tremove(private.pendingChunks[player])
if not result then
TempTable.Release(private.pendingChunks[player])
private.pendingChunks[player] = nil
end
return result
end
function private.QueueRetriesByStatus(statusFilter)
for player, account in pairs(private.accountLookup) do
if private.accountStatus[account] == statusFilter then
private.QueueRetryByPlayer(player)
end
end
end
function private.QueueRetryByPlayer(player)
local account = private.accountLookup[player]
Log.Info("Retrying (%s, %s, %s)", player, account, private.accountStatus[account])
private.accountStatus[account] = "RETRY"
Delay.AfterTime(RETRY_DELAY, private.RetryGetPlayerHashRPC)
end
function private.RetryGetPlayerHashRPC()
for player, account in pairs(private.accountLookup) do
if private.accountStatus[account] == "RETRY" then
private.GetPlayerHash(player)
end
end
end
function private.NotifyChange()
for player, account in pairs(private.accountLookup) do
if private.accountStatus[account] == "SYNCED" then
-- notify the other account that our data has changed and request the other account's latest hash ourselves
private.GetPlayerHash(player)
Sync.CallRPC("ACCOUNTING_CHANGE_NOTIFICATION", player, private.RPCChangeNotificationResultHandler, UnitName("player"))
end
end
end

View File

@ -0,0 +1,165 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Trade = TSM.Accounting:NewPackage("Trade")
local L = TSM.Include("Locale").GetTable()
local Event = TSM.Include("Util.Event")
local TempTable = TSM.Include("Util.TempTable")
local Money = TSM.Include("Util.Money")
local ItemString = TSM.Include("Util.ItemString")
local Wow = TSM.Include("Util.Wow")
local ItemInfo = TSM.Include("Service.ItemInfo")
local private = {
tradeInfo = nil,
popupContext = nil,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Trade.OnInitialize()
Event.Register("TRADE_ACCEPT_UPDATE", private.OnAcceptUpdate)
Event.Register("UI_INFO_MESSAGE", private.OnChatMsg)
end
-- ============================================================================
-- Trade Functions
-- ============================================================================
function private.OnAcceptUpdate(_, player, target)
if (player == 1 or target == 1) and not (GetTradePlayerItemLink(7) or GetTradeTargetItemLink(7)) then
-- update tradeInfo
private.tradeInfo = { player = {}, target = {} }
private.tradeInfo.player.money = tonumber(GetPlayerTradeMoney())
private.tradeInfo.target.money = tonumber(GetTargetTradeMoney())
private.tradeInfo.target.name = UnitName("NPC")
for i = 1, 6 do
local targetLink = GetTradeTargetItemLink(i)
local _, _, targetCount = GetTradeTargetItemInfo(i)
if targetLink then
tinsert(private.tradeInfo.target, { itemString = ItemString.Get(targetLink), count = targetCount })
end
local playerLink = GetTradePlayerItemLink(i)
local _, _, playerCount = GetTradePlayerItemInfo(i)
if playerLink then
tinsert(private.tradeInfo.player, { itemString = ItemString.Get(playerLink), count = playerCount })
end
end
else
private.tradeInfo = nil
end
end
function private.OnChatMsg(_, msg)
if not TSM.db.global.accountingOptions.trackTrades then
return
end
if msg == LE_GAME_ERR_TRADE_COMPLETE and private.tradeInfo then
-- trade went through
local tradeType, itemString, count, money = nil, nil, nil, nil
if private.tradeInfo.player.money > 0 and #private.tradeInfo.player == 0 and private.tradeInfo.target.money == 0 and #private.tradeInfo.target > 0 then
-- player bought items
for i = 1, #private.tradeInfo.target do
local data = private.tradeInfo.target[i]
if not itemString then
itemString = data.itemString
count = data.count
elseif itemString == data.itemString then
count = count + data.count
else
return
end
end
tradeType = "buy"
money = private.tradeInfo.player.money
elseif private.tradeInfo.player.money == 0 and #private.tradeInfo.player > 0 and private.tradeInfo.target.money > 0 and #private.tradeInfo.target == 0 then
-- player sold items
for i = 1, #private.tradeInfo.player do
local data = private.tradeInfo.player[i]
if not itemString then
itemString = data.itemString
count = data.count
elseif itemString == data.itemString then
count = count + data.count
else
return
end
end
tradeType = "sale"
money = private.tradeInfo.target.money
end
if not tradeType or not itemString or not count then
return
end
local insertInfo = TempTable.Acquire()
insertInfo.type = tradeType
insertInfo.itemString = itemString
insertInfo.price = money / count
insertInfo.count = count
insertInfo.name = private.tradeInfo.target.name
local gotText, gaveText = nil, nil
if tradeType == "buy" then
gotText = format("%sx%d", ItemInfo.GetLink(itemString), count)
gaveText = Money.ToString(money)
elseif tradeType == "sale" then
gaveText = format("%sx%d", ItemInfo.GetLink(itemString), count)
gotText = Money.ToString(money)
else
error("Invalid tradeType: "..tostring(tradeType))
end
if TSM.db.global.accountingOptions.autoTrackTrades then
private.DoInsert(insertInfo)
TempTable.Release(insertInfo)
else
if private.popupContext then
-- popup already visible so ignore this
TempTable.Release(insertInfo)
return
end
private.popupContext = insertInfo
if not StaticPopupDialogs["TSMAccountingOnTrade"] then
StaticPopupDialogs["TSMAccountingOnTrade"] = {
button1 = YES,
button2 = NO,
timeout = 0,
whileDead = true,
hideOnEscape = true,
OnAccept = function()
private.DoInsert(private.popupContext)
TempTable.Release(private.popupContext)
private.popupContext = nil
end,
OnCancel = function()
TempTable.Release(private.popupContext)
private.popupContext = nil
end,
}
end
StaticPopupDialogs["TSMAccountingOnTrade"].text = format(L["TSM detected that you just traded %s to %s in return for %s. Would you like Accounting to store a record of this trade?"], gaveText, insertInfo.name, gotText)
Wow.ShowStaticPopupDialog("TSMAccountingOnTrade")
end
end
end
function private.DoInsert(info)
if info.type == "sale" then
TSM.Accounting.Transactions.InsertTradeSale(info.itemString, info.count, info.price, info.name)
elseif info.type == "buy" then
TSM.Accounting.Transactions.InsertTradeBuy(info.itemString, info.count, info.price, info.name)
else
error("Unknown type: "..tostring(info.type))
end
end

View File

@ -0,0 +1,825 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Transactions = TSM.Accounting:NewPackage("Transactions")
local L = TSM.Include("Locale").GetTable()
local Database = TSM.Include("Util.Database")
local TempTable = TSM.Include("Util.TempTable")
local CSV = TSM.Include("Util.CSV")
local Math = TSM.Include("Util.Math")
local String = TSM.Include("Util.String")
local Log = TSM.Include("Util.Log")
local Table = TSM.Include("Util.Table")
local ItemString = TSM.Include("Util.ItemString")
local Theme = TSM.Include("Util.Theme")
local CustomPrice = TSM.Include("Service.CustomPrice")
local ItemInfo = TSM.Include("Service.ItemInfo")
local Inventory = TSM.Include("Service.Inventory")
local Settings = TSM.Include("Service.Settings")
local Threading = TSM.Include("Service.Threading")
local private = {
db = nil,
dbSummary = nil,
dataChanged = false,
baseStatsQuery = nil,
statsQuery = nil,
baseStatsMinTimeQuery = nil,
statsMinTimeQuery = nil,
syncHashesThread = nil,
isSyncHashesThreadRunning = false,
syncHashDayCache = {},
syncHashDayCacheIsInvalid = {},
pendingSyncHashCharacters = {},
}
local OLD_CSV_KEYS = {
sale = { "itemString", "stackSize", "quantity", "price", "buyer", "player", "time", "source" },
buy = { "itemString", "stackSize", "quantity", "price", "seller", "player", "time", "source" },
}
local CSV_KEYS = { "itemString", "stackSize", "quantity", "price", "otherPlayer", "player", "time", "source" }
local COMBINE_TIME_THRESHOLD = 300 -- group transactions within 5 minutes together
local MAX_CSV_RECORDS = 55000 -- the max number of records we can store without WoW corrupting the SV file
local TRIMMED_CSV_RECORDS = 50000 -- how many records to trim to if we're over the limit (so we don't trim every time)
local SECONDS_PER_DAY = 24 * 60 * 60
local SYNC_FIELDS = { "type", "itemString", "stackSize", "quantity", "price", "otherPlayer", "time", "source", "saveTime" }
-- ============================================================================
-- Module Functions
-- ============================================================================
function Transactions.OnInitialize()
if TSM.db.realm.internalData.accountingTrimmed.sales then
Log.PrintfUser(L["%sIMPORTANT:|r When Accounting data was last saved for this realm, it was too big for WoW to handle, so old data was automatically trimmed in order to avoid corruption of the saved variables. The last %s of sale data has been preserved."], Theme.GetFeedbackColor("RED"):GetTextColorPrefix(), SecondsToTime(time() - TSM.db.realm.internalData.accountingTrimmed.sales))
TSM.db.realm.internalData.accountingTrimmed.sales = nil
end
if TSM.db.realm.internalData.accountingTrimmed.buys then
Log.PrintfUser(L["%sIMPORTANT:|r When Accounting data was last saved for this realm, it was too big for WoW to handle, so old data was automatically trimmed in order to avoid corruption of the saved variables. The last %s of purchase data has been preserved."], Theme.GetFeedbackColor("RED"):GetTextColorPrefix(), SecondsToTime(time() - TSM.db.realm.internalData.accountingTrimmed.buys))
TSM.db.realm.internalData.accountingTrimmed.buys = nil
end
private.db = Database.NewSchema("TRANSACTIONS_LOG")
:AddStringField("baseItemString")
:AddStringField("type")
:AddStringField("itemString")
:AddNumberField("stackSize")
:AddNumberField("quantity")
:AddNumberField("price")
:AddStringField("otherPlayer")
:AddStringField("player")
:AddNumberField("time")
:AddStringField("source")
:AddNumberField("saveTime")
:AddIndex("baseItemString")
:AddIndex("time")
:Commit()
private.db:BulkInsertStart()
private.LoadData("sale", TSM.db.realm.internalData.csvSales, TSM.db.realm.internalData.saveTimeSales)
private.LoadData("buy", TSM.db.realm.internalData.csvBuys, TSM.db.realm.internalData.saveTimeBuys)
private.db:BulkInsertEnd()
private.dbSummary = Database.NewSchema("TRANSACTIONS_SUMMARY")
:AddUniqueStringField("itemString")
:AddNumberField("sold")
:AddNumberField("avgSellPrice")
:AddNumberField("bought")
:AddNumberField("avgBuyPrice")
:AddNumberField("avgProfit")
:AddNumberField("totalProfit")
:AddNumberField("profitPct")
:Commit()
private.baseStatsQuery = private.db:NewQuery()
:Select("quantity", "price")
:Equal("type", Database.BoundQueryParam())
:Equal("baseItemString", Database.BoundQueryParam())
:NotEqual("source", "Vendor")
private.statsQuery = private.db:NewQuery()
:Select("quantity", "price")
:Equal("type", Database.BoundQueryParam())
:Equal("baseItemString", Database.BoundQueryParam())
:Equal("itemString", Database.BoundQueryParam())
:NotEqual("source", "Vendor")
private.baseStatsMinTimeQuery = private.db:NewQuery()
:Select("quantity", "price")
:Equal("type", Database.BoundQueryParam())
:Equal("baseItemString", Database.BoundQueryParam())
:GreaterThanOrEqual("time", Database.BoundQueryParam())
:NotEqual("source", "Vendor")
private.statsMinTimeQuery = private.db:NewQuery()
:Select("quantity", "price")
:Equal("type", Database.BoundQueryParam())
:Equal("baseItemString", Database.BoundQueryParam())
:Equal("itemString", Database.BoundQueryParam())
:GreaterThanOrEqual("time", Database.BoundQueryParam())
:NotEqual("source", "Vendor")
private.syncHashesThread = Threading.New("TRANSACTIONS_SYNC_HASHES", private.SyncHashesThread)
Inventory.RegisterCallback(private.InventoryCallback)
end
function Transactions.OnDisable()
if not private.dataChanged then
-- nothing changed, so just keep the previous saved values
return
end
TSM.db.realm.internalData.csvSales, TSM.db.realm.internalData.saveTimeSales, TSM.db.realm.internalData.accountingTrimmed.sales = private.SaveData("sale")
TSM.db.realm.internalData.csvBuys, TSM.db.realm.internalData.saveTimeBuys, TSM.db.realm.internalData.accountingTrimmed.buys = private.SaveData("buy")
end
function Transactions.InsertAuctionSale(itemString, stackSize, price, buyer, timestamp)
private.InsertRecord("sale", itemString, "Auction", stackSize, price, buyer, timestamp)
end
function Transactions.InsertAuctionBuy(itemString, stackSize, price, seller, timestamp)
private.InsertRecord("buy", itemString, "Auction", stackSize, price, seller, timestamp)
end
function Transactions.InsertCODSale(itemString, stackSize, price, buyer, timestamp)
private.InsertRecord("sale", itemString, "COD", stackSize, price, buyer, timestamp)
end
function Transactions.InsertCODBuy(itemString, stackSize, price, seller, timestamp)
private.InsertRecord("buy", itemString, "COD", stackSize, price, seller, timestamp)
end
function Transactions.InsertTradeSale(itemString, stackSize, price, buyer)
private.InsertRecord("sale", itemString, "Trade", stackSize, price, buyer, time())
end
function Transactions.InsertTradeBuy(itemString, stackSize, price, seller)
private.InsertRecord("buy", itemString, "Trade", stackSize, price, seller, time())
end
function Transactions.InsertVendorSale(itemString, stackSize, price)
private.InsertRecord("sale", itemString, "Vendor", stackSize, price, "Merchant", time())
end
function Transactions.InsertVendorBuy(itemString, stackSize, price)
private.InsertRecord("buy", itemString, "Vendor", stackSize, price, "Merchant", time())
end
function Transactions.CreateQuery()
return private.db:NewQuery()
end
function Transactions.RemoveOldData(days)
private.dataChanged = true
private.db:SetQueryUpdatesPaused(true)
local numRecords = private.db:NewQuery()
:LessThan("time", time() - days * SECONDS_PER_DAY)
:DeleteAndRelease()
private.db:SetQueryUpdatesPaused(false)
private.OnItemRecordsChanged("sale")
private.OnItemRecordsChanged("buy")
TSM.Accounting.Sync.OnTransactionsChanged()
return numRecords
end
function Transactions.GetSaleStats(itemString, minTime)
local baseItemString = ItemString.GetBase(itemString)
local isBaseItemString = itemString == baseItemString
local query = nil
if minTime then
if isBaseItemString then
query = private.baseStatsMinTimeQuery:BindParams("sale", baseItemString, minTime)
else
query = private.statsMinTimeQuery:BindParams("sale", baseItemString, itemString, minTime)
end
else
if isBaseItemString then
query = private.baseStatsQuery:BindParams("sale", baseItemString)
else
query = private.statsQuery:BindParams("sale", baseItemString, itemString)
end
end
query:ResetOrderBy()
local totalPrice = query:SumOfProduct("quantity", "price")
local totalNum = query:Sum("quantity")
if not totalNum or totalNum == 0 then
return
end
return totalPrice, totalNum
end
function Transactions.GetBuyStats(itemString, isSmart)
local baseItemString = ItemString.GetBaseFast(itemString)
local isBaseItemString = itemString == baseItemString
local query = nil
if isBaseItemString then
query = private.baseStatsQuery:BindParams("buy", baseItemString)
else
query = private.statsQuery:BindParams("buy", baseItemString, itemString)
end
query:ResetOrderBy()
if isSmart then
local totalQuantity = CustomPrice.GetItemPrice(itemString, "NumInventory") or 0
if totalQuantity == 0 then
return nil, nil
end
query:OrderBy("time", false)
local remainingSmartQuantity = totalQuantity
local priceSum, quantitySum = 0, 0
for _, quantity, price in query:Iterator() do
if remainingSmartQuantity > 0 then
quantity = min(remainingSmartQuantity, quantity)
remainingSmartQuantity = remainingSmartQuantity - quantity
priceSum = priceSum + price * quantity
quantitySum = quantitySum + quantity
end
end
if priceSum == 0 then
return nil, nil
end
return priceSum, quantitySum
else
local quantitySum = query:Sum("quantity")
if not quantitySum then
return nil, nil
end
local priceSum = query:SumOfProduct("quantity", "price")
if priceSum == 0 then
return nil, nil
end
return priceSum, quantitySum
end
end
function Transactions.GetMaxSalePrice(itemString)
local baseItemString = ItemString.GetBase(itemString)
local isBaseItemString = itemString == baseItemString
local query = private.db:NewQuery()
:Select("price")
:Equal("type", "sale")
:NotEqual("source", "Vendor")
:OrderBy("price", false)
if isBaseItemString then
query:Equal("baseItemString", itemString)
else
query:Equal("baseItemString", baseItemString)
:Equal("itemString", itemString)
end
return query:GetFirstResultAndRelease()
end
function Transactions.GetMaxBuyPrice(itemString)
local baseItemString = ItemString.GetBase(itemString)
local isBaseItemString = itemString == baseItemString
local query = private.db:NewQuery()
:Select("price")
:Equal("type", "buy")
:NotEqual("source", "Vendor")
:OrderBy("price", false)
if isBaseItemString then
query:Equal("baseItemString", itemString)
else
query:Equal("baseItemString", baseItemString)
:Equal("itemString", itemString)
end
return query:GetFirstResultAndRelease()
end
function Transactions.GetMinSalePrice(itemString)
local baseItemString = ItemString.GetBase(itemString)
local isBaseItemString = itemString == baseItemString
local query = private.db:NewQuery()
:Select("price")
:Equal("type", "sale")
:NotEqual("source", "Vendor")
:OrderBy("price", true)
if isBaseItemString then
query:Equal("baseItemString", itemString)
else
query:Equal("baseItemString", baseItemString)
:Equal("itemString", itemString)
end
return query:GetFirstResultAndRelease()
end
function Transactions.GetMinBuyPrice(itemString)
local baseItemString = ItemString.GetBase(itemString)
local isBaseItemString = itemString == baseItemString
local query = private.db:NewQuery()
:Select("price")
:Equal("type", "buy")
:NotEqual("source", "Vendor")
:OrderBy("price", true)
if isBaseItemString then
query:Equal("baseItemString", itemString)
else
query:Equal("baseItemString", baseItemString)
:Equal("itemString", itemString)
end
return query:GetFirstResultAndRelease()
end
function Transactions.GetAverageSalePrice(itemString)
local totalPrice, totalNum = Transactions.GetSaleStats(itemString)
if not totalPrice or totalPrice == 0 then
return
end
return Math.Round(totalPrice / totalNum), totalNum
end
function Transactions.GetAverageBuyPrice(itemString, isSmart)
local totalPrice, totalNum = Transactions.GetBuyStats(itemString, isSmart)
return totalPrice and Math.Round(totalPrice / totalNum) or nil
end
function Transactions.GetLastSaleTime(itemString)
local baseItemString = ItemString.GetBase(itemString)
local isBaseItemString = itemString == baseItemString
local query = private.db:NewQuery()
:Select("time")
:Equal("type", "sale")
:NotEqual("source", "Vendor")
:OrderBy("time", false)
if isBaseItemString then
query:Equal("baseItemString", itemString)
else
query:Equal("baseItemString", baseItemString)
:Equal("itemString", itemString)
end
return query:GetFirstResultAndRelease()
end
function Transactions.GetLastBuyTime(itemString)
local baseItemString = ItemString.GetBase(itemString)
local isBaseItemString = itemString == baseItemString
local query = private.db:NewQuery():Select("time")
:Equal("type", "buy")
:NotEqual("source", "Vendor")
:OrderBy("time", false)
if isBaseItemString then
query:Equal("baseItemString", itemString)
else
query:Equal("baseItemString", baseItemString)
:Equal("itemString", itemString)
end
return query:GetFirstResultAndRelease()
end
function Transactions.GetQuantity(itemString, timeFilter, typeFilter)
local baseItemString = ItemString.GetBase(itemString)
local isBaseItemString = itemString == baseItemString
local query = private.db:NewQuery()
:Equal("type", typeFilter)
if isBaseItemString then
query:Equal("baseItemString", itemString)
else
query:Equal("baseItemString", baseItemString)
:Equal("itemString", itemString)
end
if timeFilter then
query:GreaterThan("time", time() - timeFilter)
end
local sum = query:Sum("quantity") or 0
query:Release()
return sum
end
function Transactions.GetAveragePrice(itemString, timeFilter, typeFilter)
local baseItemString = ItemString.GetBase(itemString)
local isBaseItemString = itemString == baseItemString
local query = private.db:NewQuery()
:Select("price", "quantity")
:Equal("type", typeFilter)
if isBaseItemString then
query:Equal("baseItemString", itemString)
else
query:Equal("baseItemString", baseItemString)
:Equal("itemString", itemString)
end
if timeFilter then
query:GreaterThan("time", time() - timeFilter)
end
local avgPrice = 0
local totalQuantity = 0
for _, price, quantity in query:IteratorAndRelease() do
avgPrice = avgPrice + price * quantity
totalQuantity = totalQuantity + quantity
end
return Math.Round(avgPrice / totalQuantity)
end
function Transactions.GetTotalPrice(itemString, timeFilter, typeFilter)
local baseItemString = ItemString.GetBase(itemString)
local isBaseItemString = itemString == baseItemString
local query = private.db:NewQuery()
:Select("price", "quantity")
:Equal("type", typeFilter)
if isBaseItemString then
query:Equal("baseItemString", itemString)
else
query:Equal("baseItemString", baseItemString)
:Equal("itemString", itemString)
end
if timeFilter then
query:GreaterThan("time", time() - timeFilter)
end
local sumPrice = query:SumOfProduct("price", "quantity") or 0
query:Release()
return sumPrice
end
function Transactions.CreateSummaryQuery()
return private.dbSummary:NewQuery()
end
function Transactions.UpdateSummaryData(groupFilter, searchFilter, typeFilter, characterFilter, minTime)
local totalSold = TempTable.Acquire()
local totalSellPrice = TempTable.Acquire()
local totalBought = TempTable.Acquire()
local totalBoughtPrice = TempTable.Acquire()
local items = private.db:NewQuery()
:Select("itemString", "price", "quantity", "type")
:LeftJoin(TSM.Groups.GetItemDBForJoin(), "itemString")
:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
if groupFilter then
items:InTable("groupPath", groupFilter)
end
if searchFilter then
items:Matches("name", String.Escape(searchFilter))
end
if typeFilter then
items:InTable("source", typeFilter)
end
if characterFilter then
items:InTable("player", characterFilter)
end
if minTime then
items:GreaterThan("time", minTime)
end
for _, itemString, price, quantity, recordType in items:IteratorAndRelease() do
if not totalSold[itemString] then
totalSold[itemString] = 0
totalSellPrice[itemString] = 0
totalBought[itemString] = 0
totalBoughtPrice[itemString] = 0
end
if recordType == "sale" then
totalSold[itemString] = totalSold[itemString] + quantity
totalSellPrice[itemString] = totalSellPrice[itemString] + price * quantity
elseif recordType == "buy" then
totalBought[itemString] = totalBought[itemString] + quantity
totalBoughtPrice[itemString] = totalBoughtPrice[itemString] + price * quantity
else
error("Invalid recordType: "..tostring(recordType))
end
end
private.dbSummary:TruncateAndBulkInsertStart()
for itemString, sold in pairs(totalSold) do
if sold > 0 and totalBought[itemString] > 0 then
local totalAvgSellPrice = totalSellPrice[itemString] / totalSold[itemString]
local totalAvgBuyPrice = totalBoughtPrice[itemString] / totalBought[itemString]
local profit = totalAvgSellPrice - totalAvgBuyPrice
local totalProfit = profit * min(totalSold[itemString], totalBought[itemString])
local profitPct = Math.Round(profit * 100 / totalAvgBuyPrice)
private.dbSummary:BulkInsertNewRow(itemString, sold, totalAvgSellPrice, totalBought[itemString], totalAvgBuyPrice, profit, totalProfit, profitPct)
end
end
private.dbSummary:BulkInsertEnd()
TempTable.Release(totalSold)
TempTable.Release(totalSellPrice)
TempTable.Release(totalBought)
TempTable.Release(totalBoughtPrice)
end
function Transactions.GetCharacters(characters)
private.db:NewQuery()
:Distinct("player")
:Select("player")
:AsTable(characters)
:Release()
return characters
end
function Transactions.CanDeleteByUUID(uuid)
return Settings.IsCurrentAccountOwner(private.db:GetRowFieldByUUID(uuid, "player"))
end
function Transactions.RemoveRowByUUID(uuid)
local recordType = private.db:GetRowFieldByUUID(uuid, "type")
local itemString = private.db:GetRowFieldByUUID(uuid, "itemString")
local player = private.db:GetRowFieldByUUID(uuid, "player")
private.db:DeleteRowByUUID(uuid)
if private.syncHashDayCache[player] then
private.syncHashDayCacheIsInvalid[player] = true
end
private.dataChanged = true
private.OnItemRecordsChanged(recordType, itemString)
TSM.Accounting.Sync.OnTransactionsChanged()
end
function Transactions.PrepareSyncHashes(player)
tinsert(private.pendingSyncHashCharacters, player)
if not private.isSyncHashesThreadRunning then
private.isSyncHashesThreadRunning = true
Threading.Start(private.syncHashesThread)
end
end
function Transactions.GetSyncHash(player)
local hashesByDay = Transactions.GetSyncHashByDay(player)
if not hashesByDay then
return
end
return Math.CalculateHash(hashesByDay)
end
function Transactions.GetSyncHashByDay(player)
if not private.syncHashDayCache[player] or private.syncHashDayCacheIsInvalid[player] then
return
end
return private.syncHashDayCache[player]
end
function Transactions.GetSyncData(player, day, result)
local query = private.db:NewQuery()
:Equal("player", player)
:GreaterThanOrEqual("time", day * SECONDS_PER_DAY)
:LessThan("time", (day + 1) * SECONDS_PER_DAY)
for _, row in query:Iterator() do
Table.Append(result, row:GetFields(unpack(SYNC_FIELDS)))
end
query:Release()
end
function Transactions.RemovePlayerDay(player, day)
private.dataChanged = true
private.db:SetQueryUpdatesPaused(true)
local query = private.db:NewQuery()
:Equal("player", player)
:GreaterThanOrEqual("time", day * SECONDS_PER_DAY)
:LessThan("time", (day + 1) * SECONDS_PER_DAY)
for _, uuid in query:UUIDIterator() do
private.db:DeleteRowByUUID(uuid)
end
query:Release()
if private.syncHashDayCache[player] then
private.syncHashDayCacheIsInvalid[player] = true
end
private.db:SetQueryUpdatesPaused(false)
private.OnItemRecordsChanged("sale")
private.OnItemRecordsChanged("buy")
end
function Transactions.HandleSyncedData(player, day, data)
assert(#data % 9 == 0)
private.dataChanged = true
private.db:SetQueryUpdatesPaused(true)
-- remove any prior data for the day
local query = private.db:NewQuery()
:Equal("player", player)
:GreaterThanOrEqual("time", day * SECONDS_PER_DAY)
:LessThan("time", (day + 1) * SECONDS_PER_DAY)
for _, uuid in query:UUIDIterator() do
private.db:DeleteRowByUUID(uuid)
end
query:Release()
if private.syncHashDayCache[player] then
private.syncHashDayCacheIsInvalid[player] = true
end
-- insert the new data
private.db:BulkInsertStart()
for i = 1, #data, 9 do
private.BulkInsertNewRowHelper(player, unpack(data, i, i + 8))
end
private.db:BulkInsertEnd()
private.db:SetQueryUpdatesPaused(false)
private.OnItemRecordsChanged("sale")
private.OnItemRecordsChanged("buy")
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.LoadData(recordType, csvRecords, csvSaveTimes)
local saveTimes = String.SafeSplit(csvSaveTimes, ",")
local decodeContext = CSV.DecodeStart(csvRecords, OLD_CSV_KEYS[recordType]) or CSV.DecodeStart(csvRecords, CSV_KEYS)
if not decodeContext then
Log.Err("Failed to decode %s records", recordType)
private.dataChanged = true
return
end
local saveTimeIndex = 1
for itemString, stackSize, quantity, price, otherPlayer, player, timestamp, source in CSV.DecodeIterator(decodeContext) do
local saveTime = 0
if saveTimes and source == "Auction" then
saveTime = tonumber(saveTimes[saveTimeIndex])
saveTimeIndex = saveTimeIndex + 1
end
private.BulkInsertNewRowHelper(player, recordType, itemString, stackSize, quantity, price, otherPlayer, timestamp, source, saveTime)
end
if not CSV.DecodeEnd(decodeContext) then
Log.Err("Failed to decode %s records", recordType)
private.dataChanged = true
end
private.OnItemRecordsChanged(recordType)
end
function private.BulkInsertNewRowHelper(player, recordType, itemString, stackSize, quantity, price, otherPlayer, timestamp, source, saveTime)
itemString = ItemString.Get(itemString)
local baseItemString = ItemString.GetBaseFast(itemString)
stackSize = tonumber(stackSize)
quantity = tonumber(quantity)
price = tonumber(price)
timestamp = tonumber(timestamp)
if itemString and stackSize and quantity and price and otherPlayer and player and timestamp and source then
local newTimestamp = floor(timestamp)
if newTimestamp ~= timestamp then
-- make sure all timestamps are stored as integers
private.dataChanged = true
timestamp = newTimestamp
end
local newPrice = floor(price)
if newPrice ~= price then
-- make sure all prices are stored as integers
private.dataChanged = true
price = newPrice
end
private.db:BulkInsertNewRowFast11(baseItemString, recordType, itemString, stackSize, quantity, price, otherPlayer, player, timestamp, source, saveTime)
else
private.dataChanged = true
end
end
function private.SaveData(recordType)
local numRecords = private.db:NewQuery()
:Equal("type", recordType)
:CountAndRelease()
if numRecords > MAX_CSV_RECORDS then
local query = private.db:NewQuery()
:Equal("type", recordType)
:OrderBy("time", false)
local count = 0
local saveTimes = {}
local shouldTrim = query:Count() > MAX_CSV_RECORDS
local lastTime = nil
local encodeContext = CSV.EncodeStart(CSV_KEYS)
for _, row in query:Iterator() do
if not shouldTrim or count <= TRIMMED_CSV_RECORDS then
-- add the save time
local saveTime = row:GetField("saveTime")
saveTime = saveTime ~= 0 and saveTime or time()
if row:GetField("source") == "Auction" then
tinsert(saveTimes, saveTime)
end
-- update the time we're trimming to
if shouldTrim then
lastTime = row:GetField("time")
end
-- add to our list of CSV lines
CSV.EncodeAddRowData(encodeContext, row)
end
count = count + 1
end
query:Release()
return CSV.EncodeEnd(encodeContext), table.concat(saveTimes, ","), lastTime
else
local saveTimes = {}
local encodeContext = CSV.EncodeStart(CSV_KEYS)
for _, _, rowRecordType, itemString, stackSize, quantity, price, otherPlayer, player, timestamp, source, saveTime in private.db:RawIterator() do
if rowRecordType == recordType then
-- add the save time
if source == "Auction" then
tinsert(saveTimes, saveTime ~= 0 and saveTime or time())
end
-- add to our list of CSV lines
CSV.EncodeAddRowDataRaw(encodeContext, itemString, stackSize, quantity, price, otherPlayer, player, timestamp, source)
end
end
return CSV.EncodeEnd(encodeContext), table.concat(saveTimes, ","), nil
end
end
function private.InsertRecord(recordType, itemString, source, stackSize, price, otherPlayer, timestamp)
private.dataChanged = true
assert(itemString and source and stackSize and price and otherPlayer and timestamp)
timestamp = floor(timestamp)
local baseItemString = ItemString.GetBase(itemString)
local player = UnitName("player")
local matchingRow = private.db:NewQuery()
:Equal("type", recordType)
:Equal("itemString", itemString)
:Equal("baseItemString", baseItemString)
:Equal("stackSize", stackSize)
:Equal("source", source)
:Equal("price", price)
:Equal("player", player)
:Equal("otherPlayer", otherPlayer)
:GreaterThan("time", timestamp - COMBINE_TIME_THRESHOLD)
:LessThan("time", timestamp + COMBINE_TIME_THRESHOLD)
:Equal("saveTime", 0)
:GetFirstResultAndRelease()
if matchingRow then
matchingRow:SetField("quantity", matchingRow:GetField("quantity") + stackSize)
matchingRow:Update()
matchingRow:Release()
else
private.db:NewRow()
:SetField("type", recordType)
:SetField("itemString", itemString)
:SetField("baseItemString", baseItemString)
:SetField("stackSize", stackSize)
:SetField("quantity", stackSize)
:SetField("price", price)
:SetField("otherPlayer", otherPlayer)
:SetField("player", player)
:SetField("time", timestamp)
:SetField("source", source)
:SetField("saveTime", 0)
:Create()
end
if private.syncHashDayCache[player] then
private.syncHashDayCacheIsInvalid[player] = true
end
private.OnItemRecordsChanged(recordType, itemString)
TSM.Accounting.Sync.OnTransactionsChanged()
end
function private.OnItemRecordsChanged(recordType, itemString)
if recordType == "sale" then
CustomPrice.OnSourceChange("AvgSell", itemString)
CustomPrice.OnSourceChange("MaxSell", itemString)
CustomPrice.OnSourceChange("MinSell", itemString)
CustomPrice.OnSourceChange("NumExpires", itemString)
elseif recordType == "buy" then
CustomPrice.OnSourceChange("AvgBuy", itemString)
CustomPrice.OnSourceChange("MaxBuy", itemString)
CustomPrice.OnSourceChange("MinBuy", itemString)
else
error("Invalid recordType: "..tostring(recordType))
end
end
function private.SyncHashesThread(otherPlayer)
private.CalculateSyncHashesThreaded(UnitName("player"))
while #private.pendingSyncHashCharacters > 0 do
local player = tremove(private.pendingSyncHashCharacters, 1)
private.CalculateSyncHashesThreaded(player)
end
private.isSyncHashesThreadRunning = false
end
function private.CalculateSyncHashesThreaded(player)
if private.syncHashDayCache[player] and not private.syncHashDayCacheIsInvalid[player] then
Log.Info("Sync hashes for player (%s) are already up to date", player)
return
end
private.syncHashDayCache[player] = private.syncHashDayCache[player] or {}
local result = private.syncHashDayCache[player]
wipe(result)
private.syncHashDayCacheIsInvalid[player] = true
while true do
local aborted = false
local query = private.db:NewQuery()
:Equal("player", player)
:OrderBy("time", false)
:OrderBy("itemString", true)
Threading.GuardDatabaseQuery(query)
for _, row in query:Iterator(true) do
local rowHash = row:CalculateHash(SYNC_FIELDS)
local day = floor(row:GetField("time") / SECONDS_PER_DAY)
result[day] = Math.CalculateHash(rowHash, result[day])
Threading.Yield()
if query:IsIteratorAborted() then
Log.Warn("Iterator was aborted for player (%s), will retry", player)
aborted = true
end
end
Threading.UnguardDatabaseQuery(query)
query:Release()
if not aborted then
break
end
end
private.syncHashDayCacheIsInvalid[player] = nil
Log.Info("Updated sync hashes for player (%s)", player)
end
function private.InventoryCallback()
CustomPrice.OnSourceChange("SmartAvgBuy")
end

View File

@ -0,0 +1,571 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local AuctionDB = TSM:NewPackage("AuctionDB")
local L = TSM.Include("Locale").GetTable()
local Event = TSM.Include("Util.Event")
local CSV = TSM.Include("Util.CSV")
local Table = TSM.Include("Util.Table")
local Math = TSM.Include("Util.Math")
local Log = TSM.Include("Util.Log")
local ItemString = TSM.Include("Util.ItemString")
local Wow = TSM.Include("Util.Wow")
local Threading = TSM.Include("Service.Threading")
local ItemInfo = TSM.Include("Service.ItemInfo")
local CustomPrice = TSM.Include("Service.CustomPrice")
local AuctionScan = TSM.Include("Service.AuctionScan")
local private = {
region = nil,
realmAppData = {
scanTime = nil,
data = {},
itemOffset = {},
fieldOffset = {},
numFields = nil,
},
regionData = nil,
regionUpdateTime = nil,
scanRealmData = {},
scanRealmTime = nil,
scanThreadId = nil,
ahOpen = false,
didScan = false,
auctionScan = nil,
isScanning = false,
}
local CSV_KEYS = { "itemString", "minBuyout", "marketValue", "numAuctions", "quantity", "lastScan" }
-- ============================================================================
-- Module Functions
-- ============================================================================
function AuctionDB.OnInitialize()
private.scanThreadId = Threading.New("AUCTIONDB_SCAN", private.ScanThread)
Threading.SetCallback(private.scanThreadId, private.ScanThreadCleanup)
Event.Register("AUCTION_HOUSE_SHOW", private.OnAuctionHouseShow)
Event.Register("AUCTION_HOUSE_CLOSED", private.OnAuctionHouseClosed)
end
function AuctionDB.OnEnable()
private.region = TSM.GetRegion()
local realmAppData = nil
local appData = TSMAPI.AppHelper and TSMAPI.AppHelper:FetchData("AUCTIONDB_MARKET_DATA") -- get app data from TSM_AppHelper if it's installed
if appData then
for _, info in ipairs(appData) do
local realm, data = unpack(info)
local downloadTime = "?"
-- try switching around "Classic-[US|EU]" to match the addon's "[US|EU]-Classic" format for classic region data
if realm == private.region or gsub(realm, "Classic-%-([A-Z]+)", "%1-Classic") == private.region then
private.regionData, private.regionUpdateTime = private.LoadRegionAppData(data)
downloadTime = SecondsToTime(time() - private.regionUpdateTime).." ago"
elseif TSMAPI.AppHelper:IsCurrentRealm(realm) then
realmAppData = private.ProcessRealmAppData(data)
downloadTime = SecondsToTime(time() - realmAppData.downloadTime).." ago"
end
Log.Info("Got AppData for %s (isCurrent=%s, %s)", realm, tostring(TSMAPI.AppHelper:IsCurrentRealm(realm)), downloadTime)
end
end
-- check if we can load realm data from the app
if realmAppData then
private.realmAppData.scanTime = realmAppData.downloadTime
for i = 2, #realmAppData.fields do
private.realmAppData.fieldOffset[realmAppData.fields[i]] = i - 1
end
private.realmAppData.numFields = #realmAppData.fields - 1
local numRawFields = #realmAppData.fields
local nextItmeOffset, nextDataOffset = 0, 1
for _, data in ipairs(realmAppData.data) do
for i = 1, numRawFields do
local value = data[i]
if i == 1 then
-- item string must be the first field
local itemString = nil
if type(value) == "number" then
itemString = "i:"..value
else
itemString = gsub(value, ":0:", "::")
end
itemString = ItemString.Get(itemString)
private.realmAppData.itemOffset[itemString] = nextItmeOffset
nextItmeOffset = nextItmeOffset + 1
else
private.realmAppData.data[nextDataOffset] = value
nextDataOffset = nextDataOffset + 1
end
end
end
end
for itemString in pairs(private.realmAppData.itemOffset) do
ItemInfo.FetchInfo(itemString)
end
if TSM.db.factionrealm.internalData.auctionDBScanTime > 0 then
private.LoadSVRealmData()
end
if not private.realmAppData.numFields and not next(private.scanRealmData) then
Log.PrintfUser(L["TSM doesn't currently have any AuctionDB pricing data for your realm. We recommend you download the TSM Desktop Application from %s to automatically update your AuctionDB data (and auto-backup your TSM settings)."], Log.ColorUserAccentText("https://tradeskillmaster.com"))
end
CustomPrice.OnSourceChange("DBMarket")
CustomPrice.OnSourceChange("DBMinBuyout")
CustomPrice.OnSourceChange("DBHistorical")
CustomPrice.OnSourceChange("DBRegionMinBuyoutAvg")
CustomPrice.OnSourceChange("DBRegionMarketAvg")
CustomPrice.OnSourceChange("DBRegionHistorical")
CustomPrice.OnSourceChange("DBRegionSaleAvg")
CustomPrice.OnSourceChange("DBRegionSaleRate")
CustomPrice.OnSourceChange("DBRegionSoldPerDay")
collectgarbage()
end
function AuctionDB.OnDisable()
if not private.didScan then
return
end
local encodeContext = CSV.EncodeStart(CSV_KEYS)
for itemString, data in pairs(private.scanRealmData) do
CSV.EncodeAddRowDataRaw(encodeContext, itemString, data.minBuyout, data.marketValue, data.numAuctions, data.quantity, data.lastScan)
end
TSM.db.factionrealm.internalData.csvAuctionDBScan = CSV.EncodeEnd(encodeContext)
TSM.db.factionrealm.internalData.auctionDBScanHash = Math.CalculateHash(TSM.db.factionrealm.internalData.csvAuctionDBScan)
end
function AuctionDB.GetAppDataUpdateTimes()
return private.realmAppData.scanTime or 0, private.regionUpdateTime or 0
end
function AuctionDB.GetLastCompleteScanTime()
local result = private.didScan and (private.scanRealmTime or 0) or (private.realmAppData.scanTime or 0)
return result ~= 0 and result or nil
end
function AuctionDB.LastScanIteratorThreaded()
local itemNumAuctions = Threading.AcquireSafeTempTable()
local itemMinBuyout = Threading.AcquireSafeTempTable()
local baseItems = Threading.AcquireSafeTempTable()
local lastScanTime = AuctionDB.GetLastCompleteScanTime()
for itemString, data in pairs(private.didScan and private.scanRealmData or private.realmAppData.itemOffset) do
if not private.didScan or data.lastScan >= lastScanTime then
itemString = ItemString.Get(itemString)
local baseItemString = ItemString.GetBaseFast(itemString)
if baseItemString ~= itemString then
baseItems[baseItemString] = true
end
local numAuctions, minBuyout = nil, nil
if private.didScan then
numAuctions = data.numAuctions
minBuyout = data.minBuyout
else
numAuctions = private.realmAppData.data[data * private.realmAppData.numFields + private.realmAppData.fieldOffset.numAuctions]
minBuyout = private.realmAppData.data[data * private.realmAppData.numFields + private.realmAppData.fieldOffset.minBuyout]
end
itemNumAuctions[itemString] = (itemNumAuctions[itemString] or 0) + numAuctions
if minBuyout and minBuyout > 0 then
itemMinBuyout[itemString] = min(itemMinBuyout[itemString] or math.huge, minBuyout)
end
end
Threading.Yield()
end
-- remove the base items since they would be double-counted with the specific variants
for itemString in pairs(baseItems) do
itemNumAuctions[itemString] = nil
itemMinBuyout[itemString] = nil
end
Threading.ReleaseSafeTempTable(baseItems)
-- convert the remaining items into a list
local itemList = Threading.AcquireSafeTempTable()
itemList.numAuctions = itemNumAuctions
itemList.minBuyout = itemMinBuyout
for itemString in pairs(itemNumAuctions) do
tinsert(itemList, itemString)
end
return Table.Iterator(itemList, private.LastScanIteratorHelper, itemList, private.LastScanIteratorCleanup)
end
function AuctionDB.GetRealmItemData(itemString, key)
local realmData = nil
if private.didScan and (key == "minBuyout" or key == "numAuctions" or key == "lastScan") then
-- always use scanRealmData for minBuyout/numAuctions/lastScan if we've done a scan
realmData = private.scanRealmData
elseif private.realmAppData.numFields then
-- use app data
return private.GetRealmAppItemDataHelper(private.realmAppData, key, itemString)
else
realmData = private.scanRealmData
end
return private.GetItemDataHelper(realmData, key, itemString)
end
function AuctionDB.GetRegionItemData(itemString, key)
return private.GetRegionItemDataHelper(private.regionData, key, itemString)
end
function AuctionDB.GetRegionSaleInfo(itemString, key)
-- need to divide the result by 100
local result = private.GetRegionItemDataHelper(private.regionData, key, itemString)
return result and (result / 100) or nil
end
function AuctionDB.RunScan()
if private.isScanning then
return
end
if not private.ahOpen then
Log.PrintUser(L["ERROR: The auction house must be open in order to do a scan."])
return
end
local canScan, canGetAllScan = CanSendAuctionQuery()
if not canScan then
Log.PrintUser(L["ERROR: The AH is currently busy with another scan. Please try again once that scan has completed."])
return
elseif not canGetAllScan then
Log.PrintUser(L["ERROR: A full AH scan has recently been performed and is on cooldown. Log out to reset this cooldown."])
return
end
if not TSM.UI.AuctionUI.StartingScan("FULL_SCAN") then
return
end
Log.PrintUser(L["Starting full AH scan. Please note that this scan may cause your game client to lag or crash. This scan generally takes 1-2 minutes."])
Threading.Start(private.scanThreadId)
private.isScanning = true
end
-- ============================================================================
-- Scan Thread
-- ============================================================================
function private.ScanThread()
assert(not private.auctionScan)
-- run the scan
local auctionScan = AuctionScan.GetManager()
:SetResolveSellers(false)
private.auctionScan = auctionScan
local query = auctionScan:NewQuery()
:SetGetAll(true)
if not auctionScan:ScanQueriesThreaded() then
Log.PrintUser(L["Failed to run full AH scan."])
return
end
-- process the results
Log.PrintfUser(L["Processing scan results..."])
wipe(private.scanRealmData)
private.scanRealmTime = time()
TSM.db.factionrealm.internalData.auctionDBScanTime = time()
TSM.db.factionrealm.internalData.csvAuctionDBScan = ""
local numScannedAuctions = 0
local subRows = Threading.AcquireSafeTempTable()
local subRowSortValue = Threading.AcquireSafeTempTable()
local itemBuyouts = Threading.AcquireSafeTempTable()
for baseItemString, row in query:BrowseResultsIterator() do
wipe(subRows)
wipe(subRowSortValue)
for _, subRow in row:SubRowIterator() do
local _, itemBuyout = subRow:GetBuyouts()
tinsert(subRows, subRow)
subRowSortValue[subRow] = itemBuyout
end
Table.SortWithValueLookup(subRows, subRowSortValue, false, true)
wipe(itemBuyouts)
for _, subRow in ipairs(subRows) do
local _, itemBuyout = subRow:GetBuyouts()
local quantity, numAuctions = subRow:GetQuantities()
numScannedAuctions = numScannedAuctions + numAuctions
for _ = 1, numAuctions do
private.ProcessScanResultItem(baseItemString, itemBuyout, quantity)
end
if itemBuyout > 0 then
for _ = 1, quantity * numAuctions do
tinsert(itemBuyouts, itemBuyout)
end
end
end
local data = private.scanRealmData[baseItemString]
data.marketValue = private.CalculateItemMarketValue(itemBuyouts, data.quantity)
assert(data.minBuyout == 0 or data.marketValue >= data.minBuyout)
Threading.Yield()
end
Threading.ReleaseSafeTempTable(subRows)
Threading.ReleaseSafeTempTable(subRowSortValue)
Threading.ReleaseSafeTempTable(itemBuyouts)
Threading.Yield()
collectgarbage()
Log.PrintfUser(L["Completed full AH scan (%d auctions)!"], numScannedAuctions)
private.didScan = true
CustomPrice.OnSourceChange("DBMinBuyout")
end
function private.ScanThreadCleanup()
private.isScanning = false
if private.auctionScan then
private.auctionScan:Release()
private.auctionScan = nil
end
TSM.UI.AuctionUI.EndedScan("FULL_SCAN")
end
function private.ProcessScanResultItem(itemString, itemBuyout, stackSize)
private.scanRealmData[itemString] = private.scanRealmData[itemString] or { numAuctions = 0, quantity = 0, minBuyout = 0 }
local data = private.scanRealmData[itemString]
data.lastScan = time()
if itemBuyout > 0 then
data.minBuyout = min(data.minBuyout > 0 and data.minBuyout or math.huge, itemBuyout)
data.quantity = data.quantity + stackSize
end
data.numAuctions = data.numAuctions + 1
end
function private.CalculateItemMarketValue(itemBuyouts, quantity)
assert(#itemBuyouts == quantity)
if quantity == 0 then
return 0
end
-- calculate the average of the lowest 15-30% of auctions
local total, num = 0, 0
local lowBucketNum = max(floor(quantity * 0.15), 1)
local midBucketNum = max(floor(quantity * 0.30), 1)
local prevItemBuyout = 0
for i = 1, midBucketNum do
local itemBuyout = itemBuyouts[i]
if num < lowBucketNum or itemBuyout < prevItemBuyout * 1.2 then
num = num + 1
total = total + itemBuyout
end
prevItemBuyout = itemBuyout
end
local avg = total / num
-- calculate the stdev of the auctions we used in the average
local stdev = nil
if num > 1 then
local stdevSum = 0
for i = 1, num do
local itemBuyout = itemBuyouts[i]
stdevSum = stdevSum + (itemBuyout - avg) ^ 2
end
stdev = sqrt(stdevSum / (num - 1))
else
stdev = 0
end
-- calculate the market value as the average of all data within 1.5 stdev of our previous average
local minItemBuyout = avg - stdev * 1.5
local maxItemBuyout = avg + stdev * 1.5
local avgTotal, avgCount = 0, 0
for i = 1, num do
local itemBuyout = itemBuyouts[i]
if itemBuyout >= minItemBuyout and itemBuyout <= maxItemBuyout then
avgTotal = avgTotal + itemBuyout
avgCount = avgCount + 1
end
end
return avgTotal > 0 and floor(avgTotal / avgCount) or 0
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.LoadSVRealmData()
local decodeContext = CSV.DecodeStart(TSM.db.factionrealm.internalData.csvAuctionDBScan, CSV_KEYS)
if not decodeContext then
Log.Err("Failed to decode records")
return
end
for itemString, minBuyout, marketValue, numAuctions, quantity, lastScan in CSV.DecodeIterator(decodeContext) do
private.scanRealmData[itemString] = {
minBuyout = tonumber(minBuyout),
marketValue = tonumber(marketValue),
numAuctions = tonumber(numAuctions),
quantity = tonumber(quantity),
lastScan = tonumber(lastScan),
}
end
if not CSV.DecodeEnd(decodeContext) then
Log.Err("Failed to decode records")
end
private.scanRealmTime = TSM.db.factionrealm.internalData.auctionDBScanTime
end
function private.ProcessRealmAppData(rawData)
if #rawData < 3500000 then
-- we can safely just use loadstring() for strings below 3.5M
return assert(loadstring(rawData)())
end
-- load the data in chunks
local leader, itemData, trailer = strmatch(rawData, "^(.+)data={({.+})}(.+)$")
local resultData = {}
local chunkStart, chunkEnd, nextChunkStart = 1, nil, nil
while chunkStart do
chunkEnd, nextChunkStart = strfind(itemData, "},{", chunkStart + 3400000)
local chunkData = assert(loadstring("return {"..strsub(itemData, chunkStart, chunkEnd).."}")())
for _, data in ipairs(chunkData) do
tinsert(resultData, data)
end
chunkStart = nextChunkStart
end
__AUCTIONDB_IMPORT_TEMP = resultData
local result = assert(loadstring(leader.."data=__AUCTIONDB_IMPORT_TEMP"..trailer)())
__AUCTIONDB_IMPORT_TEMP = nil
return result
end
function private.LoadRegionAppData(appData)
local metaDataEndIndex, dataStartIndex = strfind(appData, ",data={")
local itemData = strsub(appData, dataStartIndex + 1, -3)
local metaDataStr = strsub(appData, 1, metaDataEndIndex - 1).."}"
local metaData = assert(loadstring(metaDataStr))()
local result = { fieldLookup = {}, itemLookup = {} }
for i, field in ipairs(metaData.fields) do
result.fieldLookup[field] = i
end
for itemString, otherData in gmatch(itemData, "{([^,]+),([^}]+)}") do
if tonumber(itemString) then
itemString = "i:"..itemString
else
itemString = gsub(strsub(itemString, 2, -2), ":0:", "::")
end
result.itemLookup[itemString] = otherData
end
return result, metaData.downloadTime
end
function private.LastScanIteratorHelper(index, itemString, tbl)
return index, itemString, tbl.numAuctions[itemString], tbl.minBuyout[itemString]
end
function private.LastScanIteratorCleanup(tbl)
Threading.ReleaseSafeTempTable(tbl.numAuctions)
Threading.ReleaseSafeTempTable(tbl.minBuyout)
Threading.ReleaseSafeTempTable(tbl)
end
function private.GetItemDataHelper(tbl, key, itemString)
if not itemString or not tbl then
return nil
end
itemString = ItemString.Filter(itemString)
local value = nil
if not tbl[itemString] and not strmatch(itemString, "^[ip]:[0-9]+$") then
-- for items with random enchants or for pets, get data for the base item
itemString = private.GetBaseItemHelper(itemString)
end
if not itemString or not tbl[itemString] then
return nil
end
value = tbl[itemString][key]
return (value or 0) > 0 and value or nil
end
function private.GetRegionItemDataHelper(tbl, key, itemString)
if not itemString or not tbl then
return nil
end
itemString = ItemString.Filter(itemString)
local fieldIndex = tbl.fieldLookup[key] - 1
assert(fieldIndex and fieldIndex > 0)
local data = tbl.itemLookup[itemString]
if not data and not strmatch(itemString, "^[ip]:[0-9]+$") then
-- for items with random enchants or for pets, get data for the base item
itemString = private.GetBaseItemHelper(itemString)
itemString = ItemString.GetBase(itemString)
if not itemString then
return nil
end
data = tbl.itemLookup[itemString]
end
if type(data) == "string" then
local tblData = {strsplit(",", data)}
for i = 1, #tblData do
tblData[i] = tonumber(tblData[i])
end
tbl.itemLookup[itemString] = tblData
data = tblData
end
if not data then
return nil
end
local value = data[fieldIndex]
return (value or 0) > 0 and value or nil
end
function private.GetRealmAppItemDataHelper(appData, key, itemString)
if not itemString or not appData.numFields then
return nil
elseif key == "lastScan" then
return appData.scanTime
end
itemString = ItemString.Filter(itemString)
if not appData.itemOffset[itemString] and not strmatch(itemString, "^[ip]:[0-9]+$") then
-- for items with random enchants or for pets, get data for the base item
itemString = private.GetBaseItemHelper(itemString)
if not itemString then
return nil
end
end
if not appData.itemOffset[itemString] then
return nil
end
local value = appData.data[appData.itemOffset[itemString] * appData.numFields + appData.fieldOffset[key]]
return (value or 0) > 0 and value or nil
end
function private.GetBaseItemHelper(itemString)
local quality = ItemInfo.GetQuality(itemString)
local itemLevel = ItemInfo.GetItemLevel(itemString)
local classId = ItemInfo.GetClassId(itemString)
if quality and quality >= 2 and itemLevel and itemLevel >= TSM.CONST.MIN_BONUS_ID_ITEM_LEVEL and (classId == LE_ITEM_CLASS_WEAPON or classId == LE_ITEM_CLASS_ARMOR) then
if strmatch(itemString, "^i:[0-9]+:[0-9%-]*:") then
return nil
end
end
return ItemString.GetBaseFast(itemString)
end
function private.OnAuctionHouseShow()
private.ahOpen = true
if not TSM.IsWowClassic() or not select(2, CanSendAuctionQuery()) then
return
elseif (AuctionDB.GetLastCompleteScanTime() or 0) > time() - 60 * 60 * 2 then
-- the most recent scan is from the past 2 hours
return
elseif (TSM.db.factionrealm.internalData.auctionDBScanTime or 0) > time() - 60 * 60 * 24 then
-- this user has contributed a scan within the past 24 hours
return
end
StaticPopupDialogs["TSM_AUCTIONDB_SCAN"] = StaticPopupDialogs["TSM_AUCTIONDB_SCAN"] or {
text = L["TSM does not have recent AuctionDB data. Would you like to run a full AH scan?"],
button1 = YES,
button2 = NO,
timeout = 0,
OnAccept = AuctionDB.RunScan,
}
Wow.ShowStaticPopupDialog("TSM_AUCTIONDB_SCAN")
end
function private.OnAuctionHouseClosed()
private.ahOpen = false
end

View File

@ -0,0 +1,461 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local CancelScan = TSM.Auctioning:NewPackage("CancelScan")
local L = TSM.Include("Locale").GetTable()
local Database = TSM.Include("Util.Database")
local TempTable = TSM.Include("Util.TempTable")
local Log = TSM.Include("Util.Log")
local Threading = TSM.Include("Service.Threading")
local ItemInfo = TSM.Include("Service.ItemInfo")
local AuctionTracking = TSM.Include("Service.AuctionTracking")
local AuctionHouseWrapper = TSM.Include("Service.AuctionHouseWrapper")
local private = {
scanThreadId = nil,
queueDB = nil,
itemList = {},
usedAuctionIndex = {},
subRowsTemp = {},
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function CancelScan.OnInitialize()
-- initialize thread
private.scanThreadId = Threading.New("CANCEL_SCAN", private.ScanThread)
private.queueDB = Database.NewSchema("AUCTIONING_CANCEL_QUEUE")
:AddNumberField("auctionId")
:AddStringField("itemString")
:AddStringField("operationName")
:AddNumberField("bid")
:AddNumberField("buyout")
:AddNumberField("itemBid")
:AddNumberField("itemBuyout")
:AddNumberField("stackSize")
:AddNumberField("duration")
:AddNumberField("numStacks")
:AddNumberField("numProcessed")
:AddNumberField("numConfirmed")
:AddNumberField("numFailed")
:AddIndex("auctionId")
:AddIndex("itemString")
:Commit()
end
function CancelScan.Prepare()
return private.scanThreadId
end
function CancelScan.GetCurrentRow()
return private.queueDB:NewQuery()
:Custom(private.NextProcessRowQueryHelper)
:OrderBy("auctionId", false)
:GetFirstResultAndRelease()
end
function CancelScan.GetStatus()
return TSM.Auctioning.Util.GetQueueStatus(private.queueDB:NewQuery())
end
function CancelScan.DoProcess()
local cancelRow = CancelScan.GetCurrentRow()
local cancelItemString = cancelRow:GetField("itemString")
local query = AuctionTracking.CreateQueryUnsoldItem(cancelItemString)
:Equal("stackSize", cancelRow:GetField("stackSize"))
:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
:Equal("autoBaseItemString", cancelItemString)
:Custom(private.ProcessQueryHelper, cancelRow)
:OrderBy("auctionId", false)
:Select("auctionId", "autoBaseItemString", "currentBid", "buyout")
if not TSM.db.global.auctioningOptions.cancelWithBid then
query:Equal("highBidder", "")
end
local auctionId, itemString, currentBid, buyout = query:GetFirstResultAndRelease()
if auctionId then
local result = nil
if TSM.IsWowClassic() then
private.usedAuctionIndex[itemString..buyout..currentBid..auctionId] = true
CancelAuction(auctionId)
result = true
else
private.usedAuctionIndex[auctionId] = true
result = AuctionHouseWrapper.CancelAuction(auctionId)
end
local isRowDone = cancelRow:GetField("numProcessed") + 1 == cancelRow:GetField("numStacks")
cancelRow:SetField("numProcessed", cancelRow:GetField("numProcessed") + 1)
:Update()
cancelRow:Release()
if result and isRowDone then
-- update the log
TSM.Auctioning.Log.UpdateRowByIndex(auctionId, "state", "CANCELLED")
end
return result, false
end
-- we couldn't find this item, so mark this cancel as failed and we'll try again later
cancelRow:SetField("numProcessed", cancelRow:GetField("numProcessed") + 1)
:Update()
cancelRow:Release()
return false, false
end
function CancelScan.DoSkip()
local cancelRow = CancelScan.GetCurrentRow()
local auctionId = cancelRow:GetField("auctionId")
cancelRow:SetField("numProcessed", cancelRow:GetField("numProcessed") + 1)
:SetField("numConfirmed", cancelRow:GetField("numConfirmed") + 1)
:Update()
cancelRow:Release()
-- update the log
TSM.Auctioning.Log.UpdateRowByIndex(auctionId, "state", "SKIPPED")
end
function CancelScan.HandleConfirm(success, canRetry)
local confirmRow = private.queueDB:NewQuery()
:Custom(private.ConfirmRowQueryHelper)
:OrderBy("auctionId", true)
:GetFirstResultAndRelease()
if not confirmRow then
-- we may have cancelled something outside of TSM
return
end
if canRetry then
assert(not success)
confirmRow:SetField("numFailed", confirmRow:GetField("numFailed") + 1)
end
confirmRow:SetField("numConfirmed", confirmRow:GetField("numConfirmed") + 1)
:Update()
confirmRow:Release()
end
function CancelScan.PrepareFailedCancels()
wipe(private.usedAuctionIndex)
private.queueDB:SetQueryUpdatesPaused(true)
local query = private.queueDB:NewQuery()
:GreaterThan("numFailed", 0)
for _, row in query:Iterator() do
local numFailed, numProcessed, numConfirmed = row:GetFields("numFailed", "numProcessed", "numConfirmed")
assert(numProcessed >= numFailed and numConfirmed >= numFailed)
row:SetField("numFailed", 0)
:SetField("numProcessed", numProcessed - numFailed)
:SetField("numConfirmed", numConfirmed - numFailed)
:Update()
end
query:Release()
private.queueDB:SetQueryUpdatesPaused(false)
end
function CancelScan.Reset()
private.queueDB:Truncate()
wipe(private.usedAuctionIndex)
end
-- ============================================================================
-- Scan Thread
-- ============================================================================
function private.ScanThread(auctionScan, groupList)
auctionScan:SetScript("OnQueryDone", private.AuctionScanOnQueryDone)
-- generate the list of items we want to scan for
wipe(private.itemList)
local processedItems = TempTable.Acquire()
local query = AuctionTracking.CreateQueryUnsold()
:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
:Select("autoBaseItemString")
if not TSM.db.global.auctioningOptions.cancelWithBid then
query:Equal("highBidder", "")
end
for _, itemString in query:Iterator() do
if not processedItems[itemString] and private.CanCancelItem(itemString, groupList) then
tinsert(private.itemList, itemString)
end
processedItems[itemString] = true
end
query:Release()
TempTable.Release(processedItems)
if #private.itemList == 0 then
return
end
TSM.Auctioning.SavedSearches.RecordSearch(groupList, "cancelGroups")
-- run the scan
auctionScan:AddItemListQueriesThreaded(private.itemList)
for _, query2 in auctionScan:QueryIterator() do
query2:AddCustomFilter(private.QueryBuyoutFilter)
end
if not auctionScan:ScanQueriesThreaded() then
Log.PrintUser(L["TSM failed to scan some auctions. Please rerun the scan."])
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.CanCancelItem(itemString, groupList)
local groupPath = TSM.Groups.GetPathByItem(itemString)
if not groupPath or not tContains(groupList, groupPath) then
return false
end
local hasValidOperation, hasInvalidOperation = false, false
for _, operationName, operationSettings in TSM.Operations.GroupOperationIterator("Auctioning", groupPath) do
local isValid = private.IsOperationValid(itemString, operationName, operationSettings)
if isValid == true then
hasValidOperation = true
elseif isValid == false then
hasInvalidOperation = true
else
-- we are ignoring this operation
assert(isValid == nil, "Invalid return value")
end
end
return hasValidOperation and not hasInvalidOperation, itemString
end
function private.IsOperationValid(itemString, operationName, operationSettings)
if not operationSettings.cancelUndercut and not operationSettings.cancelRepost then
-- canceling is disabled, so ignore this operation
TSM.Auctioning.Log.AddEntry(itemString, operationName, "cancelDisabled", "", 0, 0)
return nil
end
local errMsg = nil
local minPrice = TSM.Auctioning.Util.GetPrice("minPrice", operationSettings, itemString)
local normalPrice = TSM.Auctioning.Util.GetPrice("normalPrice", operationSettings, itemString)
local maxPrice = TSM.Auctioning.Util.GetPrice("maxPrice", operationSettings, itemString)
local undercut = TSM.Auctioning.Util.GetPrice("undercut", operationSettings, itemString)
local cancelRepostThreshold = TSM.Auctioning.Util.GetPrice("cancelRepostThreshold", operationSettings, itemString)
if not minPrice then
errMsg = format(L["Did not cancel %s because your minimum price (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.minPrice)
elseif not maxPrice then
errMsg = format(L["Did not cancel %s because your maximum price (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.maxPrice)
elseif not normalPrice then
errMsg = format(L["Did not cancel %s because your normal price (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.normalPrice)
elseif operationSettings.cancelRepost and not cancelRepostThreshold then
errMsg = format(L["Did not cancel %s because your cancel to repost threshold (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.cancelRepostThreshold)
elseif not undercut then
errMsg = format(L["Did not cancel %s because your undercut (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.undercut)
elseif maxPrice < minPrice then
errMsg = format(L["Did not cancel %s because your maximum price (%s) is lower than your minimum price (%s). Check your settings."], ItemInfo.GetLink(itemString), operationSettings.maxPrice, operationSettings.minPrice)
elseif normalPrice < minPrice then
errMsg = format(L["Did not cancel %s because your normal price (%s) is lower than your minimum price (%s). Check your settings."], ItemInfo.GetLink(itemString), operationSettings.normalPrice, operationSettings.minPrice)
end
if errMsg then
if not TSM.db.global.auctioningOptions.disableInvalidMsg then
Log.PrintUser(errMsg)
end
TSM.Auctioning.Log.AddEntry(itemString, operationName, "invalidItemGroup", "", 0, 0)
return false
else
return true
end
end
function private.QueryBuyoutFilter(_, row)
local _, itemBuyout, minItemBuyout = row:GetBuyouts()
return (itemBuyout and itemBuyout == 0) or (minItemBuyout and minItemBuyout == 0)
end
function private.AuctionScanOnQueryDone(_, query)
TSM.Auctioning.Log.SetQueryUpdatesPaused(true)
for itemString in query:ItemIterator() do
local groupPath = TSM.Groups.GetPathByItem(itemString)
if groupPath then
local auctionsDBQuery = AuctionTracking.CreateQueryUnsoldItem(itemString)
:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
:Equal("autoBaseItemString", itemString)
:OrderBy("auctionId", false)
for _, auctionsDBRow in auctionsDBQuery:IteratorAndRelease() do
private.GenerateCancels(auctionsDBRow, itemString, groupPath, query)
end
else
Log.Warn("Item removed from group since start of scan: %s", itemString)
end
end
TSM.Auctioning.Log.SetQueryUpdatesPaused(false)
end
function private.GenerateCancels(auctionsDBRow, itemString, groupPath, query)
local isHandled = false
for _, operationName, operationSettings in TSM.Operations.GroupOperationIterator("Auctioning", groupPath) do
if not isHandled and private.IsOperationValid(itemString, operationName, operationSettings) then
assert(not next(private.subRowsTemp))
TSM.Auctioning.Util.GetFilteredSubRows(query, itemString, operationSettings, private.subRowsTemp)
local handled, logReason, itemBuyout, seller, auctionId = private.GenerateCancel(auctionsDBRow, itemString, operationName, operationSettings, private.subRowsTemp)
wipe(private.subRowsTemp)
if logReason then
seller = seller or ""
auctionId = auctionId or 0
TSM.Auctioning.Log.AddEntry(itemString, operationName, logReason, seller, itemBuyout, auctionId)
end
isHandled = isHandled or handled
end
end
end
function private.GenerateCancel(auctionsDBRow, itemString, operationName, operationSettings, subRows)
local auctionId, stackSize, currentBid, buyout, highBidder, duration = auctionsDBRow:GetFields("auctionId", "stackSize", "currentBid", "buyout", "highBidder", "duration")
local itemBuyout = TSM.IsWowClassic() and floor(buyout / stackSize) or buyout
local itemBid = TSM.IsWowClassic() and floor(currentBid / stackSize) or currentBid
if TSM.IsWowClassic() and operationSettings.matchStackSize and stackSize ~= TSM.Auctioning.Util.GetPrice("stackSize", operationSettings, itemString) then
return false
elseif not TSM.db.global.auctioningOptions.cancelWithBid and highBidder ~= "" then
-- Don't cancel an auction if it has a bid and we're set to not cancel those
return true, "cancelBid", itemBuyout, nil, auctionId
elseif not TSM.IsWowClassic() and C_AuctionHouse.GetCancelCost(auctionId) > GetMoney() then
return true, "cancelNoMoney", itemBuyout, nil, auctionId
end
local lowestAuction = TempTable.Acquire()
if not TSM.Auctioning.Util.GetLowestAuction(subRows, itemString, operationSettings, lowestAuction) then
TempTable.Release(lowestAuction)
lowestAuction = nil
end
local minPrice = TSM.Auctioning.Util.GetPrice("minPrice", operationSettings, itemString)
local normalPrice = TSM.Auctioning.Util.GetPrice("normalPrice", operationSettings, itemString)
local maxPrice = TSM.Auctioning.Util.GetPrice("maxPrice", operationSettings, itemString)
local resetPrice = TSM.Auctioning.Util.GetPrice("priceReset", operationSettings, itemString)
local cancelRepostThreshold = TSM.Auctioning.Util.GetPrice("cancelRepostThreshold", operationSettings, itemString)
local undercut = TSM.Auctioning.Util.GetPrice("undercut", operationSettings, itemString)
local aboveMax = TSM.Auctioning.Util.GetPrice("aboveMax", operationSettings, itemString)
if not lowestAuction then
-- all auctions which are posted (including ours) have been ignored, so check if we should cancel to repost higher
if operationSettings.cancelRepost and normalPrice - itemBuyout > cancelRepostThreshold then
private.AddToQueue(itemString, operationName, itemBid, itemBuyout, stackSize, duration, auctionId)
return true, "cancelRepost", itemBuyout, nil, auctionId
else
return false, "cancelNotUndercut", itemBuyout
end
elseif lowestAuction.hasInvalidSeller then
Log.PrintfUser(L["The seller name of the lowest auction for %s was not given by the server. Skipping this item."], ItemInfo.GetLink(itemString))
TempTable.Release(lowestAuction)
return false, "invalidSeller", itemBuyout
end
local shouldCancel, logReason = false, nil
local playerLowestItemBuyout, playerLowestAuctionId = TSM.Auctioning.Util.GetPlayerLowestBuyout(subRows, itemString, operationSettings)
local secondLowestBuyout = TSM.Auctioning.Util.GetNextLowestItemBuyout(subRows, itemString, lowestAuction, operationSettings)
local nonPlayerLowestAuctionId = not TSM.IsWowClassic() and playerLowestItemBuyout and TSM.Auctioning.Util.GetLowestNonPlayerAuctionId(subRows, itemString, operationSettings, playerLowestItemBuyout)
if itemBuyout < minPrice and not lowestAuction.isBlacklist then
-- this auction is below the min price
if operationSettings.cancelRepost and resetPrice and itemBuyout < (resetPrice - cancelRepostThreshold) then
-- canceling to post at reset price
shouldCancel = true
logReason = "cancelReset"
else
logReason = "cancelBelowMin"
end
elseif lowestAuction.buyout < minPrice and not lowestAuction.isBlacklist then
-- lowest buyout is below min price, so do nothing
logReason = "cancelBelowMin"
elseif operationSettings.cancelUndercut and playerLowestItemBuyout and ((itemBuyout - undercut) > playerLowestItemBuyout or (not TSM.IsWowClassic() and (itemBuyout - undercut) == playerLowestItemBuyout and auctionId ~= playerLowestAuctionId and auctionId < (nonPlayerLowestAuctionId or 0))) then
-- we've undercut this auction
shouldCancel = true
logReason = "cancelPlayerUndercut"
elseif TSM.Auctioning.Util.IsPlayerOnlySeller(subRows, itemString, operationSettings) then
-- we are the only auction
if operationSettings.cancelRepost and (normalPrice - itemBuyout) > cancelRepostThreshold then
-- we can repost higher
shouldCancel = true
logReason = "cancelRepost"
else
logReason = "cancelAtNormal"
end
elseif lowestAuction.isPlayer and secondLowestBuyout and secondLowestBuyout > maxPrice then
-- we are posted at the aboveMax price with no competition under our max price
if operationSettings.cancelRepost and operationSettings.aboveMax ~= "none" and (aboveMax - itemBuyout) > cancelRepostThreshold then
-- we can repost higher
shouldCancel = true
logReason = "cancelRepost"
else
logReason = "cancelAtAboveMax"
end
elseif lowestAuction.isPlayer then
-- we are the loewst auction
if operationSettings.cancelRepost and secondLowestBuyout and ((secondLowestBuyout - undercut) - lowestAuction.buyout) > cancelRepostThreshold then
-- we can repost higher
shouldCancel = true
logReason = "cancelRepost"
else
logReason = "cancelNotUndercut"
end
elseif not operationSettings.cancelUndercut then
-- we're undercut but not canceling undercut auctions
elseif lowestAuction.isWhitelist and itemBuyout == lowestAuction.buyout then
-- at whitelisted player price
logReason = "cancelAtWhitelist"
elseif not lowestAuction.isWhitelist then
-- we've been undercut by somebody not on our whitelist
shouldCancel = true
logReason = "cancelUndercut"
elseif itemBuyout ~= lowestAuction.buyout or itemBid ~= lowestAuction.bid then
-- somebody on our whitelist undercut us (or their bid is lower)
shouldCancel = true
logReason = "cancelWhitelistUndercut"
else
error("Should not get here")
end
local seller = lowestAuction.seller
TempTable.Release(lowestAuction)
if shouldCancel then
private.AddToQueue(itemString, operationName, itemBid, itemBuyout, stackSize, duration, auctionId)
end
return shouldCancel, logReason, itemBuyout, seller, shouldCancel and auctionId or nil
end
function private.AddToQueue(itemString, operationName, itemBid, itemBuyout, stackSize, duration, auctionId)
private.queueDB:NewRow()
:SetField("auctionId", auctionId)
:SetField("itemString", itemString)
:SetField("operationName", operationName)
:SetField("bid", itemBid * stackSize)
:SetField("buyout", itemBuyout * stackSize)
:SetField("itemBid", itemBid)
:SetField("itemBuyout", itemBuyout)
:SetField("stackSize", stackSize)
:SetField("duration", duration)
:SetField("numStacks", 1)
:SetField("numProcessed", 0)
:SetField("numConfirmed", 0)
:SetField("numFailed", 0)
:Create()
end
function private.ProcessQueryHelper(row, cancelRow)
if TSM.IsWowClassic() then
local auctionId, itemString, stackSize, currentBid, buyout = row:GetFields("auctionId", "autoBaseItemString", "stackSize", "currentBid", "buyout")
local itemBid = floor(currentBid / stackSize)
local itemBuyout = floor(buyout / stackSize)
return not private.usedAuctionIndex[itemString..buyout..currentBid..auctionId] and cancelRow:GetField("itemBid") == itemBid and cancelRow:GetField("itemBuyout") == itemBuyout
else
local auctionId = row:GetField("auctionId")
return not private.usedAuctionIndex[auctionId] and cancelRow:GetField("auctionId") == auctionId
end
end
function private.ConfirmRowQueryHelper(row)
return row:GetField("numConfirmed") < row:GetField("numProcessed")
end
function private.NextProcessRowQueryHelper(row)
return row:GetField("numProcessed") < row:GetField("numStacks")
end

View File

@ -0,0 +1,8 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
TSM:NewPackage("Auctioning")

View File

@ -0,0 +1,142 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Log = TSM.Auctioning:NewPackage("Log")
local L = TSM.Include("Locale").GetTable()
local Database = TSM.Include("Util.Database")
local Theme = TSM.Include("Util.Theme")
local ItemInfo = TSM.Include("Service.ItemInfo")
local private = {
db = nil,
}
local REASON_INFO = {
-- general
invalidItemGroup = { color = "RED", str = L["Item/Group is invalid (see chat)."] },
invalidSeller = { color = "RED", str = L["Invalid seller data returned by server."] },
-- post scan
postDisabled = { color = "ORANGE", str = L["Posting disabled."] },
postNotEnough = { color = "ORANGE", str = L["Not enough items in bags."] },
postMaxExpires = { color = "ORANGE", str = L["Above max expires."] },
postBelowMin = { color = "ORANGE", str = L["Cheapest auction below min price."] },
postTooMany = { color = "BLUE", str = L["Maximum amount already posted."] },
postNormal = { color = "GREEN", str = L["Posting at normal price."] },
postResetMin = { color = "GREEN", str = L["Below min price. Posting at min."] },
postResetMax = { color = "GREEN", str = L["Below min price. Posting at max."] },
postResetNormal = { color = "GREEN", str = L["Below min price. Posting at normal."] },
postAboveMaxMin = { color = "GREEN", str = L["Above max price. Posting at min."] },
postAboveMaxMax = { color = "GREEN", str = L["Above max price. Posting at max."] },
postAboveMaxNormal = { color = "GREEN", str = L["Above max price. Posting at normal."] },
postAboveMaxNoPost = { color = "ORANGE", str = L["Above max price. Not posting."] },
postUndercut = { color = "GREEN", str = L["Undercutting competition."] },
postPlayer = { color = "GREEN", str = L["Posting at your current price."] },
postWhitelist = { color = "GREEN", str = L["Posting at whitelisted player's price."] },
postWhitelistNoPost = { color = "ORANGE", str = L["Lowest auction by whitelisted player."] },
postBlacklist = { color = "GREEN", str = L["Undercutting blacklisted player."] },
-- cancel scan
cancelDisabled = { color = "ORANGE", str = L["Canceling disabled."] },
cancelNotUndercut = { color = "GREEN", str = L["Your auction has not been undercut."] },
cancelBid = { color = "BLUE", str = L["Auction has been bid on."] },
cancelNoMoney = { color = "BLUE", str = L["Not enough money to cancel."] },
cancelKeepPosted = { color = "BLUE", str = L["Keeping undercut auctions posted."] },
cancelBelowMin = { color = "ORANGE", str = L["Not canceling auction below min price."] },
cancelAtReset = { color = "GREEN", str = L["Not canceling auction at reset price."] },
cancelAtNormal = { color = "GREEN", str = L["At normal price and not undercut."] },
cancelAtAboveMax = { color = "GREEN", str = L["At above max price and not undercut."] },
cancelAtWhitelist = { color = "GREEN", str = L["Posted at whitelisted player's price."] },
cancelUndercut = { color = "RED", str = L["You've been undercut."] },
cancelRepost = { color = "BLUE", str = L["Canceling to repost at higher price."] },
cancelReset = { color = "BLUE", str = L["Canceling to repost at reset price."] },
cancelWhitelistUndercut = { color = "RED", str = L["Undercut by whitelisted player."] },
cancelPlayerUndercut = { color = "BLUE", str = L["Canceling auction you've undercut."] },
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Log.OnInitialize()
private.db = Database.NewSchema("AUCTIONING_LOG")
:AddNumberField("index")
:AddStringField("itemString")
:AddStringField("seller")
:AddNumberField("buyout")
:AddStringField("operation")
:AddStringField("reasonStr")
:AddStringField("reasonKey")
:AddStringField("state")
:AddIndex("index")
:Commit()
end
function Log.Truncate()
private.db:Truncate()
end
function Log.CreateQuery()
return private.db:NewQuery()
:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
:OrderBy("index", true)
end
function Log.UpdateRowByIndex(index, field, value)
local row = private.db:NewQuery()
:Equal("index", index)
:GetFirstResultAndRelease()
if field == "state" then
assert(value == "POSTED" or value == "CANCELLED" or value == "SKIPPED")
if not row then
return
end
end
row:SetField(field, value)
:Update()
row:Release()
end
function Log.SetQueryUpdatesPaused(paused)
private.db:SetQueryUpdatesPaused(paused)
end
function Log.AddEntry(itemString, operationName, reasonKey, seller, buyout, index)
private.db:NewRow()
:SetField("itemString", itemString)
:SetField("seller", seller)
:SetField("buyout", buyout)
:SetField("operation", operationName)
:SetField("reasonStr", REASON_INFO[reasonKey].str)
:SetField("reasonKey", reasonKey)
:SetField("index", index)
:SetField("state", "PENDING")
:Create()
end
function Log.GetColorFromReasonKey(reasonKey)
return Theme.GetFeedbackColor(REASON_INFO[reasonKey].color)
end
function Log.GetInfoStr(row)
local state, reasonKey = row:GetFields("state", "reasonKey")
local reasonInfo = REASON_INFO[reasonKey]
local color = nil
if state == "PENDING" then
return Theme.GetFeedbackColor(reasonInfo.color):ColorText(reasonInfo.str)
elseif state == "POSTED" then
return Theme.GetColor("INDICATOR"):ColorText(L["Posted:"]).." "..reasonInfo.str
elseif state == "CANCELLED" then
return Theme.GetColor("INDICATOR"):ColorText(L["Cancelled:"]).." "..reasonInfo.str
elseif state == "SKIPPED" then
return Theme.GetColor("INDICATOR"):ColorText(L["Skipped:"]).." "..reasonInfo.str
else
error("Invalid state: "..tostring(state))
end
return color:ColorText(reasonInfo.str)
end

View File

@ -0,0 +1,965 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local PostScan = TSM.Auctioning:NewPackage("PostScan")
local L = TSM.Include("Locale").GetTable()
local Database = TSM.Include("Util.Database")
local TempTable = TSM.Include("Util.TempTable")
local SlotId = TSM.Include("Util.SlotId")
local Delay = TSM.Include("Util.Delay")
local Math = TSM.Include("Util.Math")
local Log = TSM.Include("Util.Log")
local Event = TSM.Include("Util.Event")
local ItemString = TSM.Include("Util.ItemString")
local Threading = TSM.Include("Service.Threading")
local ItemInfo = TSM.Include("Service.ItemInfo")
local BagTracking = TSM.Include("Service.BagTracking")
local AuctionHouseWrapper = TSM.Include("Service.AuctionHouseWrapper")
local private = {
scanThreadId = nil,
queueDB = nil,
nextQueueIndex = 1,
bagDB = nil,
itemList = {},
operationDB = nil,
debugLog = {},
itemLocation = ItemLocation:CreateEmpty(),
subRowsTemp = {},
groupsQuery = nil, --luacheck: ignore 1004 - just stored for GC reasons
operationsQuery = nil, --luacheck: ignore 1004 - just stored for GC reasons
isAHOpen = false,
}
local RESET_REASON_LOOKUP = {
minPrice = "postResetMin",
maxPrice = "postResetMax",
normalPrice = "postResetNormal"
}
local ABOVE_MAX_REASON_LOOKUP = {
minPrice = "postAboveMaxMin",
maxPrice = "postAboveMaxMax",
normalPrice = "postAboveMaxNormal",
none = "postAboveMaxNoPost"
}
local MAX_COMMODITY_STACKS_PER_AUCTION = 40
-- ============================================================================
-- Module Functions
-- ============================================================================
function PostScan.OnInitialize()
BagTracking.RegisterCallback(private.UpdateOperationDB)
Event.Register("AUCTION_HOUSE_SHOW", private.AuctionHouseShowHandler)
Event.Register("AUCTION_HOUSE_CLOSED", private.AuctionHouseClosedHandler)
private.operationDB = Database.NewSchema("AUCTIONING_OPERATIONS")
:AddUniqueStringField("autoBaseItemString")
:AddStringField("firstOperation")
:Commit()
private.scanThreadId = Threading.New("POST_SCAN", private.ScanThread)
private.queueDB = Database.NewSchema("AUCTIONING_POST_QUEUE")
:AddNumberField("auctionId")
:AddStringField("itemString")
:AddStringField("operationName")
:AddNumberField("bid")
:AddNumberField("buyout")
:AddNumberField("itemBuyout")
:AddNumberField("stackSize")
:AddNumberField("numStacks")
:AddNumberField("postTime")
:AddNumberField("numProcessed")
:AddNumberField("numConfirmed")
:AddNumberField("numFailed")
:AddIndex("auctionId")
:AddIndex("itemString")
:Commit()
-- We maintain our own bag database rather than using the one in BagTracking since we need to be able to remove items
-- as they are posted, without waiting for bag update events, and control when our DB updates.
private.bagDB = Database.NewSchema("AUCTIONING_POST_BAGS")
:AddStringField("itemString")
:AddNumberField("bag")
:AddNumberField("slot")
:AddNumberField("quantity")
:AddUniqueNumberField("slotId")
:AddIndex("itemString")
:AddIndex("slotId")
:Commit()
-- create a groups and operations query just to register for updates
private.groupsQuery = TSM.Groups.CreateQuery()
:SetUpdateCallback(private.OnGroupsOperationsChanged)
private.operationsQuery = TSM.Operations.CreateQuery()
:SetUpdateCallback(private.OnGroupsOperationsChanged)
end
function PostScan.CreateBagsQuery()
return BagTracking.CreateQueryBagsAuctionable()
:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
:Distinct("autoBaseItemString")
:LeftJoin(private.operationDB, "autoBaseItemString")
:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
:OrderBy("name", true)
end
function PostScan.Prepare()
return private.scanThreadId
end
function PostScan.GetCurrentRow()
return private.queueDB:NewQuery()
:Custom(private.NextProcessRowQueryHelper)
:OrderBy("auctionId", true)
:GetFirstResultAndRelease()
end
function PostScan.GetStatus()
return TSM.Auctioning.Util.GetQueueStatus(private.queueDB:NewQuery())
end
function PostScan.DoProcess()
local result, noRetry = nil, false
local postRow = PostScan.GetCurrentRow()
local itemString, stackSize, bid, buyout, itemBuyout, postTime = postRow:GetFields("itemString", "stackSize", "bid", "buyout", "itemBuyout", "postTime")
local bag, slot = private.GetPostBagSlot(itemString, stackSize)
if bag then
local _, bagQuantity = GetContainerItemInfo(bag, slot)
Log.Info("Posting %s x %d from %d,%d (%d)", itemString, stackSize, bag, slot, bagQuantity or -1)
if TSM.IsWowClassic() then
-- need to set the duration in the default UI to avoid Blizzard errors
AuctionFrameAuctions.duration = postTime
ClearCursor()
PickupContainerItem(bag, slot)
ClickAuctionSellItemButton(AuctionsItemButton, "LeftButton")
PostAuction(bid, buyout, postTime, stackSize, 1)
ClearCursor()
result = true
else
bid = Math.Round(bid / stackSize, COPPER_PER_SILVER)
buyout = Math.Round(buyout / stackSize, COPPER_PER_SILVER)
itemBuyout = Math.Round(itemBuyout, COPPER_PER_SILVER)
private.itemLocation:Clear()
private.itemLocation:SetBagAndSlot(bag, slot)
local commodityStatus = C_AuctionHouse.GetItemCommodityStatus(private.itemLocation)
if commodityStatus == Enum.ItemCommodityStatus.Item then
result = AuctionHouseWrapper.PostItem(private.itemLocation, postTime, stackSize, bid < buyout and bid or nil, buyout)
elseif commodityStatus == Enum.ItemCommodityStatus.Commodity then
result = AuctionHouseWrapper.PostCommodity(private.itemLocation, postTime, stackSize, itemBuyout)
else
error("Unknown commodity status: "..tostring(itemString))
end
if not result then
Log.Err("Failed to post (%s, %s, %s)", itemString, bag, slot)
end
end
else
-- we couldn't find this item, so mark this post as failed and we'll try again later
result = false
noRetry = slot
if noRetry then
Log.PrintfUser(L["Failed to post %sx%d as the item no longer exists in your bags."], ItemInfo.GetLink(itemString), stackSize)
end
end
if result then
private.DebugLogInsert(itemString, "Posting %d from %d, %d", stackSize, bag, slot)
if postRow:GetField("numProcessed") + 1 == postRow:GetField("numStacks") then
-- update the log
local auctionId = postRow:GetField("auctionId")
TSM.Auctioning.Log.UpdateRowByIndex(auctionId, "state", "POSTED")
end
end
postRow:SetField("numProcessed", postRow:GetField("numProcessed") + 1)
:Update()
postRow:Release()
return result, noRetry
end
function PostScan.DoSkip()
local postRow = PostScan.GetCurrentRow()
local auctionId = postRow:GetField("auctionId")
local numStacks = postRow:GetField("numStacks")
postRow:SetField("numProcessed", numStacks)
:SetField("numConfirmed", numStacks)
:Update()
postRow:Release()
-- update the log
TSM.Auctioning.Log.UpdateRowByIndex(auctionId, "state", "SKIPPED")
end
function PostScan.HandleConfirm(success, canRetry)
if not success then
ClearCursor()
end
local confirmRow = private.queueDB:NewQuery()
:Custom(private.ConfirmRowQueryHelper)
:OrderBy("auctionId", true)
:GetFirstResultAndRelease()
if not confirmRow then
-- we may have posted something outside of TSM
return
end
private.DebugLogInsert(confirmRow:GetField("itemString"), "HandleConfirm(success=%s) x %d", tostring(success), confirmRow:GetField("stackSize"))
if canRetry then
assert(not success)
confirmRow:SetField("numFailed", confirmRow:GetField("numFailed") + 1)
end
confirmRow:SetField("numConfirmed", confirmRow:GetField("numConfirmed") + 1)
:Update()
confirmRow:Release()
end
function PostScan.PrepareFailedPosts()
private.queueDB:SetQueryUpdatesPaused(true)
local query = private.queueDB:NewQuery()
:GreaterThan("numFailed", 0)
:OrderBy("auctionId", true)
for _, row in query:Iterator() do
local numFailed, numProcessed, numConfirmed = row:GetFields("numFailed", "numProcessed", "numConfirmed")
assert(numProcessed >= numFailed and numConfirmed >= numFailed)
private.DebugLogInsert(row:GetField("itemString"), "Preparing failed (%d, %d, %d)", numFailed, numProcessed, numConfirmed)
row:SetField("numFailed", 0)
:SetField("numProcessed", numProcessed - numFailed)
:SetField("numConfirmed", numConfirmed - numFailed)
:Update()
end
query:Release()
private.queueDB:SetQueryUpdatesPaused(false)
private.UpdateBagDB()
end
function PostScan.Reset()
private.queueDB:Truncate()
private.nextQueueIndex = 1
private.bagDB:Truncate()
end
function PostScan.ChangePostDetail(field, value)
local postRow = PostScan.GetCurrentRow()
local isCommodity = ItemInfo.IsCommodity(postRow:GetField("itemString"))
if field == "bid" then
assert(not isCommodity)
value = min(max(value, 1), postRow:GetField("buyout"))
elseif field == "buyout" then
if not isCommodity and value < postRow:GetField("bid") then
postRow:SetField("bid", value)
end
TSM.Auctioning.Log.UpdateRowByIndex(postRow:GetField("auctionId"), field, value)
end
postRow:SetField((field == "buyout" and isCommodity) and "itemBuyout" or field, value)
:Update()
postRow:Release()
end
-- ============================================================================
-- Private Helper Functions (General)
-- ============================================================================
function private.AuctionHouseShowHandler()
private.isAHOpen = true
private.UpdateOperationDB()
end
function private.AuctionHouseClosedHandler()
private.isAHOpen = false
end
function private.OnGroupsOperationsChanged()
Delay.AfterFrame("POST_GROUP_OPERATIONS_CHANGED", 1, private.UpdateOperationDB)
end
function private.UpdateOperationDB()
if not private.isAHOpen then
return
end
private.operationDB:TruncateAndBulkInsertStart()
local query = BagTracking.CreateQueryBagsAuctionable()
:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
:Select("autoBaseItemString")
:Distinct("autoBaseItemString")
for _, itemString in query:Iterator() do
local firstOperation = TSM.Operations.GetFirstOperationByItem("Auctioning", itemString)
if firstOperation then
private.operationDB:BulkInsertNewRow(itemString, firstOperation)
end
end
query:Release()
private.operationDB:BulkInsertEnd()
end
-- ============================================================================
-- Scan Thread
-- ============================================================================
function private.ScanThread(auctionScan, scanContext)
wipe(private.debugLog)
auctionScan:SetScript("OnQueryDone", private.AuctionScanOnQueryDone)
private.UpdateBagDB()
-- get the state of the player's bags
local bagCounts = TempTable.Acquire()
local bagQuery = private.bagDB:NewQuery()
:Select("itemString", "quantity")
for _, itemString, quantity in bagQuery:Iterator() do
bagCounts[itemString] = (bagCounts[itemString] or 0) + quantity
end
bagQuery:Release()
-- generate the list of items we want to scan for
wipe(private.itemList)
for itemString, numHave in pairs(bagCounts) do
private.DebugLogInsert(itemString, "Scan thread has %d", numHave)
local groupPath = TSM.Groups.GetPathByItem(itemString)
local contextFilter = scanContext.isItems and itemString or groupPath
if groupPath and tContains(scanContext, contextFilter) and private.CanPostItem(itemString, groupPath, numHave) then
tinsert(private.itemList, itemString)
end
end
TempTable.Release(bagCounts)
if #private.itemList == 0 then
return
end
-- record this search
TSM.Auctioning.SavedSearches.RecordSearch(scanContext, scanContext.isItems and "postItems" or "postGroups")
-- run the scan
auctionScan:AddItemListQueriesThreaded(private.itemList)
for _, query in auctionScan:QueryIterator() do
query:SetIsBrowseDoneFunction(private.QueryIsBrowseDoneFunction)
query:AddCustomFilter(private.QueryBuyoutFilter)
end
if not auctionScan:ScanQueriesThreaded() then
Log.PrintUser(L["TSM failed to scan some auctions. Please rerun the scan."])
end
end
-- ============================================================================
-- Private Helper Functions for Scanning
-- ============================================================================
function private.UpdateBagDB()
private.bagDB:TruncateAndBulkInsertStart()
local query = BagTracking.CreateQueryBagsAuctionable()
:OrderBy("slotId", true)
:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
:Select("slotId", "bag", "slot", "autoBaseItemString", "quantity")
for _, slotId, bag, slot, itemString, quantity in query:Iterator() do
private.DebugLogInsert(itemString, "Updating bag DB with %d in %d, %d", quantity, bag, slot)
private.bagDB:BulkInsertNewRow(itemString, bag, slot, quantity, slotId)
end
query:Release()
private.bagDB:BulkInsertEnd()
end
function private.CanPostItem(itemString, groupPath, numHave)
local hasValidOperation, hasInvalidOperation = false, false
for _, operationName, operationSettings in TSM.Operations.GroupOperationIterator("Auctioning", groupPath) do
local isValid, numUsed = private.IsOperationValid(itemString, numHave, operationName, operationSettings)
if isValid == true then
assert(numUsed and numUsed > 0)
numHave = numHave - numUsed
hasValidOperation = true
elseif isValid == false then
hasInvalidOperation = true
else
-- we are ignoring this operation
assert(isValid == nil, "Invalid return value")
end
end
return hasValidOperation and not hasInvalidOperation
end
function private.IsOperationValid(itemString, num, operationName, operationSettings)
local postCap = TSM.Auctioning.Util.GetPrice("postCap", operationSettings, itemString)
if not postCap then
-- invalid postCap setting
if not TSM.db.global.auctioningOptions.disableInvalidMsg then
Log.PrintfUser(L["Did not post %s because your post cap (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.postCap)
end
TSM.Auctioning.Log.AddEntry(itemString, operationName, "invalidItemGroup", "", 0, math.huge)
return nil
elseif postCap == 0 then
-- posting is disabled, so ignore this operation
TSM.Auctioning.Log.AddEntry(itemString, operationName, "postDisabled", "", 0, math.huge)
return nil
end
local stackSize = nil
local minPostQuantity = nil
if not TSM.IsWowClassic() then
minPostQuantity = 1
else
-- check the stack size
stackSize = TSM.Auctioning.Util.GetPrice("stackSize", operationSettings, itemString)
if not stackSize then
-- invalid stackSize setting
if not TSM.db.global.auctioningOptions.disableInvalidMsg then
Log.PrintfUser(L["Did not post %s because your stack size (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.stackSize)
end
TSM.Auctioning.Log.AddEntry(itemString, operationName, "invalidItemGroup", "", 0, math.huge)
return nil
end
local maxStackSize = ItemInfo.GetMaxStack(itemString)
minPostQuantity = operationSettings.stackSizeIsCap and 1 or stackSize
if not maxStackSize then
-- couldn't lookup item info for this item (shouldn't happen)
if not TSM.db.global.auctioningOptions.disableInvalidMsg then
Log.PrintfUser(L["Did not post %s because Blizzard didn't provide all necessary information for it. Try again later."], ItemInfo.GetLink(itemString))
end
TSM.Auctioning.Log.AddEntry(itemString, operationName, "invalidItemGroup", "", 0, math.huge)
return false
elseif maxStackSize < minPostQuantity then
-- invalid stack size
return nil
end
end
-- check that we have enough to post
local keepQuantity = TSM.Auctioning.Util.GetPrice("keepQuantity", operationSettings, itemString)
if not keepQuantity then
-- invalid keepQuantity setting
if not TSM.db.global.auctioningOptions.disableInvalidMsg then
Log.PrintfUser(L["Did not post %s because your keep quantity (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.keepQuantity)
end
TSM.Auctioning.Log.AddEntry(itemString, operationName, "invalidItemGroup", "", 0, math.huge)
return nil
end
num = num - keepQuantity
if num < minPostQuantity then
-- not enough items to post for this operation
TSM.Auctioning.Log.AddEntry(itemString, operationName, "postNotEnough", "", 0, math.huge)
return nil
end
-- check the max expires
local maxExpires = TSM.Auctioning.Util.GetPrice("maxExpires", operationSettings, itemString)
if not maxExpires then
-- invalid maxExpires setting
if not TSM.db.global.auctioningOptions.disableInvalidMsg then
Log.PrintfUser(L["Did not post %s because your max expires (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.maxExpires)
end
TSM.Auctioning.Log.AddEntry(itemString, operationName, "invalidItemGroup", "", 0, math.huge)
return nil
end
if maxExpires > 0 then
local numExpires = TSM.Accounting.Auctions.GetNumExpiresSinceSale(itemString)
if numExpires and numExpires > maxExpires then
-- too many expires, so ignore this operation
TSM.Auctioning.Log.AddEntry(itemString, operationName, "postMaxExpires", "", 0, math.huge)
return nil
end
end
local errMsg = nil
local minPrice = TSM.Auctioning.Util.GetPrice("minPrice", operationSettings, itemString)
local normalPrice = TSM.Auctioning.Util.GetPrice("normalPrice", operationSettings, itemString)
local maxPrice = TSM.Auctioning.Util.GetPrice("maxPrice", operationSettings, itemString)
local undercut = TSM.Auctioning.Util.GetPrice("undercut", operationSettings, itemString)
if not minPrice then
errMsg = format(L["Did not post %s because your minimum price (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.minPrice)
elseif not maxPrice then
errMsg = format(L["Did not post %s because your maximum price (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.maxPrice)
elseif not normalPrice then
errMsg = format(L["Did not post %s because your normal price (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.normalPrice)
elseif not undercut then
errMsg = format(L["Did not post %s because your undercut (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.undercut)
elseif normalPrice < minPrice then
errMsg = format(L["Did not post %s because your normal price (%s) is lower than your minimum price (%s). Check your settings."], ItemInfo.GetLink(itemString), operationSettings.normalPrice, operationSettings.minPrice)
elseif maxPrice < minPrice then
errMsg = format(L["Did not post %s because your maximum price (%s) is lower than your minimum price (%s). Check your settings."], ItemInfo.GetLink(itemString), operationSettings.maxPrice, operationSettings.minPrice)
end
if errMsg then
if not TSM.db.global.auctioningOptions.disableInvalidMsg then
Log.PrintUser(errMsg)
end
TSM.Auctioning.Log.AddEntry(itemString, operationName, "invalidItemGroup", "", 0, math.huge)
return false
else
local vendorSellPrice = ItemInfo.GetVendorSell(itemString) or 0
if vendorSellPrice > 0 and minPrice <= vendorSellPrice / 0.95 then
-- just a warning, not an error
Log.PrintfUser(L["WARNING: Your minimum price for %s is below its vendorsell price (with AH cut taken into account). Consider raising your minimum price, or vendoring the item."], ItemInfo.GetLink(itemString))
end
return true, (TSM.IsWowClassic() and stackSize or 1) * postCap
end
end
function private.QueryBuyoutFilter(_, row)
local _, itemBuyout, minItemBuyout = row:GetBuyouts()
return (itemBuyout and itemBuyout == 0) or (minItemBuyout and minItemBuyout == 0)
end
function private.QueryIsBrowseDoneFunction(query)
if not TSM.IsWowClassic() then
return false
end
local isDone = true
for itemString in query:ItemIterator() do
isDone = isDone and private.QueryIsBrowseDoneForItem(query, itemString)
end
return isDone
end
function private.QueryIsBrowseDoneForItem(query, itemString)
local groupPath = TSM.Groups.GetPathByItem(itemString)
if not groupPath then
return true
end
local isFilterDone = true
for _, _, operationSettings in TSM.Operations.GroupOperationIterator("Auctioning", groupPath) do
if isFilterDone then
local numBuyouts, minItemBuyout, maxItemBuyout = 0, nil, nil
for _, subRow in query:ItemSubRowIterator(itemString) do
local _, itemBuyout = subRow:GetBuyouts()
local timeLeft = subRow:GetListingInfo()
if itemBuyout > 0 and timeLeft > operationSettings.ignoreLowDuration then
numBuyouts = numBuyouts + 1
minItemBuyout = min(minItemBuyout or math.huge, itemBuyout)
maxItemBuyout = max(maxItemBuyout or 0, itemBuyout)
end
end
if numBuyouts <= 1 then
-- there is only one distinct item buyout, so can't stop yet
isFilterDone = false
else
local minPrice = TSM.Auctioning.Util.GetPrice("minPrice", operationSettings, itemString)
local undercut = TSM.Auctioning.Util.GetPrice("undercut", operationSettings, itemString)
if not minPrice or not undercut then
-- the min price or undercut is not valid, so just keep scanning
isFilterDone = false
elseif minItemBuyout - undercut <= minPrice then
local resetPrice = TSM.Auctioning.Util.GetPrice("priceReset", operationSettings, itemString)
if operationSettings.priceReset == "ignore" or (resetPrice and maxItemBuyout <= resetPrice) then
-- we need to keep scanning to handle the reset price (always keep scanning for "ignore")
isFilterDone = false
end
end
end
end
end
return isFilterDone
end
function private.AuctionScanOnQueryDone(_, query)
for itemString in query:ItemIterator() do
local groupPath = TSM.Groups.GetPathByItem(itemString)
if groupPath then
local numHave = 0
local bagQuery = private.bagDB:NewQuery()
:Select("quantity", "bag", "slot")
:Equal("itemString", itemString)
for _, quantity, bag, slot in bagQuery:Iterator() do
numHave = numHave + quantity
private.DebugLogInsert(itemString, "Filter done and have %d in %d, %d", numHave, bag, slot)
end
bagQuery:Release()
for _, operationName, operationSettings in TSM.Operations.GroupOperationIterator("Auctioning", groupPath) do
if private.IsOperationValid(itemString, numHave, operationName, operationSettings) then
local keepQuantity = TSM.Auctioning.Util.GetPrice("keepQuantity", operationSettings, itemString)
assert(keepQuantity)
local operationNumHave = numHave - keepQuantity
if operationNumHave > 0 then
assert(not next(private.subRowsTemp))
TSM.Auctioning.Util.GetFilteredSubRows(query, itemString, operationSettings, private.subRowsTemp)
local reason, numUsed, itemBuyout, seller, auctionId = private.GeneratePosts(itemString, operationName, operationSettings, operationNumHave, private.subRowsTemp)
wipe(private.subRowsTemp)
numHave = numHave - (numUsed or 0)
seller = seller or ""
auctionId = auctionId or math.huge
TSM.Auctioning.Log.AddEntry(itemString, operationName, reason, seller, itemBuyout or 0, auctionId)
end
end
end
assert(numHave >= 0)
else
Log.Warn("Item removed from group since start of scan: %s", itemString)
end
end
end
function private.GeneratePosts(itemString, operationName, operationSettings, numHave, subRows)
if numHave == 0 then
return "postNotEnough"
end
local perAuction, maxCanPost = nil, nil
local postCap = TSM.Auctioning.Util.GetPrice("postCap", operationSettings, itemString)
if not TSM.IsWowClassic() then
perAuction = min(postCap, numHave)
maxCanPost = 1
else
local stackSize = TSM.Auctioning.Util.GetPrice("stackSize", operationSettings, itemString)
local maxStackSize = ItemInfo.GetMaxStack(itemString)
if stackSize > maxStackSize and not operationSettings.stackSizeIsCap then
return "postNotEnough"
end
perAuction = min(stackSize, maxStackSize)
maxCanPost = min(floor(numHave / perAuction), postCap)
if maxCanPost == 0 then
if operationSettings.stackSizeIsCap then
perAuction = numHave
maxCanPost = 1
else
-- not enough for single post
return "postNotEnough"
end
end
end
local lowestAuction = TempTable.Acquire()
if not TSM.Auctioning.Util.GetLowestAuction(subRows, itemString, operationSettings, lowestAuction) then
TempTable.Release(lowestAuction)
lowestAuction = nil
end
local minPrice = TSM.Auctioning.Util.GetPrice("minPrice", operationSettings, itemString)
local normalPrice = TSM.Auctioning.Util.GetPrice("normalPrice", operationSettings, itemString)
local maxPrice = TSM.Auctioning.Util.GetPrice("maxPrice", operationSettings, itemString)
local undercut = TSM.Auctioning.Util.GetPrice("undercut", operationSettings, itemString)
local resetPrice = TSM.Auctioning.Util.GetPrice("priceReset", operationSettings, itemString)
local aboveMax = TSM.Auctioning.Util.GetPrice("aboveMax", operationSettings, itemString)
local reason, bid, buyout, seller, activeAuctions = nil, nil, nil, nil, 0
if not lowestAuction then
-- post as many as we can at the normal price
reason = "postNormal"
buyout = normalPrice
elseif lowestAuction.hasInvalidSeller then
-- we didn't get all the necessary seller info
Log.PrintfUser(L["The seller name of the lowest auction for %s was not given by the server. Skipping this item."], ItemInfo.GetLink(itemString))
TempTable.Release(lowestAuction)
return "invalidSeller"
elseif lowestAuction.isBlacklist and lowestAuction.isPlayer then
Log.PrintfUser(L["Did not post %s because you or one of your alts (%s) is on the blacklist which is not allowed. Remove this character from your blacklist."], ItemInfo.GetLink(itemString), lowestAuction.seller)
TempTable.Release(lowestAuction)
return "invalidItemGroup"
elseif lowestAuction.isBlacklist and lowestAuction.isWhitelist then
Log.PrintfUser(L["Did not post %s because the owner of the lowest auction (%s) is on both the blacklist and whitelist which is not allowed. Adjust your settings to correct this issue."], ItemInfo.GetLink(itemString), lowestAuction.seller)
TempTable.Release(lowestAuction)
return "invalidItemGroup"
elseif lowestAuction.buyout - undercut < minPrice then
seller = lowestAuction.seller
if resetPrice then
-- lowest is below the min price, but there is a reset price
assert(RESET_REASON_LOOKUP[operationSettings.priceReset], "Unexpected 'below minimum price' setting: "..tostring(operationSettings.priceReset))
reason = RESET_REASON_LOOKUP[operationSettings.priceReset]
buyout = resetPrice
bid = max(bid or buyout * operationSettings.bidPercent, minPrice)
activeAuctions = TSM.Auctioning.Util.GetPlayerAuctionCount(subRows, itemString, operationSettings, floor(bid), buyout, perAuction)
elseif lowestAuction.isBlacklist then
-- undercut the blacklisted player
reason = "postBlacklist"
buyout = lowestAuction.buyout - undercut
else
-- don't post this item
TempTable.Release(lowestAuction)
return "postBelowMin", nil, nil, seller
end
elseif lowestAuction.isPlayer or (lowestAuction.isWhitelist and TSM.db.global.auctioningOptions.matchWhitelist) then
-- we (or a whitelisted play we should match) are lowest, so match the current price and post as many as we can
activeAuctions = TSM.Auctioning.Util.GetPlayerAuctionCount(subRows, itemString, operationSettings, lowestAuction.bid, lowestAuction.buyout, perAuction)
if lowestAuction.isPlayer then
reason = "postPlayer"
else
reason = "postWhitelist"
end
bid = lowestAuction.bid
buyout = lowestAuction.buyout
seller = lowestAuction.seller
elseif lowestAuction.isWhitelist then
-- don't undercut a whitelisted player
seller = lowestAuction.seller
TempTable.Release(lowestAuction)
return "postWhitelistNoPost", nil, nil, seller
elseif (lowestAuction.buyout - undercut) > maxPrice then
-- we'd be posting above the max price, so resort to the aboveMax setting
seller = lowestAuction.seller
if operationSettings.aboveMax == "none" then
TempTable.Release(lowestAuction)
return "postAboveMaxNoPost", nil, nil, seller
end
assert(ABOVE_MAX_REASON_LOOKUP[operationSettings.aboveMax], "Unexpected 'above max price' setting: "..tostring(operationSettings.aboveMax))
reason = ABOVE_MAX_REASON_LOOKUP[operationSettings.aboveMax]
buyout = aboveMax
else
-- we just need to do a normal undercut of the lowest auction
reason = "postUndercut"
buyout = lowestAuction.buyout - undercut
seller = lowestAuction.seller
end
if reason == "postBlacklist" then
bid = bid or buyout * operationSettings.bidPercent
else
buyout = max(buyout, minPrice)
bid = max(bid or buyout * operationSettings.bidPercent, minPrice)
end
if lowestAuction then
TempTable.Release(lowestAuction)
end
if TSM.IsWowClassic() then
bid = floor(bid)
else
bid = max(Math.Round(bid, COPPER_PER_SILVER), COPPER_PER_SILVER)
buyout = max(Math.Round(buyout, COPPER_PER_SILVER), COPPER_PER_SILVER)
end
bid = min(bid, TSM.IsWowClassic() and MAXIMUM_BID_PRICE or MAXIMUM_BID_PRICE - 99)
buyout = min(buyout, TSM.IsWowClassic() and MAXIMUM_BID_PRICE or MAXIMUM_BID_PRICE - 99)
-- check if we can't post anymore
local queueQuery = private.queueDB:NewQuery()
:Select("numStacks")
:Equal("itemString", itemString)
:Equal("stackSize", perAuction)
:Equal("itemBuyout", buyout)
for _, numStacks in queueQuery:Iterator() do
activeAuctions = activeAuctions + numStacks
end
queueQuery:Release()
if TSM.IsWowClassic() then
maxCanPost = min(postCap - activeAuctions, maxCanPost)
else
perAuction = min(postCap - activeAuctions, perAuction)
end
if maxCanPost <= 0 or perAuction <= 0 then
return "postTooMany"
end
if TSM.IsWowClassic() and (bid * perAuction > MAXIMUM_BID_PRICE or buyout * perAuction > MAXIMUM_BID_PRICE) then
Log.PrintfUser(L["The buyout price for %s would be above the maximum allowed price. Skipping this item."], ItemInfo.GetLink(itemString))
return "invalidItemGroup"
end
-- insert the posts into our DB
local auctionId = private.nextQueueIndex
local postTime = operationSettings.duration
local extraStack = 0
if TSM.IsWowClassic() then
private.AddToQueue(itemString, operationName, bid, buyout, perAuction, maxCanPost, postTime)
-- check if we can post an extra partial stack
extraStack = (maxCanPost < postCap and operationSettings.stackSizeIsCap and (numHave % perAuction)) or 0
else
assert(maxCanPost == 1)
if ItemInfo.IsCommodity(itemString) then
local maxPerAuction = ItemInfo.GetMaxStack(itemString) * MAX_COMMODITY_STACKS_PER_AUCTION
maxCanPost = floor(perAuction / maxPerAuction)
-- check if we can post an extra partial stack
extraStack = perAuction % maxPerAuction
perAuction = min(perAuction, maxPerAuction)
else
-- post non-commodities as single stacks
maxCanPost = perAuction
perAuction = 1
end
assert(maxCanPost > 0 or extraStack > 0)
if maxCanPost > 0 then
private.AddToQueue(itemString, operationName, bid, buyout, perAuction, maxCanPost, postTime)
end
end
if extraStack > 0 then
private.AddToQueue(itemString, operationName, bid, buyout, extraStack, 1, postTime)
end
return reason, (perAuction * maxCanPost) + extraStack, buyout, seller, auctionId
end
function private.AddToQueue(itemString, operationName, itemBid, itemBuyout, stackSize, numStacks, postTime)
private.DebugLogInsert(itemString, "Queued %d stacks of %d", stackSize, numStacks)
private.queueDB:NewRow()
:SetField("auctionId", private.nextQueueIndex)
:SetField("itemString", itemString)
:SetField("operationName", operationName)
:SetField("bid", itemBid * stackSize)
:SetField("buyout", itemBuyout * stackSize)
:SetField("itemBuyout", itemBuyout)
:SetField("stackSize", stackSize)
:SetField("numStacks", numStacks)
:SetField("postTime", postTime)
:SetField("numProcessed", 0)
:SetField("numConfirmed", 0)
:SetField("numFailed", 0)
:Create()
private.nextQueueIndex = private.nextQueueIndex + 1
end
-- ============================================================================
-- Private Helper Functions for Posting
-- ============================================================================
function private.GetPostBagSlot(itemString, quantity)
-- start with the slot which is closest to the desired stack size
local bag, slot = private.bagDB:NewQuery()
:Select("bag", "slot")
:Equal("itemString", itemString)
:GreaterThanOrEqual("quantity", quantity)
:OrderBy("quantity", true)
:GetFirstResultAndRelease()
if not bag then
bag, slot = private.bagDB:NewQuery()
:Select("bag", "slot")
:Equal("itemString", itemString)
:LessThanOrEqual("quantity", quantity)
:OrderBy("quantity", false)
:GetFirstResultAndRelease()
end
if not bag or not slot then
-- this item was likely removed from the player's bags, so just give up
Log.Err("Failed to find initial bag / slot (%s, %d)", itemString, quantity)
return nil, true
end
local removeContext = TempTable.Acquire()
bag, slot = private.ItemBagSlotHelper(itemString, bag, slot, quantity, removeContext)
local bagItemString = ItemString.Get(GetContainerItemLink(bag, slot))
if not bagItemString or TSM.Groups.TranslateItemString(bagItemString) ~= itemString then
-- something changed with the player's bags so we can't post the item right now
TempTable.Release(removeContext)
private.DebugLogInsert(itemString, "Bags changed")
return nil, nil
end
local _, _, _, quality = GetContainerItemInfo(bag, slot)
assert(quality)
if quality == -1 then
-- the game client doesn't have item info cached for this item, so we can't post it yet
TempTable.Release(removeContext)
private.DebugLogInsert(itemString, "No item info")
return nil, nil
end
for slotId, removeQuantity in pairs(removeContext) do
private.RemoveBagQuantity(slotId, removeQuantity)
end
TempTable.Release(removeContext)
private.DebugLogInsert(itemString, "GetPostBagSlot(%d) -> %d, %d", quantity, bag, slot)
return bag, slot
end
function private.ItemBagSlotHelper(itemString, bag, slot, quantity, removeContext)
local slotId = SlotId.Join(bag, slot)
-- try to post completely from the selected slot
local found = private.bagDB:NewQuery()
:Select("slotId")
:Equal("slotId", slotId)
:GreaterThanOrEqual("quantity", quantity)
:GetFirstResultAndRelease()
if found then
removeContext[slotId] = quantity
return bag, slot
end
-- try to find a stack at a lower slot which has enough to post from
local foundSlotId, foundBag, foundSlot = private.bagDB:NewQuery()
:Select("slotId", "bag", "slot")
:Equal("itemString", itemString)
:LessThan("slotId", slotId)
:GreaterThanOrEqual("quantity", quantity)
:OrderBy("slotId", true)
:GetFirstResultAndRelease()
if foundSlotId then
removeContext[foundSlotId] = quantity
return foundBag, foundSlot
end
-- try to post using the selected slot and the lower slots
local selectedQuantity = private.bagDB:NewQuery()
:Select("quantity")
:Equal("slotId", slotId)
:GetFirstResultAndRelease()
local query = private.bagDB:NewQuery()
:Select("slotId", "quantity")
:Equal("itemString", itemString)
:LessThan("slotId", slotId)
:OrderBy("slotId", true)
local numNeeded = quantity - selectedQuantity
local numUsed = 0
local usedSlotIds = TempTable.Acquire()
for _, rowSlotId, rowQuantity in query:Iterator() do
if numNeeded ~= numUsed then
numUsed = min(numUsed + rowQuantity, numNeeded)
tinsert(usedSlotIds, rowSlotId)
end
end
query:Release()
if numNeeded == numUsed then
removeContext[slotId] = selectedQuantity
for _, rowSlotId in TempTable.Iterator(usedSlotIds) do
local rowQuantity = private.bagDB:GetUniqueRowField("slotId", rowSlotId, "quantity")
local rowNumUsed = min(numUsed, rowQuantity)
numUsed = numUsed - rowNumUsed
removeContext[rowSlotId] = (removeContext[rowSlotId] or 0) + rowNumUsed
end
return bag, slot
else
TempTable.Release(usedSlotIds)
end
-- try posting from the next highest slot
local rowBag, rowSlot = private.bagDB:NewQuery()
:Select("bag", "slot")
:Equal("itemString", itemString)
:GreaterThan("slotId", slotId)
:OrderBy("slotId", true)
:GetFirstResultAndRelease()
if not rowBag or not rowSlot then
private.ErrorForItem(itemString, "Failed to find next highest bag / slot")
end
return private.ItemBagSlotHelper(itemString, rowBag, rowSlot, quantity, removeContext)
end
function private.RemoveBagQuantity(slotId, quantity)
local row = private.bagDB:GetUniqueRow("slotId", slotId)
local remainingQuantity = row:GetField("quantity") - quantity
private.DebugLogInsert(row:GetField("itemString"), "Removing %d (%d remain) from %d", quantity, remainingQuantity, slotId)
if remainingQuantity > 0 then
row:SetField("quantity", remainingQuantity)
:Update()
else
assert(remainingQuantity == 0)
private.bagDB:DeleteRow(row)
end
row:Release()
end
function private.ConfirmRowQueryHelper(row)
return row:GetField("numConfirmed") < row:GetField("numProcessed")
end
function private.NextProcessRowQueryHelper(row)
return row:GetField("numProcessed") < row:GetField("numStacks")
end
function private.DebugLogInsert(itemString, ...)
tinsert(private.debugLog, itemString)
tinsert(private.debugLog, format(...))
end
function private.ErrorForItem(itemString, errorStr)
for i = 1, #private.debugLog, 2 do
if private.debugLog[i] == itemString then
Log.Info(private.debugLog[i + 1])
end
end
Log.Info("Bag state:")
for b = 0, NUM_BAG_SLOTS do
for s = 1, GetContainerNumSlots(b) do
if ItemString.GetBase(GetContainerItemLink(b, s)) == itemString then
local _, q = GetContainerItemInfo(b, s)
Log.Info("%d in %d, %d", q, b, s)
end
end
end
error(errorStr, 2)
end

View File

@ -0,0 +1,204 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local SavedSearches = TSM.Auctioning:NewPackage("SavedSearches")
local L = TSM.Include("Locale").GetTable()
local Log = TSM.Include("Util.Log")
local String = TSM.Include("Util.String")
local Database = TSM.Include("Util.Database")
local TempTable = TSM.Include("Util.TempTable")
local Theme = TSM.Include("Util.Theme")
local Settings = TSM.Include("Service.Settings")
local private = {
settings = nil,
db = nil,
}
local FILTER_SEP = "\001"
local MAX_RECENT_SEARCHES = 500
-- ============================================================================
-- Module Functions
-- ============================================================================
function SavedSearches.OnInitialize()
private.settings = Settings.NewView()
:AddKey("global", "userData", "savedAuctioningSearches")
-- remove duplicates
local seen = TempTable.Acquire()
for i = #private.settings.savedAuctioningSearches.filters, 1, -1 do
local filter = private.settings.savedAuctioningSearches.filters[i]
if seen[filter] then
tremove(private.settings.savedAuctioningSearches.filters, i)
tremove(private.settings.savedAuctioningSearches.searchTypes, i)
private.settings.savedAuctioningSearches.name[filter] = nil
private.settings.savedAuctioningSearches.isFavorite[filter] = nil
else
seen[filter] = true
end
end
TempTable.Release(seen)
-- remove old recent searches
local remainingRecentSearches = MAX_RECENT_SEARCHES
local numRemoved = 0
for i = #private.settings.savedAuctioningSearches.filters, 1, -1 do
local filter = private.settings.savedAuctioningSearches.filters
if not private.settings.savedAuctioningSearches.isFavorite[filter] then
if remainingRecentSearches > 0 then
remainingRecentSearches = remainingRecentSearches - 1
else
tremove(private.settings.savedAuctioningSearches.filters, i)
tremove(private.settings.savedAuctioningSearches.searchTypes, i)
private.settings.savedAuctioningSearches.name[filter] = nil
private.settings.savedAuctioningSearches.isFavorite[filter] = nil
numRemoved = numRemoved + 1
end
end
end
if numRemoved > 0 then
Log.Info("Removed %d old recent searches", numRemoved)
end
private.db = Database.NewSchema("AUCTIONING_SAVED_SEARCHES")
:AddUniqueNumberField("index")
:AddBooleanField("isFavorite")
:AddStringField("searchType")
:AddStringField("filter")
:AddStringField("name")
:AddIndex("index")
:Commit()
private.RebuildDB()
end
function SavedSearches.CreateRecentSearchesQuery()
return private.db:NewQuery()
:OrderBy("index", false)
end
function SavedSearches.CreateFavoriteSearchesQuery()
return private.db:NewQuery()
:Equal("isFavorite", true)
:OrderBy("name", true)
end
function SavedSearches.SetSearchIsFavorite(dbRow, isFavorite)
local filter = dbRow:GetField("filter")
private.settings.savedAuctioningSearches.isFavorite[filter] = isFavorite or nil
dbRow:SetField("isFavorite", isFavorite)
:Update()
end
function SavedSearches.RenameSearch(dbRow, newName)
local filter = dbRow:GetField("filter")
private.settings.savedAuctioningSearches.name[filter] = newName
dbRow:SetField("name", newName)
:Update()
end
function SavedSearches.DeleteSearch(dbRow)
local index, filter = dbRow:GetFields("index", "filter")
tremove(private.settings.savedAuctioningSearches.filters, index)
tremove(private.settings.savedAuctioningSearches.searchTypes, index)
private.settings.savedAuctioningSearches.name[filter] = nil
private.settings.savedAuctioningSearches.isFavorite[filter] = nil
private.RebuildDB()
end
function SavedSearches.RecordSearch(searchList, searchType)
assert(searchType == "postItems" or searchType == "postGroups" or searchType == "cancelGroups")
local filter = table.concat(searchList, FILTER_SEP)
for i, existingFilter in ipairs(private.settings.savedAuctioningSearches.filters) do
local existingSearchType = private.settings.savedAuctioningSearches.searchTypes[i]
if filter == existingFilter and searchType == existingSearchType then
-- move this to the end of the list and rebuild the DB
-- insert the existing filter so we don't need to update the isFavorite and name tables
tremove(private.settings.savedAuctioningSearches.filters, i)
tinsert(private.settings.savedAuctioningSearches.filters, existingFilter)
tremove(private.settings.savedAuctioningSearches.searchTypes, i)
tinsert(private.settings.savedAuctioningSearches.searchTypes, existingSearchType)
private.RebuildDB()
return
end
end
-- didn't find an existing entry, so add a new one
tinsert(private.settings.savedAuctioningSearches.filters, filter)
tinsert(private.settings.savedAuctioningSearches.searchTypes, searchType)
assert(#private.settings.savedAuctioningSearches.filters == #private.settings.savedAuctioningSearches.searchTypes)
private.db:NewRow()
:SetField("index", #private.settings.savedAuctioningSearches.filters)
:SetField("isFavorite", false)
:SetField("searchType", searchType)
:SetField("filter", filter)
:SetField("name", private.GetSearchName(filter, searchType))
:Create()
end
function SavedSearches.FiltersToTable(dbRow, tbl)
String.SafeSplit(dbRow:GetField("filter"), FILTER_SEP, tbl)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.RebuildDB()
assert(#private.settings.savedAuctioningSearches.filters == #private.settings.savedAuctioningSearches.searchTypes)
private.db:TruncateAndBulkInsertStart()
for index, filter in ipairs(private.settings.savedAuctioningSearches.filters) do
local searchType = private.settings.savedAuctioningSearches.searchTypes[index]
assert(searchType == "postItems" or searchType == "postGroups" or searchType == "cancelGroups")
local name = private.settings.savedAuctioningSearches.name[filter] or private.GetSearchName(filter, searchType)
local isFavorite = private.settings.savedAuctioningSearches.isFavorite[filter] and true or false
private.db:BulkInsertNewRow(index, isFavorite, searchType, filter, name)
end
private.db:BulkInsertEnd()
end
function private.GetSearchName(filter, searchType)
local filters = TempTable.Acquire()
local searchTypeStr, numFiltersStr = nil, nil
if filter == "" or string.sub(filter, 1, 1) == FILTER_SEP then
tinsert(filters, L["Base Group"])
end
if searchType == "postGroups" or searchType == "cancelGroups" then
for groupPath in gmatch(filter, "[^"..FILTER_SEP.."]+") do
local groupName = TSM.Groups.Path.GetName(groupPath)
local level = select('#', strsplit(TSM.CONST.GROUP_SEP, groupPath))
local color = Theme.GetGroupColor(level)
tinsert(filters, color:ColorText(groupName))
end
searchTypeStr = searchType == "postGroups" and L["Post Scan"] or L["Cancel Scan"]
numFiltersStr = #filters == 1 and L["1 Group"] or format(L["%d Groups"], #filters)
elseif searchType == "postItems" then
local numItems = 0
for itemString in gmatch(filter, "[^"..FILTER_SEP.."]+") do
numItems = numItems + 1
local coloredName = TSM.UI.GetColoredItemName(itemString)
if coloredName then
tinsert(filters, coloredName)
end
end
searchTypeStr = L["Post Scan"]
numFiltersStr = numItems == 1 and L["1 Item"] or format(L["%d Items"], numItems)
else
error("Unknown searchType: "..tostring(searchType))
end
local groupList = nil
if #filters > 10 then
groupList = table.concat(filters, ", ", 1, 10)..",..."
TempTable.Release(filters)
else
groupList = strjoin(", ", TempTable.UnpackAndRelease(filters))
end
return format("%s (%s): %s", searchTypeStr, numFiltersStr, groupList)
end

View File

@ -0,0 +1,326 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Util = TSM.Auctioning:NewPackage("Util")
local TempTable = TSM.Include("Util.TempTable")
local Vararg = TSM.Include("Util.Vararg")
local String = TSM.Include("Util.String")
local Math = TSM.Include("Util.Math")
local CustomPrice = TSM.Include("Service.CustomPrice")
local PlayerInfo = TSM.Include("Service.PlayerInfo")
local private = {
priceCache = {},
}
local INVALID_PRICE = {}
local VALID_PRICE_KEYS = {
minPrice = true,
normalPrice = true,
maxPrice = true,
undercut = true,
cancelRepostThreshold = true,
priceReset = true,
aboveMax = true,
postCap = true,
stackSize = true,
keepQuantity = true,
maxExpires = true,
}
local IS_GOLD_PRICE_KEY = {
minPrice = true,
normalPrice = true,
maxPrice = true,
undercut = TSM.IsWowClassic(),
priceReset = true,
aboveMax = true,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Util.GetPrice(key, operation, itemString)
assert(VALID_PRICE_KEYS[key])
local cacheKey = key..tostring(operation)..itemString
if private.priceCache.updateTime ~= GetTime() then
wipe(private.priceCache)
private.priceCache.updateTime = GetTime()
end
if not private.priceCache[cacheKey] then
local value = nil
if key == "aboveMax" or key == "priceReset" then
-- redirect to the selected price (if applicable)
local priceKey = operation[key]
if VALID_PRICE_KEYS[priceKey] then
value = Util.GetPrice(priceKey, operation, itemString)
end
else
value = CustomPrice.GetValue(operation[key], itemString, not IS_GOLD_PRICE_KEY[key])
end
if not TSM.IsWowClassic() and IS_GOLD_PRICE_KEY[key] then
value = value and Math.Ceil(value, COPPER_PER_SILVER) or nil
else
value = value and Math.Round(value) or nil
end
local minValue, maxValue = TSM.Operations.Auctioning.GetMinMaxValues(key)
private.priceCache[cacheKey] = (value and value >= minValue and value <= maxValue) and value or INVALID_PRICE
end
if private.priceCache[cacheKey] == INVALID_PRICE then
return nil
end
return private.priceCache[cacheKey]
end
function Util.GetLowestAuction(subRows, itemString, operationSettings, resultTbl)
if not TSM.IsWowClassic() then
local foundLowest = false
for _, subRow in ipairs(subRows) do
local _, itemBuyout = subRow:GetBuyouts()
local quantity = subRow:GetQuantities()
local timeLeft = subRow:GetListingInfo()
if not foundLowest and not Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft) then
local ownerStr = subRow:GetOwnerInfo()
local _, auctionId = subRow:GetListingInfo()
local _, itemMinBid = subRow:GetBidInfo()
local firstSeller = strsplit(",", ownerStr)
resultTbl.buyout = itemBuyout
resultTbl.bid = itemMinBid
resultTbl.seller = firstSeller
resultTbl.auctionId = auctionId
resultTbl.isWhitelist = TSM.db.factionrealm.auctioningOptions.whitelist[strlower(firstSeller)] and true or false
resultTbl.isPlayer = PlayerInfo.IsPlayer(firstSeller, true, true, true)
if not subRow:HasOwners() then
resultTbl.hasInvalidSeller = true
end
foundLowest = true
end
end
return foundLowest
else
local hasInvalidSeller = nil
local ignoreWhitelist = nil
local lowestItemBuyout = nil
local lowestAuction = nil
for _, subRow in ipairs(subRows) do
local _, itemBuyout = subRow:GetBuyouts()
local quantity = subRow:GetQuantities()
local timeLeft = subRow:GetListingInfo()
if not Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft) then
assert(itemBuyout and itemBuyout > 0)
lowestItemBuyout = lowestItemBuyout or itemBuyout
if itemBuyout == lowestItemBuyout then
local ownerStr = subRow:GetOwnerInfo()
local _, auctionId = subRow:GetListingInfo()
local _, itemMinBid = subRow:GetBidInfo()
local temp = TempTable.Acquire()
temp.buyout = itemBuyout
temp.bid = itemMinBid
temp.seller = ownerStr
temp.auctionId = auctionId
temp.isWhitelist = TSM.db.factionrealm.auctioningOptions.whitelist[strlower(ownerStr)] and true or false
temp.isPlayer = PlayerInfo.IsPlayer(ownerStr, true, true, true)
if not temp.isWhitelist and not temp.isPlayer then
-- there is a non-whitelisted competitor, so we don't care if a whitelisted competitor also posts at this price
ignoreWhitelist = true
end
if not subRow:HasOwners() and next(TSM.db.factionrealm.auctioningOptions.whitelist) then
hasInvalidSeller = true
end
if operationSettings.blacklist then
for _, player in Vararg.Iterator(strsplit(",", operationSettings.blacklist)) do
if String.SeparatedContains(strlower(ownerStr), ",", strlower(strtrim(player))) then
temp.isBlacklist = true
end
end
end
if not lowestAuction then
lowestAuction = temp
elseif private.LowestAuctionCompare(temp, lowestAuction) then
TempTable.Release(lowestAuction)
lowestAuction = temp
else
TempTable.Release(temp)
end
end
end
end
if not lowestAuction then
return false
end
for k, v in pairs(lowestAuction) do
resultTbl[k] = v
end
TempTable.Release(lowestAuction)
if resultTbl.isWhitelist and ignoreWhitelist then
resultTbl.isWhitelist = false
end
resultTbl.hasInvalidSeller = hasInvalidSeller
return true
end
end
function Util.GetPlayerAuctionCount(subRows, itemString, operationSettings, findBid, findBuyout, findStackSize)
local playerQuantity = 0
for _, subRow in ipairs(subRows) do
local _, itemBuyout = subRow:GetBuyouts()
local quantity = subRow:GetQuantities()
local timeLeft = subRow:GetListingInfo()
if not Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft) then
local _, itemMinBid = subRow:GetBidInfo()
if itemMinBid == findBid and itemBuyout == findBuyout and (not TSM.IsWowClassic() or quantity == findStackSize) then
local count = private.GetPlayerAuctionCount(subRow)
if not TSM.IsWowClassic() and count == 0 and playerQuantity > 0 then
-- there's another player's auction after ours, so stop counting
break
end
playerQuantity = playerQuantity + count
end
end
end
return playerQuantity
end
function Util.GetPlayerLowestBuyout(subRows, itemString, operationSettings)
local lowestItemBuyout, lowestItemAuctionId = nil, nil
for _, subRow in ipairs(subRows) do
local _, itemBuyout = subRow:GetBuyouts()
local quantity = subRow:GetQuantities()
local timeLeft, auctionId = subRow:GetListingInfo()
if not lowestItemBuyout and not Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft) and private.GetPlayerAuctionCount(subRow) > 0 then
lowestItemBuyout = itemBuyout
lowestItemAuctionId = auctionId
end
end
return lowestItemBuyout, lowestItemAuctionId
end
function Util.GetLowestNonPlayerAuctionId(subRows, itemString, operationSettings, lowestItemBuyout)
local lowestItemAuctionId = nil
for _, subRow in ipairs(subRows) do
local _, itemBuyout = subRow:GetBuyouts()
local quantity = subRow:GetQuantities()
local timeLeft, auctionId = subRow:GetListingInfo()
if not lowestItemAuctionId and not Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft) and private.GetPlayerAuctionCount(subRow) == 0 and itemBuyout == lowestItemBuyout then
lowestItemAuctionId = auctionId
end
end
return lowestItemAuctionId
end
function Util.IsPlayerOnlySeller(subRows, itemString, operationSettings)
local isOnly = true
for _, subRow in ipairs(subRows) do
local _, itemBuyout = subRow:GetBuyouts()
local quantity = subRow:GetQuantities()
local timeLeft = subRow:GetListingInfo()
if isOnly and not Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft) and private.GetPlayerAuctionCount(subRow) < (TSM.IsWowClassic() and 1 or quantity) then
isOnly = false
end
end
return isOnly
end
function Util.GetNextLowestItemBuyout(subRows, itemString, lowestAuction, operationSettings)
local nextLowestItemBuyout = nil
for _, subRow in ipairs(subRows) do
local _, itemBuyout = subRow:GetBuyouts()
local quantity = subRow:GetQuantities()
local timeLeft, auctionId = subRow:GetListingInfo()
local isLower = itemBuyout > lowestAuction.buyout or (itemBuyout == lowestAuction.buyout and auctionId < lowestAuction.auctionId)
if not nextLowestItemBuyout and not Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft) and isLower then
nextLowestItemBuyout = itemBuyout
end
end
return nextLowestItemBuyout
end
function Util.GetQueueStatus(query)
local numProcessed, numConfirmed, numFailed, totalNum = 0, 0, 0, 0
query:OrderBy("auctionId", true)
for _, row in query:Iterator() do
local rowNumStacks, rowNumProcessed, rowNumConfirmed, rowNumFailed = row:GetFields("numStacks", "numProcessed", "numConfirmed", "numFailed")
totalNum = totalNum + rowNumStacks
numProcessed = numProcessed + rowNumProcessed
numConfirmed = numConfirmed + rowNumConfirmed
numFailed = numFailed + rowNumFailed
end
query:Release()
return numProcessed, numConfirmed, numFailed, totalNum
end
function Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft)
if timeLeft <= operationSettings.ignoreLowDuration then
-- ignoring low duration
return true
elseif TSM.IsWowClassic() and operationSettings.matchStackSize and quantity ~= Util.GetPrice("stackSize", operationSettings, itemString) then
-- matching stack size
return true
elseif operationSettings.priceReset == "ignore" then
local minPrice = Util.GetPrice("minPrice", operationSettings, itemString)
local undercut = Util.GetPrice("undercut", operationSettings, itemString)
if minPrice and itemBuyout - undercut < minPrice then
-- ignoring auctions below threshold
return true
end
end
return false
end
function Util.GetFilteredSubRows(query, itemString, operationSettings, result)
for _, subRow in query:ItemSubRowIterator(itemString) do
local _, itemBuyout = subRow:GetBuyouts()
local quantity = subRow:GetQuantities()
local timeLeft = subRow:GetListingInfo()
if not Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft) then
tinsert(result, subRow)
end
end
sort(result, private.SubRowSortHelper)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.SubRowSortHelper(a, b)
local _, aItemBuyout = a:GetBuyouts()
local _, bItemBuyout = b:GetBuyouts()
if aItemBuyout ~= bItemBuyout then
return aItemBuyout < bItemBuyout
end
local _, aAuctionId = a:GetListingInfo()
local _, bAuctionId = b:GetListingInfo()
return aAuctionId > bAuctionId
end
function private.LowestAuctionCompare(a, b)
if a.isBlacklist ~= b.isBlacklist then
return a.isBlacklist
end
if a.isWhitelist ~= b.isWhitelist then
return a.isWhitelist
end
if a.auctionId ~= b.auctionId then
return a.auctionId > b.auctionId
end
if a.isPlayer ~= b.isPlayer then
return b.isPlayer
end
return tostring(a) < tostring(b)
end
function private.GetPlayerAuctionCount(subRow)
local ownerStr, numOwnerItems = subRow:GetOwnerInfo()
if TSM.IsWowClassic() then
return PlayerInfo.IsPlayer(ownerStr, true, true, true) and select(2, subRow:GetQuantities()) or 0
else
return numOwnerItems
end
end

View File

@ -0,0 +1,123 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Auctioning = TSM.Banking:NewPackage("Auctioning")
local TempTable = TSM.Include("Util.TempTable")
local BagTracking = TSM.Include("Service.BagTracking")
local Inventory = TSM.Include("Service.Inventory")
local private = {}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Auctioning.MoveGroupsToBank(callback, groups)
local items = TempTable.Acquire()
TSM.Banking.Util.PopulateGroupItemsFromBags(items, groups, private.GroupsGetNumToMoveToBank)
TSM.Banking.MoveToBank(items, callback)
TempTable.Release(items)
end
function Auctioning.PostCapToBags(callback, groups)
local items = TempTable.Acquire()
TSM.Banking.Util.PopulateGroupItemsFromOpenBank(items, groups, private.GetNumToMoveToBags)
TSM.Banking.MoveToBag(items, callback)
TempTable.Release(items)
end
function Auctioning.ShortfallToBags(callback, groups)
local items = TempTable.Acquire()
TSM.Banking.Util.PopulateGroupItemsFromOpenBank(items, groups, private.GetNumToMoveToBags, true)
TSM.Banking.MoveToBag(items, callback)
TempTable.Release(items)
end
function Auctioning.MaxExpiresToBank(callback, groups)
local items = TempTable.Acquire()
TSM.Banking.Util.PopulateGroupItemsFromBags(items, groups, private.MaxExpiresGetNumToMoveToBank)
TSM.Banking.MoveToBank(items, callback)
TempTable.Release(items)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GroupsGetNumToMoveToBank(itemString, numHave)
-- move everything
return numHave
end
function private.GetNumToMoveToBags(itemString, numHave, includeAH)
local totalNumToMove = 0
local numAvailable = numHave
local numInBags = BagTracking.CreateQueryBagsItem(itemString)
:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
:Equal("autoBaseItemString", itemString)
:SumAndRelease("quantity") or 0
if includeAH then
numInBags = numInBags + select(3, Inventory.GetPlayerTotals(itemString)) + Inventory.GetMailQuantity(itemString)
end
for _, _, operationSettings in TSM.Operations.GroupOperationIterator("Auctioning", TSM.Groups.GetPathByItem(itemString)) do
local maxExpires = TSM.Auctioning.Util.GetPrice("maxExpires", operationSettings, itemString)
local operationHasExpired = false
if maxExpires and maxExpires > 0 then
local numExpires = TSM.Accounting.Auctions.GetNumExpiresSinceSale(itemString)
if numExpires and numExpires > maxExpires then
operationHasExpired = true
end
end
local postCap = TSM.Auctioning.Util.GetPrice("postCap", operationSettings, itemString)
local stackSize = (TSM.IsWowClassic() and TSM.Auctioning.Util.GetPrice("stackSize", operationSettings, itemString)) or (not TSM.IsWowClassic() and 1)
if not operationHasExpired and postCap and stackSize then
local numNeeded = stackSize * postCap
if numInBags > numNeeded then
-- we can satisfy this operation from the bags
numInBags = numInBags - numNeeded
numNeeded = 0
elseif numInBags > 0 then
-- we can partially satisfy this operation from the bags
numNeeded = numNeeded - numInBags
numInBags = 0
end
local numToMove = min(numAvailable, numNeeded)
if numToMove > 0 then
numAvailable = numAvailable - numToMove
totalNumToMove = totalNumToMove + numToMove
end
end
end
return totalNumToMove
end
function private.MaxExpiresGetNumToMoveToBank(itemString, numHave)
local numToKeepInBags = 0
for _, _, operationSettings in TSM.Operations.GroupOperationIterator("Auctioning", TSM.Groups.GetPathByItem(itemString)) do
local maxExpires = TSM.Auctioning.Util.GetPrice("maxExpires", operationSettings, itemString)
local operationHasExpired = false
if maxExpires and maxExpires > 0 then
local numExpires = TSM.Accounting.Auctions.GetNumExpiresSinceSale(itemString)
if numExpires and numExpires > maxExpires then
operationHasExpired = true
end
end
local postCap = TSM.Auctioning.Util.GetPrice("postCap", operationSettings, itemString)
local stackSize = (TSM.IsWowClassic() and TSM.Auctioning.Util.GetPrice("stackSize", operationSettings, itemString)) or (not TSM.IsWowClassic() and 1)
if not operationHasExpired and postCap and stackSize then
numToKeepInBags = numToKeepInBags + stackSize * postCap
end
end
return max(numHave - numToKeepInBags, 0)
end

View File

@ -0,0 +1,321 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Banking = TSM:NewPackage("Banking")
local Event = TSM.Include("Util.Event")
local TempTable = TSM.Include("Util.TempTable")
local String = TSM.Include("Util.String")
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 private = {
moveThread = nil,
moveItems = {},
restoreItems = {},
restoreFrame = nil,
callback = nil,
openFrame = nil,
frameCallbacks = {},
}
local MOVE_WAIT_TIMEOUT = 2
-- ============================================================================
-- Module Functions
-- ============================================================================
function Banking.OnInitialize()
private.moveThread = Threading.New("BANKING_MOVE", private.MoveThread)
Event.Register("BANKFRAME_OPENED", private.BankOpened)
Event.Register("BANKFRAME_CLOSED", private.BankClosed)
if not TSM.IsWowClassic() then
Event.Register("GUILDBANKFRAME_OPENED", private.GuildBankOpened)
Event.Register("GUILDBANKFRAME_CLOSED", private.GuildBankClosed)
end
end
function Banking.RegisterFrameCallback(callback)
tinsert(private.frameCallbacks, callback)
end
function Banking.IsGuildBankOpen()
return private.openFrame == "GUILD_BANK"
end
function Banking.IsBankOpen()
return private.openFrame == "BANK"
end
function Banking.MoveToBag(items, callback)
assert(private.openFrame)
local context = Banking.IsGuildBankOpen() and Banking.MoveContext.GetGuildBankToBag() or Banking.MoveContext.GetBankToBag()
private.StartMove(items, context, callback)
end
function Banking.MoveToBank(items, callback)
assert(private.openFrame)
local context = Banking.IsGuildBankOpen() and Banking.MoveContext.GetBagToGuildBank() or Banking.MoveContext.GetBagToBank()
private.StartMove(items, context, callback)
end
function Banking.EmptyBags(callback)
assert(private.openFrame)
local items = TempTable.Acquire()
for _, _, _, itemString, quantity in Banking.Util.BagIterator(false) do
items[itemString] = (items[itemString] or 0) + quantity
end
wipe(private.restoreItems)
private.restoreFrame = private.openFrame
private.callback = callback
local context = Banking.IsGuildBankOpen() and Banking.MoveContext.GetBagToGuildBank() or Banking.MoveContext.GetBagToBank()
private.StartMove(items, context, private.EmptyBagsThreadCallbackWrapper)
TempTable.Release(items)
end
function Banking.RestoreBags(callback)
assert(private.openFrame)
assert(Banking.CanRestoreBags())
private.callback = callback
local context = Banking.IsGuildBankOpen() and Banking.MoveContext.GetGuildBankToBag() or Banking.MoveContext.GetBankToBag()
private.StartMove(private.restoreItems, context, private.RestoreBagsThreadCallbackWrapper)
end
function Banking.CanRestoreBags()
assert(private.openFrame)
return private.openFrame == private.restoreFrame
end
function Banking.PutByFilter(filterStr)
if not private.openFrame then
return
end
local filterItemString = ItemString.Get(filterStr)
filterStr = String.Escape(strlower(filterStr))
local items = TempTable.Acquire()
for _, _, _, itemString, quantity in Banking.Util.BagIterator(false) do
items[itemString] = (items[itemString] or 0) + quantity
end
for itemString in pairs(items) do
if not private.MatchesFilter(itemString, filterStr, filterItemString) then
-- remove this item
items[itemString] = nil
end
end
Banking.MoveToBank(items, private.GetPutCallback)
TempTable.Release(items)
end
function Banking.GetByFilter(filterStr)
if not private.openFrame then
return
end
local filterItemString = ItemString.Get(filterStr)
filterStr = String.Escape(strlower(filterStr))
local items = TempTable.Acquire()
for _, _, _, itemString, quantity in Banking.Util.OpenBankIterator(false) do
items[itemString] = (items[itemString] or 0) + quantity
end
for itemString in pairs(items) do
if not private.MatchesFilter(itemString, filterStr, filterItemString) then
-- remove this item
items[itemString] = nil
end
end
Banking.MoveToBag(items, private.GetPutCallback)
TempTable.Release(items)
end
-- ============================================================================
-- Threads
-- ============================================================================
function private.MoveThread(context, callback)
local numMoves = 0
local emptySlotIds = Threading.AcquireSafeTempTable()
context:GetEmptySlotsThreaded(emptySlotIds)
local slotIds = Threading.AcquireSafeTempTable()
local slotItemString = Threading.AcquireSafeTempTable()
local slotMoveQuantity = Threading.AcquireSafeTempTable()
local slotEndQuantity = Threading.AcquireSafeTempTable()
for itemString, numQueued in pairs(private.moveItems) do
for _, slotId, quantity in context:SlotIdIterator(itemString) do
if numQueued > 0 then
-- find a suitable empty slot
local targetSlotId = context:GetTargetSlotId(itemString, emptySlotIds)
if targetSlotId then
assert(not slotIds[slotId])
slotIds[slotId] = targetSlotId
slotItemString[slotId] = itemString
slotMoveQuantity[slotId] = min(quantity, numQueued)
slotEndQuantity[slotId] = max(quantity - numQueued, 0)
numQueued = numQueued - slotMoveQuantity[slotId]
numMoves = numMoves + 1
else
Log.Err("No target slot")
end
end
end
if numQueued > 0 then
Log.Err("No slots with item (%s)", itemString)
end
end
local numDone = 0
while next(slotIds) do
local movedSlotId = nil
-- do all the pending moves
for slotId, targetSlotId in pairs(slotIds) do
context:MoveSlot(slotId, targetSlotId, slotMoveQuantity[slotId])
Threading.Yield()
if private.openFrame == "GUILD_BANK" then
movedSlotId = slotId
break
end
end
-- wait for at least one to finish or the timeout to elapse
local didMove = false
local timeout = GetTime() + MOVE_WAIT_TIMEOUT
while not didMove and GetTime() < timeout do
-- check which moves are done
for slotId in pairs(slotIds) do
if private.openFrame ~= "GUILD_BANK" or slotId == movedSlotId then
if context:GetSlotQuantity(slotId) <= slotEndQuantity[slotId] then
didMove = true
slotIds[slotId] = nil
numDone = numDone + 1
callback("MOVED", slotItemString[slotId], slotMoveQuantity[slotId])
end
if didMove and slotId == movedSlotId then
break
end
Threading.Yield()
end
end
if didMove then
callback("PROGRESS", numDone / numMoves)
end
Threading.Yield(true)
end
end
if private.openFrame == "GUILD_BANK" then
QueryGuildBankTab(GetCurrentGuildBankTab())
end
Threading.ReleaseSafeTempTable(slotIds)
Threading.ReleaseSafeTempTable(slotItemString)
Threading.ReleaseSafeTempTable(slotMoveQuantity)
Threading.ReleaseSafeTempTable(slotEndQuantity)
Threading.ReleaseSafeTempTable(emptySlotIds)
callback("DONE")
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.BankOpened()
if private.openFrame == "BANK" then
return
end
assert(not private.openFrame)
private.openFrame = "BANK"
for _, callback in ipairs(private.frameCallbacks) do
callback(private.openFrame)
end
end
function private.GuildBankOpened()
if private.openFrame == "GUILD_BANK" then
return
end
assert(not private.openFrame)
private.openFrame = "GUILD_BANK"
for _, callback in ipairs(private.frameCallbacks) do
callback(private.openFrame)
end
end
function private.BankClosed()
if not private.openFrame then
return
end
private.openFrame = nil
private.StopMove()
for _, callback in ipairs(private.frameCallbacks) do
callback(private.openFrame)
end
end
function private.GuildBankClosed()
if not private.openFrame then
return
end
private.openFrame = nil
private.StopMove()
for _, callback in ipairs(private.frameCallbacks) do
callback(private.openFrame)
end
end
function private.StartMove(items, context, callback)
private.StopMove()
wipe(private.moveItems)
for itemString, quantity in pairs(items) do
private.moveItems[itemString] = quantity
end
Threading.Start(private.moveThread, context, callback)
end
function private.StopMove()
Threading.Kill(private.moveThread)
end
function private.EmptyBagsThreadCallbackWrapper(event, ...)
if event == "MOVED" then
local itemString, numMoved = ...
private.restoreItems[itemString] = (private.restoreItems[itemString] or 0) + numMoved
elseif event == "DONE" then
if not next(private.restoreItems) then
private.restoreFrame = private.openFrame
end
end
private.callback(event, ...)
end
function private.RestoreBagsThreadCallbackWrapper(event, ...)
if event == "DONE" then
wipe(private.restoreItems)
private.restoreFrame = nil
end
private.callback(event, ...)
end
function private.GetPutCallback(event)
if event == "DONE" then
Log.PrintUser(DONE)
end
end
function private.MatchesFilter(itemString, filterStr, filterItemString)
local name = strlower(ItemInfo.GetName(itemString) or "")
return strmatch(ItemString.GetBase(itemString), filterStr) or strmatch(name, filterStr) or (filterItemString and itemString == filterItemString)
end

View File

@ -0,0 +1,105 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Mailing = TSM.Banking:NewPackage("Mailing")
local TempTable = TSM.Include("Util.TempTable")
local Inventory = TSM.Include("Service.Inventory")
local PlayerInfo = TSM.Include("Service.PlayerInfo")
local private = {}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Mailing.MoveGroupsToBank(callback, groups)
local items = TempTable.Acquire()
TSM.Banking.Util.PopulateGroupItemsFromBags(items, groups, private.GroupsGetNumToMoveToBank)
TSM.Banking.MoveToBank(items, callback)
TempTable.Release(items)
end
function Mailing.NongroupToBank(callback)
local items = TempTable.Acquire()
TSM.Banking.Util.PopulateItemsFromBags(items, private.NongroupGetNumToBank)
TSM.Banking.MoveToBank(items, callback)
TempTable.Release(items)
end
function Mailing.TargetShortfallToBags(callback, groups)
local items = TempTable.Acquire()
TSM.Banking.Util.PopulateGroupItemsFromOpenBank(items, groups, private.TargetShortfallGetNumToBags)
TSM.Banking.MoveToBag(items, callback)
TempTable.Release(items)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GroupsGetNumToMoveToBank(itemString, numHave)
-- move everything
return numHave
end
function private.NongroupGetNumToBank(itemString, numHave)
local hasOperations = false
for _ in TSM.Operations.GroupOperationIterator("Mailing", TSM.Groups.GetPathByItem(itemString)) do
hasOperations = true
end
return not hasOperations and numHave or 0
end
function private.TargetShortfallGetNumToBags(itemString, numHave)
local totalNumToSend = 0
for _, _, operationSettings in TSM.Operations.GroupOperationIterator("Mailing", TSM.Groups.GetPathByItem(itemString)) do
local numAvailable = numHave - operationSettings.keepQty
local numToSend = 0
if numAvailable > 0 then
if operationSettings.maxQtyEnabled then
if operationSettings.restock then
local targetQty = private.GetTargetQuantity(operationSettings.target, itemString, operationSettings.restockSources)
if PlayerInfo.IsPlayer(operationSettings.target) and targetQty <= operationSettings.maxQty then
numToSend = numAvailable
else
numToSend = min(numAvailable, operationSettings.maxQty - targetQty)
end
if PlayerInfo.IsPlayer(operationSettings.target) then
-- if using restock and target == player ensure that subsequent operations don't take reserved bag inventory
numHave = numHave - max((numAvailable - (targetQty - operationSettings.maxQty)), 0)
end
else
numToSend = min(numAvailable, operationSettings.maxQty)
end
else
numToSend = numAvailable
end
end
totalNumToSend = totalNumToSend + numToSend
numHave = numHave - numToSend
end
return totalNumToSend
end
function private.GetTargetQuantity(player, itemString, sources)
if player then
player = strtrim(strmatch(player, "^[^-]+"))
end
local num = Inventory.GetBagQuantity(itemString, player) + Inventory.GetMailQuantity(itemString, player) + Inventory.GetAuctionQuantity(itemString, player)
if sources then
if sources.guild then
num = num + Inventory.GetGuildQuantity(itemString, PlayerInfo.GetPlayerGuild(player))
end
if sources.bank then
num = num + Inventory.GetBankQuantity(itemString, player) + Inventory.GetReagentBankQuantity(itemString, player)
end
end
return num
end

View File

@ -0,0 +1,292 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local MoveContext = TSM.Banking:NewPackage("MoveContext")
local Table = TSM.Include("Util.Table")
local SlotId = TSM.Include("Util.SlotId")
local Threading = TSM.Include("Service.Threading")
local ItemInfo = TSM.Include("Service.ItemInfo")
local InventoryInfo = TSM.Include("Service.InventoryInfo")
local BagTracking = TSM.Include("Service.BagTracking")
local GuildTracking = TSM.Include("Service.GuildTracking")
local private = {
bagToBank = nil,
bankToBag = nil,
bagToGuildBank = nil,
guildBankToBag = nil,
}
-- don't use MAX_GUILDBANK_SLOTS_PER_TAB since it isn't available right away
local GUILD_BANK_TAB_SLOTS = 98
-- ============================================================================
-- BaseMoveContext Class
-- ============================================================================
local BaseMoveContext = TSM.Include("LibTSMClass").DefineClass("BaseMoveContext", nil, "ABSTRACT")
-- ============================================================================
-- BagToBankMoveContext Class
-- ============================================================================
local BagToBankMoveContext = TSM.Include("LibTSMClass").DefineClass("BagToBankMoveContext", BaseMoveContext)
function BagToBankMoveContext.MoveSlot(self, fromSlotId, toSlotId, quantity)
local fromBag, fromSlot = SlotId.Split(fromSlotId)
SplitContainerItem(fromBag, fromSlot, quantity)
if GetCursorInfo() == "item" then
PickupContainerItem(SlotId.Split(toSlotId))
end
ClearCursor()
end
function BagToBankMoveContext.GetSlotQuantity(self, slotId)
return private.BagBankGetSlotQuantity(slotId)
end
function BagToBankMoveContext.SlotIdIterator(self, itemString)
return private.BagSlotIdIterator(itemString)
end
function BagToBankMoveContext.GetEmptySlotsThreaded(self, emptySlotIds)
local sortValue = Threading.AcquireSafeTempTable()
if not TSM.IsWowClassic() then
private.GetEmptySlotsHelper(REAGENTBANK_CONTAINER, emptySlotIds, sortValue)
end
private.GetEmptySlotsHelper(BANK_CONTAINER, emptySlotIds, sortValue)
for bag = NUM_BAG_SLOTS + 1, NUM_BAG_SLOTS + NUM_BANKBAGSLOTS do
private.GetEmptySlotsHelper(bag, emptySlotIds, sortValue)
end
Table.SortWithValueLookup(emptySlotIds, sortValue)
Threading.ReleaseSafeTempTable(sortValue)
end
function BagToBankMoveContext.GetTargetSlotId(self, itemString, emptySlotIds)
return private.BagBankGetTargetSlotId(itemString, emptySlotIds)
end
-- ============================================================================
-- BankToBagMoveContext Class
-- ============================================================================
local BankToBagMoveContext = TSM.Include("LibTSMClass").DefineClass("BankToBagMoveContext", BaseMoveContext)
function BankToBagMoveContext.MoveSlot(self, fromSlotId, toSlotId, quantity)
local fromBag, fromSlot = SlotId.Split(fromSlotId)
SplitContainerItem(fromBag, fromSlot, quantity)
if GetCursorInfo() == "item" then
PickupContainerItem(SlotId.Split(toSlotId))
end
ClearCursor()
end
function BankToBagMoveContext.GetSlotQuantity(self, slotId)
return private.BagBankGetSlotQuantity(slotId)
end
function BankToBagMoveContext.SlotIdIterator(self, itemString)
itemString = TSM.Groups.TranslateItemString(itemString)
return BagTracking.CreateQueryBankItem(itemString)
:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
:Equal("autoBaseItemString", itemString)
:Select("slotId", "quantity")
:IteratorAndRelease()
end
function BankToBagMoveContext.GetEmptySlotsThreaded(self, emptySlotIds)
private.BagGetEmptySlotsThreaded(emptySlotIds)
end
function BankToBagMoveContext.GetTargetSlotId(self, itemString, emptySlotIds)
return private.BagBankGetTargetSlotId(itemString, emptySlotIds)
end
-- ============================================================================
-- BagToGuildBankMoveContext Class
-- ============================================================================
local BagToGuildBankMoveContext = TSM.Include("LibTSMClass").DefineClass("BagToGuildBankMoveContext", BaseMoveContext)
function BagToGuildBankMoveContext.MoveSlot(self, fromSlotId, toSlotId, quantity)
local fromBag, fromSlot = SlotId.Split(fromSlotId)
SplitContainerItem(fromBag, fromSlot, quantity)
if GetCursorInfo() == "item" then
PickupGuildBankItem(SlotId.Split(toSlotId))
end
ClearCursor()
end
function BagToGuildBankMoveContext.GetSlotQuantity(self, slotId)
return private.BagBankGetSlotQuantity(slotId)
end
function BagToGuildBankMoveContext.SlotIdIterator(self, itemString)
return private.BagSlotIdIterator(itemString)
end
function BagToGuildBankMoveContext.GetEmptySlotsThreaded(self, emptySlotIds)
local currentTab = GetCurrentGuildBankTab()
local _, _, _, _, numWithdrawals = GetGuildBankTabInfo(currentTab)
if numWithdrawals == -1 or numWithdrawals >= GUILD_BANK_TAB_SLOTS then
for slot = 1, GUILD_BANK_TAB_SLOTS do
if not GetGuildBankItemInfo(currentTab, slot) then
tinsert(emptySlotIds, SlotId.Join(currentTab, slot))
end
end
end
for tab = 1, GetNumGuildBankTabs() do
if tab ~= currentTab then
-- only use tabs which we have at least enough withdrawals to withdraw every slot
_, _, _, _, numWithdrawals = GetGuildBankTabInfo(tab)
if numWithdrawals == -1 or numWithdrawals >= GUILD_BANK_TAB_SLOTS then
for slot = 1, GUILD_BANK_TAB_SLOTS do
if not GetGuildBankItemInfo(tab, slot) then
tinsert(emptySlotIds, SlotId.Join(tab, slot))
end
end
end
end
end
end
function BagToGuildBankMoveContext.GetTargetSlotId(self, itemString, emptySlotIds)
return tremove(emptySlotIds, 1)
end
-- ============================================================================
-- GuildBankToBagMoveContext Class
-- ============================================================================
local GuildBankToBagMoveContext = TSM.Include("LibTSMClass").DefineClass("GuildBankToBagMoveContext", BaseMoveContext)
function GuildBankToBagMoveContext.MoveSlot(self, fromSlotId, toSlotId, quantity)
local fromTab, fromSlot = SlotId.Split(fromSlotId)
SplitGuildBankItem(fromTab, fromSlot, quantity)
if GetCursorInfo() == "item" then
PickupContainerItem(SlotId.Split(toSlotId))
end
ClearCursor()
end
function GuildBankToBagMoveContext.GetSlotQuantity(self, slotId)
local tab, slot = SlotId.Split(slotId)
QueryGuildBankTab(tab)
local _, quantity = GetGuildBankItemInfo(tab, slot)
return quantity or 0
end
function GuildBankToBagMoveContext.SlotIdIterator(self, itemString)
itemString = TSM.Groups.TranslateItemString(itemString)
return GuildTracking.CreateQueryItem(itemString)
:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
:Equal("autoBaseItemString", itemString)
:Select("slotId", "quantity")
:IteratorAndRelease()
end
function GuildBankToBagMoveContext.GetEmptySlotsThreaded(self, emptySlotIds)
private.BagGetEmptySlotsThreaded(emptySlotIds)
end
function GuildBankToBagMoveContext.GetTargetSlotId(self, itemString, emptySlotIds)
return private.BagBankGetTargetSlotId(itemString, emptySlotIds)
end
-- ============================================================================
-- Module Functions
-- ============================================================================
function MoveContext.GetBagToBank()
private.bagToBank = private.bagToBank or BagToBankMoveContext()
return private.bagToBank
end
function MoveContext.GetBankToBag()
private.bankToBag = private.bankToBag or BankToBagMoveContext()
return private.bankToBag
end
function MoveContext.GetBagToGuildBank()
private.bagToGuildBank = private.bagToGuildBank or BagToGuildBankMoveContext()
return private.bagToGuildBank
end
function MoveContext.GetGuildBankToBag()
private.guildBankToBag = private.guildBankToBag or GuildBankToBagMoveContext()
return private.guildBankToBag
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.BagBankGetSlotQuantity(slotId)
local _, quantity = GetContainerItemInfo(SlotId.Split(slotId))
return quantity or 0
end
function private.BagSlotIdIterator(itemString)
itemString = TSM.Groups.TranslateItemString(itemString)
local query = BagTracking.CreateQueryBagsItem(itemString)
:Select("slotId", "quantity")
:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
:Equal("autoBaseItemString", itemString)
if TSM.Banking.IsGuildBankOpen() then
query:Equal("isBoA", false)
query:Equal("isBoP", false)
end
return query:IteratorAndRelease()
end
function private.BagGetEmptySlotsThreaded(emptySlotIds)
local sortValue = Threading.AcquireSafeTempTable()
for bag = BACKPACK_CONTAINER, NUM_BAG_SLOTS do
private.GetEmptySlotsHelper(bag, emptySlotIds, sortValue)
end
Table.SortWithValueLookup(emptySlotIds, sortValue)
Threading.ReleaseSafeTempTable(sortValue)
end
function private.GetEmptySlotsHelper(bag, emptySlotIds, sortValue)
local isSpecial = nil
if bag == REAGENTBANK_CONTAINER then
isSpecial = true
elseif bag == BACKPACK_CONTAINER or bag == BANK_CONTAINER then
isSpecial = false
else
isSpecial = (GetItemFamily(GetInventoryItemLink("player", ContainerIDToInventoryID(bag))) or 0) ~= 0
end
for slot = 1, GetContainerNumSlots(bag) do
if not GetContainerItemInfo(bag, slot) then
local slotId = SlotId.Join(bag, slot)
tinsert(emptySlotIds, slotId)
sortValue[slotId] = slotId + (isSpecial and 0 or 100000)
end
end
end
function private.BagBankGetTargetSlotId(itemString, emptySlotIds)
for i, slotId in ipairs(emptySlotIds) do
local bag = SlotId.Split(slotId)
if InventoryInfo.ItemWillGoInBag(ItemInfo.GetLink(itemString), bag) then
return tremove(emptySlotIds, i)
end
end
end

View File

@ -0,0 +1,115 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Util = TSM.Banking:NewPackage("Util")
local TempTable = TSM.Include("Util.TempTable")
local BagTracking = TSM.Include("Service.BagTracking")
local GuildTracking = TSM.Include("Service.GuildTracking")
local private = {}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Util.BagIterator(autoBaseItems)
local query = BagTracking.CreateQueryBags()
:OrderBy("slotId", true)
if autoBaseItems then
query:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
:Select("bag", "slot", "autoBaseItemString", "quantity")
else
query:Select("bag", "slot", "itemString", "quantity")
end
if TSM.Banking.IsGuildBankOpen() then
query:Equal("isBoP", false)
:Equal("isBoA", false)
end
return query:IteratorAndRelease()
end
function Util.OpenBankIterator(autoBaseItems)
if TSM.Banking.IsGuildBankOpen() then
local query = GuildTracking.CreateQuery()
if autoBaseItems then
query:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
:Select("tab", "slot", "autoBaseItemString", "quantity")
else
query:Select("tab", "slot", "itemString", "quantity")
end
return query:IteratorAndRelease()
else
local query = BagTracking.CreateQueryBank()
:OrderBy("slotId", true)
if autoBaseItems then
query:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
:Select("bag", "slot", "autoBaseItemString", "quantity")
else
query:Select("bag", "slot", "itemString", "quantity")
end
return query:IteratorAndRelease()
end
end
function Util.PopulateGroupItemsFromBags(items, groups, getNumFunc, ...)
local itemQuantity = TempTable.Acquire()
for _, _, _, itemString, quantity in Util.BagIterator(true) do
if private.InGroups(itemString, groups) then
itemQuantity[itemString] = (itemQuantity[itemString] or 0) + quantity
end
end
for itemString, numHave in pairs(itemQuantity) do
local numToMove = getNumFunc(itemString, numHave, ...)
if numToMove > 0 then
items[itemString] = numToMove
end
end
TempTable.Release(itemQuantity)
end
function Util.PopulateGroupItemsFromOpenBank(items, groups, getNumFunc, ...)
local itemQuantity = TempTable.Acquire()
for _, _, _, itemString, quantity in Util.OpenBankIterator(true) do
if private.InGroups(itemString, groups) then
itemQuantity[itemString] = (itemQuantity[itemString] or 0) + quantity
end
end
for itemString, numHave in pairs(itemQuantity) do
local numToMove = getNumFunc(itemString, numHave, ...)
if numToMove > 0 then
items[itemString] = numToMove
end
end
TempTable.Release(itemQuantity)
end
function Util.PopulateItemsFromBags(items, getNumFunc, ...)
local itemQuantity = TempTable.Acquire()
for _, _, _, itemString, quantity in Util.BagIterator(true) do
itemQuantity[itemString] = (itemQuantity[itemString] or 0) + quantity
end
for itemString, numHave in pairs(itemQuantity) do
local numToMove = getNumFunc(itemString, numHave, ...)
if numToMove > 0 then
items[itemString] = numToMove
end
end
TempTable.Release(itemQuantity)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.InGroups(itemString, groups)
local groupPath = TSM.Groups.GetPathByItem(itemString)
-- TODO: support the base group
return groupPath and groupPath ~= TSM.CONST.ROOT_GROUP_PATH and groups[groupPath]
end

View File

@ -0,0 +1,92 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Warehousing = TSM.Banking:NewPackage("Warehousing")
local TempTable = TSM.Include("Util.TempTable")
local Math = TSM.Include("Util.Math")
local BagTracking = TSM.Include("Service.BagTracking")
local private = {}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Warehousing.MoveGroupsToBank(callback, groups)
local items = TempTable.Acquire()
TSM.Banking.Util.PopulateGroupItemsFromBags(items, groups, private.GetNumToMoveToBank)
TSM.Banking.MoveToBank(items, callback)
TempTable.Release(items)
end
function Warehousing.MoveGroupsToBags(callback, groups)
local items = TempTable.Acquire()
TSM.Banking.Util.PopulateGroupItemsFromOpenBank(items, groups, private.GetNumToMoveToBags)
TSM.Banking.MoveToBag(items, callback)
TempTable.Release(items)
end
function Warehousing.RestockBags(callback, groups)
local items = TempTable.Acquire()
TSM.Banking.Util.PopulateGroupItemsFromOpenBank(items, groups, private.GetNumToMoveRestock)
TSM.Banking.MoveToBag(items, callback)
TempTable.Release(items)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetNumToMoveToBank(itemString, numToMove)
local _, operationSettings = TSM.Operations.GetFirstOperationByItem("Warehousing", itemString)
if not operationSettings then
return 0
end
if operationSettings.keepBagQuantity ~= 0 then
numToMove = max(numToMove - operationSettings.keepBagQuantity, 0)
end
if operationSettings.moveQuantity ~= 0 then
numToMove = min(numToMove, operationSettings.moveQuantity)
end
return numToMove
end
function private.GetNumToMoveToBags(itemString, numToMove)
local _, operationSettings = TSM.Operations.GetFirstOperationByItem("Warehousing", itemString)
if not operationSettings then
return 0
end
if operationSettings.keepBankQuantity ~= 0 then
numToMove = max(numToMove - operationSettings.keepBankQuantity, 0)
end
if operationSettings.moveQuantity ~= 0 then
numToMove = min(numToMove, operationSettings.moveQuantity)
end
return Math.Floor(numToMove, operationSettings.stackSize ~= 0 and operationSettings.stackSize or 1)
end
function private.GetNumToMoveRestock(itemString, numToMove)
local _, operationSettings = TSM.Operations.GetFirstOperationByItem("Warehousing", itemString)
if not operationSettings then
return 0
end
local numInBags = BagTracking.CreateQueryBagsItem(itemString)
:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
:Equal("autoBaseItemString", itemString)
:SumAndRelease("quantity") or 0
if operationSettings.restockQuantity == 0 or numInBags >= operationSettings.restockQuantity then
return 0
end
if operationSettings.restockKeepBankQuantity ~= 0 then
numToMove = max(numToMove - operationSettings.restockKeepBankQuantity, 0)
end
numToMove = min(numToMove, operationSettings.restockQuantity - numInBags)
return Math.Floor(numToMove, operationSettings.restockStackSize ~= 0 and operationSettings.restockStackSize or 1)
end

View File

@ -0,0 +1,776 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Crafting = TSM:NewPackage("Crafting")
local L = TSM.Include("Locale").GetTable()
local ProfessionInfo = TSM.Include("Data.ProfessionInfo")
local Database = TSM.Include("Util.Database")
local TempTable = TSM.Include("Util.TempTable")
local Table = TSM.Include("Util.Table")
local Math = TSM.Include("Util.Math")
local Money = TSM.Include("Util.Money")
local String = TSM.Include("Util.String")
local Vararg = TSM.Include("Util.Vararg")
local Log = TSM.Include("Util.Log")
local ItemString = TSM.Include("Util.ItemString")
local ItemInfo = TSM.Include("Service.ItemInfo")
local CustomPrice = TSM.Include("Service.CustomPrice")
local Conversions = TSM.Include("Service.Conversions")
local Inventory = TSM.Include("Service.Inventory")
local private = {
spellDB = nil,
matDB = nil,
matItemDB = nil,
matDBSpellIdQuery = nil,
matDBMatsInTableQuery = nil,
matDBMatNamesQuery = nil,
ignoredCooldownDB = nil,
}
local CHARACTER_KEY = UnitName("player").." - "..GetRealmName()
local IGNORED_COOLDOWN_SEP = "\001"
local PROFESSION_SEP = ","
local PLAYER_SEP = ","
local BAD_CRAFTING_PRICE_SOURCES = {
crafting = true,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Crafting.OnInitialize()
local used = TempTable.Acquire()
for _, craftInfo in pairs(TSM.db.factionrealm.internalData.crafts) do
for itemString in pairs(craftInfo.mats) do
if strmatch(itemString, "^o:") then
local _, _, matList = strsplit(":", itemString)
for matItemId in String.SplitIterator(matList, ",") do
used["i:"..matItemId] = true
end
else
used[itemString] = true
end
end
end
for itemString in pairs(used) do
TSM.db.factionrealm.internalData.mats[itemString] = TSM.db.factionrealm.internalData.mats[itemString] or {}
end
for itemString in pairs(TSM.db.factionrealm.internalData.mats) do
if not used[itemString] then
TSM.db.factionrealm.internalData.mats[itemString] = nil
end
end
TempTable.Release(used)
local professionItems = TempTable.Acquire()
local matSpellCount = TempTable.Acquire()
local matFirstItemString = TempTable.Acquire()
local matFirstQuantity = TempTable.Acquire()
private.matDB = Database.NewSchema("CRAFTING_MATS")
:AddNumberField("spellId")
:AddStringField("itemString")
:AddNumberField("quantity")
:AddIndex("spellId")
:AddIndex("itemString")
:Commit()
private.matDB:BulkInsertStart()
private.spellDB = Database.NewSchema("CRAFTING_SPELLS")
:AddUniqueNumberField("spellId")
:AddStringField("itemString")
:AddStringField("itemName")
:AddStringField("name")
:AddStringField("profession")
:AddNumberField("numResult")
:AddStringField("players")
:AddBooleanField("hasCD")
:AddIndex("itemString")
:Commit()
private.spellDB:BulkInsertStart()
local playersTemp = TempTable.Acquire()
for spellId, craftInfo in pairs(TSM.db.factionrealm.internalData.crafts) do
wipe(playersTemp)
for player in pairs(craftInfo.players) do
tinsert(playersTemp, player)
end
sort(playersTemp)
local playersStr = table.concat(playersTemp, PLAYER_SEP)
local itemName = ItemInfo.GetName(craftInfo.itemString) or ""
private.spellDB:BulkInsertNewRow(spellId, craftInfo.itemString, itemName, craftInfo.name or "", craftInfo.profession, craftInfo.numResult, playersStr, craftInfo.hasCD and true or false)
for matItemString, matQuantity in pairs(craftInfo.mats) do
private.matDB:BulkInsertNewRow(spellId, matItemString, matQuantity)
professionItems[craftInfo.profession] = professionItems[craftInfo.profession] or TempTable.Acquire()
matSpellCount[spellId] = (matSpellCount[spellId] or 0) + 1
if matQuantity > 0 then
matFirstItemString[spellId] = matItemString
matFirstQuantity[spellId] = matQuantity
end
if strmatch(matItemString, "^o:") then
local _, _, matList = strsplit(":", matItemString)
for matItemId in String.SplitIterator(matList, ",") do
local optionalMatItemString = "i:"..matItemId
professionItems[craftInfo.profession][optionalMatItemString] = true
end
else
professionItems[craftInfo.profession][matItemString] = true
end
end
end
TempTable.Release(playersTemp)
private.spellDB:BulkInsertEnd()
private.matDB:BulkInsertEnd()
private.matDBMatsInTableQuery = private.matDB:NewQuery()
:Select("itemString", "quantity")
:Equal("spellId", Database.BoundQueryParam())
:GreaterThan("quantity", 0)
private.matDBMatNamesQuery = private.matDB:NewQuery()
:Select("name")
:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
:Equal("spellId", Database.BoundQueryParam())
:GreaterThan("quantity", 0)
private.matItemDB = Database.NewSchema("CRAFTING_MAT_ITEMS")
:AddUniqueStringField("itemString")
:AddStringField("professions")
:AddStringField("customValue")
:Commit()
private.matItemDB:BulkInsertStart()
local professionsTemp = TempTable.Acquire()
for itemString, info in pairs(TSM.db.factionrealm.internalData.mats) do
wipe(professionsTemp)
for profession, items in pairs(professionItems) do
if items[itemString] then
tinsert(professionsTemp, profession)
end
end
sort(professionsTemp)
local professionsStr = table.concat(professionsTemp)
private.matItemDB:BulkInsertNewRow(itemString, professionsStr, info.customValue or "")
end
TempTable.Release(professionsTemp)
private.matItemDB:BulkInsertEnd()
for _, tbl in pairs(professionItems) do
TempTable.Release(tbl)
end
TempTable.Release(professionItems)
private.matDBSpellIdQuery = private.matDB:NewQuery()
:Equal("spellId", Database.BoundQueryParam())
-- register 1:1 crafting conversions
local addedConversion = false
local query = private.spellDB:NewQuery()
:Select("spellId", "itemString", "numResult")
:Equal("hasCD", false)
for _, spellId, itemString, numResult in query:Iterator() do
if not ProfessionInfo.IsMassMill(spellId) and matSpellCount[spellId] == 1 then
Conversions.AddCraft(itemString, matFirstItemString[spellId], numResult / matFirstQuantity[spellId])
addedConversion = true
end
end
query:Release()
TempTable.Release(matSpellCount)
TempTable.Release(matFirstItemString)
TempTable.Release(matFirstQuantity)
if addedConversion then
CustomPrice.OnSourceChange("Destroy")
end
local isValid, err = CustomPrice.Validate(TSM.db.global.craftingOptions.defaultCraftPriceMethod, BAD_CRAFTING_PRICE_SOURCES)
if not isValid then
Log.PrintfUser(L["Your default craft value method was invalid so it has been returned to the default. Details: %s"], err)
TSM.db.global.craftingOptions.defaultCraftPriceMethod = TSM.db:GetDefault("global", "craftingOptions", "defaultCraftPriceMethod")
end
private.ignoredCooldownDB = Database.NewSchema("IGNORED_COOLDOWNS")
:AddStringField("characterKey")
:AddNumberField("spellId")
:Commit()
private.ignoredCooldownDB:BulkInsertStart()
for entry in pairs(TSM.db.factionrealm.userData.craftingCooldownIgnore) do
local characterKey, spellId = strsplit(IGNORED_COOLDOWN_SEP, entry)
spellId = tonumber(spellId)
if Crafting.HasSpellId(spellId) then
private.ignoredCooldownDB:BulkInsertNewRow(characterKey, spellId)
else
TSM.db.factionrealm.userData.craftingCooldownIgnore[entry] = nil
end
end
private.ignoredCooldownDB:BulkInsertEnd()
end
function Crafting.HasSpellId(spellId)
return private.spellDB:HasUniqueRow("spellId", spellId)
end
function Crafting.CreateRawCraftsQuery()
return private.spellDB:NewQuery()
end
function Crafting.CreateCraftsQuery()
return private.spellDB:NewQuery()
:LeftJoin(TSM.Crafting.Queue.GetDBForJoin(), "spellId")
:VirtualField("bagQuantity", "number", Inventory.GetBagQuantity, "itemString")
:VirtualField("auctionQuantity", "number", Inventory.GetAuctionQuantity, "itemString")
:VirtualField("craftingCost", "number", private.CraftingCostVirtualField, "spellId")
:VirtualField("itemValue", "number", private.ItemValueVirtualField, "itemString")
:VirtualField("profit", "number", private.ProfitVirtualField, "spellId")
:VirtualField("profitPct", "number", private.ProfitPctVirtualField, "spellId")
:VirtualField("saleRate", "number", private.SaleRateVirtualField, "itemString")
end
function Crafting.CreateQueuedCraftsQuery()
return private.spellDB:NewQuery()
:InnerJoin(TSM.Crafting.Queue.GetDBForJoin(), "spellId")
end
function Crafting.CreateCooldownSpellsQuery()
return private.spellDB:NewQuery()
:Equal("hasCD", true)
end
function Crafting.CreateRawMatItemQuery()
return private.matItemDB:NewQuery()
end
function Crafting.CreateMatItemQuery()
return private.matItemDB:NewQuery()
:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
:VirtualField("matCost", "number", private.MatCostVirtualField, "itemString")
:VirtualField("totalQuantity", "number", private.GetTotalQuantity, "itemString")
end
function Crafting.SpellIterator()
return private.spellDB:NewQuery()
:Select("spellId")
:IteratorAndRelease()
end
function Crafting.GetSpellIdsByItem(itemString)
local query = private.spellDB:NewQuery()
:Equal("itemString", itemString)
:Select("spellId", "hasCD")
return query:IteratorAndRelease()
end
function Crafting.GetMostProfitableSpellIdByItem(itemString, playerFilter, noCD)
local maxProfit, bestSpellId = nil, nil
local maxProfitCD, bestSpellIdCD = nil, nil
for _, spellId, hasCD in Crafting.GetSpellIdsByItem(itemString) do
if not playerFilter or playerFilter == "" or Vararg.In(playerFilter, Crafting.GetPlayers(spellId)) then
local profit = TSM.Crafting.Cost.GetProfitBySpellId(spellId)
if hasCD then
if profit and profit > (maxProfitCD or -math.huge) then
maxProfitCD = profit
bestSpellIdCD = spellId
elseif not maxProfitCD then
bestSpellIdCD = spellId
end
else
if profit and profit > (maxProfit or -math.huge) then
maxProfit = profit
bestSpellId = spellId
elseif not maxProfit then
bestSpellId = spellId
end
end
end
end
if noCD then
maxProfitCD = nil
bestSpellIdCD = nil
end
if maxProfit then
return bestSpellId, maxProfit
elseif maxProfitCD then
return bestSpellIdCD, maxProfitCD
else
return bestSpellId or bestSpellIdCD or nil, nil
end
end
function Crafting.GetItemString(spellId)
return private.spellDB:GetUniqueRowField("spellId", spellId, "itemString")
end
function Crafting.GetProfession(spellId)
return private.spellDB:GetUniqueRowField("spellId", spellId, "profession")
end
function Crafting.GetNumResult(spellId)
return private.spellDB:GetUniqueRowField("spellId", spellId, "numResult")
end
function Crafting.GetPlayers(spellId)
local players = private.spellDB:GetUniqueRowField("spellId", spellId, "players")
if not players then
return
end
return strsplit(PLAYER_SEP, players)
end
function Crafting.GetName(spellId)
return private.spellDB:GetUniqueRowField("spellId", spellId, "name")
end
function Crafting.MatIterator(spellId)
return private.matDB:NewQuery()
:Select("itemString", "quantity")
:Equal("spellId", spellId)
:GreaterThan("quantity", 0)
:IteratorAndRelease()
end
function Crafting.GetOptionalMatIterator(spellId)
return private.matDB:NewQuery()
:Select("itemString", "slotId", "text")
:VirtualField("slotId", "number", private.OptionalMatSlotIdVirtualField, "itemString")
:VirtualField("text", "string", private.OptionalMatTextVirtualField, "itemString")
:Equal("spellId", spellId)
:LessThan("quantity", 0)
:OrderBy("slotId", true)
:IteratorAndRelease()
end
function Crafting.GetMatsAsTable(spellId, tbl)
private.matDBMatsInTableQuery
:BindParams(spellId)
:AsTable(tbl)
end
function Crafting.RemovePlayers(spellId, playersToRemove)
local shouldRemove = TempTable.Acquire()
for _, player in ipairs(playersToRemove) do
shouldRemove[player] = true
end
local players = TempTable.Acquire(Crafting.GetPlayers(spellId))
for i = #players, 1, -1 do
local player = players[i]
if shouldRemove[player] then
TSM.db.factionrealm.internalData.crafts[spellId].players[player] = nil
tremove(players, i)
end
end
TempTable.Release(shouldRemove)
local query = private.spellDB:NewQuery()
:Equal("spellId", spellId)
local row = query:GetFirstResult()
local playersStr = strjoin(PLAYER_SEP, TempTable.UnpackAndRelease(players))
if playersStr ~= "" then
row:SetField("players", playersStr)
:Update()
query:Release()
return true
end
-- no more players so remove this spell and all its mats
private.spellDB:DeleteRow(row)
query:Release()
TSM.db.factionrealm.internalData.crafts[spellId] = nil
local removedMats = TempTable.Acquire()
private.matDB:SetQueryUpdatesPaused(true)
query = private.matDB:NewQuery()
:Equal("spellId", spellId)
for _, matRow in query:Iterator() do
removedMats[matRow:GetField("itemString")] = true
private.matDB:DeleteRow(matRow)
end
query:Release()
private.matDB:SetQueryUpdatesPaused(false)
private.ProcessRemovedMats(removedMats)
TempTable.Release(removedMats)
return false
end
function Crafting.RemovePlayerSpells(inactiveSpellIds)
local playerName = UnitName("player")
local query = private.spellDB:NewQuery()
:InTable("spellId", inactiveSpellIds)
:Custom(private.QueryPlayerFilter, playerName)
local removedSpellIds = TempTable.Acquire()
local toRemove = TempTable.Acquire()
private.spellDB:SetQueryUpdatesPaused(true)
if query:Count() > 0 then
Log.Info("Removing %d inactive spellds", query:Count())
end
for _, row in query:Iterator() do
local players = row:GetField("players")
if row:GetField("players") == playerName then
-- the current player was the only player, so we'll delete the entire row and all its mats
local spellId = row:GetField("spellId")
removedSpellIds[spellId] = true
TSM.db.factionrealm.internalData.crafts[spellId] = nil
tinsert(toRemove, row)
else
-- remove this player form the row
local playersTemp = TempTable.Acquire(strsplit(PLAYER_SEP, players))
assert(Table.RemoveByValue(playersTemp, playerName) == 1)
row:SetField("players", strjoin(PLAYER_SEP, TempTable.UnpackAndRelease(playersTemp)))
:Update()
end
end
for _, row in ipairs(toRemove) do
private.spellDB:DeleteRow(row)
end
TempTable.Release(toRemove)
query:Release()
private.spellDB:SetQueryUpdatesPaused(false)
local removedMats = TempTable.Acquire()
private.matDB:SetQueryUpdatesPaused(true)
local matQuery = private.matDB:NewQuery()
:InTable("spellId", removedSpellIds)
for _, matRow in matQuery:Iterator() do
removedMats[matRow:GetField("itemString")] = true
private.matDB:DeleteRow(matRow)
end
TempTable.Release(removedSpellIds)
matQuery:Release()
private.matDB:SetQueryUpdatesPaused(false)
private.ProcessRemovedMats(removedMats)
TempTable.Release(removedMats)
end
function Crafting.SetSpellDBQueryUpdatesPaused(paused)
private.spellDB:SetQueryUpdatesPaused(paused)
end
function Crafting.CreateOrUpdate(spellId, itemString, profession, name, numResult, player, hasCD)
local row = private.spellDB:GetUniqueRow("spellId", spellId)
if row then
local playersStr = row:GetField("players")
local foundPlayer = String.SeparatedContains(playersStr, PLAYER_SEP, player)
if not foundPlayer then
assert(playersStr ~= "")
playersStr = playersStr .. PLAYER_SEP .. player
end
row:SetField("itemString", itemString)
:SetField("profession", profession)
:SetField("itemName", ItemInfo.GetName(itemString) or "")
:SetField("name", name)
:SetField("numResult", numResult)
:SetField("players", playersStr)
:SetField("hasCD", hasCD)
:Update()
row:Release()
local craftInfo = TSM.db.factionrealm.internalData.crafts[spellId]
craftInfo.itemString = itemString
craftInfo.profession = profession
craftInfo.name = name
craftInfo.numResult = numResult
craftInfo.players[player] = true
craftInfo.hasCD = hasCD or nil
else
TSM.db.factionrealm.internalData.crafts[spellId] = {
mats = {},
players = { [player] = true },
queued = 0,
itemString = itemString,
name = name,
profession = profession,
numResult = numResult,
hasCD = hasCD,
}
private.spellDB:NewRow()
:SetField("spellId", spellId)
:SetField("itemString", itemString)
:SetField("profession", profession)
:SetField("itemName", ItemInfo.GetName(itemString) or "")
:SetField("name", name)
:SetField("numResult", numResult)
:SetField("players", player)
:SetField("hasCD", hasCD)
:Create()
end
end
function Crafting.AddPlayer(spellId, player)
if TSM.db.factionrealm.internalData.crafts[spellId].players[player] then
return
end
local row = private.spellDB:GetUniqueRow("spellId", spellId)
local playersStr = row:GetField("players")
assert(playersStr ~= "")
playersStr = playersStr .. PLAYER_SEP .. player
row:SetField("players", playersStr)
row:Update()
row:Release()
TSM.db.factionrealm.internalData.crafts[spellId].players[player] = true
end
function Crafting.SetMats(spellId, matQuantities)
if Table.Equal(TSM.db.factionrealm.internalData.crafts[spellId].mats, matQuantities) then
-- nothing changed
return
end
wipe(TSM.db.factionrealm.internalData.crafts[spellId].mats)
for itemString, quantity in pairs(matQuantities) do
TSM.db.factionrealm.internalData.crafts[spellId].mats[itemString] = quantity
end
private.matDB:SetQueryUpdatesPaused(true)
local removedMats = TempTable.Acquire()
local usedMats = TempTable.Acquire()
private.matDBSpellIdQuery:BindParams(spellId)
for _, row in private.matDBSpellIdQuery:Iterator() do
local itemString = row:GetField("itemString")
local quantity = matQuantities[itemString]
if not quantity then
-- remove this row
private.matDB:DeleteRow(row)
removedMats[itemString] = true
else
usedMats[itemString] = true
row:SetField("quantity", quantity)
:Update()
end
end
local profession = Crafting.GetProfession(spellId)
for itemString, quantity in pairs(matQuantities) do
if not usedMats[itemString] then
private.matDB:NewRow()
:SetField("spellId", spellId)
:SetField("itemString", itemString)
:SetField("quantity", quantity)
:Create()
if quantity > 0 then
private.MatItemDBUpdateOrInsert(itemString, profession)
else
local _, _, matList = strsplit(":", itemString)
for matItemId in String.SplitIterator(matList, ",") do
private.MatItemDBUpdateOrInsert("i:"..matItemId, profession)
end
end
end
end
TempTable.Release(usedMats)
private.matDB:SetQueryUpdatesPaused(false)
private.ProcessRemovedMats(removedMats)
TempTable.Release(removedMats)
end
function Crafting.SetMatCustomValue(itemString, value)
TSM.db.factionrealm.internalData.mats[itemString].customValue = value
private.matItemDB:GetUniqueRow("itemString", itemString)
:SetField("customValue", value or "")
:Update()
end
function Crafting.CanCraftItem(itemString)
local count = private.spellDB:NewQuery()
:Equal("itemString", itemString)
:CountAndRelease()
return count > 0
end
function Crafting.RestockHelp(link)
local itemString = ItemString.Get(link)
if not itemString then
Log.PrintUser(L["No item specified. Usage: /tsm restock_help [ITEM_LINK]"])
return
end
local msg = private.GetRestockHelpMessage(itemString)
Log.PrintfUser(L["Restock help for %s: %s"], link, msg)
end
function Crafting.IgnoreCooldown(spellId)
assert(not TSM.db.factionrealm.userData.craftingCooldownIgnore[CHARACTER_KEY..IGNORED_COOLDOWN_SEP..spellId])
TSM.db.factionrealm.userData.craftingCooldownIgnore[CHARACTER_KEY..IGNORED_COOLDOWN_SEP..spellId] = true
private.ignoredCooldownDB:NewRow()
:SetField("characterKey", CHARACTER_KEY)
:SetField("spellId", spellId)
:Create()
end
function Crafting.IsCooldownIgnored(spellId)
return TSM.db.factionrealm.userData.craftingCooldownIgnore[CHARACTER_KEY..IGNORED_COOLDOWN_SEP..spellId]
end
function Crafting.CreateIgnoredCooldownQuery()
return private.ignoredCooldownDB:NewQuery()
end
function Crafting.RemoveIgnoredCooldown(characterKey, spellId)
assert(TSM.db.factionrealm.userData.craftingCooldownIgnore[characterKey..IGNORED_COOLDOWN_SEP..spellId])
TSM.db.factionrealm.userData.craftingCooldownIgnore[characterKey..IGNORED_COOLDOWN_SEP..spellId] = nil
local row = private.ignoredCooldownDB:NewQuery()
:Equal("characterKey", characterKey)
:Equal("spellId", spellId)
:GetFirstResultAndRelease()
assert(row)
private.ignoredCooldownDB:DeleteRow(row)
row:Release()
end
function Crafting.GetMatNames(spellId)
return private.matDBMatNamesQuery:BindParams(spellId)
:JoinedString("name", "")
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.ProcessRemovedMats(removedMats)
private.matItemDB:SetQueryUpdatesPaused(true)
for itemString in pairs(removedMats) do
local numSpells = private.matDB:NewQuery()
:Equal("itemString", itemString)
:CountAndRelease()
if numSpells == 0 then
local matItemRow = private.matItemDB:GetUniqueRow("itemString", itemString)
private.matItemDB:DeleteRow(matItemRow)
matItemRow:Release()
end
end
private.matItemDB:SetQueryUpdatesPaused(false)
end
function private.CraftingCostVirtualField(spellId)
return TSM.Crafting.Cost.GetCraftingCostBySpellId(spellId) or Math.GetNan()
end
function private.ItemValueVirtualField(itemString)
return TSM.Crafting.Cost.GetCraftedItemValue(itemString) or Math.GetNan()
end
function private.ProfitVirtualField(spellId)
return TSM.Crafting.Cost.GetProfitBySpellId(spellId) or Math.GetNan()
end
function private.ProfitPctVirtualField(spellId)
local craftingCost, _, profit = TSM.Crafting.Cost.GetCostsBySpellId(spellId)
return (craftingCost and profit) and floor(profit * 100 / craftingCost) or Math.GetNan()
end
function private.SaleRateVirtualField(itemString)
local saleRate = TSM.AuctionDB.GetRegionItemData(itemString, "regionSalePercent")
return saleRate and (saleRate / 100) or Math.GetNan()
end
function private.MatCostVirtualField(itemString)
return TSM.Crafting.Cost.GetMatCost(itemString) or Math.GetNan()
end
function private.OptionalMatSlotIdVirtualField(matStr)
local _, slotId = strsplit(":", matStr)
return tonumber(slotId)
end
function private.OptionalMatTextVirtualField(matStr)
local _, _, matList = strsplit(":", matStr)
return TSM.Crafting.ProfessionUtil.GetOptionalMatText(matList) or OPTIONAL_REAGENT_POSTFIX
end
function private.GetRestockHelpMessage(itemString)
-- check if the item is in a group
local groupPath = TSM.Groups.GetPathByItem(itemString)
if not groupPath then
return L["This item is not in a TSM group."]
end
-- check that there's a crafting operation applied
if not TSM.Operations.Crafting.HasOperation(itemString) then
return format(L["There is no Crafting operation applied to this item's TSM group (%s)."], TSM.Groups.Path.Format(groupPath))
end
-- check if it's an invalid operation
local isValid, err = TSM.Operations.Crafting.IsValid(itemString)
if not isValid then
return err
end
-- check that this item is craftable
if not TSM.Crafting.CanCraftItem(itemString) then
return L["You don't know how to craft this item."]
end
-- check the restock quantity
local neededQuantity = TSM.Operations.Crafting.GetRestockQuantity(itemString, private.GetTotalQuantity(itemString))
if neededQuantity == 0 then
return L["You either already have at least your max restock quantity of this item or the number which would be queued is less than the min restock quantity."]
end
-- check if we would actually queue any
local cost, spellId = TSM.Crafting.Cost.GetLowestCostByItem(itemString)
local numResult = spellId and TSM.Crafting.GetNumResult(spellId)
if neededQuantity < numResult then
return format(L["A single craft makes %d and you only need to restock %d."], numResult, neededQuantity)
end
-- check the prices on the item and the min profit
local hasMinProfit, minProfit = TSM.Operations.Crafting.GetMinProfit(itemString)
if hasMinProfit then
local craftedValue = TSM.Crafting.Cost.GetCraftedItemValue(itemString)
local profit = cost and craftedValue and (craftedValue - cost) or nil
-- check that there's a crafted value
if not craftedValue then
return L["The 'Craft Value Method' did not return a value for this item."]
end
-- check that there's a crafted cost
if not cost then
return L["This item does not have a crafting cost. Check that all of its mats have mat prices."]
end
-- check that there's a profit
assert(profit)
if not minProfit then
return L["The min profit did not evalulate to a valid value for this item."]
end
if profit < minProfit then
return format(L["The profit of this item (%s) is below the min profit (%s)."], Money.ToString(profit), Money.ToString(minProfit))
end
end
return L["This item will be added to the queue when you restock its group. If this isn't happening, please visit http://support.tradeskillmaster.com for further assistance."]
end
function private.QueryPlayerFilter(row, player)
return String.SeparatedContains(row:GetField("players"), ",", player)
end
function private.GetTotalQuantity(itemString)
return CustomPrice.GetItemPrice(itemString, "NumInventory") or 0
end
function private.MatItemDBUpdateOrInsert(itemString, profession)
local matItemRow = private.matItemDB:GetUniqueRow("itemString", itemString)
if matItemRow then
-- update the professions if necessary
local professions = TempTable.Acquire(strsplit(PROFESSION_SEP, matItemRow:GetField("professions")))
if not Table.KeyByValue(professions, profession) then
tinsert(professions, profession)
sort(professions)
matItemRow:SetField("professions", table.concat(professions, PROFESSION_SEP))
:Update()
end
TempTable.Release(professions)
else
private.matItemDB:NewRow()
:SetField("itemString", itemString)
:SetField("professions", profession)
:SetField("customValue", TSM.db.factionrealm.internalData.mats[itemString].customValue or "")
:Create()
end
end

View File

@ -0,0 +1,156 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Cost = TSM.Crafting:NewPackage("Cost")
local TempTable = TSM.Include("Util.TempTable")
local Math = TSM.Include("Util.Math")
local ItemString = TSM.Include("Util.ItemString")
local CustomPrice = TSM.Include("Service.CustomPrice")
local private = {
matsVisited = {},
matCostCache = {},
matsTemp = {},
matsTempInUse = false,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Cost.GetMatCost(itemString)
itemString = ItemString.GetBase(itemString)
if not TSM.db.factionrealm.internalData.mats[itemString] then
return
end
if private.matsVisited[itemString] then
-- there's a loop in the mat cost, so bail
return
end
local prevHash = private.matsVisited.hash
local hash = nil
if prevHash == nil then
-- this is a top-level mat, so just use the itemString as the hash
hash = itemString
else
if type(prevHash) == "string" then
-- this is a second-level mat where the previous hash is the itemString which needs to be hashed itself
prevHash = Math.CalculateHash(prevHash)
end
hash = Math.CalculateHash(itemString, prevHash)
end
private.matsVisited.hash = hash
private.matsVisited[itemString] = true
if private.matCostCache.lastUpdate ~= GetTime() then
wipe(private.matCostCache)
private.matCostCache.lastUpdate = GetTime()
end
if not private.matCostCache[hash] then
local priceStr = TSM.db.factionrealm.internalData.mats[itemString].customValue or TSM.db.global.craftingOptions.defaultMatCostMethod
private.matCostCache[hash] = CustomPrice.GetValue(priceStr, itemString)
end
private.matsVisited[itemString] = nil
private.matsVisited.hash = prevHash
return private.matCostCache[hash]
end
function Cost.GetCraftingCostBySpellId(spellId)
local cost = 0
local hasMats = false
local mats = nil
if private.matsTempInUse then
mats = TempTable.Acquire()
else
mats = private.matsTemp
private.matsTempInUse = true
wipe(mats)
end
TSM.Crafting.GetMatsAsTable(spellId, mats)
for itemString, quantity in pairs(mats) do
hasMats = true
local matCost = Cost.GetMatCost(itemString)
if not matCost then
cost = nil
elseif cost then
cost = cost + matCost * quantity
end
end
if mats == private.matsTemp then
private.matsTempInUse = false
else
TempTable.Release(mats)
end
if not cost or not hasMats then
return
end
cost = Math.Round(cost / TSM.Crafting.GetNumResult(spellId))
return cost > 0 and cost or nil
end
function Cost.GetCraftedItemValue(itemString)
local hasCraftPriceMethod, craftPrice = TSM.Operations.Crafting.GetCraftedItemValue(itemString)
if hasCraftPriceMethod then
return craftPrice
end
return CustomPrice.GetValue(TSM.db.global.craftingOptions.defaultCraftPriceMethod, itemString)
end
function Cost.GetProfitBySpellId(spellId)
local _, _, profit = Cost.GetCostsBySpellId(spellId)
return profit
end
function Cost.GetCostsBySpellId(spellId)
local craftingCost = Cost.GetCraftingCostBySpellId(spellId)
local itemString = TSM.Crafting.GetItemString(spellId)
local craftedItemValue = itemString and Cost.GetCraftedItemValue(itemString) or nil
return craftingCost, craftedItemValue, craftingCost and craftedItemValue and (craftedItemValue - craftingCost) or nil
end
function Cost.GetSaleRateBySpellId(spellId)
local itemString = TSM.Crafting.GetItemString(spellId)
return itemString and CustomPrice.GetItemPrice(itemString, "DBRegionSaleRate") or nil
end
function Cost.GetLowestCostByItem(itemString)
itemString = ItemString.GetBase(itemString)
local lowestCost, lowestSpellId = nil, nil
local cdCost, cdSpellId = nil, nil
local numSpells = 0
local singleSpellId = nil
for _, spellId, hasCD in TSM.Crafting.GetSpellIdsByItem(itemString) do
if not hasCD then
if singleSpellId == nil then
singleSpellId = spellId
elseif singleSpellId then
singleSpellId = 0
end
end
numSpells = numSpells + 1
local cost = Cost.GetCraftingCostBySpellId(spellId)
if cost and (not lowestCost or cost < lowestCost) then
-- exclude spells with cooldown if option to ignore is enabled and there is more than one way to craft
if hasCD then
cdCost = cost
cdSpellId = spellId
else
lowestCost = cost
lowestSpellId = spellId
end
end
end
if singleSpellId == 0 then
singleSpellId = nil
end
if numSpells == 1 and not lowestCost and cdCost then
-- only way to craft it is with a CD craft, so use that
lowestCost = cdCost
lowestSpellId = cdSpellId
end
return lowestCost, lowestSpellId or singleSpellId
end

View File

@ -0,0 +1,525 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Gathering = TSM.Crafting:NewPackage("Gathering")
local DisenchantInfo = TSM.Include("Data.DisenchantInfo")
local Database = TSM.Include("Util.Database")
local Table = TSM.Include("Util.Table")
local Delay = TSM.Include("Util.Delay")
local String = TSM.Include("Util.String")
local TempTable = TSM.Include("Util.TempTable")
local ItemInfo = TSM.Include("Service.ItemInfo")
local Conversions = TSM.Include("Service.Conversions")
local BagTracking = TSM.Include("Service.BagTracking")
local Inventory = TSM.Include("Service.Inventory")
local PlayerInfo = TSM.Include("Service.PlayerInfo")
local private = {
db = nil,
queuedCraftsUpdateQuery = nil, -- luacheck: ignore 1004 - just stored for GC reasons
crafterList = {},
professionList = {},
contextChangedCallback = nil,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Gathering.OnInitialize()
if TSM.IsWowClassic() then
Table.RemoveByValue(TSM.db.profile.gatheringOptions.sources, "guildBank")
Table.RemoveByValue(TSM.db.profile.gatheringOptions.sources, "altGuildBank")
end
end
function Gathering.OnEnable()
private.db = Database.NewSchema("GATHERING_MATS")
:AddUniqueStringField("itemString")
:AddNumberField("numNeed")
:AddNumberField("numHave")
:AddStringField("sourcesStr")
:Commit()
private.queuedCraftsUpdateQuery = TSM.Crafting.CreateQueuedCraftsQuery()
:SetUpdateCallback(private.OnQueuedCraftsUpdated)
private.OnQueuedCraftsUpdated()
BagTracking.RegisterCallback(function()
Delay.AfterTime("GATHERING_BAG_UPDATE", 1, private.UpdateDB)
end)
end
function Gathering.SetContextChangedCallback(callback)
private.contextChangedCallback = callback
end
function Gathering.CreateQuery()
return private.db:NewQuery()
end
function Gathering.SetCrafter(crafter)
if crafter == TSM.db.factionrealm.gatheringContext.crafter then
return
end
TSM.db.factionrealm.gatheringContext.crafter = crafter
wipe(TSM.db.factionrealm.gatheringContext.professions)
private.UpdateProfessionList()
private.UpdateDB()
end
function Gathering.SetProfessions(professions)
local numProfessions = Table.Count(TSM.db.factionrealm.gatheringContext.professions)
local didChange = false
if numProfessions ~= #professions then
didChange = true
else
for _, profession in ipairs(professions) do
if not TSM.db.factionrealm.gatheringContext.professions[profession] then
didChange = true
end
end
end
if not didChange then
return
end
wipe(TSM.db.factionrealm.gatheringContext.professions)
for _, profession in ipairs(professions) do
assert(private.professionList[profession])
TSM.db.factionrealm.gatheringContext.professions[profession] = true
end
private.UpdateDB()
end
function Gathering.GetCrafterList()
return private.crafterList
end
function Gathering.GetCrafter()
return TSM.db.factionrealm.gatheringContext.crafter ~= "" and TSM.db.factionrealm.gatheringContext.crafter or nil
end
function Gathering.GetProfessionList()
return private.professionList
end
function Gathering.GetProfessions()
return TSM.db.factionrealm.gatheringContext.professions
end
function Gathering.SourcesStrToTable(sourcesStr, info, alts)
for source, num, characters in gmatch(sourcesStr, "([a-zA-Z]+)/([0-9]+)/([^,]*)") do
info[source] = tonumber(num)
if source == "alt" or source == "altGuildBank" then
for character in gmatch(characters, "([^`]+)") do
alts[character] = true
end
end
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.UpdateCrafterList()
local query = TSM.Crafting.CreateQueuedCraftsQuery()
:Select("players")
:Distinct("players")
wipe(private.crafterList)
for _, players in query:Iterator() do
for character in gmatch(players, "[^,]+") do
if not private.crafterList[character] then
private.crafterList[character] = true
tinsert(private.crafterList, character)
end
end
end
query:Release()
if TSM.db.factionrealm.gatheringContext.crafter ~= "" and not private.crafterList[TSM.db.factionrealm.gatheringContext.crafter] then
-- the crafter which was selected no longer exists, so clear the selection
TSM.db.factionrealm.gatheringContext.crafter = ""
elseif #private.crafterList == 1 then
-- there is only one crafter in the list, so select it
TSM.db.factionrealm.gatheringContext.crafter = private.crafterList[1]
end
if TSM.db.factionrealm.gatheringContext.crafter == "" then
wipe(TSM.db.factionrealm.gatheringContext.professions)
end
end
function private.UpdateProfessionList()
-- update the professionList
wipe(private.professionList)
if TSM.db.factionrealm.gatheringContext.crafter ~= "" then
-- populate the list of professions
local query = TSM.Crafting.CreateQueuedCraftsQuery()
:Select("profession")
:Custom(private.QueryPlayerFilter, TSM.db.factionrealm.gatheringContext.crafter)
:Distinct("profession")
for _, profession in query:Iterator() do
private.professionList[profession] = true
tinsert(private.professionList, profession)
end
query:Release()
end
-- remove selected professions which are no longer in the list
for profession in pairs(TSM.db.factionrealm.gatheringContext.professions) do
if not private.professionList[profession] then
TSM.db.factionrealm.gatheringContext.professions[profession] = nil
end
end
-- select all professions by default
if not next(TSM.db.factionrealm.gatheringContext.professions) then
for _, profession in ipairs(private.professionList) do
TSM.db.factionrealm.gatheringContext.professions[profession] = true
end
end
end
function private.OnQueuedCraftsUpdated()
private.UpdateCrafterList()
private.UpdateProfessionList()
private.UpdateDB()
private.contextChangedCallback()
end
function private.UpdateDB()
-- delay the update if we're in combat
if InCombatLockdown() then
Delay.AfterTime("DELAYED_GATHERING_UPDATE", 1, private.UpdateDB)
return
end
local crafter = TSM.db.factionrealm.gatheringContext.crafter
if crafter == "" or not next(TSM.db.factionrealm.gatheringContext.professions) then
private.db:Truncate()
return
end
local matsNumNeed = TempTable.Acquire()
local query = TSM.Crafting.CreateQueuedCraftsQuery()
:Select("spellId", "num")
:Custom(private.QueryPlayerFilter, crafter)
:Or()
for profession in pairs(TSM.db.factionrealm.gatheringContext.professions) do
query:Equal("profession", profession)
end
query:End()
for _, spellId, numQueued in query:Iterator() do
for _, itemString, quantity in TSM.Crafting.MatIterator(spellId) do
matsNumNeed[itemString] = (matsNumNeed[itemString] or 0) + quantity * numQueued
end
end
query:Release()
local matQueue = TempTable.Acquire()
local matsNumHave = TempTable.Acquire()
local matsNumHaveExtra = TempTable.Acquire()
for itemString, numNeed in pairs(matsNumNeed) do
matsNumHave[itemString] = private.GetCrafterInventoryQuantity(itemString)
local numUsed = nil
numNeed, numUsed = private.HandleNumHave(itemString, numNeed, matsNumHave[itemString])
if numUsed < matsNumHave[itemString] then
matsNumHaveExtra[itemString] = matsNumHave[itemString] - numUsed
end
if numNeed > 0 then
matsNumNeed[itemString] = numNeed
tinsert(matQueue, itemString)
else
matsNumNeed[itemString] = nil
end
end
local sourceList = TempTable.Acquire()
local matSourceList = TempTable.Acquire()
while #matQueue > 0 do
local itemString = tremove(matQueue)
wipe(sourceList)
local numNeed = matsNumNeed[itemString]
-- always add a task to get mail on the crafter if possible
numNeed = private.ProcessSource(itemString, numNeed, "openMail", sourceList)
assert(numNeed >= 0)
for _, source in ipairs(TSM.db.profile.gatheringOptions.sources) do
local isCraftSource = source == "craftProfit" or source == "craftNoProfit"
local ignoreSource = false
if isCraftSource then
-- check if we are already crafting some materials of this craft so shouldn't craft this item
local spellId = TSM.Crafting.GetMostProfitableSpellIdByItem(itemString, crafter, true)
if spellId then
for _, matItemString in TSM.Crafting.MatIterator(spellId) do
if not ignoreSource and matSourceList[matItemString] and strmatch(matSourceList[matItemString], "craft[a-zA-Z]+/[^,]+/") then
ignoreSource = true
end
end
else
-- can't craft this item
ignoreSource = true
end
end
if not ignoreSource then
local prevNumNeed = numNeed
numNeed = private.ProcessSource(itemString, numNeed, source, sourceList)
assert(numNeed >= 0)
if numNeed == 0 then
if isCraftSource then
-- we are crafting these, so add the necessary mats
local spellId = TSM.Crafting.GetMostProfitableSpellIdByItem(itemString, crafter, true)
assert(spellId)
local numToCraft = ceil(prevNumNeed / TSM.Crafting.GetNumResult(spellId))
for _, intMatItemString, intMatQuantity in TSM.Crafting.MatIterator(spellId) do
local intMatNumNeed, numUsed = private.HandleNumHave(intMatItemString, numToCraft * intMatQuantity, matsNumHaveExtra[intMatItemString] or 0)
if numUsed > 0 then
matsNumHaveExtra[intMatItemString] = matsNumHaveExtra[intMatItemString] - numUsed
end
if intMatNumNeed > 0 then
if not matsNumNeed[intMatItemString] then
local intMatNumHave = private.GetCrafterInventoryQuantity(intMatItemString)
if intMatNumNeed > intMatNumHave then
matsNumHave[intMatItemString] = intMatNumHave
matsNumNeed[intMatItemString] = intMatNumNeed - intMatNumHave
tinsert(matQueue, intMatItemString)
elseif intMatNumHave > intMatNumNeed then
matsNumHaveExtra[intMatItemString] = intMatNumHave - intMatNumNeed
end
else
matsNumNeed[intMatItemString] = (matsNumNeed[intMatItemString] or 0) + intMatNumNeed
if matSourceList[intMatItemString] then
-- already processed this item, so queue it again
tinsert(matQueue, intMatItemString)
end
end
end
end
end
break
end
end
end
sort(sourceList)
matSourceList[itemString] = table.concat(sourceList, ",")
end
private.db:TruncateAndBulkInsertStart()
for itemString, numNeed in pairs(matsNumNeed) do
private.db:BulkInsertNewRow(itemString, numNeed, matsNumHave[itemString], matSourceList[itemString])
end
private.db:BulkInsertEnd()
TempTable.Release(sourceList)
TempTable.Release(matSourceList)
TempTable.Release(matsNumNeed)
TempTable.Release(matsNumHave)
TempTable.Release(matsNumHaveExtra)
TempTable.Release(matQueue)
end
function private.ProcessSource(itemString, numNeed, source, sourceList)
local crafter = TSM.db.factionrealm.gatheringContext.crafter
local playerName = UnitName("player")
if source == "openMail" then
local crafterMailQuantity = Inventory.GetMailQuantity(itemString, crafter)
if crafterMailQuantity > 0 then
crafterMailQuantity = min(crafterMailQuantity, numNeed)
if crafter == playerName then
tinsert(sourceList, "openMail/"..crafterMailQuantity.."/")
else
tinsert(sourceList, "alt/"..crafterMailQuantity.."/"..crafter)
end
return numNeed - crafterMailQuantity
end
elseif source == "vendor" then
if ItemInfo.GetVendorBuy(itemString) then
-- assume we can buy all we need from the vendor
tinsert(sourceList, "vendor/"..numNeed.."/")
return 0
end
elseif source == "guildBank" then
local guild = PlayerInfo.GetPlayerGuild(crafter)
local guildBankQuantity = guild and Inventory.GetGuildQuantity(itemString, guild) or 0
if guildBankQuantity > 0 then
guildBankQuantity = min(guildBankQuantity, numNeed)
if crafter == playerName then
-- we are on the crafter
tinsert(sourceList, "guildBank/"..guildBankQuantity.."/")
else
-- need to switch to the crafter to get items from the guild bank
tinsert(sourceList, "altGuildBank/"..guildBankQuantity.."/"..crafter)
end
return numNeed - guildBankQuantity
end
elseif source == "alt" then
if ItemInfo.IsSoulbound(itemString) then
-- can't mail soulbound items
return numNeed
end
if crafter ~= playerName then
-- we are on the alt, so see if we can gather items from this character
local bagQuantity = Inventory.GetBagQuantity(itemString)
local bankQuantity = Inventory.GetBankQuantity(itemString) + Inventory.GetReagentBankQuantity(itemString)
local mailQuantity = Inventory.GetMailQuantity(itemString)
if bagQuantity > 0 then
bagQuantity = min(numNeed, bagQuantity)
tinsert(sourceList, "sendMail/"..bagQuantity.."/")
numNeed = numNeed - bagQuantity
if numNeed == 0 then
return 0
end
end
if mailQuantity > 0 then
mailQuantity = min(numNeed, mailQuantity)
tinsert(sourceList, "openMail/"..mailQuantity.."/")
numNeed = numNeed - mailQuantity
if numNeed == 0 then
return 0
end
end
if bankQuantity > 0 then
bankQuantity = min(numNeed, bankQuantity)
tinsert(sourceList, "bank/"..bankQuantity.."/")
numNeed = numNeed - bankQuantity
if numNeed == 0 then
return 0
end
end
end
-- check alts
local altNum = 0
local altCharacters = TempTable.Acquire()
for factionrealm in TSM.db:GetConnectedRealmIterator("factionrealm") do
for _, character in TSM.db:FactionrealmCharacterIterator(factionrealm) do
local characterKey = nil
if factionrealm == UnitFactionGroup("player").." - "..GetRealmName() then
characterKey = character
else
characterKey = character.." - "..factionrealm
end
if characterKey ~= crafter and characterKey ~= playerName then
local num = 0
num = num + Inventory.GetBagQuantity(itemString, character, factionrealm)
num = num + Inventory.GetBankQuantity(itemString, character, factionrealm)
num = num + Inventory.GetReagentBankQuantity(itemString, character, factionrealm)
num = num + Inventory.GetMailQuantity(itemString, character, factionrealm)
if num > 0 then
tinsert(altCharacters, characterKey)
altNum = altNum + num
end
end
end
end
local altCharactersStr = table.concat(altCharacters, "`")
TempTable.Release(altCharacters)
if altNum > 0 then
altNum = min(altNum, numNeed)
tinsert(sourceList, "alt/"..altNum.."/"..altCharactersStr)
return numNeed - altNum
end
elseif source == "altGuildBank" then
local currentGuild = PlayerInfo.GetPlayerGuild(playerName)
if currentGuild and crafter ~= playerName then
-- we are on an alt, so see if we can gather items from this character's guild bank
local guildBankQuantity = Inventory.GetGuildQuantity(itemString)
if guildBankQuantity > 0 then
guildBankQuantity = min(numNeed, guildBankQuantity)
tinsert(sourceList, "guildBank/"..guildBankQuantity.."/")
numNeed = numNeed - guildBankQuantity
if numNeed == 0 then
return 0
end
end
end
-- check alts
local totalGuildBankQuantity = 0
local altCharacters = TempTable.Acquire()
for _, character in PlayerInfo.CharacterIterator(true) do
local guild = PlayerInfo.GetPlayerGuild(character)
if guild and guild ~= currentGuild then
local guildBankQuantity = Inventory.GetGuildQuantity(itemString, guild)
if guildBankQuantity > 0 then
tinsert(altCharacters, character)
totalGuildBankQuantity = totalGuildBankQuantity + guildBankQuantity
end
end
end
local altCharactersStr = table.concat(altCharacters, "`")
TempTable.Release(altCharacters)
if totalGuildBankQuantity > 0 then
totalGuildBankQuantity = min(totalGuildBankQuantity, numNeed)
tinsert(sourceList, "altGuildBank/"..totalGuildBankQuantity.."/"..altCharactersStr)
return numNeed - totalGuildBankQuantity
end
elseif source == "craftProfit" or source == "craftNoProfit" then
local spellId, maxProfit = TSM.Crafting.GetMostProfitableSpellIdByItem(itemString, crafter, true)
if spellId and (source == "craftNoProfit" or (maxProfit and maxProfit > 0)) then
-- assume we can craft all we need
local numToCraft = ceil(numNeed / TSM.Crafting.GetNumResult(spellId))
tinsert(sourceList, source.."/"..numToCraft.."/")
return 0
end
elseif source == "auction" then
if ItemInfo.IsSoulbound(itemString) then
-- can't buy soulbound items
return numNeed
end
-- assume we can buy all we need from the AH
tinsert(sourceList, "auction/"..numNeed.."/")
return 0
elseif source == "auctionCrafting" then
if ItemInfo.IsSoulbound(itemString) then
-- can't buy soulbound items
return numNeed
end
if not Conversions.GetSourceItems(itemString) then
-- can't convert to get this item
return numNeed
end
-- assume we can buy all we need from the AH
tinsert(sourceList, "auctionCrafting/"..numNeed.."/")
return 0
elseif source == "auctionDE" then
if ItemInfo.IsSoulbound(itemString) then
-- can't buy soulbound items
return numNeed
end
if not DisenchantInfo.IsTargetItem(itemString) then
-- can't disenchant to get this item
return numNeed
end
-- assume we can buy all we need from the AH
tinsert(sourceList, "auctionDE/"..numNeed.."/")
return 0
else
error("Unkown source: "..tostring(source))
end
return numNeed
end
function private.QueryPlayerFilter(row, player)
return String.SeparatedContains(row:GetField("players"), ",", player)
end
function private.GetCrafterInventoryQuantity(itemString)
local crafter = TSM.db.factionrealm.gatheringContext.crafter
return Inventory.GetBagQuantity(itemString, crafter) + Inventory.GetReagentBankQuantity(itemString, crafter) + Inventory.GetBankQuantity(itemString, crafter)
end
function private.HandleNumHave(itemString, numNeed, numHave)
if numNeed > numHave then
-- use everything we have
numNeed = numNeed - numHave
return numNeed, numHave
else
-- we have at least as many as we need, so use all of them
return 0, numNeed
end
end

View File

@ -0,0 +1,303 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local PlayerProfessions = TSM.Crafting:NewPackage("PlayerProfessions")
local ProfessionInfo = TSM.Include("Data.ProfessionInfo")
local Database = TSM.Include("Util.Database")
local Event = TSM.Include("Util.Event")
local Delay = TSM.Include("Util.Delay")
local TempTable = TSM.Include("Util.TempTable")
local Vararg = TSM.Include("Util.Vararg")
local Threading = TSM.Include("Service.Threading")
local private = {
playerProfessionsThread = nil,
playerProfessionsThreadRunning = false,
db = nil,
query = nil,
}
local TAILORING_ES = "Sastrería"
local TAILORING_SKILL_ES = "Costura"
local LEATHERWORKING_ES = "Peletería"
local LEATHERWORKING_SKILL_ES = "Marroquinería"
local ENGINEERING_FR = "Ingénieur"
local ENGINEERING_SKILL_FR = "Ingénierie"
local FIRST_AID_FR = "Premiers soins"
local FIRST_AID_SKILL_FR = "Secourisme"
-- ============================================================================
-- Module Functions
-- ============================================================================
function PlayerProfessions.OnInitialize()
private.db = Database.NewSchema("PLAYER_PROFESSIONS")
:AddStringField("player")
:AddStringField("profession")
:AddNumberField("skillId")
:AddNumberField("level")
:AddNumberField("maxLevel")
:AddBooleanField("isSecondary")
:AddIndex("player")
:Commit()
private.query = private.db:NewQuery()
:Select("player", "profession", "skillId", "level", "maxLevel")
:OrderBy("isSecondary", true)
:OrderBy("level", false)
:OrderBy("profession", true)
private.playerProfessionsThread = Threading.New("PLAYER_PROFESSIONS", private.PlayerProfessionsThread)
private.StartPlayerProfessionsThread()
Event.Register("SKILL_LINES_CHANGED", private.PlayerProfessionsSkillUpdate)
Event.Register("LEARNED_SPELL_IN_TAB", private.StartPlayerProfessionsThread)
end
function PlayerProfessions.CreateQuery()
return private.db:NewQuery()
end
function PlayerProfessions.Iterator()
return private.query:Iterator()
end
-- ============================================================================
-- Player Professions Thread
-- ============================================================================
function private.StartPlayerProfessionsThread()
if private.playerProfessionsThreadRunning then
Threading.Kill(private.playerProfessionsThread)
end
private.playerProfessionsThreadRunning = true
Threading.Start(private.playerProfessionsThread)
end
function private.UpdatePlayerProfessionInfo(name, skillId, level, maxLevel, isSecondary)
local professionInfo = TSM.db.sync.internalData.playerProfessions[name] or {}
TSM.db.sync.internalData.playerProfessions[name] = professionInfo
-- preserve whether or not we've prompted to create groups and the profession link if possible
local oldPrompted = professionInfo.prompted or nil
local oldLink = professionInfo.link or nil
wipe(professionInfo)
professionInfo.skillId = skillId
professionInfo.level = level
professionInfo.maxLevel = maxLevel
professionInfo.isSecondary = isSecondary
professionInfo.prompted = oldPrompted
professionInfo.link = oldLink
end
function private.PlayerProfessionsSkillUpdate()
if TSM.IsWowClassic() then
local _, _, offset, numSpells = GetSpellTabInfo(1)
for i = offset + 1, offset + numSpells do
local name, subName = GetSpellBookItemName(i, BOOKTYPE_SPELL)
if not subName then
Delay.AfterTime(0.05, private.PlayerProfessionsSkillUpdate)
return
end
if name and subName and (ProfessionInfo.IsSubNameClassic(strtrim(subName, " ")) or name == ProfessionInfo.GetName("Smelting") or name == ProfessionInfo.GetName("Poisons") or name == LEATHERWORKING_ES or name == TAILORING_ES or name == ENGINEERING_FR or name == FIRST_AID_FR) and not TSM.UI.CraftingUI.IsProfessionIgnored(name) then
local level, maxLevel = nil, nil
for j = 1, GetNumSkillLines() do
local skillName, _, _, skillRank, _, _, skillMaxRank = GetSkillLineInfo(j)
if skillName == name then
level = skillRank
maxLevel = skillMaxRank
break
elseif name == ProfessionInfo.GetName("Smelting") and skillName == ProfessionInfo.GetName("Mining") then
name = ProfessionInfo.GetName("Mining")
level = skillRank
maxLevel = skillMaxRank
break
elseif name == LEATHERWORKING_ES and skillName == LEATHERWORKING_SKILL_ES then
name = LEATHERWORKING_SKILL_ES
level = skillRank
maxLevel = skillMaxRank
break
elseif name == TAILORING_ES and skillName == TAILORING_SKILL_ES then
name = TAILORING_SKILL_ES
level = skillRank
maxLevel = skillMaxRank
break
elseif name == ENGINEERING_FR and skillName == ENGINEERING_SKILL_FR then
name = ENGINEERING_SKILL_FR
level = skillRank
maxLevel = skillMaxRank
break
elseif name == FIRST_AID_FR and skillName == FIRST_AID_SKILL_FR then
name = FIRST_AID_SKILL_FR
level = skillRank
maxLevel = skillMaxRank
break
end
end
if level and maxLevel and not TSM.UI.CraftingUI.IsProfessionIgnored(name) then -- exclude ignored professions
private.UpdatePlayerProfessionInfo(name, -1, level, maxLevel, name == GetSpellInfo(129))
end
end
end
else
local professionIds = TempTable.Acquire(GetProfessions())
for i, id in pairs(professionIds) do -- needs to be pairs since there might be holes
if id ~= 8 and id ~= 9 then -- ignore fishing and arheology
local name, _, level, maxLevel, _, _, skillId = GetProfessionInfo(id)
if not TSM.UI.CraftingUI.IsProfessionIgnored(name) then -- exclude ignored professions
private.UpdatePlayerProfessionInfo(name, skillId, level, maxLevel, i > 2)
end
end
end
TempTable.Release(professionIds)
end
-- update our DB
private.db:TruncateAndBulkInsertStart()
for _, character in TSM.db:FactionrealmCharacterIterator() do
local playerProfessions = TSM.db:Get("sync", TSM.db:GetSyncScopeKeyByCharacter(character), "internalData", "playerProfessions")
if playerProfessions then
for name, info in pairs(playerProfessions) do
private.db:BulkInsertNewRow(character, name, info.skillId or -1, info.level, info.maxLevel, info.isSecondary)
end
end
end
private.db:BulkInsertEnd()
end
function private.PlayerProfessionsThread()
-- get the player's tradeskills
if TSM.IsWowClassic() then
SpellBookFrame_UpdateSkillLineTabs()
else
SpellBook_UpdateProfTab()
end
local forgetProfession = Threading.AcquireSafeTempTable()
for name in pairs(TSM.db.sync.internalData.playerProfessions) do
forgetProfession[name] = true
end
if TSM.IsWowClassic() then
local _, _, offset, numSpells = GetSpellTabInfo(1)
for i = offset + 1, offset + numSpells do
local name, subName = GetSpellBookItemName(i, BOOKTYPE_SPELL)
if name and subName and (ProfessionInfo.IsSubNameClassic(strtrim(subName, " ")) or name == ProfessionInfo.GetName("Smelting") or name == ProfessionInfo.GetName("Poisons") or name == LEATHERWORKING_ES or name == TAILORING_ES or name == ENGINEERING_FR or name == FIRST_AID_FR) and not TSM.UI.CraftingUI.IsProfessionIgnored(name) then
local level, maxLevel = nil, nil
for j = 1, GetNumSkillLines() do
local skillName, _, _, skillRank, _, _, skillMaxRank = GetSkillLineInfo(j)
if skillName == name then
level = skillRank
maxLevel = skillMaxRank
break
elseif name == ProfessionInfo.GetName("Smelting") and skillName == ProfessionInfo.GetName("Mining") then
name = ProfessionInfo.GetName("Mining")
level = skillRank
maxLevel = skillMaxRank
break
elseif name == LEATHERWORKING_ES and skillName == LEATHERWORKING_SKILL_ES then
name = LEATHERWORKING_SKILL_ES
level = skillRank
maxLevel = skillMaxRank
break
elseif name == TAILORING_ES and skillName == TAILORING_SKILL_ES then
name = TAILORING_SKILL_ES
level = skillRank
maxLevel = skillMaxRank
break
elseif name == ENGINEERING_FR and skillName == ENGINEERING_SKILL_FR then
name = ENGINEERING_SKILL_FR
level = skillRank
maxLevel = skillMaxRank
break
elseif name == FIRST_AID_FR and skillName == FIRST_AID_SKILL_FR then
name = FIRST_AID_SKILL_FR
level = skillRank
maxLevel = skillMaxRank
break
end
end
if level and maxLevel and not TSM.UI.CraftingUI.IsProfessionIgnored(name) then -- exclude ignored professions
forgetProfession[name] = nil
private.UpdatePlayerProfessionInfo(name, -1, level, maxLevel, name == GetSpellInfo(129))
end
end
end
else
Threading.WaitForFunction(GetProfessions)
local professionIds = Threading.AcquireSafeTempTable(GetProfessions())
-- ignore archeology and fishing which are in the 3rd and 4th slots respectively
professionIds[3] = nil
professionIds[4] = nil
for i, id in pairs(professionIds) do -- needs to be pairs since there might be holes
local name, _, level, maxLevel, _, _, skillId = Threading.WaitForFunction(GetProfessionInfo, id)
if not TSM.UI.CraftingUI.IsProfessionIgnored(name) then -- exclude ignored professions
forgetProfession[name] = nil
private.UpdatePlayerProfessionInfo(name, skillId, level, maxLevel, i > 2)
end
end
Threading.ReleaseSafeTempTable(professionIds)
end
for name in pairs(forgetProfession) do
TSM.db.sync.internalData.playerProfessions[name] = nil
end
Threading.ReleaseSafeTempTable(forgetProfession)
-- clean up crafts which are no longer known
local matUsed = Threading.AcquireSafeTempTable()
local spellIds = Threading.AcquireSafeTempTable()
for _, spellId in TSM.Crafting.SpellIterator() do
tinsert(spellIds, spellId)
end
for _, spellId in ipairs(spellIds) do
local playersToRemove = TempTable.Acquire()
for _, player in Vararg.Iterator(TSM.Crafting.GetPlayers(spellId)) do
-- check if the player still exists and still has this profession
local playerProfessions = TSM.db:Get("sync", TSM.db:GetSyncScopeKeyByCharacter(player), "internalData", "playerProfessions")
if not playerProfessions or not playerProfessions[TSM.Crafting.GetProfession(spellId)] then
tinsert(playersToRemove, player)
end
end
local stillExists = true
if #playersToRemove > 0 then
stillExists = TSM.Crafting.RemovePlayers(spellId, playersToRemove)
end
TempTable.Release(playersToRemove)
if stillExists then
for _, itemString in TSM.Crafting.MatIterator(spellId) do
matUsed[itemString] = true
end
end
Threading.Yield()
end
Threading.ReleaseSafeTempTable(spellIds)
-- clean up mats which aren't used anymore
local toRemove = TempTable.Acquire()
for itemString, matInfo in pairs(TSM.db.factionrealm.internalData.mats) do
-- clear out old names
matInfo.name = nil
if not matUsed[itemString] then
tinsert(toRemove, itemString)
end
end
Threading.ReleaseSafeTempTable(matUsed)
for _, itemString in ipairs(toRemove) do
TSM.db.factionrealm.internalData.mats[itemString] = nil
end
TempTable.Release(toRemove)
-- update our DB
private.db:TruncateAndBulkInsertStart()
for _, character in TSM.db:FactionrealmCharacterIterator() do
local playerProfessions = TSM.db:Get("sync", TSM.db:GetSyncScopeKeyByCharacter(character), "internalData", "playerProfessions")
if playerProfessions then
for name, info in pairs(playerProfessions) do
private.db:BulkInsertNewRow(character, name, info.skillId or -1, info.level, info.maxLevel, info.isSecondary)
end
end
end
private.db:BulkInsertEnd()
private.playerProfessionsThreadRunning = false
end

View File

@ -0,0 +1,554 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local ProfessionScanner = TSM.Crafting:NewPackage("ProfessionScanner")
local ProfessionInfo = TSM.Include("Data.ProfessionInfo")
local Database = TSM.Include("Util.Database")
local Event = TSM.Include("Util.Event")
local Delay = TSM.Include("Util.Delay")
local TempTable = TSM.Include("Util.TempTable")
local Math = TSM.Include("Util.Math")
local Log = TSM.Include("Util.Log")
local String = TSM.Include("Util.String")
local ItemString = TSM.Include("Util.ItemString")
local ItemInfo = TSM.Include("Service.ItemInfo")
local private = {
db = nil,
hasScanned = false,
callbacks = {},
disabled = false,
ignoreUpdatesUntil = 0,
optionalMatArrayTemp = { { itemID = nil, count = 1, index = nil } },
}
-- don't want to scan a bunch of times when the profession first loads so add a 10 frame debounce to update events
local SCAN_DEBOUNCE_FRAMES = 10
-- ============================================================================
-- Module Functions
-- ============================================================================
function ProfessionScanner.OnInitialize()
private.db = Database.NewSchema("CRAFTING_RECIPES")
:AddUniqueNumberField("index")
:AddUniqueNumberField("spellId")
:AddStringField("name")
:AddNumberField("categoryId")
:AddStringField("difficulty")
:AddNumberField("rank")
:AddNumberField("numSkillUps")
:Commit()
TSM.Crafting.ProfessionState.RegisterUpdateCallback(private.ProfessionStateUpdate)
if TSM.IsWowClassic() then
Event.Register("CRAFT_UPDATE", private.OnTradeSkillUpdateEvent)
Event.Register("TRADE_SKILL_UPDATE", private.OnTradeSkillUpdateEvent)
else
Event.Register("TRADE_SKILL_LIST_UPDATE", private.OnTradeSkillUpdateEvent)
end
Event.Register("CHAT_MSG_SKILL", private.ChatMsgSkillEventHandler)
end
function ProfessionScanner.SetDisabled(disabled)
if private.disabled == disabled then
return
end
private.disabled = disabled
if not disabled then
private.ScanProfession()
end
end
function ProfessionScanner.HasScanned()
return private.hasScanned
end
function ProfessionScanner.HasSkills()
return private.hasScanned and private.db:GetNumRows() > 0
end
function ProfessionScanner.RegisterHasScannedCallback(callback)
tinsert(private.callbacks, callback)
end
function ProfessionScanner.IgnoreNextProfessionUpdates()
private.ignoreUpdatesUntil = GetTime() + 1
end
function ProfessionScanner.CreateQuery()
return private.db:NewQuery()
end
function ProfessionScanner.GetIndexBySpellId(spellId)
assert(TSM.IsWowClassic() or private.hasScanned)
return private.db:GetUniqueRowField("spellId", spellId, "index")
end
function ProfessionScanner.GetCategoryIdBySpellId(spellId)
assert(private.hasScanned)
return private.db:GetUniqueRowField("spellId", spellId, "categoryId")
end
function ProfessionScanner.GetNameBySpellId(spellId)
assert(private.hasScanned)
return private.db:GetUniqueRowField("spellId", spellId, "name")
end
function ProfessionScanner.GetRankBySpellId(spellId)
assert(private.hasScanned)
return private.db:GetUniqueRowField("spellId", spellId, "rank")
end
function ProfessionScanner.GetNumSkillupsBySpellId(spellId)
assert(private.hasScanned)
return private.db:GetUniqueRowField("spellId", spellId, "numSkillUps")
end
function ProfessionScanner.GetDifficultyBySpellId(spellId)
assert(private.hasScanned)
return private.db:GetUniqueRowField("spellId", spellId, "difficulty")
end
function ProfessionScanner.GetFirstSpellId()
if not private.hasScanned then
return
end
return private.db:NewQuery()
:Select("spellId")
:OrderBy("index", true)
:GetFirstResultAndRelease()
end
function ProfessionScanner.HasSpellId(spellId)
return private.hasScanned and private.db:GetUniqueRowField("spellId", spellId, "index") and true or false
end
-- ============================================================================
-- Event Handlers
-- ============================================================================
function private.ProfessionStateUpdate()
private.hasScanned = false
for _, callback in ipairs(private.callbacks) do
callback()
end
if TSM.Crafting.ProfessionState.GetCurrentProfession() then
private.db:Truncate()
private.OnTradeSkillUpdateEvent()
else
Delay.Cancel("PROFESSION_SCAN_DELAY")
end
end
function private.OnTradeSkillUpdateEvent()
Delay.Cancel("PROFESSION_SCAN_DELAY")
private.QueueProfessionScan()
end
function private.ChatMsgSkillEventHandler(_, msg)
local professionName = TSM.Crafting.ProfessionState.GetCurrentProfession()
if not professionName or not strmatch(msg, professionName) then
return
end
private.ignoreUpdatesUntil = 0
private.QueueProfessionScan()
end
-- ============================================================================
-- Profession Scanning
-- ============================================================================
function private.QueueProfessionScan()
Delay.AfterFrame("PROFESSION_SCAN_DELAY", SCAN_DEBOUNCE_FRAMES, private.ScanProfession)
end
function private.ScanProfession()
if InCombatLockdown() then
-- we are in combat, so try again in a bit
private.QueueProfessionScan()
return
elseif private.disabled then
return
elseif GetTime() < private.ignoreUpdatesUntil then
return
end
local professionName = TSM.Crafting.ProfessionState.GetCurrentProfession()
if not professionName then
-- profession hasn't fully opened yet
private.QueueProfessionScan()
return
end
assert(professionName and TSM.Crafting.ProfessionUtil.IsDataStable())
if TSM.IsWowClassic() then
-- TODO: check and clear filters on classic
else
local hadFilter = false
if C_TradeSkillUI.GetOnlyShowUnlearnedRecipes() then
C_TradeSkillUI.SetOnlyShowLearnedRecipes(true)
C_TradeSkillUI.SetOnlyShowUnlearnedRecipes(false)
hadFilter = true
end
if C_TradeSkillUI.GetOnlyShowMakeableRecipes() then
C_TradeSkillUI.SetOnlyShowMakeableRecipes(false)
hadFilter = true
end
if C_TradeSkillUI.GetOnlyShowSkillUpRecipes() then
C_TradeSkillUI.SetOnlyShowSkillUpRecipes(false)
hadFilter = true
end
if C_TradeSkillUI.AnyRecipeCategoriesFiltered() then
C_TradeSkillUI.ClearRecipeCategoryFilter()
hadFilter = true
end
if C_TradeSkillUI.AreAnyInventorySlotsFiltered() then
C_TradeSkillUI.ClearInventorySlotFilter()
hadFilter = true
end
for i = 1, C_PetJournal.GetNumPetSources() do
if C_TradeSkillUI.IsAnyRecipeFromSource(i) and C_TradeSkillUI.IsRecipeSourceTypeFiltered(i) then
C_TradeSkillUI.ClearRecipeSourceTypeFilter()
hadFilter = true
break
end
end
if C_TradeSkillUI.GetRecipeItemNameFilter() ~= "" then
C_TradeSkillUI.SetRecipeItemNameFilter(nil)
hadFilter = true
end
local minItemLevel, maxItemLevel = C_TradeSkillUI.GetRecipeItemLevelFilter()
if minItemLevel ~= 0 or maxItemLevel ~= 0 then
C_TradeSkillUI.SetRecipeItemLevelFilter(0, 0)
hadFilter = true
end
if hadFilter then
-- an update event will be triggered
return
end
end
if TSM.IsWowClassic() then
local lastHeaderIndex = 0
private.db:TruncateAndBulkInsertStart()
for i = 1, TSM.Crafting.ProfessionState.IsClassicCrafting() and GetNumCrafts() or GetNumTradeSkills() do
local name, _, skillType, hash = nil, nil, nil, nil
if TSM.Crafting.ProfessionState.IsClassicCrafting() then
name, _, skillType = GetCraftInfo(i)
if skillType ~= "header" then
hash = Math.CalculateHash(name)
for j = 1, GetCraftNumReagents(i) do
local _, _, quantity = GetCraftReagentInfo(i, j)
hash = Math.CalculateHash(ItemString.Get(GetCraftReagentItemLink(i, j)), hash)
hash = Math.CalculateHash(quantity, hash)
end
end
else
name, skillType = GetTradeSkillInfo(i)
if skillType ~= "header" then
hash = Math.CalculateHash(name)
for j = 1, GetTradeSkillNumReagents(i) do
local _, _, quantity = GetTradeSkillReagentInfo(i, j)
hash = Math.CalculateHash(ItemString.Get(GetTradeSkillReagentItemLink(i, j)), hash)
hash = Math.CalculateHash(quantity, hash)
end
end
end
if skillType == "header" then
lastHeaderIndex = i
else
if name then
private.db:BulkInsertNewRow(i, hash, name, lastHeaderIndex, skillType, -1, 1)
end
end
end
private.db:BulkInsertEnd()
else
local prevRecipeIds = TempTable.Acquire()
local nextRecipeIds = TempTable.Acquire()
local recipeLearned = TempTable.Acquire()
local recipes = TempTable.Acquire()
assert(C_TradeSkillUI.GetFilteredRecipeIDs(recipes) == recipes)
local spellIdIndex = TempTable.Acquire()
for index, spellId in ipairs(recipes) do
-- There's a Blizzard bug where First Aid duplicates spellIds, so check that we haven't seen this before
if not spellIdIndex[spellId] then
spellIdIndex[spellId] = index
local info = nil
if not TSM.IsShadowlands() then
info = TempTable.Acquire()
assert(C_TradeSkillUI.GetRecipeInfo(spellId, info) == info)
else
info = C_TradeSkillUI.GetRecipeInfo(spellId)
end
if info.previousRecipeID then
prevRecipeIds[spellId] = info.previousRecipeID
nextRecipeIds[info.previousRecipeID] = spellId
end
if info.nextRecipeID then
nextRecipeIds[spellId] = info.nextRecipeID
prevRecipeIds[info.nextRecipeID] = spellId
end
recipeLearned[spellId] = info.learned
if not TSM.IsShadowlands() then
TempTable.Release(info)
end
end
end
private.db:TruncateAndBulkInsertStart()
local inactiveSpellIds = TempTable.Acquire()
for index, spellId in ipairs(recipes) do
local hasHigherRank = nextRecipeIds[spellId] and recipeLearned[nextRecipeIds[spellId]]
-- TODO: show unlearned recipes in the TSM UI
-- There's a Blizzard bug where First Aid duplicates spellIds, so check that this is the right index
if spellIdIndex[spellId] == index and recipeLearned[spellId] and not hasHigherRank then
local info = nil
if not TSM.IsShadowlands() then
info = TempTable.Acquire()
assert(C_TradeSkillUI.GetRecipeInfo(spellId, info) == info)
else
info = C_TradeSkillUI.GetRecipeInfo(spellId)
end
local rank = -1
if prevRecipeIds[spellId] or nextRecipeIds[spellId] then
rank = 1
local tempSpellId = spellId
while prevRecipeIds[tempSpellId] do
rank = rank + 1
tempSpellId = prevRecipeIds[tempSpellId]
end
end
local numSkillUps = info.difficulty == "optimal" and info.numSkillUps or 1
private.db:BulkInsertNewRow(index, spellId, info.name, info.categoryID, info.difficulty, rank, numSkillUps)
if not TSM.IsShadowlands() then
TempTable.Release(info)
end
else
inactiveSpellIds[spellId] = true
end
end
private.db:BulkInsertEnd()
-- remove spells which are not active (i.e. older ranks)
if next(inactiveSpellIds) then
TSM.Crafting.RemovePlayerSpells(inactiveSpellIds)
end
TempTable.Release(inactiveSpellIds)
TempTable.Release(spellIdIndex)
TempTable.Release(recipes)
TempTable.Release(prevRecipeIds)
TempTable.Release(nextRecipeIds)
TempTable.Release(recipeLearned)
end
if TSM.Crafting.ProfessionUtil.IsNPCProfession() or TSM.Crafting.ProfessionUtil.IsLinkedProfession() or TSM.Crafting.ProfessionUtil.IsGuildProfession() then
-- we don't want to store this profession in our DB, so we're done
if not private.hasScanned then
private.hasScanned = true
for _, callback in ipairs(private.callbacks) do
callback()
end
end
return
end
if not TSM.db.sync.internalData.playerProfessions[professionName] then
-- we are in combat or the player's professions haven't been scanned yet by PlayerProfessions.lua, so try again in a bit
private.QueueProfessionScan()
return
end
-- update the link for this profession
TSM.db.sync.internalData.playerProfessions[professionName].link = not TSM.IsWowClassic() and C_TradeSkillUI.GetTradeSkillListLink() or nil
-- scan all the recipes
TSM.Crafting.SetSpellDBQueryUpdatesPaused(true)
local query = private.db:NewQuery()
:Select("spellId")
local numFailed = 0
for _, spellId in query:Iterator() do
if not private.ScanRecipe(professionName, spellId) then
numFailed = numFailed + 1
end
end
query:Release()
TSM.Crafting.SetSpellDBQueryUpdatesPaused(false)
Log.Info("Scanned %s (failed to scan %d)", professionName, numFailed)
if numFailed > 0 then
-- didn't completely scan, so we'll try again
private.QueueProfessionScan()
end
if not private.hasScanned then
private.hasScanned = true
for _, callback in ipairs(private.callbacks) do
callback()
end
end
-- explicitly run GC
collectgarbage()
end
function private.ScanRecipe(professionName, spellId)
-- get the links
local itemLink, lNum, hNum = TSM.Crafting.ProfessionUtil.GetRecipeInfo(TSM.IsWowClassic() and ProfessionScanner.GetIndexBySpellId(spellId) or spellId)
assert(itemLink, "Invalid craft: "..tostring(spellId))
-- get the itemString and craft name
local itemString, craftName = nil, nil
if strfind(itemLink, "enchant:") then
if TSM.IsWowClassic() then
return true
else
-- result of craft is not an item
itemString = ProfessionInfo.GetIndirectCraftResult(spellId)
if not itemString then
-- we don't care about this craft
return true
end
craftName = GetSpellInfo(spellId)
end
elseif strfind(itemLink, "item:") then
-- result of craft is item
itemString = ItemString.GetBase(itemLink)
craftName = ItemInfo.GetName(itemLink)
-- Blizzard broke Brilliant Scarlet Ruby in 8.3, so just hard-code a workaround
if spellId == 53946 and not itemString and not craftName then
itemString = "i:39998"
craftName = GetSpellInfo(spellId)
end
else
error("Invalid craft: "..tostring(spellId))
end
if not itemString or not craftName then
Log.Warn("No itemString (%s) or craftName (%s) found (%s, %s)", tostring(itemString), tostring(craftName), tostring(professionName), tostring(spellId))
return false
end
-- get the result number
local numResult = nil
local isEnchant = professionName == GetSpellInfo(7411) and strfind(itemLink, "enchant:")
if isEnchant then
numResult = 1
else
-- workaround for incorrect values returned for Temporal Crystal
if spellId == 169092 and itemString == "i:113588" then
lNum, hNum = 1, 1
end
-- workaround for incorrect values returned for new mass milling recipes
if ProfessionInfo.IsMassMill(spellId) then
if spellId == 210116 then -- Yseralline
lNum, hNum = 4, 4 -- always four
elseif spellId == 209664 then -- Felwort
lNum, hNum = 42, 42 -- amount is variable but the values are conservative
elseif spellId == 247861 then -- Astral Glory
lNum, hNum = 4, 4 -- amount is variable but the values are conservative
else
lNum, hNum = 8, 8.8
end
end
numResult = floor(((lNum or 1) + (hNum or 1)) / 2)
end
-- store general info about this recipe
local hasCD = TSM.Crafting.ProfessionUtil.HasCooldown(spellId)
TSM.Crafting.CreateOrUpdate(spellId, itemString, professionName, craftName, numResult, UnitName("player"), hasCD)
-- get the mat quantities and add mats to our DB
local matQuantities = TempTable.Acquire()
local haveInvalidMats = false
local numReagents = TSM.Crafting.ProfessionUtil.GetNumMats(spellId)
for i = 1, numReagents do
local matItemLink, name, _, quantity = TSM.Crafting.ProfessionUtil.GetMatInfo(spellId, i)
local matItemString = ItemString.GetBase(matItemLink)
if not matItemString then
Log.Warn("Failed to get itemString for mat %d (%s, %s)", i, tostring(professionName), tostring(spellId))
haveInvalidMats = true
break
end
if not name or not quantity then
Log.Warn("Failed to get name (%s) or quantity (%s) for mat (%s, %s, %d)", tostring(name), tostring(quantity), tostring(professionName), tostring(spellId), i)
haveInvalidMats = true
break
end
ItemInfo.StoreItemName(matItemString, name)
TSM.db.factionrealm.internalData.mats[matItemString] = TSM.db.factionrealm.internalData.mats[matItemString] or {}
matQuantities[matItemString] = quantity
end
-- if this is an enchant, add a vellum to the list of mats
if isEnchant then
local matItemString = ProfessionInfo.GetVellumItemString()
TSM.db.factionrealm.internalData.mats[matItemString] = TSM.db.factionrealm.internalData.mats[matItemString] or {}
matQuantities[matItemString] = 1
end
if not haveInvalidMats then
local optionalMats = private.GetOptionalMats(spellId)
if optionalMats then
for _, matStr in ipairs(optionalMats) do
local _, _, mats = strsplit(":", matStr)
for itemId in String.SplitIterator(mats, ",") do
local matItemString = "i:"..itemId
TSM.db.factionrealm.internalData.mats[matItemString] = TSM.db.factionrealm.internalData.mats[matItemString] or {}
end
matQuantities[matStr] = -1
end
end
TSM.Crafting.SetMats(spellId, matQuantities)
end
TempTable.Release(matQuantities)
return not haveInvalidMats
end
function private.GetOptionalMats(spellId)
local optionalMats = TSM.IsShadowlands() and C_TradeSkillUI.GetOptionalReagentInfo(spellId) or nil
if not optionalMats or #optionalMats == 0 then
return nil
end
for i, info in ipairs(optionalMats) do
if info.requiredSkillRank ~= 0 then
-- TODO: handle this case
return nil
else
-- process the options
assert(#info.options > 0)
-- sort the optional mats by itemId
sort(info.options)
-- cache the optional mat info
for _, itemId in ipairs(info.options) do
assert(type(itemId) == "number")
private.CacheOptionalMatInfo(spellId, i, itemId)
end
local matList = table.concat(info.options, ",")
TSM.Crafting.ProfessionUtil.StoreOptionalMatText(matList, info.slotText)
optionalMats[i] = "o:"..i..":"..matList
end
end
return optionalMats
end
function private.CacheOptionalMatInfo(spellId, index, itemId)
if TSM.db.global.internalData.optionalMatBonusIdLookup[itemId] then
return
end
if not TSMScanTooltip then
CreateFrame("GameTooltip", "TSMScanTooltip", UIParent, "GameTooltipTemplate")
end
private.optionalMatArrayTemp.itemID = itemId
private.optionalMatArrayTemp.slot = index
TSMScanTooltip:SetOwner(UIParent, "ANCHOR_NONE")
TSMScanTooltip:ClearLines()
TSMScanTooltip:SetRecipeResultItem(spellId, private.optionalMatArrayTemp)
local _, itemLink = TSMScanTooltip:GetItem()
local bonusId = strmatch(itemLink, "item:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:2:3524:([0-9]+)")
TSM.db.global.internalData.optionalMatBonusIdLookup[itemId] = tonumber(bonusId)
end

View File

@ -0,0 +1,191 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local ProfessionState = TSM.Crafting:NewPackage("ProfessionState")
local Event = TSM.Include("Util.Event")
local Delay = TSM.Include("Util.Delay")
local FSM = TSM.Include("Util.FSM")
local Log = TSM.Include("Util.Log")
local private = {
fsm = nil,
updateCallbacks = {},
isClosed = true,
craftOpen = nil,
tradeSkillOpen = nil,
professionName = nil,
}
local WAIT_FRAME_DELAY = 5
-- ============================================================================
-- Module Functions
-- ============================================================================
function ProfessionState.OnInitialize()
private.CreateFSM()
end
function ProfessionState.RegisterUpdateCallback(callback)
tinsert(private.updateCallbacks, callback)
end
function ProfessionState.GetIsClosed()
return private.isClosed
end
function ProfessionState.IsClassicCrafting()
return TSM.IsWowClassic() and private.craftOpen
end
function ProfessionState.SetCraftOpen(open)
private.craftOpen = open
end
function ProfessionState.GetCurrentProfession()
return private.professionName
end
-- ============================================================================
-- FSM
-- ============================================================================
function private.CreateFSM()
if TSM.IsWowClassic() and not IsAddOnLoaded("Blizzard_CraftUI") then
LoadAddOn("Blizzard_CraftUI")
end
Event.Register("TRADE_SKILL_SHOW", function()
private.tradeSkillOpen = true
private.fsm:ProcessEvent("EV_TRADE_SKILL_SHOW")
private.fsm:ProcessEvent("EV_TRADE_SKILL_DATA_SOURCE_CHANGING")
private.fsm:ProcessEvent("EV_TRADE_SKILL_DATA_SOURCE_CHANGED")
end)
Event.Register("TRADE_SKILL_CLOSE", function()
private.tradeSkillOpen = false
if not private.craftOpen then
private.fsm:ProcessEvent("EV_TRADE_SKILL_CLOSE")
end
end)
if not TSM.IsWowClassic() then
Event.Register("GARRISON_TRADESKILL_NPC_CLOSED", function()
private.fsm:ProcessEvent("EV_TRADE_SKILL_CLOSE")
end)
Event.Register("TRADE_SKILL_DATA_SOURCE_CHANGED", function()
private.fsm:ProcessEvent("EV_TRADE_SKILL_DATA_SOURCE_CHANGED")
end)
Event.Register("TRADE_SKILL_DATA_SOURCE_CHANGING", function()
private.fsm:ProcessEvent("EV_TRADE_SKILL_DATA_SOURCE_CHANGING")
end)
else
Event.Register("CRAFT_SHOW", function()
private.craftOpen = true
private.fsm:ProcessEvent("EV_TRADE_SKILL_SHOW")
private.fsm:ProcessEvent("EV_TRADE_SKILL_DATA_SOURCE_CHANGING")
private.fsm:ProcessEvent("EV_TRADE_SKILL_DATA_SOURCE_CHANGED")
end)
Event.Register("CRAFT_CLOSE", function()
private.craftOpen = false
if not private.tradeSkillOpen then
private.fsm:ProcessEvent("EV_TRADE_SKILL_CLOSE")
end
end)
Event.Register("CRAFT_UPDATE", function()
private.fsm:ProcessEvent("EV_TRADE_SKILL_DATA_SOURCE_CHANGED")
end)
end
local function ToggleDefaultCraftButton()
if not CraftCreateButton then
return
end
if private.craftOpen then
CraftCreateButton:Show()
else
CraftCreateButton:Hide()
end
end
local function FrameDelayCallback()
private.fsm:ProcessEvent("EV_FRAME_DELAY")
end
private.fsm = FSM.New("PROFESSION_STATE")
:AddState(FSM.NewState("ST_CLOSED")
:SetOnEnter(function()
private.isClosed = true
private.RunUpdateCallbacks()
end)
:SetOnExit(function()
private.isClosed = false
private.RunUpdateCallbacks()
end)
:AddTransition("ST_WAITING_FOR_DATA")
:AddEventTransition("EV_TRADE_SKILL_SHOW", "ST_WAITING_FOR_DATA")
)
:AddState(FSM.NewState("ST_WAITING_FOR_DATA")
:AddTransition("ST_WAITING_FOR_READY")
:AddTransition("ST_CLOSED")
:AddEventTransition("EV_TRADE_SKILL_DATA_SOURCE_CHANGED", "ST_WAITING_FOR_READY")
:AddEventTransition("EV_TRADE_SKILL_CLOSE", "ST_CLOSED")
)
:AddState(FSM.NewState("ST_WAITING_FOR_READY")
:SetOnEnter(function()
Delay.AfterFrame("PROFESSION_STATE_TIME", WAIT_FRAME_DELAY, FrameDelayCallback, WAIT_FRAME_DELAY)
end)
:SetOnExit(function()
Delay.Cancel("PROFESSION_STATE_TIME")
end)
:AddTransition("ST_SHOWN")
:AddTransition("ST_DATA_CHANGING")
:AddTransition("ST_CLOSED")
:AddEvent("EV_FRAME_DELAY", function()
if TSM.Crafting.ProfessionUtil.IsDataStable() then
return "ST_SHOWN"
end
end)
:AddEventTransition("EV_TRADE_SKILL_DATA_SOURCE_CHANGING", "ST_DATA_CHANGING")
:AddEventTransition("EV_TRADE_SKILL_CLOSE", "ST_CLOSED")
)
:AddState(FSM.NewState("ST_SHOWN")
:SetOnEnter(function()
local name = TSM.Crafting.ProfessionUtil.GetCurrentProfessionName()
assert(name)
Log.Info("Showing profession: %s", name)
private.professionName = name
if TSM.IsWowClassic() then
ToggleDefaultCraftButton()
end
private.RunUpdateCallbacks()
end)
:SetOnExit(function()
private.professionName = nil
private.RunUpdateCallbacks()
end)
:AddTransition("ST_DATA_CHANGING")
:AddTransition("ST_CLOSED")
:AddEventTransition("EV_TRADE_SKILL_DATA_SOURCE_CHANGING", "ST_DATA_CHANGING")
:AddEventTransition("EV_TRADE_SKILL_CLOSE", "ST_CLOSED")
)
:AddState(FSM.NewState("ST_DATA_CHANGING")
:AddTransition("ST_WAITING_FOR_READY")
:AddTransition("ST_CLOSED")
:AddEventTransition("EV_TRADE_SKILL_DATA_SOURCE_CHANGED", "ST_WAITING_FOR_READY")
:AddEventTransition("EV_TRADE_SKILL_CLOSE", "ST_CLOSED")
)
:Init("ST_CLOSED")
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.RunUpdateCallbacks()
for _, callback in ipairs(private.updateCallbacks) do
callback(private.professionName)
end
end

View File

@ -0,0 +1,480 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local ProfessionUtil = TSM.Crafting:NewPackage("ProfessionUtil")
local ProfessionInfo = TSM.Include("Data.ProfessionInfo")
local Event = TSM.Include("Util.Event")
local TempTable = TSM.Include("Util.TempTable")
local Log = TSM.Include("Util.Log")
local Delay = TSM.Include("Util.Delay")
local ItemString = TSM.Include("Util.ItemString")
local ItemInfo = TSM.Include("Service.ItemInfo")
local BagTracking = TSM.Include("Service.BagTracking")
local Inventory = TSM.Include("Service.Inventory")
local CustomPrice = TSM.Include("Service.CustomPrice")
local private = {
craftQuantity = nil,
craftSpellId = nil,
craftCallback = nil,
craftName = nil,
castingTimeout = nil,
craftTimeout = nil,
preparedSpellId = nil,
preparedTime = 0,
categoryInfoTemp = {},
}
local PROFESSION_LOOKUP = {
["Costura"] = "Sastrería",
["Marroquinería"] = "Peletería",
["Ingénierie"] = "Ingénieur",
["Secourisme"] = "Premiers soins",
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function ProfessionUtil.OnInitialize()
Event.Register("UNIT_SPELLCAST_SUCCEEDED", function(_, unit, _, spellId)
if unit ~= "player" then
return
end
if (TSM.IsWowClassic() and GetSpellInfo(spellId) ~= private.craftName) or (not TSM.IsWowClassic() and spellId ~= private.craftSpellId) then
return
end
-- check if we need to update bank quantity manually
for _, itemString, quantity in TSM.Crafting.MatIterator(private.craftSpellId) do
local bankUsed = quantity - (Inventory.GetBagQuantity(itemString) + Inventory.GetReagentBankQuantity(itemString))
if bankUsed > 0 and bankUsed <= Inventory.GetBankQuantity(itemString) then
Log.Info("Used %d from bank", bankUsed)
BagTracking.ForceBankQuantityDeduction(itemString, bankUsed)
end
end
local callback = private.craftCallback
assert(callback)
private.craftQuantity = private.craftQuantity - 1
private.DoCraftCallback(true, private.craftQuantity == 0)
-- ignore profession updates from crafting something
TSM.Crafting.ProfessionScanner.IgnoreNextProfessionUpdates()
-- restart the timeout
end)
local function SpellcastFailedEventHandler(_, unit, _, spellId)
if unit ~= "player" then
return
end
if (TSM.IsWowClassic() and GetSpellInfo(spellId) ~= private.craftName) or (not TSM.IsWowClassic() and spellId ~= private.craftSpellId) then
return
end
private.DoCraftCallback(false, true)
end
local function ClearCraftCast()
private.craftQuantity = nil
private.craftSpellId = nil
private.craftName = nil
private.castingTimeout = nil
private.craftTimeout = nil
end
Event.Register("UNIT_SPELLCAST_INTERRUPTED", SpellcastFailedEventHandler)
Event.Register("UNIT_SPELLCAST_FAILED", SpellcastFailedEventHandler)
Event.Register("UNIT_SPELLCAST_FAILED_QUIET", SpellcastFailedEventHandler)
Event.Register("TRADE_SKILL_CLOSE", ClearCraftCast)
if TSM.IsWowClassic() then
Event.Register("CRAFT_CLOSE", ClearCraftCast)
end
end
function ProfessionUtil.GetCurrentProfessionName()
if TSM.IsWowClassic() then
local name = TSM.Crafting.ProfessionState.IsClassicCrafting() and GetCraftSkillLine(1) or GetTradeSkillLine()
return name
else
local _, name, _, _, _, _, parentName = C_TradeSkillUI.GetTradeSkillLine()
return parentName or name
end
end
function ProfessionUtil.GetResultInfo(spellId)
-- get the links
local itemLink = ProfessionUtil.GetRecipeInfo(spellId)
assert(itemLink, "Invalid craft: "..tostring(spellId))
if strfind(itemLink, "enchant:") then
-- result of craft is not an item
local itemString = ProfessionInfo.GetIndirectCraftResult(spellId)
if itemString and not TSM.IsWowClassic() then
return TSM.UI.GetColoredItemName(itemString), itemString, ItemInfo.GetTexture(itemString)
elseif ProfessionInfo.IsEngineeringTinker(spellId) then
local name, _, icon = GetSpellInfo(spellId)
return name, nil, icon
else
local name, _, icon = GetSpellInfo(TSM.Crafting.ProfessionState.IsClassicCrafting() and GetCraftInfo(TSM.IsWowClassic() and TSM.Crafting.ProfessionScanner.GetIndexBySpellId(spellId) or spellId) or spellId)
return name, nil, icon
end
elseif strfind(itemLink, "item:") then
-- result of craft is an item
return TSM.UI.GetColoredItemName(itemLink), ItemString.Get(itemLink), ItemInfo.GetTexture(itemLink)
else
error("Invalid craft: "..tostring(spellId))
end
end
function ProfessionUtil.GetNumCraftable(spellId)
local num, numAll = math.huge, math.huge
for i = 1, ProfessionUtil.GetNumMats(spellId) do
local matItemLink, _, _, quantity = ProfessionUtil.GetMatInfo(spellId, i)
local itemString = ItemString.Get(matItemLink)
local totalQuantity = CustomPrice.GetItemPrice(itemString, "NumInventory") or 0
if not itemString or not quantity or totalQuantity == 0 then
return 0, 0
end
local bagQuantity = Inventory.GetBagQuantity(itemString)
if not TSM.IsWowClassic() then
bagQuantity = bagQuantity + Inventory.GetReagentBankQuantity(itemString) + Inventory.GetBankQuantity(itemString)
end
num = min(num, floor(bagQuantity / quantity))
numAll = min(numAll, floor(totalQuantity / quantity))
end
if num == math.huge or numAll == math.huge then
return 0, 0
end
return num, numAll
end
function ProfessionUtil.IsCraftable(spellId)
for i = 1, ProfessionUtil.GetNumMats(spellId) do
local matItemLink, _, _, quantity = ProfessionUtil.GetMatInfo(spellId, i)
local itemString = ItemString.Get(matItemLink)
if not itemString or not quantity then
return false
end
local bagQuantity = Inventory.GetBagQuantity(itemString)
if not TSM.IsWowClassic() then
bagQuantity = bagQuantity + Inventory.GetReagentBankQuantity(itemString) + Inventory.GetBankQuantity(itemString)
end
if floor(bagQuantity / quantity) == 0 then
return false
end
end
return true
end
function ProfessionUtil.GetNumCraftableFromDB(spellId)
local num = math.huge
for _, itemString, quantity in TSM.Crafting.MatIterator(spellId) do
local bagQuantity = Inventory.GetBagQuantity(itemString)
if not TSM.IsWowClassic() then
bagQuantity = bagQuantity + Inventory.GetReagentBankQuantity(itemString) + Inventory.GetBankQuantity(itemString)
end
num = min(num, floor(bagQuantity / quantity))
end
if num == math.huge then
return 0
end
return num
end
function ProfessionUtil.IsEnchant(spellId)
local name = ProfessionUtil.GetCurrentProfessionName()
if name ~= GetSpellInfo(7411) or TSM.IsWowClassic() then
return false
end
if not strfind(C_TradeSkillUI.GetRecipeItemLink(spellId), "enchant:") then
return false
end
local recipeInfo = nil
if not TSM.IsShadowlands() then
recipeInfo = TempTable.Acquire()
assert(C_TradeSkillUI.GetRecipeInfo(spellId, recipeInfo) == recipeInfo)
else
recipeInfo = C_TradeSkillUI.GetRecipeInfo(spellId)
end
local altVerb = recipeInfo.alternateVerb
if not TSM.IsShadowlands() then
TempTable.Release(recipeInfo)
end
return altVerb and true or false
end
function ProfessionUtil.OpenProfession(profession, skillId)
if TSM.IsWowClassic() then
if profession == ProfessionInfo.GetName("Mining") then
-- mining needs to be opened as smelting
profession = ProfessionInfo.GetName("Smelting")
end
if PROFESSION_LOOKUP[profession] then
profession = PROFESSION_LOOKUP[profession]
end
CastSpellByName(profession)
else
C_TradeSkillUI.OpenTradeSkill(skillId)
end
end
function ProfessionUtil.PrepareToCraft(spellId, quantity)
quantity = min(quantity, ProfessionUtil.GetNumCraftable(spellId))
if quantity == 0 then
return
end
if ProfessionUtil.IsEnchant(spellId) then
quantity = 1
end
if not TSM.IsWowClassic() then
C_TradeSkillUI.SetRecipeRepeatCount(spellId, quantity)
end
private.preparedSpellId = spellId
private.preparedTime = GetTime()
end
function ProfessionUtil.Craft(spellId, quantity, useVellum, callback)
assert(TSM.Crafting.ProfessionScanner.HasSpellId(spellId))
if private.craftSpellId then
private.craftCallback = callback
private.DoCraftCallback(false, true)
return 0
end
quantity = min(quantity, ProfessionUtil.GetNumCraftable(spellId))
if quantity == 0 then
return 0
end
local isEnchant = ProfessionUtil.IsEnchant(spellId)
if isEnchant then
quantity = 1
elseif spellId ~= private.preparedSpellId or private.preparedTime == GetTime() then
-- We can only craft one of this item due to a bug on Blizzard's end
quantity = 1
end
private.craftQuantity = quantity
private.craftSpellId = spellId
private.craftCallback = callback
if TSM.IsWowClassic() then
spellId = TSM.Crafting.ProfessionScanner.GetIndexBySpellId(spellId)
if TSM.Crafting.ProfessionState.IsClassicCrafting() then
private.craftName = GetCraftInfo(spellId)
else
private.craftName = GetTradeSkillInfo(spellId)
DoTradeSkill(spellId, quantity)
end
else
C_TradeSkillUI.CraftRecipe(spellId, quantity)
end
if useVellum and isEnchant then
UseItemByName(ItemInfo.GetName(ProfessionInfo.GetVellumItemString()))
end
private.castingTimeout = nil
private.craftTimeout = nil
Delay.AfterTime("PROFESSION_CRAFT_TIMEOUT_MONITOR", 0.5, private.CraftTimeoutMonitor, 0.5)
return quantity
end
function ProfessionUtil.IsDataStable()
return TSM.IsWowClassic() or (C_TradeSkillUI.IsTradeSkillReady() and not C_TradeSkillUI.IsDataSourceChanging())
end
function ProfessionUtil.HasCooldown(spellId)
if TSM.IsWowClassic() then
return GetTradeSkillCooldown(spellId) and true or false
else
return select(2, C_TradeSkillUI.GetRecipeCooldown(spellId)) and true or false
end
end
function ProfessionUtil.GetRemainingCooldown(spellId)
if TSM.IsWowClassic() then
return GetTradeSkillCooldown(spellId)
else
return C_TradeSkillUI.GetRecipeCooldown(spellId)
end
end
function ProfessionUtil.GetRecipeInfo(spellId)
local itemLink, lNum, hNum, toolsStr, hasTools = nil, nil, nil, nil, nil
if TSM.IsWowClassic() then
spellId = TSM.Crafting.ProfessionScanner.GetIndexBySpellId(spellId) or spellId
itemLink = TSM.Crafting.ProfessionState.IsClassicCrafting() and GetCraftItemLink(spellId) or GetTradeSkillItemLink(spellId)
if TSM.Crafting.ProfessionState.IsClassicCrafting() then
lNum, hNum = 1, 1
toolsStr, hasTools = GetCraftSpellFocus(spellId)
else
lNum, hNum = GetTradeSkillNumMade(spellId)
toolsStr, hasTools = GetTradeSkillTools(spellId)
end
else
itemLink = C_TradeSkillUI.GetRecipeItemLink(spellId)
lNum, hNum = C_TradeSkillUI.GetRecipeNumItemsProduced(spellId)
toolsStr, hasTools = C_TradeSkillUI.GetRecipeTools(spellId)
end
return itemLink, lNum, hNum, toolsStr, hasTools
end
function ProfessionUtil.GetNumMats(spellId)
local numMats = nil
if TSM.IsWowClassic() then
spellId = TSM.Crafting.ProfessionScanner.GetIndexBySpellId(spellId) or spellId
numMats = TSM.Crafting.ProfessionState.IsClassicCrafting() and GetCraftNumReagents(spellId) or GetTradeSkillNumReagents(spellId)
else
numMats = C_TradeSkillUI.GetRecipeNumReagents(spellId)
end
return numMats
end
function ProfessionUtil.GetMatInfo(spellId, index)
local itemLink, name, texture, quantity = nil, nil, nil, nil
if TSM.IsWowClassic() then
spellId = TSM.Crafting.ProfessionScanner.GetIndexBySpellId(spellId) or spellId
itemLink = TSM.Crafting.ProfessionState.IsClassicCrafting() and GetCraftReagentItemLink(spellId, index) or GetTradeSkillReagentItemLink(spellId, index)
if TSM.Crafting.ProfessionState.IsClassicCrafting() then
name, texture, quantity = GetCraftReagentInfo(spellId, index)
else
name, texture, quantity = GetTradeSkillReagentInfo(spellId, index)
end
else
itemLink = C_TradeSkillUI.GetRecipeReagentItemLink(spellId, index)
name, texture, quantity = C_TradeSkillUI.GetRecipeReagentInfo(spellId, index)
if itemLink then
name = name or ItemInfo.GetName(itemLink)
texture = texture or ItemInfo.GetTexture(itemLink)
end
end
return itemLink, name, texture, quantity
end
function ProfessionUtil.CloseTradeSkill(closeBoth)
if TSM.IsWowClassic() then
if closeBoth then
CloseCraft()
CloseTradeSkill()
else
if TSM.Crafting.ProfessionState.IsClassicCrafting() then
CloseCraft()
else
CloseTradeSkill()
end
end
else
C_TradeSkillUI.CloseTradeSkill()
C_Garrison.CloseGarrisonTradeskillNPC()
end
end
function ProfessionUtil.IsNPCProfession()
return not TSM.IsWowClassic() and C_TradeSkillUI.IsNPCCrafting()
end
function ProfessionUtil.IsLinkedProfession()
if TSM.IsWowClassic() then
return nil, nil
else
return C_TradeSkillUI.IsTradeSkillLinked()
end
end
function ProfessionUtil.IsGuildProfession()
return not TSM.IsWowClassic() and C_TradeSkillUI.IsTradeSkillGuild()
end
function ProfessionUtil.GetCategoryInfo(categoryId)
local name, numIndents, parentCategoryId, currentSkillLevel, maxSkillLevel = nil, nil, nil, nil, nil
if TSM.IsWowClassic() then
name = TSM.Crafting.ProfessionState.IsClassicCrafting() and GetCraftDisplaySkillLine() or (categoryId and GetTradeSkillInfo(categoryId) or nil)
numIndents = 0
parentCategoryId = nil
else
C_TradeSkillUI.GetCategoryInfo(categoryId, private.categoryInfoTemp)
assert(private.categoryInfoTemp.numIndents)
name = private.categoryInfoTemp.name
numIndents = private.categoryInfoTemp.numIndents
parentCategoryId = private.categoryInfoTemp.numIndents ~= 0 and private.categoryInfoTemp.parentCategoryID or nil
currentSkillLevel = private.categoryInfoTemp.skillLineCurrentLevel
maxSkillLevel = private.categoryInfoTemp.skillLineMaxLevel
wipe(private.categoryInfoTemp)
end
return name, numIndents, parentCategoryId, currentSkillLevel, maxSkillLevel
end
function ProfessionUtil.StoreOptionalMatText(matList, text)
TSM.db.global.internalData.optionalMatTextLookup[matList] = TSM.db.global.internalData.optionalMatTextLookup[matList] or text
end
function ProfessionUtil.GetOptionalMatText(matList)
return TSM.db.global.internalData.optionalMatTextLookup[matList]
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.DoCraftCallback(result, isDone)
local callback = private.craftCallback
assert(callback)
-- reset timeouts
private.castingTimeout = nil
private.craftTimeout = nil
if isDone then
private.craftQuantity = nil
private.craftSpellId = nil
private.craftCallback = nil
private.craftName = nil
Delay.Cancel("PROFESSION_CRAFT_TIMEOUT_MONITOR")
end
callback(result, isDone)
end
function private.CraftTimeoutMonitor()
if not private.craftSpellId then
Log.Info("No longer crafting")
private.castingTimeout = nil
private.craftTimeout = nil
Delay.Cancel("PROFESSION_CRAFT_TIMEOUT_MONITOR")
return
end
local _, _, _, _, castEndTimeMs, _, _, _, spellId = private.GetPlayerCastingInfo()
if spellId then
private.castingTimeout = nil
else
private.craftTimeout = nil
end
if not spellId then
-- no active cast
if GetTime() > (private.castingTimeout or math.huge) then
Log.Err("Craft timed out (%s)", private.craftSpellId)
private.DoCraftCallback(false, true)
return
end
-- set the casting timeout to 1 second from now
private.castingTimeout = GetTime() + 1
return
elseif private.craftSpellId ~= spellId then
Log.Err("Crafting something else (%s, %s)", private.craftSpellId, spellId)
private.castingTimeout = nil
private.craftTimeout = nil
Delay.Cancel("PROFESSION_CRAFT_TIMEOUT_MONITOR")
return
end
if GetTime() > (private.craftTimeout or math.huge) then
Log.Err("Craft timed out (%s)", private.craftSpellId)
private.DoCraftCallback(false, true)
return
end
-- set the timeout to 1 second after the end time
private.craftTimeout = castEndTimeMs / 1000 + 1
end
function private.GetPlayerCastingInfo()
if TSM.IsWowClassic() then
return CastingInfo()
else
return UnitCastingInfo("player")
end
end

View File

@ -0,0 +1,186 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Queue = TSM.Crafting:NewPackage("Queue")
local Database = TSM.Include("Util.Database")
local Math = TSM.Include("Util.Math")
local Log = TSM.Include("Util.Log")
local Inventory = TSM.Include("Service.Inventory")
local CustomPrice = TSM.Include("Service.CustomPrice")
local private = {
db = nil,
}
local MAX_NUM_QUEUED = 9999
-- ============================================================================
-- Module Functions
-- ============================================================================
function Queue.OnEnable()
private.db = Database.NewSchema("CRAFTING_QUEUE")
:AddUniqueNumberField("spellId")
:AddNumberField("num")
:Commit()
private.db:SetQueryUpdatesPaused(true)
for spellId, data in pairs(TSM.db.factionrealm.internalData.crafts) do
Queue.SetNum(spellId, data.queued) -- sanitize / cache the number queued
end
private.db:SetQueryUpdatesPaused(false)
end
function Queue.GetDBForJoin()
return private.db
end
function Queue.CreateQuery()
return private.db:NewQuery()
end
function Queue.SetNum(spellId, num)
local craftInfo = TSM.db.factionrealm.internalData.crafts[spellId]
if not craftInfo then
Log.Err("Could not find craft: "..spellId)
return
end
craftInfo.queued = min(max(Math.Round(num or 0), 0), MAX_NUM_QUEUED)
local query = private.db:NewQuery()
:Equal("spellId", spellId)
local row = query:GetFirstResult()
if row and craftInfo.queued == 0 then
-- delete this row
private.db:DeleteRow(row)
elseif row then
-- update this row
row:SetField("num", craftInfo.queued)
:Update()
elseif craftInfo.queued > 0 then
-- insert a new row
private.db:NewRow()
:SetField("spellId", spellId)
:SetField("num", craftInfo.queued)
:Create()
end
query:Release()
end
function Queue.GetNum(spellId)
return private.db:GetUniqueRowField("spellId", spellId, "num") or 0
end
function Queue.Add(spellId, quantity)
Queue.SetNum(spellId, Queue.GetNum(spellId) + quantity)
end
function Queue.Remove(spellId, quantity)
Queue.SetNum(spellId, Queue.GetNum(spellId) - quantity)
end
function Queue.Clear()
local query = private.db:NewQuery()
:Select("spellId")
for _, spellId in query:Iterator() do
local craftInfo = TSM.db.factionrealm.internalData.crafts[spellId]
if craftInfo then
craftInfo.queued = 0
end
end
query:Release()
private.db:Truncate()
end
function Queue.GetNumItems()
return private.db:NewQuery():CountAndRelease()
end
function Queue.GetTotals()
local totalCost, totalProfit, totalCastTimeMs, totalNumQueued = nil, nil, nil, 0
local query = private.db:NewQuery()
:Select("spellId", "num")
for _, spellId, numQueued in query:Iterator() do
local numResult = TSM.db.factionrealm.internalData.crafts[spellId] and TSM.db.factionrealm.internalData.crafts[spellId].numResult or 0
local cost, _, profit = TSM.Crafting.Cost.GetCostsBySpellId(spellId)
if cost then
totalCost = (totalCost or 0) + cost * numQueued * numResult
end
if profit then
totalProfit = (totalProfit or 0) + profit * numQueued * numResult
end
local castTime = select(4, GetSpellInfo(spellId))
if castTime then
totalCastTimeMs = (totalCastTimeMs or 0) + castTime * numQueued
end
totalNumQueued = totalNumQueued + numQueued
end
query:Release()
return totalCost, totalProfit, totalCastTimeMs and ceil(totalCastTimeMs / 1000) or nil, totalNumQueued
end
function Queue.RestockGroups(groups)
private.db:SetQueryUpdatesPaused(true)
for _, groupPath in ipairs(groups) do
if groupPath ~= TSM.CONST.ROOT_GROUP_PATH then
for _, itemString in TSM.Groups.ItemIterator(groupPath) do
if TSM.Crafting.CanCraftItem(itemString) then
local isValid, err = TSM.Operations.Crafting.IsValid(itemString)
if isValid then
private.RestockItem(itemString)
elseif err then
Log.PrintUser(err)
end
end
end
end
end
private.db:SetQueryUpdatesPaused(false)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.RestockItem(itemString)
local cheapestCost, cheapestSpellId = TSM.Crafting.Cost.GetLowestCostByItem(itemString)
if not cheapestSpellId then
-- can't craft this item
return
end
local itemValue = TSM.Crafting.Cost.GetCraftedItemValue(itemString)
local profit = itemValue and cheapestCost and (itemValue - cheapestCost) or nil
local hasMinProfit, minProfit = TSM.Operations.Crafting.GetMinProfit(itemString)
if hasMinProfit and (not minProfit or not profit or profit < minProfit) then
-- profit is too low
return
end
local haveQuantity = CustomPrice.GetItemPrice(itemString, "NumInventory") or 0
for guild, ignored in pairs(TSM.db.global.craftingOptions.ignoreGuilds) do
if ignored then
haveQuantity = haveQuantity - Inventory.GetGuildQuantity(itemString, guild)
end
end
for player, ignored in pairs(TSM.db.global.craftingOptions.ignoreCharacters) do
if ignored then
haveQuantity = haveQuantity - Inventory.GetBagQuantity(itemString, player)
haveQuantity = haveQuantity - Inventory.GetBankQuantity(itemString, player)
haveQuantity = haveQuantity - Inventory.GetReagentBankQuantity(itemString, player)
haveQuantity = haveQuantity - Inventory.GetAuctionQuantity(itemString, player)
haveQuantity = haveQuantity - Inventory.GetMailQuantity(itemString, player)
end
end
assert(haveQuantity >= 0)
local neededQuantity = TSM.Operations.Crafting.GetRestockQuantity(itemString, haveQuantity)
if neededQuantity == 0 then
return
end
-- queue only if it satisfies all operation criteria
Queue.SetNum(cheapestSpellId, floor(neededQuantity / TSM.Crafting.GetNumResult(cheapestSpellId)))
end

View File

@ -0,0 +1,227 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local CraftingSync = TSM.Crafting:NewPackage("Sync")
local L = TSM.Include("Locale").GetTable()
local Delay = TSM.Include("Util.Delay")
local TempTable = TSM.Include("Util.TempTable")
local String = TSM.Include("Util.String")
local Log = TSM.Include("Util.Log")
local Theme = TSM.Include("Util.Theme")
local Sync = TSM.Include("Service.Sync")
local private = {
hashesTemp = {},
spellsTemp = {},
spellsProfessionLookupTemp = {},
spellInfoTemp = {
spellIds = {},
mats = {},
itemStrings = {},
names = {},
numResults = {},
hasCDs = {},
},
accountLookup = {},
accountStatus = {},
}
local RETRY_DELAY = 5
local PROFESSION_HASH_FIELDS = { "spellId", "itemString" }
-- ============================================================================
-- Module Functions
-- ============================================================================
function CraftingSync.OnInitialize()
Sync.RegisterConnectionChangedCallback(private.ConnectionChangedHandler)
Sync.RegisterRPC("CRAFTING_GET_HASHES", private.RPCGetHashes)
Sync.RegisterRPC("CRAFTING_GET_SPELLS", private.RPCGetSpells)
Sync.RegisterRPC("CRAFTING_GET_SPELL_INFO", private.RPCGetSpellInfo)
end
function CraftingSync.GetStatus(account)
local status = private.accountStatus[account]
if not status then
return Theme.GetFeedbackColor("RED"):ColorText(L["Not Connected"])
elseif status == "UPDATING" or status == "RETRY" then
return Theme.GetFeedbackColor("YELLOW"):ColorText(L["Updating"])
elseif status == "SYNCED" then
return Theme.GetFeedbackColor("GREEN"):ColorText(L["Up to date"])
else
error("Invalid status: "..tostring(status))
end
end
-- ============================================================================
-- RPC Functions and Result Handlers
-- ============================================================================
function private.RPCGetHashes()
wipe(private.hashesTemp)
local player = UnitName("player")
private.GetPlayerProfessionHashes(player, private.hashesTemp)
return player, private.hashesTemp
end
function private.RPCGetHashesResultHandler(player, data)
if not player or not private.accountLookup[player] then
-- request timed out, so try again
Log.Warn("Getting hashes timed out")
if private.accountLookup[player] then
private.accountStatus[private.accountLookup[player]] = "RETRY"
Delay.AfterTime(RETRY_DELAY, private.RetryGetHashesRPC)
end
return
end
local currentInfo = TempTable.Acquire()
private.GetPlayerProfessionHashes(player, currentInfo)
local requestProfessions = TempTable.Acquire()
for profession, hash in pairs(data) do
if hash == currentInfo[profession] then
Log.Info("%s data for %s already up to date", profession, player)
else
Log.Info("Need updated %s data from %s (%s, %s)", profession, player, hash, tostring(currentInfo[hash]))
requestProfessions[profession] = true
end
end
TempTable.Release(currentInfo)
if next(requestProfessions) then
private.accountStatus[private.accountLookup[player]] = "UPDATING"
Sync.CallRPC("CRAFTING_GET_SPELLS", player, private.RPCGetSpellsResultHandler, requestProfessions)
else
private.accountStatus[private.accountLookup[player]] = "SYNCED"
end
TempTable.Release(requestProfessions)
end
function private.RPCGetSpells(professions)
wipe(private.spellsProfessionLookupTemp)
wipe(private.spellsTemp)
local player = UnitName("player")
local query = TSM.Crafting.CreateRawCraftsQuery()
:Select("spellId", "profession")
:Custom(private.QueryProfessionFilter, professions)
:Custom(private.QueryPlayerFilter, player)
:OrderBy("spellId", true)
for _, spellId, profession in query:Iterator() do
private.spellsProfessionLookupTemp[spellId] = profession
tinsert(private.spellsTemp, spellId)
end
query:Release()
return player, private.spellsProfessionLookupTemp, private.spellsTemp
end
function private.RPCGetSpellsResultHandler(player, professionLookup, spells)
if not player or not private.accountLookup[player] then
-- request timed out, so try again from the start
Log.Warn("Getting spells timed out")
if private.accountLookup[player] then
private.accountStatus[private.accountLookup[player]] = "RETRY"
Delay.AfterTime(RETRY_DELAY, private.RetryGetHashesRPC)
end
return
end
for i = #spells, 1, -1 do
local spellId = spells[i]
if TSM.Crafting.HasSpellId(spellId) then
-- already have this spell so just make sure this player is added
TSM.Crafting.AddPlayer(spellId, player)
tremove(spells, i)
end
end
if #spells == 0 then
Log.Info("Spells up to date for %s", player)
private.accountStatus[private.accountLookup[player]] = "SYNCED"
else
Log.Info("Requesting %d spells from %s", #spells, player)
Sync.CallRPC("CRAFTING_GET_SPELL_INFO", player, private.RPCGetSpellInfoResultHandler, professionLookup, spells)
end
end
function private.RPCGetSpellInfo(professionLookup, spells)
for _, tbl in pairs(private.spellInfoTemp) do
wipe(tbl)
end
for i, spellId in ipairs(spells) do
private.spellInfoTemp.spellIds[i] = spellId
private.spellInfoTemp.mats[i] = TSM.db.factionrealm.internalData.crafts[spellId].mats
private.spellInfoTemp.itemStrings[i] = TSM.db.factionrealm.internalData.crafts[spellId].itemString
private.spellInfoTemp.names[i] = TSM.db.factionrealm.internalData.crafts[spellId].name
private.spellInfoTemp.numResults[i] = TSM.db.factionrealm.internalData.crafts[spellId].numResult
private.spellInfoTemp.hasCDs[i] = TSM.db.factionrealm.internalData.crafts[spellId].hasCD
end
Log.Info("Sent %d spells", #private.spellInfoTemp.spellIds)
return UnitName("player"), professionLookup, private.spellInfoTemp
end
function private.RPCGetSpellInfoResultHandler(player, professionLookup, spellInfo)
if not player or not professionLookup or not spellInfo or not private.accountLookup[player] then
-- request timed out, so try again from the start
Log.Warn("Getting spell info timed out")
if private.accountLookup[player] then
private.accountStatus[private.accountLookup[player]] = "RETRY"
Delay.AfterTime(RETRY_DELAY, private.RetryGetHashesRPC)
end
return
end
for i, spellId in ipairs(spellInfo.spellIds) do
TSM.Crafting.CreateOrUpdate(spellId, spellInfo.itemStrings[i], professionLookup[spellId], spellInfo.names[i], spellInfo.numResults[i], player, spellInfo.hasCDs[i] and true or false)
for itemString in pairs(spellInfo.mats[i]) do
TSM.db.factionrealm.internalData.mats[itemString] = TSM.db.factionrealm.internalData.mats[itemString] or {}
end
TSM.Crafting.SetMats(spellId, spellInfo.mats[i])
end
Log.Info("Added %d spells from %s", #spellInfo.spellIds, player)
private.accountStatus[private.accountLookup[player]] = "SYNCED"
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.ConnectionChangedHandler(account, player, connected)
if connected then
private.accountLookup[player] = account
private.accountStatus[account] = "UPDATING"
-- issue a request for profession info
Sync.CallRPC("CRAFTING_GET_HASHES", player, private.RPCGetHashesResultHandler)
else
private.accountLookup[player] = nil
private.accountStatus[account] = nil
end
end
function private.RetryGetHashesRPC()
for player, account in pairs(private.accountLookup) do
if private.accountStatus[account] == "RETRY" then
Sync.CallRPC("CRAFTING_GET_HASHES", player, private.RPCGetHashesResultHandler)
end
end
end
function private.QueryProfessionFilter(row, professions)
return professions[row:GetField("profession")]
end
function private.QueryPlayerFilter(row, player)
return String.SeparatedContains(row:GetField("players"), ",", player)
end
function private.GetPlayerProfessionHashes(player, resultTbl)
local query = TSM.Crafting.CreateRawCraftsQuery()
:Custom(private.QueryPlayerFilter, player)
:OrderBy("spellId", true)
query:GroupedHash(PROFESSION_HASH_FIELDS, "profession", resultTbl)
query:Release()
end

View File

@ -0,0 +1,442 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Destroying = TSM:NewPackage("Destroying")
local Database = TSM.Include("Util.Database")
local Event = TSM.Include("Util.Event")
local SlotId = TSM.Include("Util.SlotId")
local TempTable = TSM.Include("Util.TempTable")
local ItemString = TSM.Include("Util.ItemString")
local Future = TSM.Include("Util.Future")
local Threading = TSM.Include("Service.Threading")
local ItemInfo = TSM.Include("Service.ItemInfo")
local CustomPrice = TSM.Include("Service.CustomPrice")
local Conversions = TSM.Include("Service.Conversions")
local BagTracking = TSM.Include("Service.BagTracking")
local Settings = TSM.Include("Service.Settings")
local private = {
combineThread = nil,
destroyThread = nil,
destroyThreadRunning = false,
settings = nil,
canDestroyCache = {},
destroyQuantityCache = {},
pendingCombines = {},
newBagUpdate = false,
bagUpdateCallback = nil,
pendingSpellId = nil,
ignoreDB = nil,
destroyInfoDB = nil,
combineFuture = Future.New("DESTROYING_COMBINE_FUTURE"),
destroyFuture = Future.New("DESTROYING_DESTROY_FUTURE"),
}
local SPELL_IDS = {
milling = 51005,
prospect = 31252,
disenchant = 13262,
}
local ITEM_SUB_CLASS_METAL_AND_STONE = 7
local ITEM_SUB_CLASS_HERB = 9
local TARGET_SLOT_ID_MULTIPLIER = 1000000
local GEM_CHIPS = {
["i:129099"] = "i:129100",
["i:130200"] = "i:129100",
["i:130201"] = "i:129100",
["i:130202"] = "i:129100",
["i:130203"] = "i:129100",
["i:130204"] = "i:129100",
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Destroying.OnInitialize()
private.combineThread = Threading.New("COMBINE_STACKS", private.CombineThread)
Threading.SetCallback(private.combineThread, private.CombineThreadDone)
private.destroyThread = Threading.New("DESTROY", private.DestroyThread)
Threading.SetCallback(private.destroyThread, private.DestroyThreadDone)
BagTracking.RegisterCallback(private.UpdateBagDB)
private.settings = Settings.NewView()
:AddKey("global", "internalData", "destroyingHistory")
:AddKey("global", "destroyingOptions", "deAbovePrice")
:AddKey("global", "destroyingOptions", "deMaxQuality")
:AddKey("global", "destroyingOptions", "includeSoulbound")
:AddKey("global", "userData", "destroyingIgnore")
:RegisterCallback("deAbovePrice", private.UpdateBagDB)
:RegisterCallback("deMaxQuality", private.UpdateBagDB)
:RegisterCallback("includeSoulbound", private.UpdateBagDB)
private.ignoreDB = Database.NewSchema("DESTROYING_IGNORE")
:AddUniqueStringField("itemString")
:AddBooleanField("ignoreSession")
:AddBooleanField("ignorePermanent")
:Commit()
private.ignoreDB:BulkInsertStart()
local used = TempTable.Acquire()
for itemString in pairs(private.settings.destroyingIgnore) do
itemString = ItemString.Get(itemString)
if not used[itemString] then
used[itemString] = true
private.ignoreDB:BulkInsertNewRow(itemString, false, true)
end
end
TempTable.Release(used)
private.ignoreDB:BulkInsertEnd()
private.destroyInfoDB = Database.NewSchema("DESTROYING_INFO")
:AddUniqueStringField("itemString")
:AddNumberField("minQuantity")
:AddNumberField("spellId")
:Commit()
Event.Register("LOOT_CLOSED", private.SendEventToThread)
Event.Register("BAG_UPDATE_DELAYED", private.SendEventToThread)
Event.Register("UNIT_SPELLCAST_START", private.SpellCastEventHandler)
Event.Register("UNIT_SPELLCAST_FAILED", private.SpellCastEventHandler)
Event.Register("UNIT_SPELLCAST_FAILED_QUIET", private.SpellCastEventHandler)
Event.Register("UNIT_SPELLCAST_INTERRUPTED", private.SpellCastEventHandler)
Event.Register("UNIT_SPELLCAST_SUCCEEDED", private.SpellCastEventHandler)
private.destroyFuture:SetScript("OnCleanup", function()
private.destroyThreadRunning = false
Threading.Kill(private.destroyThread)
end)
private.combineFuture:SetScript("OnCleanup", function()
Threading.Kill(private.combineThread)
end)
end
function Destroying.SetBagUpdateCallback(callback)
assert(not private.bagUpdateCallback)
private.bagUpdateCallback = callback
end
function Destroying.CreateBagQuery()
return BagTracking.CreateQueryBags()
:LeftJoin(private.ignoreDB, "itemString")
:InnerJoin(private.destroyInfoDB, "itemString")
:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
:NotEqual("ignoreSession", true)
:NotEqual("ignorePermanent", true)
:GreaterThanOrEqual("quantity", Database.OtherFieldQueryParam("minQuantity"))
end
function Destroying.CanCombine()
return #private.pendingCombines > 0
end
function Destroying.StartCombine()
private.combineFuture:Start()
Threading.Start(private.combineThread)
return private.combineFuture
end
function Destroying.StartDestroy(button, row, callback)
private.destroyFuture:Start()
private.destroyThreadRunning = true
Threading.Start(private.destroyThread, button, row)
-- we need the thread to run now so send it a sync message
Threading.SendSyncMessage(private.destroyThread)
return private.destroyFuture
end
function Destroying.IgnoreItemSession(itemString)
local row = private.ignoreDB:GetUniqueRow("itemString", itemString)
if row then
assert(not row:GetField("ignoreSession"))
row:SetField("ignoreSession", true)
row:Update()
row:Release()
else
private.ignoreDB:NewRow()
:SetField("itemString", itemString)
:SetField("ignoreSession", true)
:SetField("ignorePermanent", false)
:Create()
end
end
function Destroying.IgnoreItemPermanent(itemString)
assert(not private.settings.destroyingIgnore[itemString])
private.settings.destroyingIgnore[itemString] = true
local row = private.ignoreDB:GetUniqueRow("itemString", itemString)
if row then
assert(not row:GetField("ignorePermanent"))
row:SetField("ignorePermanent", true)
row:Update()
row:Release()
else
private.ignoreDB:NewRow()
:SetField("itemString", itemString)
:SetField("ignoreSession", false)
:SetField("ignorePermanent", true)
:Create()
end
end
function Destroying.ForgetIgnoreItemPermanent(itemString)
assert(private.settings.destroyingIgnore[itemString])
private.settings.destroyingIgnore[itemString] = nil
local row = private.ignoreDB:GetUniqueRow("itemString", itemString)
assert(row and row:GetField("ignorePermanent"))
if row:GetField("ignoreSession") then
row:SetField("ignorePermanent", false)
row:Update()
else
private.ignoreDB:DeleteRow(row)
end
row:Release()
end
function Destroying.CreateIgnoreQuery()
return private.ignoreDB:NewQuery()
:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
:Equal("ignorePermanent", true)
:OrderBy("name", true)
end
-- ============================================================================
-- Combine Stacks Thread
-- ============================================================================
function private.CombineThread()
while Destroying.CanCombine() do
for _, combineSlotId in ipairs(private.pendingCombines) do
local sourceBag, sourceSlot, targetBag, targetSlot = private.CombineSlotIdToBagSlot(combineSlotId)
PickupContainerItem(sourceBag, sourceSlot)
PickupContainerItem(targetBag, targetSlot)
end
-- wait for the bagDB to change
private.newBagUpdate = false
Threading.WaitForFunction(private.HasNewBagUpdate)
end
end
function private.CombineSlotIdToBagSlot(combineSlotId)
local sourceSlotId = combineSlotId % TARGET_SLOT_ID_MULTIPLIER
local targetSlotId = floor(combineSlotId / TARGET_SLOT_ID_MULTIPLIER)
local sourceBag, sourceSlot = SlotId.Split(sourceSlotId)
local targetBag, targetSlot = SlotId.Split(targetSlotId)
return sourceBag, sourceSlot, targetBag, targetSlot
end
function private.HasNewBagUpdate()
return private.newBagUpdate
end
function private.CombineThreadDone(result)
private.combineFuture:Done(result)
end
-- ============================================================================
-- Destroy Thread
-- ============================================================================
function private.DestroyThread(button, row)
-- we get sent a sync message so we run right away
Threading.ReceiveMessage()
local itemString, spellId, bag, slot = row:GetFields("itemString", "spellId", "bag", "slot")
local spellName = GetSpellInfo(spellId)
button:SetMacroText(format("/cast %s;\n/use %d %d", spellName, bag, slot))
-- wait for the spell cast to start or fail
private.pendingSpellId = spellId
local event = Threading.ReceiveMessage()
if event ~= "UNIT_SPELLCAST_START" then
-- the spell cast failed for some reason
ClearCursor()
return false
end
-- discard any other messages
Threading.Yield(true)
while Threading.HasPendingMessage() do
Threading.ReceiveMessage()
end
-- wait for the spell cast to finish
event = Threading.ReceiveMessage()
if event ~= "UNIT_SPELLCAST_SUCCEEDED" then
-- the spell cast was interrupted
return false
end
-- wait for the loot window to open
Threading.WaitForEvent("LOOT_READY")
-- add to the log
local newEntry = {
item = itemString,
time = time(),
result = {},
}
assert(GetNumLootItems() > 0)
for i = 1, GetNumLootItems() do
local lootItemString = ItemString.Get(GetLootSlotLink(i))
local _, _, quantity = GetLootSlotInfo(i)
if lootItemString and (quantity or 0) > 0 then
lootItemString = GEM_CHIPS[lootItemString] or lootItemString
newEntry.result[lootItemString] = quantity
end
end
private.settings.destroyingHistory[spellName] = private.settings.destroyingHistory[spellName] or {}
tinsert(private.settings.destroyingHistory[spellName], newEntry)
-- wait for the loot window to close
local hasLootClosed, hasBagUpdateDelayed = false, false
while not hasLootClosed or not hasBagUpdateDelayed do
event = Threading.ReceiveMessage()
if event == "LOOT_CLOSED" then
hasLootClosed = true
elseif event == "BAG_UPDATE_DELAYED" then
hasBagUpdateDelayed = true
end
end
-- we're done
return true
end
function private.SendEventToThread(event)
if not private.destroyThreadRunning then
return
end
Threading.SendMessage(private.destroyThread, event)
end
function private.SpellCastEventHandler(event, unit, _, spellId)
if unit ~= "player" or spellId ~= private.pendingSpellId then
return
end
private.SendEventToThread(event)
end
function private.DestroyThreadDone(result)
private.destroyThreadRunning = false
private.destroyFuture:Done(result)
end
-- ============================================================================
-- Bag Update Functions
-- ============================================================================
function private.UpdateBagDB()
wipe(private.pendingCombines)
private.destroyInfoDB:TruncateAndBulkInsertStart()
local itemPrevSlotId = TempTable.Acquire()
local checkedItem = TempTable.Acquire()
local query = BagTracking.CreateQueryBags()
:OrderBy("slotId", true)
:Select("slotId", "itemString", "quantity")
if not private.settings.includeSoulbound then
query:Equal("isBoP", false)
:Equal("isBoA", false)
end
for _, slotId, itemString, quantity in query:Iterator() do
local minQuantity = nil
if checkedItem[itemString] then
minQuantity = private.destroyInfoDB:GetUniqueRowField("itemString", itemString, "minQuantity")
else
checkedItem[itemString] = true
local spellId = nil
minQuantity, spellId = private.ProcessBagItem(itemString)
if minQuantity then
private.destroyInfoDB:BulkInsertNewRow(itemString, minQuantity, spellId)
end
end
if minQuantity and quantity % minQuantity ~= 0 then
if itemPrevSlotId[itemString] then
-- we can combine this with the previous partial stack
tinsert(private.pendingCombines, itemPrevSlotId[itemString] * TARGET_SLOT_ID_MULTIPLIER + slotId)
itemPrevSlotId[itemString] = nil
else
itemPrevSlotId[itemString] = slotId
end
end
end
query:Release()
TempTable.Release(checkedItem)
TempTable.Release(itemPrevSlotId)
private.destroyInfoDB:BulkInsertEnd()
private.newBagUpdate = true
if private.bagUpdateCallback then
private.bagUpdateCallback()
end
end
function private.ProcessBagItem(itemString)
if private.ignoreDB:HasUniqueRow("itemString", itemString) then
return
end
local spellId, minQuantity = private.IsDestroyable(itemString)
if not spellId then
return
elseif spellId == SPELL_IDS.disenchant then
local deAbovePrice = CustomPrice.GetValue(private.settings.deAbovePrice, itemString) or 0
local deValue = CustomPrice.GetValue("Destroy", itemString) or math.huge
if deValue < deAbovePrice then
return
end
end
return minQuantity, spellId
end
function private.IsDestroyable(itemString)
if private.destroyQuantityCache[itemString] then
return private.canDestroyCache[itemString], private.destroyQuantityCache[itemString]
end
-- disenchanting
local quality = ItemInfo.GetQuality(itemString)
if ItemInfo.IsDisenchantable(itemString) and quality <= private.settings.deMaxQuality then
private.canDestroyCache[itemString] = IsSpellKnown(SPELL_IDS.disenchant) and SPELL_IDS.disenchant
private.destroyQuantityCache[itemString] = 1
return private.canDestroyCache[itemString], private.destroyQuantityCache[itemString]
end
local conversionMethod, destroySpellId = nil, nil
local classId = ItemInfo.GetClassId(itemString)
local subClassId = ItemInfo.GetSubClassId(itemString)
if classId == LE_ITEM_CLASS_TRADEGOODS and subClassId == ITEM_SUB_CLASS_HERB then
conversionMethod = Conversions.METHOD.MILL
destroySpellId = SPELL_IDS.milling
elseif classId == LE_ITEM_CLASS_TRADEGOODS and subClassId == ITEM_SUB_CLASS_METAL_AND_STONE then
conversionMethod = Conversions.METHOD.PROSPECT
destroySpellId = SPELL_IDS.prospect
else
private.canDestroyCache[itemString] = false
private.destroyQuantityCache[itemString] = nil
return private.canDestroyCache[itemString], private.destroyQuantityCache[itemString]
end
local hasSourceItem = false
for _ in Conversions.TargetItemsByMethodIterator(itemString, conversionMethod) do
hasSourceItem = true
end
if hasSourceItem then
private.canDestroyCache[itemString] = IsSpellKnown(destroySpellId) and destroySpellId
private.destroyQuantityCache[itemString] = 5
return private.canDestroyCache[itemString], private.destroyQuantityCache[itemString]
end
return private.canDestroyCache[itemString], private.destroyQuantityCache[itemString]
end

View File

@ -0,0 +1,623 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Groups = TSM:NewPackage("Groups")
local Database = TSM.Include("Util.Database")
local TempTable = TSM.Include("Util.TempTable")
local Table = TSM.Include("Util.Table")
local SmartMap = TSM.Include("Util.SmartMap")
local String = TSM.Include("Util.String")
local Log = TSM.Include("Util.Log")
local ItemString = TSM.Include("Util.ItemString")
local private = {
db = nil,
itemDB = nil,
itemStringMap = nil,
itemStringMapReader = nil,
baseItemStringItemIteratorQuery = nil,
groupListCache = {},
}
-- ============================================================================
-- New Modules Functions
-- ============================================================================
function Groups.OnInitialize()
private.db = Database.NewSchema("GROUPS")
:AddStringField("groupPath")
:AddStringField("orderStr")
:AddBooleanField("hasAuctioningOperation")
:AddBooleanField("hasCraftingOperation")
:AddBooleanField("hasMailingOperation")
:AddBooleanField("hasShoppingOperation")
:AddBooleanField("hasSniperOperation")
:AddBooleanField("hasVendoringOperation")
:AddBooleanField("hasWarehousingOperation")
:AddIndex("groupPath")
:Commit()
private.itemDB = Database.NewSchema("GROUP_ITEMS")
:AddUniqueStringField("itemString")
:AddStringField("groupPath")
:AddSmartMapField("baseItemString", ItemString.GetBaseMap(), "itemString")
:AddIndex("groupPath")
:AddIndex("baseItemString")
:Commit()
private.itemStringMapReader = private.itemStringMap:CreateReader()
Groups.RebuildDatabase()
private.baseItemStringItemIteratorQuery = private.itemDB:NewQuery()
:Select("itemString", "groupPath")
:Equal("baseItemString", Database.BoundQueryParam())
end
function Groups.RebuildDatabase()
wipe(private.groupListCache)
-- convert ignoreRandomEnchants to ignoreItemVariations
for _, info in pairs(TSM.db.profile.userData.groups) do
if info.ignoreRandomEnchants ~= nil then
info.ignoreItemVariations = info.ignoreRandomEnchants
info.ignoreRandomEnchants = nil
end
end
for groupPath, groupInfo in pairs(TSM.db.profile.userData.groups) do
if type(groupPath) == "string" and not strmatch(groupPath, TSM.CONST.GROUP_SEP..TSM.CONST.GROUP_SEP) then
-- check the contents of groupInfo
for _, moduleName in TSM.Operations.ModuleIterator() do
groupInfo[moduleName] = groupInfo[moduleName] or {}
if groupPath == TSM.CONST.ROOT_GROUP_PATH then
-- root group should have override flag set
groupInfo[moduleName].override = true
end
end
for key in pairs(groupInfo) do
if TSM.Operations.ModuleExists(key) then
-- this is a set of module operations
local operations = groupInfo[key]
while #operations > TSM.Operations.GetMaxNumber(key) do
-- remove extra operations
tremove(operations)
end
for key2 in pairs(operations) do
if key2 == "override" then
-- ensure the override field is either true or nil
operations.override = operations.override and true or nil
elseif type(key2) ~= "number" or key2 <= 0 or key2 > #operations then
-- this is an invalid key
Log.Err("Removing invalid operations key (%s, %s): %s", groupPath, key, tostring(key2))
operations[key2] = nil
end
end
for i = #operations, 1, -1 do
if type(operations[i]) ~= "string" or operations[i] == "" or not TSM.Operations.Exists(key, operations[i]) then
-- remove operations which no longer exist
-- we used to have a bunch of placeholder "" operations, so don't log for those
if operations[i] ~= "" then
Log.Err("Removing invalid operation from group (%s): %s, %s", groupPath, key, tostring(operations[i]))
end
tremove(operations, i)
end
end
elseif key ~= "ignoreItemVariations" then
-- invalid key
Log.Err("Removing invalid groupInfo key (%s): %s", groupPath, tostring(key))
groupInfo[key] = nil
end
end
else
-- remove invalid group paths
Log.Err("Removing invalid group path: %s", tostring(groupPath))
TSM.db.profile.userData.groups[groupPath] = nil
end
end
if not TSM.db.profile.userData.groups[TSM.CONST.ROOT_GROUP_PATH] then
-- set the override flag for all top-level groups and then create it
for groupPath, moduleOperations in pairs(TSM.db.profile.userData.groups) do
if not strfind(groupPath, TSM.CONST.GROUP_SEP) then
for _, moduleName in TSM.Operations.ModuleIterator() do
moduleOperations[moduleName].override = true
end
end
end
-- create the root group manually with default operations
TSM.db.profile.userData.groups[TSM.CONST.ROOT_GROUP_PATH] = {}
for _, moduleName in TSM.Operations.ModuleIterator() do
assert(TSM.Operations.Exists(moduleName, "#Default"))
TSM.db.profile.userData.groups[TSM.CONST.ROOT_GROUP_PATH][moduleName] = { "#Default", override = true }
end
end
for _, groupPath in Groups.GroupIterator() do
local parentPath = TSM.Groups.Path.GetParent(groupPath)
if not TSM.db.profile.userData.groups[parentPath] then
-- the parent group doesn't exist, so remove this group
Log.Err("Removing group with non-existent parent: %s", tostring(groupPath))
TSM.db.profile.userData.groups[groupPath] = nil
else
for _, moduleName in TSM.Operations.ModuleIterator() do
if not Groups.HasOperationOverride(groupPath, moduleName) then
private.InheritParentOperations(groupPath, moduleName)
end
end
end
end
-- fix up any invalid items
local newPaths = TempTable.Acquire()
for itemString, groupPath in pairs(TSM.db.profile.userData.items) do
local newItemString = ItemString.Get(itemString)
if not newItemString then
-- this itemstring is invalid
Log.Err("Itemstring (%s) is invalid", tostring(itemString))
TSM.db.profile.userData.items[itemString] = nil
elseif groupPath == TSM.CONST.ROOT_GROUP_PATH or not TSM.db.profile.userData.groups[groupPath] then
-- this group doesn't exist
Log.Err("Group (%s) doesn't exist, so removing item (%s)", groupPath, itemString)
TSM.db.profile.userData.items[itemString] = nil
elseif newItemString ~= itemString then
-- remove this invalid itemstring from this group
Log.Err("Itemstring changed (%s -> %s), so removing it from group (%s)", itemString, newItemString, groupPath)
TSM.db.profile.userData.items[itemString] = nil
-- add this new item to this group if it's not already in one
if not TSM.db.profile.userData.items[newItemString] then
newPaths[newItemString] = groupPath
Log.Err("Adding to group instead (%s)", groupPath)
end
end
end
for itemString, groupPath in pairs(newPaths) do
TSM.db.profile.userData.items[itemString] = groupPath
end
TempTable.Release(newPaths)
-- populate our database
private.itemDB:TruncateAndBulkInsertStart()
for itemString, groupPath in pairs(TSM.db.profile.userData.items) do
private.itemDB:BulkInsertNewRow(itemString, groupPath)
end
private.itemDB:BulkInsertEnd()
private.itemStringMap:SetCallbacksPaused(true)
for key in private.itemStringMap:Iterator() do
private.itemStringMap:ValueChanged(key)
end
private.RebuildDB()
private.itemStringMap:SetCallbacksPaused(false)
end
function Groups.TranslateItemString(itemString)
return private.itemStringMapReader[itemString]
end
function Groups.GetAutoBaseItemStringSmartMap()
return private.itemStringMap
end
function Groups.GetItemDBForJoin()
return private.itemDB
end
function Groups.CreateQuery()
return private.db:NewQuery()
:OrderBy("orderStr", true)
end
function Groups.Create(groupPath)
private.CreateGroup(groupPath)
private.RebuildDB()
end
function Groups.Move(groupPath, newGroupPath)
assert(not TSM.db.profile.userData.groups[newGroupPath], "Target group already exists")
assert(groupPath ~= TSM.CONST.ROOT_GROUP_PATH, "Can't move root group")
assert(TSM.db.profile.userData.groups[groupPath], "Group doesn't exist")
local newParentPath = TSM.Groups.Path.GetParent(newGroupPath)
assert(newParentPath and TSM.db.profile.userData.groups[newParentPath], "Parent of target is invalid")
local changes = TempTable.Acquire()
private.itemDB:SetQueryUpdatesPaused(true)
-- get a list of group path changes for this group and all its subgroups
local gsubEscapedNewGroupPath = gsub(newGroupPath, "%%", "%%%%")
for path in pairs(TSM.db.profile.userData.groups) do
if path == groupPath or TSM.Groups.Path.IsChild(path, groupPath) then
changes[path] = gsub(path, "^"..String.Escape(groupPath), gsubEscapedNewGroupPath)
end
end
for oldPath, newPath in pairs(changes) do
-- move the group
assert(TSM.db.profile.userData.groups[oldPath] and not TSM.db.profile.userData.groups[newPath])
TSM.db.profile.userData.groups[newPath] = TSM.db.profile.userData.groups[oldPath]
TSM.db.profile.userData.groups[oldPath] = nil
-- move the items
local query = private.itemDB:NewQuery()
:Equal("groupPath", oldPath)
for _, row in query:Iterator() do
local itemString = row:GetField("itemString")
assert(TSM.db.profile.userData.items[itemString])
TSM.db.profile.userData.items[itemString] = newPath
row:SetField("groupPath", newPath)
:Update()
end
query:Release()
end
-- update the operations all groups which were moved
for _, moduleName in TSM.Operations.ModuleIterator() do
if not Groups.HasOperationOverride(newGroupPath, moduleName) then
private.InheritParentOperations(newGroupPath, moduleName)
private.UpdateChildGroupOperations(newGroupPath, moduleName)
end
end
TempTable.Release(changes)
private.RebuildDB()
private.itemDB:SetQueryUpdatesPaused(false)
end
function Groups.Delete(groupPath)
assert(groupPath ~= TSM.CONST.ROOT_GROUP_PATH and TSM.db.profile.userData.groups[groupPath])
local parentPath = TSM.Groups.Path.GetParent(groupPath)
assert(parentPath)
if parentPath == TSM.CONST.ROOT_GROUP_PATH then
parentPath = nil
end
-- delete this group and all subgroups
for path in pairs(TSM.db.profile.userData.groups) do
if path == groupPath or TSM.Groups.Path.IsChild(path, groupPath) then
-- delete this group
TSM.db.profile.userData.groups[path] = nil
end
end
-- remove all items from our DB
private.itemDB:SetQueryUpdatesPaused(true)
local query = private.itemDB:NewQuery()
:Or()
:Equal("groupPath", groupPath)
:Matches("groupPath", "^"..String.Escape(groupPath)..TSM.CONST.GROUP_SEP)
:End()
local updateMapItems = TempTable.Acquire()
for _, row in query:Iterator() do
local itemString = row:GetField("itemString")
assert(TSM.db.profile.userData.items[itemString])
TSM.db.profile.userData.items[itemString] = nil
private.itemDB:DeleteRow(row)
updateMapItems[itemString] = true
end
query:Release()
private.itemStringMap:SetCallbacksPaused(true)
for itemString in private.itemStringMap:Iterator() do
if updateMapItems[itemString] or updateMapItems[ItemString.GetBaseFast(itemString)] then
-- either this item itself was removed from a group, or the base item was - in either case trigger an update
private.itemStringMap:ValueChanged(itemString)
end
end
private.itemStringMap:SetCallbacksPaused(false)
TempTable.Release(updateMapItems)
private.RebuildDB()
private.itemDB:SetQueryUpdatesPaused(false)
end
function Groups.Exists(groupPath)
return TSM.db.profile.userData.groups[groupPath] and true or false
end
function Groups.SetItemGroup(itemString, groupPath)
assert(not groupPath or (groupPath ~= TSM.CONST.ROOT_GROUP_PATH and TSM.db.profile.userData.groups[groupPath]))
local row = private.itemDB:GetUniqueRow("itemString", itemString)
local updateMap = false
if row then
if groupPath then
row:SetField("groupPath", groupPath)
:Update()
row:Release()
else
private.itemDB:DeleteRow(row)
row:Release()
-- we just removed an item from a group, so update the map
updateMap = true
end
else
assert(groupPath)
private.itemDB:NewRow()
:SetField("itemString", itemString)
:SetField("groupPath", groupPath)
:Create()
-- we just added a new item to a group, so update the map
updateMap = true
end
TSM.db.profile.userData.items[itemString] = groupPath
if updateMap then
private.itemStringMap:SetCallbacksPaused(true)
private.itemStringMap:ValueChanged(itemString)
if itemString == ItemString.GetBaseFast(itemString) then
-- this is a base item string, so need to also update all other items whose base item is equal to this item
for mapItemString in private.itemStringMap:Iterator() do
if ItemString.GetBaseFast(mapItemString) == itemString then
private.itemStringMap:ValueChanged(mapItemString)
end
end
end
private.itemStringMap:SetCallbacksPaused(false)
end
end
function Groups.BulkCreateFromImport(groupName, items, groups, groupOperations, moveExistingItems)
-- create all the groups
assert(not TSM.db.profile.userData.groups[groupName])
for relGroupPath in pairs(groups) do
local groupPath = relGroupPath == "" and groupName or TSM.Groups.Path.Join(groupName, relGroupPath)
if not TSM.db.profile.userData.groups[groupPath] then
private.CreateGroup(groupPath)
end
end
for relGroupPath, moduleOperations in pairs(groupOperations) do
local groupPath = relGroupPath == "" and groupName or TSM.Groups.Path.Join(groupName, relGroupPath)
for moduleName, operations in pairs(moduleOperations) do
if operations.override then
TSM.db.profile.userData.groups[groupPath][moduleName] = operations
private.UpdateChildGroupOperations(groupPath, moduleName)
end
end
end
local numItems = 0
for itemString, relGroupPath in pairs(items) do
if moveExistingItems or not Groups.IsItemInGroup(itemString) then
local groupPath = relGroupPath == "" and groupName or TSM.Groups.Path.Join(groupName, relGroupPath)
Groups.SetItemGroup(itemString, groupPath)
numItems = numItems + 1
end
end
private.RebuildDB()
return numItems
end
function Groups.GetPathByItem(itemString)
itemString = TSM.Groups.TranslateItemString(itemString)
assert(itemString)
local groupPath = private.itemDB:GetUniqueRowField("itemString", itemString, "groupPath") or TSM.CONST.ROOT_GROUP_PATH
assert(TSM.db.profile.userData.groups[groupPath])
return groupPath
end
function Groups.IsItemInGroup(itemString)
return private.itemDB:HasUniqueRow("itemString", itemString)
end
function Groups.ItemByBaseItemStringIterator(baseItemString)
private.baseItemStringItemIteratorQuery:BindParams(baseItemString)
return private.baseItemStringItemIteratorQuery:Iterator()
end
function Groups.ItemIterator(groupPathFilter, includeSubGroups)
assert(groupPathFilter ~= TSM.CONST.ROOT_GROUP_PATH)
local query = private.itemDB:NewQuery()
:Select("itemString", "groupPath")
if groupPathFilter then
if includeSubGroups then
query:StartsWith("groupPath", groupPathFilter)
query:Custom(private.GroupPathQueryFilter, groupPathFilter)
else
query:Equal("groupPath", groupPathFilter)
end
end
return query:IteratorAndRelease()
end
function private.GroupPathQueryFilter(row, groupPathFilter)
return row:GetField("groupPath") == groupPathFilter or strmatch(row:GetField("groupPath"), "^"..String.Escape(groupPathFilter)..TSM.CONST.GROUP_SEP)
end
function Groups.GetNumItems(groupPathFilter)
assert(groupPathFilter ~= TSM.CONST.ROOT_GROUP_PATH)
return private.itemDB:NewQuery()
:Equal("groupPath", groupPathFilter)
:CountAndRelease()
end
function Groups.GroupIterator()
if #private.groupListCache == 0 then
for groupPath in pairs(TSM.db.profile.userData.groups) do
if groupPath ~= TSM.CONST.ROOT_GROUP_PATH then
tinsert(private.groupListCache, groupPath)
end
end
Groups.SortGroupList(private.groupListCache)
end
return ipairs(private.groupListCache)
end
function Groups.SortGroupList(list)
Table.Sort(list, private.GroupSortFunction)
end
function Groups.SetOperationOverride(groupPath, moduleName, override)
assert(TSM.db.profile.userData.groups[groupPath])
assert(groupPath ~= TSM.CONST.ROOT_GROUP_PATH)
if override == (TSM.db.profile.userData.groups[groupPath][moduleName].override and true or false) then
return
end
if not override then
TSM.db.profile.userData.groups[groupPath][moduleName].override = nil
private.InheritParentOperations(groupPath, moduleName)
private.UpdateChildGroupOperations(groupPath, moduleName)
else
wipe(TSM.db.profile.userData.groups[groupPath][moduleName])
TSM.db.profile.userData.groups[groupPath][moduleName].override = true
private.UpdateChildGroupOperations(groupPath, moduleName)
end
private.RebuildDB()
end
function Groups.HasOperationOverride(groupPath, moduleName)
return TSM.db.profile.userData.groups[groupPath][moduleName].override
end
function Groups.OperationIterator(groupPath, moduleName)
return ipairs(TSM.db.profile.userData.groups[groupPath][moduleName])
end
function Groups.AppendOperation(groupPath, moduleName, operationName)
assert(TSM.Operations.Exists(moduleName, operationName))
local groupOperations = TSM.db.profile.userData.groups[groupPath][moduleName]
assert(groupOperations.override and #groupOperations < TSM.Operations.GetMaxNumber(moduleName))
tinsert(groupOperations, operationName)
private.UpdateChildGroupOperations(groupPath, moduleName)
private.RebuildDB()
end
function Groups.RemoveOperation(groupPath, moduleName, operationIndex)
local groupOperations = TSM.db.profile.userData.groups[groupPath][moduleName]
assert(groupOperations.override and groupOperations[operationIndex])
tremove(groupOperations, operationIndex)
private.UpdateChildGroupOperations(groupPath, moduleName)
private.RebuildDB()
end
function Groups.RemoveOperationByName(groupPath, moduleName, operationName)
local groupOperations = TSM.db.profile.userData.groups[groupPath][moduleName]
assert(groupOperations.override)
assert(Table.RemoveByValue(groupOperations, operationName) > 0)
private.UpdateChildGroupOperations(groupPath, moduleName)
private.RebuildDB()
end
function Groups.RemoveOperationFromAllGroups(moduleName, operationName)
-- just blindly remove from all groups - no need to check for override
Table.RemoveByValue(TSM.db.profile.userData.groups[TSM.CONST.ROOT_GROUP_PATH][moduleName], operationName)
for _, groupPath in Groups.GroupIterator() do
Table.RemoveByValue(TSM.db.profile.userData.groups[groupPath][moduleName], operationName)
end
private.RebuildDB()
end
function Groups.SwapOperation(groupPath, moduleName, fromIndex, toIndex)
local groupOperations = TSM.db.profile.userData.groups[groupPath][moduleName]
groupOperations[fromIndex], groupOperations[toIndex] = groupOperations[toIndex], groupOperations[fromIndex]
private.UpdateChildGroupOperations(groupPath, moduleName)
end
function Groups.OperationRenamed(moduleName, oldName, newName)
-- just blindly rename in all groups - no need to check for override
for _, info in pairs(TSM.db.profile.userData.groups) do
for i = 1, #info[moduleName] do
if info[moduleName][i] == oldName then
info[moduleName][i] = newName
end
end
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.RebuildDB()
private.db:TruncateAndBulkInsertStart()
for groupPath in pairs(TSM.db.profile.userData.groups) do
local orderStr = gsub(groupPath, TSM.CONST.GROUP_SEP, "\001")
orderStr = strlower(orderStr)
local hasAuctioningOperation = false
for _ in TSM.Operations.GroupOperationIterator("Auctioning", groupPath) do
hasAuctioningOperation = true
end
local hasCraftingOperation = false
for _ in TSM.Operations.GroupOperationIterator("Crafting", groupPath) do
hasCraftingOperation = true
end
local hasMailingOperation = false
for _ in TSM.Operations.GroupOperationIterator("Mailing", groupPath) do
hasMailingOperation = true
end
local hasShoppingOperation = false
for _ in TSM.Operations.GroupOperationIterator("Shopping", groupPath) do
hasShoppingOperation = true
end
local hasSniperOperation = false
for _ in TSM.Operations.GroupOperationIterator("Sniper", groupPath) do
hasSniperOperation = true
end
local hasVendoringOperation = false
for _ in TSM.Operations.GroupOperationIterator("Vendoring", groupPath) do
hasVendoringOperation = true
end
local hasWarehousingOperation = false
for _ in TSM.Operations.GroupOperationIterator("Warehousing", groupPath) do
hasWarehousingOperation = true
end
private.db:BulkInsertNewRow(groupPath, orderStr, hasAuctioningOperation, hasCraftingOperation, hasMailingOperation, hasShoppingOperation, hasSniperOperation, hasVendoringOperation, hasWarehousingOperation)
end
private.db:BulkInsertEnd()
wipe(private.groupListCache)
end
function private.CreateGroup(groupPath)
assert(not TSM.db.profile.userData.groups[groupPath])
local parentPath = TSM.Groups.Path.GetParent(groupPath)
assert(parentPath)
if parentPath ~= TSM.CONST.ROOT_GROUP_PATH and not TSM.db.profile.userData.groups[parentPath] then
-- recursively create the parent group first
private.CreateGroup(parentPath)
end
TSM.db.profile.userData.groups[groupPath] = {}
for _, moduleName in TSM.Operations.ModuleIterator() do
TSM.db.profile.userData.groups[groupPath][moduleName] = {}
-- assign all parent operations to this group
for _, operationName in ipairs(TSM.db.profile.userData.groups[parentPath][moduleName]) do
tinsert(TSM.db.profile.userData.groups[groupPath][moduleName], operationName)
end
end
end
function private.GroupSortFunction(a, b)
return strlower(gsub(a, TSM.CONST.GROUP_SEP, "\001")) < strlower(gsub(b, TSM.CONST.GROUP_SEP, "\001"))
end
function private.InheritParentOperations(groupPath, moduleName)
local parentGroupPath = TSM.Groups.Path.GetParent(groupPath)
local override = TSM.db.profile.userData.groups[groupPath][moduleName].override
wipe(TSM.db.profile.userData.groups[groupPath][moduleName])
TSM.db.profile.userData.groups[groupPath][moduleName].override = override
for _, operationName in ipairs(TSM.db.profile.userData.groups[parentGroupPath][moduleName]) do
tinsert(TSM.db.profile.userData.groups[groupPath][moduleName], operationName)
end
end
function private.UpdateChildGroupOperations(groupPath, moduleName)
for _, childGroupPath in Groups.GroupIterator() do
if TSM.Groups.Path.IsChild(childGroupPath, groupPath) and not Groups.HasOperationOverride(childGroupPath, moduleName) then
private.InheritParentOperations(childGroupPath, moduleName)
end
end
end
-- ============================================================================
-- Item String Smart Map
-- ============================================================================
do
private.itemStringMap = SmartMap.New("string", "string", function(itemString)
if Groups.IsItemInGroup(itemString) then
-- this item is in a group, so just return it
return itemString
end
local baseItemString = ItemString.GetBaseFast(itemString)
-- return the base item if it's in a group; otherwise return the original item
return Groups.IsItemInGroup(baseItemString) and baseItemString or itemString
end)
end

View File

@ -0,0 +1,869 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local ImportExport = TSM.Groups:NewPackage("ImportExport")
local L = TSM.Include("Locale").GetTable()
local TempTable = TSM.Include("Util.TempTable")
local Table = TSM.Include("Util.Table")
local Log = TSM.Include("Util.Log")
local String = TSM.Include("Util.String")
local ItemString = TSM.Include("Util.ItemString")
local CustomPrice = TSM.Include("Service.CustomPrice")
local AceSerializer = LibStub("AceSerializer-3.0")
local LibDeflate = LibStub("LibDeflate")
local LibSerialize = LibStub("LibSerialize")
local private = {
isOperationSettingsTable = {},
importContext = {
groupName = nil,
items = nil,
groups = nil,
groupOperations = nil,
operations = nil,
customSources = nil,
numChangedOperations = nil,
filteredGroups = {},
},
}
local MAGIC_STR = "TSM_EXPORT"
local VERSION = 1
local EXPORT_OPERATION_MODULES = {
Auctioning = true,
Crafting = true,
Shopping = true,
Sniper = true,
Vendoring = true,
Warehousing = true,
}
local EXPORT_CUSTOM_STRINGS = {
Auctioning = {
postCap = true,
keepQuantity = true,
maxExpires = true,
undercut = true,
minPrice = true,
maxPrice = true,
normalPrice = true,
cancelRepostThreshold = true,
stackSize = TSM.IsWowClassic() or nil,
},
Crafting = {
minRestock = true,
maxRestock = true,
minProfit = true,
craftPriceMethod = true,
},
Shopping = {
restockQuantity = true,
maxPrice = true,
},
Sniper = {
belowPrice = true,
},
Vendoring = {
vsMarketValue = true,
vsMaxMarketValue = true,
vsDestroyValue = true,
vsMaxDestroyValue = true,
},
Warehousing = {},
}
local SERIALIZE_OPTIONS = {
stable = true,
filter = function(tbl, key, value)
return not private.isOperationSettingsTable[tbl] or not TSM.Operations.IsCommonKey(key)
end,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function ImportExport.GenerateExport(exportGroupPath, includeSubGroups, excludeOperations, excludeCustomSources)
assert(exportGroupPath ~= TSM.CONST.ROOT_GROUP_PATH)
local exportGroupName = TSM.Groups.Path.GetName(exportGroupPath)
-- collect the items and sub groups
local items = TempTable.Acquire()
local groups = TempTable.Acquire()
local groupOperations = TempTable.Acquire()
local operations = TempTable.Acquire()
local customSources = TempTable.Acquire()
for moduleName in pairs(EXPORT_OPERATION_MODULES) do
operations[moduleName] = {}
end
assert(not next(private.isOperationSettingsTable))
for _, groupPath in TSM.Groups.GroupIterator() do
local relGroupPath = nil
if TSM.Groups.Path.IsChild(groupPath, exportGroupPath) then
relGroupPath = TSM.Groups.Path.GetRelative(groupPath, exportGroupPath)
if not includeSubGroups[relGroupPath] then
relGroupPath = nil
end
elseif groupPath == exportGroupPath then
relGroupPath = TSM.CONST.ROOT_GROUP_PATH
end
if relGroupPath then
groups[relGroupPath] = true
for _, itemString in TSM.Groups.ItemIterator(groupPath) do
items[itemString] = relGroupPath
end
if not excludeOperations then
groupOperations[relGroupPath] = {}
for moduleName in pairs(EXPORT_OPERATION_MODULES) do
groupOperations[relGroupPath][moduleName] = {
-- always override at the top-level
override = TSM.Groups.HasOperationOverride(groupPath, moduleName) or groupPath == exportGroupPath or nil,
}
for _, operationName, operationSettings in TSM.Operations.GroupOperationIterator(moduleName, groupPath) do
tinsert(groupOperations[relGroupPath][moduleName], operationName)
operations[moduleName][operationName] = operationSettings
private.isOperationSettingsTable[operationSettings] = true
if not excludeCustomSources then
for key in pairs(EXPORT_CUSTOM_STRINGS[moduleName]) do
private.GetCustomSources(operationSettings[key], customSources)
end
end
end
end
end
end
end
local serialized = LibSerialize:SerializeEx(SERIALIZE_OPTIONS, MAGIC_STR, VERSION, exportGroupName, items, groups, groupOperations, operations, customSources)
local compressed = LibDeflate:EncodeForPrint(LibDeflate:CompressDeflate(serialized))
local numItems = Table.Count(items)
local numSubGroups = Table.Count(groups) - 1
local numOperations = 0
for _, moduleOperations in pairs(operations) do
numOperations = numOperations + Table.Count(moduleOperations)
end
local numCustomSources = Table.Count(customSources)
wipe(private.isOperationSettingsTable)
TempTable.Release(customSources)
TempTable.Release(operations)
TempTable.Release(groupOperations)
TempTable.Release(groups)
TempTable.Release(items)
return compressed, numItems, numSubGroups, numOperations, numCustomSources
end
function ImportExport.ProcessImport(str)
return private.DecodeNewImport(str) or private.DecodeOldImport(str) or private.DecodeOldGroupOrItemListImport(str)
end
function ImportExport.GetImportTotals()
local numExistingItems = 0
for itemString, groupPath in pairs(private.importContext.items) do
if not private.importContext.filteredGroups[groupPath] then
if TSM.Groups.IsItemInGroup(itemString) then
numExistingItems = numExistingItems + 1
end
end
end
wipe(private.importContext.customSources)
local numOperations, numExistingOperations = 0, 0
for moduleName, moduleOperations in pairs(private.importContext.operations) do
local usedOperations = TempTable.Acquire()
for groupPath, operations in pairs(private.importContext.groupOperations) do
if not private.importContext.filteredGroups[groupPath] then
for _, operationName in ipairs(operations[moduleName]) do
usedOperations[operationName] = true
end
end
end
for operationName, operationSettings in pairs(moduleOperations) do
if usedOperations[operationName] then
numOperations = numOperations + 1
if TSM.Operations.Exists(moduleName, operationName) then
numExistingOperations = numExistingOperations + 1
end
for key in pairs(EXPORT_CUSTOM_STRINGS[moduleName]) do
private.GetCustomSources(operationSettings[key], private.importContext.customSources)
end
end
end
TempTable.Release(usedOperations)
end
local numExistingCustomSources = 0
for name in pairs(private.importContext.customSources) do
if TSM.db.global.userData.customPriceSources[name] then
numExistingCustomSources = numExistingCustomSources + 1
end
end
local numItems = 0
for _, groupPath in pairs(private.importContext.items) do
if not private.importContext.filteredGroups[groupPath] then
numItems = numItems + 1
end
end
local numGroups = 0
for groupPath in pairs(private.importContext.groups) do
if not private.importContext.filteredGroups[groupPath] then
numGroups = numGroups + 1
end
end
return numItems, numGroups, numExistingItems, numOperations, numExistingOperations, numExistingCustomSources
end
function ImportExport.PendingImportGroupIterator()
assert(private.importContext.groupName)
return pairs(private.importContext.groups)
end
function ImportExport.GetPendingImportGroupName()
assert(private.importContext.groupName)
return private.importContext.groupName
end
function ImportExport.SetGroupFiltered(groupPath, isFiltered)
private.importContext.filteredGroups[groupPath] = isFiltered or nil
end
function ImportExport.CommitImport(moveExistingItems, includeOperations, replaceOperations)
assert(private.importContext.groupName)
local numOperations, numCustomSources = 0, 0
if includeOperations and next(private.importContext.operations) then
-- remove filtered operations
for moduleName, moduleOperations in pairs(private.importContext.operations) do
local usedOperations = TempTable.Acquire()
for groupPath, operations in pairs(private.importContext.groupOperations) do
if not private.importContext.filteredGroups[groupPath] then
for _, operationName in ipairs(operations[moduleName]) do
usedOperations[operationName] = true
end
end
end
for operationName in pairs(moduleOperations) do
if not usedOperations[operationName] then
moduleOperations[operationName] = nil
end
end
TempTable.Release(usedOperations)
end
if not replaceOperations then
-- remove existing operations and custom sources from the import context
for moduleName, moduleOperations in pairs(private.importContext.operations) do
for operationName in pairs(moduleOperations) do
if TSM.Operations.Exists(moduleName, operationName) then
moduleOperations[operationName] = nil
end
end
if not next(moduleOperations) then
private.importContext.operations[moduleName] = nil
end
end
for name in pairs(private.importContext.customSources) do
if TSM.db.global.userData.customPriceSources[name] then
private.importContext.customSources[name] = nil
end
end
end
if next(private.importContext.customSources) then
-- regenerate the list of custom sources in case some operations were filtered out
wipe(private.importContext.customSources)
for moduleName, moduleOperations in pairs(private.importContext.operations) do
for _, operationSettings in pairs(moduleOperations) do
for key in pairs(EXPORT_CUSTOM_STRINGS[moduleName]) do
private.GetCustomSources(operationSettings[key], private.importContext.customSources)
end
end
end
-- create the custom sources
numCustomSources = Table.Count(private.importContext.customSources)
CustomPrice.BulkCreateCustomPriceSourcesFromImport(private.importContext.customSources, replaceOperations)
end
-- create the operations
for _, moduleOperations in pairs(private.importContext.operations) do
numOperations = numOperations + Table.Count(moduleOperations)
end
TSM.Operations.BulkCreateFromImport(private.importContext.operations, replaceOperations)
end
if not includeOperations then
wipe(private.importContext.groupOperations)
end
-- filter the groups
for groupPath in pairs(private.importContext.filteredGroups) do
private.importContext.groups[groupPath] = nil
private.importContext.groupOperations[groupPath] = nil
end
for itemString, groupPath in pairs(private.importContext.items) do
if private.importContext.filteredGroups[groupPath] then
private.importContext.items[itemString] = nil
end
end
-- create the groups
local numItems = TSM.Groups.BulkCreateFromImport(private.importContext.groupName, private.importContext.items, private.importContext.groups, private.importContext.groupOperations, moveExistingItems)
-- print the message
Log.PrintfUser(L["Imported group (%s) with %d items, %d operations, and %d custom sources."], private.importContext.groupName, numItems, numOperations, numCustomSources)
ImportExport.ClearImportContext()
end
function ImportExport.ClearImportContext()
private.importContext.groupName = nil
private.importContext.items = nil
private.importContext.groups = nil
private.importContext.groupOperations = nil
private.importContext.operations = nil
private.importContext.customSources = nil
wipe(private.importContext.filteredGroups)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetCustomSources(str, result)
for _, name, customSourceStr in CustomPrice.DependantCustomSourceIterator(str) do
if not result[name] then
result[name] = customSourceStr
private.GetCustomSources(customSourceStr, result)
end
end
end
function private.DecodeNewImport(str)
-- decode and decompress (if it's not a new import, the decode should fail)
str = LibDeflate:DecodeForPrint(str)
if not str then
Log.Info("Not a valid new import string")
return false
end
local numExtraBytes = nil
str, numExtraBytes = LibDeflate:DecompressDeflate(str)
if not str then
Log.Err("Failed to decompress new import string")
return false
elseif numExtraBytes > 0 then
Log.Err("Import string had extra bytes")
return false
end
-- deserialize and validate the data
local success, magicStr, version, groupName, items, groups, groupOperations, operations, customSources = LibSerialize:Deserialize(str)
if not success then
Log.Err("Failed to deserialize new import string")
return false
elseif magicStr ~= MAGIC_STR then
Log.Err("Invalid magic string: "..tostring(magicStr))
return false
elseif version ~= VERSION then
Log.Err("Invalid version: "..tostring(version))
return false
elseif type(groupName) ~= "string" or groupName == "" or strmatch(groupName, TSM.CONST.GROUP_SEP) then
Log.Err("Invalid groupName: "..tostring(groupName))
return false
elseif type(items) ~= "table" then
Log.Err("Invalid items type: "..tostring(items))
return false
elseif type(groups) ~= "table" then
Log.Err("Invalid groups type: "..tostring(groups))
return false
elseif type(groupOperations) ~= "table" then
Log.Err("Invalid groupOperations type: "..tostring(groupOperations))
return false
elseif type(operations) ~= "table" then
Log.Err("Invalid operations type: "..tostring(operations))
return false
elseif type(customSources) ~= "table" then
Log.Err("Invalid customSources type: "..tostring(customSources))
return false
end
-- validate the groups table
for groupPath, trueValue in pairs(groups) do
if not private.IsValidGroupPath(groupPath) then
Log.Err("Invalid groupPath (%s)", tostring(groupPath))
return false
elseif trueValue ~= true then
Log.Err("Invalid true value (%s)", tostring(trueValue))
return false
end
end
for groupPath in pairs(groups) do
local parentPath = TSM.Groups.Path.Split(groupPath)
while parentPath do
if not groups[parentPath] then
Log.Err("Orphaned group (%s)", groupPath)
return false
end
parentPath = TSM.Groups.Path.Split(parentPath)
end
end
-- validate the items table
local numInvalidItems = 0
for itemString, groupPath in pairs(items) do
if not private.IsValidGroupPath(groupPath) then
Log.Err("Invalid groupPath (%s, %s)", tostring(itemString), tostring(groupPath))
return false
elseif not groups[groupPath] then
Log.Err("Invalid item group (%s, %s)", itemString, groupPath)
return false
end
local newItemString = type(itemString) == "string" and ItemString.Get(itemString) or nil
if itemString ~= newItemString then
-- just remove this one item and continue
Log.Warn("Invalid itemString (%s, %s)", tostring(itemString), tostring(newItemString))
items[itemString] = nil
numInvalidItems = numInvalidItems + 1
end
end
if not next(items) and numInvalidItems > 0 then
Log.Err("All items were invalid")
return false
end
-- validate the customSources table
for name, customSourceStr in pairs(customSources) do
if type(name) ~= "string" or name == "" or gsub(name, "([a-z]+)", "") ~= "" then
Log.Err("Invalid name (%s)", tostring(name))
return false
elseif type(str) ~= "string" then
Log.Err("Invalid str (%s)", tostring(customSourceStr))
return false
end
end
-- validate the operations table
local numChangedOperations = private.ValidateOperationsTable(operations, true)
if not numChangedOperations then
return false
end
-- validate the groupOperations table
if not private.ValidateGroupOperationsTable(groupOperations, groups, operations, true) then
return false
end
if numInvalidItems > 0 then
Log.PrintfUser(L["NOTE: The import contained %d invalid items which were ignored."], numInvalidItems)
end
if numChangedOperations > 0 then
Log.PrintfUser(L["NOTE: The import contained %d operations with at least one invalid setting which was reset."], numChangedOperations)
end
Log.Info("Decoded new import string")
private.importContext.groupName = private.DedupImportGroupName(groupName)
private.importContext.items = items
private.importContext.groups = groups
private.importContext.groupOperations = groupOperations
private.importContext.operations = operations
private.importContext.customSources = customSources
return true
end
function private.DecodeOldImport(str)
if strsub(str, 1, 1) ~= "^" then
Log.Info("Not an old import string")
return false
end
local isValid, data = AceSerializer:Deserialize(str)
if not isValid then
Log.Err("Failed to deserialize")
return false
elseif type(data) ~= "table" then
Log.Err("Invalid data type (%s)", tostring(data))
return false
elseif data.operations ~= nil and type(data.operations) ~= "table" then
Log.Err("Invalid operations type (%s)", tostring(data.operations))
return false
elseif data.groupExport ~= nil and type(data.groupExport) ~= "string" then
Log.Err("Invalid groupExport type (%s)", tostring(data.groupExport))
return false
elseif data.groupOperations ~= nil and type(data.groupOperations) ~= "table" then
Log.Err("Invalid groupOperations type (%s)", tostring(data.groupOperations))
return false
elseif not data.operations and not data.groupExport then
Log.Err("Doesn't contain operations or groupExport")
return false
end
local operations, numChangedOperations = nil, 0
if data.operations then
numChangedOperations = private.ValidateOperationsTable(data.operations, false)
if not numChangedOperations then
return false
end
operations = data.operations
else
operations = {}
end
local items, groups, numInvalidItems = nil, nil, nil
if data.groupExport then
items, groups, numInvalidItems = private.DecodeGroupExportHelper(data.groupExport)
if not items then
Log.Err("No items found")
return false
end
else
items = {}
groups = {}
numInvalidItems = 0
end
local groupOperations = nil
if data.groupOperations then
Log.Info("Parsing group operations")
local changeGroupPaths = TempTable.Acquire()
for groupPath in pairs(data.groupOperations) do
-- We export a "," in a group path as "``"
local newGroupPath = type(groupPath) == "string" and gsub(groupPath, "``", ",")
if newGroupPath and newGroupPath ~= groupPath then
changeGroupPaths[groupPath] = newGroupPath
if data.groupOperations[newGroupPath] then
Log.Err("Duplicated group operations (%s, %s)", tostring(groupPath), tostring(newGroupPath))
return false
end
end
end
for groupPath, newGroupPath in pairs(changeGroupPaths) do
data.groupOperations[newGroupPath] = data.groupOperations[groupPath]
data.groupOperations[groupPath] = nil
end
TempTable.Release(changeGroupPaths)
if not private.ValidateGroupOperationsTable(data.groupOperations, groups, operations, false) then
Log.Err("Invalid group operations")
return false
end
groupOperations = data.groupOperations
else
groupOperations = {}
end
-- check if there's a common top-level group within the import
local commonTopLevelGroup = private.GetCommonTopLevelGroup(items, groups, groupOperations)
if commonTopLevelGroup then
private.UpdateTopLevelGroup(commonTopLevelGroup, items, groups, groupOperations)
end
if numInvalidItems > 0 then
Log.PrintfUser(L["NOTE: The import contained %d invalid items which were ignored."], numInvalidItems)
end
if numChangedOperations > 0 then
Log.PrintfUser(L["NOTE: The import contained %d operations with at least one invalid setting which was reset."], numChangedOperations)
end
Log.Info("Decoded old import string")
private.importContext.groupName = private.DedupImportGroupName(commonTopLevelGroup or L["Imported Group"])
private.importContext.items = items
private.importContext.groups = groups
private.importContext.groupOperations = groupOperations
private.importContext.operations = operations
private.importContext.customSources = {}
return true
end
function private.DecodeOldGroupOrItemListImport(str)
local items, groups, numInvalidItems = private.DecodeGroupExportHelper(str)
if not items then
Log.Err("No items found")
return false
end
local groupOperations = {}
-- check if there's a common top-level group within the import
local commonTopLevelGroup = private.GetCommonTopLevelGroup(items, groups, groupOperations)
if commonTopLevelGroup then
private.UpdateTopLevelGroup(commonTopLevelGroup, items, groups, groupOperations)
end
if numInvalidItems > 0 then
Log.PrintfUser(L["NOTE: The import contained %d invalid items which were ignored."], numInvalidItems)
end
Log.Info("Decoded old group or item list")
private.importContext.groupName = private.DedupImportGroupName(commonTopLevelGroup or L["Imported Group"])
private.importContext.items = items
private.importContext.groups = groups
private.importContext.groupOperations = groupOperations
private.importContext.operations = {}
private.importContext.customSources = {}
return true
end
function private.DecodeGroupExportHelper(str)
local items, groups, numInvalidItems = nil, nil, 0
if strmatch(str, "^[ip0-9%-:;]+$") then
-- this is likely a list of itemStrings separated by semicolons instead of commas, so attempt to fix it
str = gsub(str, ";", ",")
end
if strmatch(str, "^[0-9,]+$") then
-- this is likely a list of itemIds separated by commas, so attempt to fix it
str = gsub(str, "[0-9]+", "i:%1")
end
local relativePath = TSM.CONST.ROOT_GROUP_PATH
for part in String.SplitIterator(str, ",") do
part = strtrim(part)
local groupPath = strmatch(part, "^group:(.+)$")
local itemString = strmatch(part, "^[ip]?:?[0-9%-:]+$")
local newItemString = itemString and ItemString.Get(itemString) or nil
if newItemString and newItemString ~= itemString then
itemString = newItemString
numInvalidItems = numInvalidItems + 1
end
assert(not groupPath or not itemString)
if groupPath then
-- We export a "," in a group path as "``"
groupPath = gsub(groupPath, "``", ",")
if not private.IsValidGroupPath(groupPath) then
Log.Err("Invalid groupPath (%s)", tostring(groupPath))
return
end
relativePath = groupPath
groups = groups or {}
-- create the groups all the way up to the root
while groupPath do
groups[groupPath] = true
groupPath = TSM.Groups.Path.GetParent(groupPath)
end
elseif itemString then
items = items or {}
groups = groups or {}
groups[relativePath] = true
items[itemString] = relativePath
else
Log.Err("Unknown part: %s", part)
return
end
end
return items, groups, numInvalidItems
end
function private.ValidateOperationsTable(operations, strict)
local numChangedOperations = 0
for moduleName, moduleOperations in pairs(operations) do
local isInvalidModuleName, isNotExportOperationModule = private.IsValidOperationModule(moduleName)
if not isInvalidModuleName then
Log.Err("Invalid module name")
return nil
elseif isNotExportOperationModule then
if strict then
Log.Err("Invalid moduleName (%s)", tostring(moduleName))
return nil
else
Log.Warn("Ignoring module (%s)", moduleName)
operations[moduleName] = nil
wipe(moduleOperations)
end
elseif type(moduleOperations) ~= "table" then
Log.Err("Invalid moduleOperations type (%s)", tostring(moduleOperations))
return nil
end
for operationName, operationSettings in pairs(moduleOperations) do
if type(operationName) ~= "string" or not TSM.Operations.IsValidName(operationName) then
Log.Err("Invalid operationName (%s)", tostring(operationName))
return nil
elseif type(operationSettings) ~= "table" then
Log.Err("Invalid operationSettings type (%s)", tostring(operationSettings))
return nil
end
-- sanitize the operation settings
if TSM.Operations.SanitizeSettings(moduleName, operationName, operationSettings, true, true) then
numChangedOperations = numChangedOperations + 1
end
end
end
return numChangedOperations
end
function private.ValidateGroupOperationsTable(groupOperations, groups, operations, strict)
for groupPath, groupsOperationsTable in pairs(groupOperations) do
if not private.IsValidGroupPath(groupPath) then
Log.Err("Invalid groupPath (%s)", tostring(groupPath))
return false
elseif not groups[groupPath] then
if strict then
Log.Err("Invalid group (%s)", groupPath)
return false
else
Log.Info("Creating group with operations (%s)", groupPath)
groups[groupPath] = true
end
end
if not strict then
groupsOperationsTable.ignoreItemVariations = nil
end
for moduleName, moduleOperations in pairs(groupsOperationsTable) do
local isInvalidModuleName, isNotExportOperationModule = private.IsValidOperationModule(moduleName)
if not isInvalidModuleName then
Log.Err("Invalid module name")
return false
elseif isNotExportOperationModule then
if strict then
Log.Err("Invalid moduleName (%s)", tostring(moduleName))
return false
else
Log.Warn("Ignoring module (%s)", moduleName)
groupsOperationsTable[moduleName] = nil
wipe(moduleOperations)
end
elseif type(moduleOperations) ~= "table" then
Log.Err("Invalid moduleOperations type (%s)", tostring(moduleOperations))
return false
elseif moduleOperations.override ~= nil and moduleOperations.override ~= true then
Log.Err("Invalid moduleOperations override type (%s)", tostring(moduleOperations.override))
return false
elseif groupPath == TSM.CONST.ROOT_GROUP_PATH and not moduleOperations.override then
if strict then
Log.Err("Top-level group does not have override set")
return false
else
Log.Info("Setting override for top-level group")
moduleOperations.override = true
end
end
local numOperations = #moduleOperations
if numOperations > TSM.Operations.GetMaxNumber(moduleName) then
Log.Err("Too many operations (%s, %s, %d)", groupPath, moduleName, numOperations)
return false
end
for k, v in pairs(moduleOperations) do
if k == "override" then
-- pass
elseif type(k) ~= "number" or k < 1 or k > numOperations then
Log.Err("Unknown key (%s, %s, %s, %s)", groupPath, moduleName, tostring(k), tostring(v))
return false
elseif type(v) ~= "string" then
Log.Err("Invalid value (%s, %s, %s, %s)", groupPath, moduleName, k, tostring(v))
return false
end
end
-- some old imports had "" operations attached to groups, so remove them
for i = #moduleOperations, 1, -1 do
if moduleOperations[i] == "" then
tremove(moduleOperations, i)
end
end
for _, operationName in ipairs(moduleOperations) do
if type(operationName) ~= "string" or not TSM.Operations.IsValidName(operationName) then
Log.Err("Invalid operationName (%s)", tostring(operationName))
return false
elseif not operations[moduleName][operationName] then
Log.Err("Unknown operation (%s)", operationName)
return false
end
end
end
end
return true
end
function private.DedupImportGroupName(groupName)
if TSM.Groups.Exists(groupName) then
local num = 1
while TSM.Groups.Exists(groupName.." "..num) do
num = num + 1
end
groupName = groupName.." "..num
end
return groupName
end
function private.IsValidGroupPath(groupPath)
return type(groupPath) == "string" and not strmatch(groupPath, "^`") and not strmatch(groupPath, "`$") and not strmatch(groupPath, "``")
end
function private.IsValidOperationModule(moduleName)
if type(moduleName) ~= "string" then
Log.Err("Invalid moduleName (%s)", tostring(moduleName))
return false
elseif not TSM.Operations.ModuleExists(moduleName) then
Log.Err("Invalid moduleName (%s)", tostring(moduleName))
return false
elseif not EXPORT_OPERATION_MODULES[moduleName] then
return true, true
end
return true
end
function private.GetCommonTopLevelGroup(items, groups, groupOperations)
local commonTopLevelGroup = nil
-- check the items
for _, groupPath in pairs(items) do
if groupPath == TSM.CONST.ROOT_GROUP_PATH then
return nil
end
local topLevelGroup = TSM.Groups.Path.GetTopLevel(groupPath)
if not commonTopLevelGroup then
commonTopLevelGroup = topLevelGroup
elseif topLevelGroup ~= commonTopLevelGroup then
return nil
end
end
-- check the groups
for groupPath in pairs(groups) do
if groupPath ~= TSM.CONST.ROOT_GROUP_PATH then
local topLevelGroup = TSM.Groups.Path.GetTopLevel(groupPath)
if not commonTopLevelGroup then
commonTopLevelGroup = topLevelGroup
elseif topLevelGroup ~= commonTopLevelGroup then
return nil
end
end
end
-- check the groupOperations
for groupPath in pairs(groupOperations) do
if groupPath == TSM.CONST.ROOT_GROUP_PATH then
return nil
end
local topLevelGroup = TSM.Groups.Path.GetTopLevel(groupPath)
if not commonTopLevelGroup then
commonTopLevelGroup = topLevelGroup
elseif topLevelGroup ~= commonTopLevelGroup then
return nil
end
end
return commonTopLevelGroup
end
function private.UpdateTopLevelGroup(topLevelGroup, items, groups, groupOperations)
-- update items
for itemString, groupPath in pairs(items) do
items[itemString] = TSM.Groups.Path.GetRelative(groupPath, topLevelGroup)
end
-- update groups
local newGroups = TempTable.Acquire()
groups[TSM.CONST.ROOT_GROUP_PATH] = nil
for groupPath in pairs(groups) do
newGroups[TSM.Groups.Path.GetRelative(groupPath, topLevelGroup)] = true
end
wipe(groups)
for groupPath in pairs(newGroups) do
groups[groupPath] = true
end
TempTable.Release(newGroups)
-- update groupOperations
local newGroupOperations = TempTable.Acquire()
for groupPath, groupOperationsTable in pairs(groupOperations) do
newGroupOperations[TSM.Groups.Path.GetRelative(groupPath, topLevelGroup)] = groupOperationsTable
end
wipe(groupOperations)
for groupPath, groupOperationsTable in pairs(newGroupOperations) do
groupOperations[groupPath] = groupOperationsTable
end
TempTable.Release(newGroupOperations)
-- set override on new top-level group
if groupOperations[TSM.CONST.ROOT_GROUP_PATH] then
for _, moduleOperations in pairs(groupOperations[TSM.CONST.ROOT_GROUP_PATH]) do
moduleOperations.override = true
end
end
end

View File

@ -0,0 +1,81 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Path = TSM.Groups:NewPackage("Path")
local String = TSM.Include("Util.String")
local private = {}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Path.GetName(groupPath)
local _, name = private.SplitPath(groupPath)
return name
end
function Path.GetParent(groupPath)
local parentPath = private.SplitPath(groupPath)
return parentPath
end
function Path.Split(groupPath)
return private.SplitPath(groupPath)
end
function Path.Join(...)
if select(1, ...) == TSM.CONST.ROOT_GROUP_PATH then
return Path.Join(select(2, ...))
end
return strjoin(TSM.CONST.GROUP_SEP, ...)
end
function Path.IsChild(groupPath, parentPath)
if parentPath == TSM.CONST.ROOT_GROUP_PATH then
return groupPath ~= TSM.CONST.ROOT_GROUP_PATH
end
return strmatch(groupPath, "^"..String.Escape(parentPath)..TSM.CONST.GROUP_SEP) and true or false
end
function Path.Format(groupPath)
if not groupPath then return end
local result = gsub(groupPath, TSM.CONST.GROUP_SEP, "->")
return result
end
function Path.GetRelative(groupPath, prefixGroupPath)
if groupPath == prefixGroupPath then
return TSM.CONST.ROOT_GROUP_PATH
end
local relativePath, numSubs = gsub(groupPath, "^"..String.Escape(prefixGroupPath)..TSM.CONST.GROUP_SEP, "")
assert(numSubs == 1 and relativePath)
return relativePath
end
function Path.GetTopLevel(groupPath)
assert(groupPath ~= TSM.CONST.ROOT_GROUP_PATH)
return strmatch(groupPath, "^([^"..TSM.CONST.GROUP_SEP.."]+)")
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.SplitPath(groupPath)
local parentPath, groupName = strmatch(groupPath, "^(.+)"..TSM.CONST.GROUP_SEP.."([^"..TSM.CONST.GROUP_SEP.."]+)$")
if parentPath then
return parentPath, groupName
elseif groupPath ~= TSM.CONST.ROOT_GROUP_PATH then
return TSM.CONST.ROOT_GROUP_PATH, groupPath
else
return nil, groupPath
end
end

View File

@ -0,0 +1,99 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local GroupsSync = TSM.Groups:NewPackage("Sync")
local L = TSM.Include("Locale").GetTable()
local TempTable = TSM.Include("Util.TempTable")
local Math = TSM.Include("Util.Math")
local Log = TSM.Include("Util.Log")
local Sync = TSM.Include("Service.Sync")
local private = {}
-- ============================================================================
-- New Modules Functions
-- ============================================================================
function GroupsSync.OnInitialize()
Sync.RegisterRPC("CREATE_PROFILE", private.RPCCreateProfile)
end
function GroupsSync.SendCurrentProfile(targetPlayer)
local profileName = TSM.db:GetCurrentProfile()
local data = TempTable.Acquire()
data.groups = TempTable.Acquire()
for groupPath, moduleOperations in pairs(TSM.db:Get("profile", profileName, "userData", "groups")) do
data.groups[groupPath] = {}
for _, module in TSM.Operations.ModuleIterator() do
local operations = moduleOperations[module]
if operations.override then
data.groups[groupPath][module] = operations
end
end
end
data.items = TSM.db:Get("profile", profileName, "userData", "items")
data.operations = TSM.db:Get("profile", profileName, "userData", "operations")
local result, estimatedTime = Sync.CallRPC("CREATE_PROFILE", targetPlayer, private.RPCCreateProfileResultHandler, profileName, UnitName("player"), data)
if result then
estimatedTime = max(Math.Round(estimatedTime, 60), 60)
Log.PrintfUser(L["Sending your '%s' profile to %s. Please keep both characters online until this completes. This will take approximately: %s"], profileName, targetPlayer, SecondsToTime(estimatedTime))
else
Log.PrintUser(L["Failed to send profile. Ensure both characters are online and try again."])
end
TempTable.Release(data.groups)
TempTable.Release(data)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.CopyTable(srcTbl, dstTbl)
for k, v in pairs(srcTbl) do
dstTbl[k] = v
end
end
function private.RPCCreateProfile(profileName, playerName, data)
assert(TSM.db:IsValidProfileName(profileName))
if TSM.db:ProfileExists(profileName) then
return false, L["A profile with that name already exists on the target account. Rename it first and try again."]
end
-- create and switch to the new profile
local currentProfile = TSM.db:GetCurrentProfile()
TSM.db:SetProfile(profileName)
-- copy all the data into this profile
private.CopyTable(data.groups, TSM.db.profile.userData.groups)
private.CopyTable(data.items, TSM.db.profile.userData.items)
TSM.Operations.ReplaceProfileOperations(data.operations)
-- switch back to our previous profile
TSM.db:SetProfile(currentProfile)
Log.PrintfUser(L["Added '%s' profile which was received from %s."], profileName, playerName)
return true, profileName, UnitName("player")
end
function private.RPCCreateProfileResultHandler(success, ...)
if success == nil then
Log.PrintUser(L["Failed to send profile."].." "..L["Ensure both characters are online and try again."])
return
elseif not success then
local errMsg = ...
Log.PrintUser(L["Failed to send profile."].." "..errMsg)
return
end
local profileName, targetPlayer = ...
Log.PrintfUser(L["Successfully sent your '%s' profile to %s!"], profileName, targetPlayer)
end

View File

@ -0,0 +1,55 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Mailing = TSM:NewPackage("Mailing")
local Event = TSM.Include("Util.Event")
local private = {
mailOpen = false,
frameCallbacks = {},
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Mailing.OnInitialize()
Event.Register("MAIL_SHOW", private.MailShow)
Event.Register("MAIL_CLOSED", private.MailClosed)
end
function Mailing.RegisterFrameCallback(callback)
tinsert(private.frameCallbacks, callback)
end
function Mailing.IsOpen()
return private.mailOpen
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.MailShow()
private.mailOpen = true
for _, callback in ipairs(private.frameCallbacks) do
callback(true)
end
end
function private.MailClosed()
if not private.mailOpen then
return
end
private.mailOpen = false
for _, callback in ipairs(private.frameCallbacks) do
callback(false)
end
end

View File

@ -0,0 +1,161 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Groups = TSM.Mailing:NewPackage("Groups")
local L = TSM.Include("Locale").GetTable()
local Log = TSM.Include("Util.Log")
local Threading = TSM.Include("Service.Threading")
local Inventory = TSM.Include("Service.Inventory")
local PlayerInfo = TSM.Include("Service.PlayerInfo")
local BagTracking = TSM.Include("Service.BagTracking")
local private = {
thread = nil,
sendDone = false,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Groups.OnInitialize()
private.thread = Threading.New("MAIL_GROUPS", private.GroupsMailThread)
end
function Groups.KillThread()
Threading.Kill(private.thread)
end
function Groups.StartSending(callback, groupList, sendRepeat, isDryRun)
Threading.Kill(private.thread)
Threading.SetCallback(private.thread, callback)
Threading.Start(private.thread, groupList, sendRepeat, isDryRun)
end
-- ============================================================================
-- Group Sending Thread
-- ============================================================================
function private.GroupsMailThread(groupList, sendRepeat, isDryRun)
while true do
local targets = Threading.AcquireSafeTempTable()
local numMailable = Threading.AcquireSafeTempTable()
for _, groupPath in ipairs(groupList) do
if groupPath ~= TSM.CONST.ROOT_GROUP_PATH then
local used = Threading.AcquireSafeTempTable()
local keep = Threading.AcquireSafeTempTable()
for _, _, operationSettings in TSM.Operations.GroupOperationIterator("Mailing", groupPath) do
local target = operationSettings.target
if target ~= "" then
local targetItems = targets[target] or Threading.AcquireSafeTempTable()
for _, itemString in TSM.Groups.ItemIterator(groupPath) do
itemString = TSM.Groups.TranslateItemString(itemString)
used[itemString] = used[itemString] or 0
keep[itemString] = max(keep[itemString] or 0, operationSettings.keepQty)
numMailable[itemString] = numMailable[itemString] or BagTracking.GetNumMailable(itemString)
local numAvailable = numMailable[itemString] - used[itemString] - keep[itemString]
local quantity = private.GetItemQuantity(itemString, numAvailable, operationSettings)
assert(quantity >= 0)
if PlayerInfo.IsPlayer(target) then
keep[itemString] = max(keep[itemString], quantity)
else
used[itemString] = used[itemString] + quantity
if quantity > 0 then
targetItems[itemString] = quantity
end
end
end
if next(targetItems) then
targets[target] = targetItems
else
Threading.ReleaseSafeTempTable(targetItems)
end
end
end
Threading.ReleaseSafeTempTable(used)
Threading.ReleaseSafeTempTable(keep)
end
end
Threading.ReleaseSafeTempTable(numMailable)
if not next(targets) then
Log.PrintUser(L["Nothing to send."])
end
for name, items in pairs(targets) do
private.SendItems(name, items, isDryRun)
Threading.ReleaseSafeTempTable(items)
Threading.Sleep(0.5)
end
Threading.ReleaseSafeTempTable(targets)
if sendRepeat then
Threading.Sleep(TSM.db.global.mailingOptions.resendDelay * 60)
else
break
end
end
end
function private.SendItems(target, items, isDryRun)
private.sendDone = false
TSM.Mailing.Send.StartSending(private.SendCallback, target, "", "", 0, items, true, isDryRun)
while not private.sendDone do
Threading.Yield(true)
end
end
function private.SendCallback()
private.sendDone = true
end
function private.GetItemQuantity(itemString, numAvailable, operationSettings)
if numAvailable <= 0 then
return 0
end
local numToSend = 0
local isTargetPlayer = PlayerInfo.IsPlayer(operationSettings.target)
if operationSettings.maxQtyEnabled then
if operationSettings.restock then
local targetQty = private.GetTargetQuantity(operationSettings.target, itemString, operationSettings.restockSources)
if isTargetPlayer and targetQty <= operationSettings.maxQty then
numToSend = numAvailable
else
numToSend = min(numAvailable, operationSettings.maxQty - targetQty)
end
if isTargetPlayer then
numToSend = numAvailable - (targetQty - operationSettings.maxQty)
end
else
numToSend = min(numAvailable, operationSettings.maxQty)
end
elseif not isTargetPlayer then
numToSend = numAvailable
end
return max(numToSend, 0)
end
function private.GetTargetQuantity(player, itemString, sources)
if player then
player = strtrim(strmatch(player, "^[^-]+"))
end
local num = Inventory.GetBagQuantity(itemString, player) + Inventory.GetMailQuantity(itemString, player) + Inventory.GetAuctionQuantity(itemString, player)
if sources then
if sources.guild then
num = num + Inventory.GetGuildQuantity(itemString, PlayerInfo.GetPlayerGuild(player))
end
if sources.bank then
num = num + Inventory.GetBankQuantity(itemString, player) + Inventory.GetReagentBankQuantity(itemString, player)
end
end
return num
end

View File

@ -0,0 +1,53 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Inbox = TSM.Mailing:NewPackage("Inbox")
local Database = TSM.Include("Util.Database")
local TempTable = TSM.Include("Util.TempTable")
local MailTracking = TSM.Include("Service.MailTracking")
local private = {
itemsQuery = nil,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Inbox.OnInitialize()
private.itemsQuery = MailTracking.CreateMailItemQuery()
:Equal("index", Database.BoundQueryParam())
end
function Inbox.CreateQuery()
return MailTracking.CreateMailInboxQuery()
:VirtualField("itemList", "string", private.GetVirtualItemList)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetVirtualItemList(row)
private.itemsQuery:BindParams(row:GetField("index"))
local items = TempTable.Acquire()
for _, itemsRow in private.itemsQuery:Iterator() do
local itemName = TSM.UI.GetColoredItemName(itemsRow:GetField("itemLink")) or ""
local qty = itemsRow:GetField("quantity")
tinsert(items, qty > 1 and (itemName.." (x"..qty..")") or itemName)
end
local result = table.concat(items, ", ")
TempTable.Release(items)
return result
end

View File

@ -0,0 +1,233 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Open = TSM.Mailing:NewPackage("Open")
local L = TSM.Include("Locale").GetTable()
local Delay = TSM.Include("Util.Delay")
local Event = TSM.Include("Util.Event")
local String = TSM.Include("Util.String")
local Money = TSM.Include("Util.Money")
local Log = TSM.Include("Util.Log")
local ItemString = TSM.Include("Util.ItemString")
local Theme = TSM.Include("Util.Theme")
local Threading = TSM.Include("Service.Threading")
local ItemInfo = TSM.Include("Service.ItemInfo")
local MailTracking = TSM.Include("Service.MailTracking")
local private = {
thread = nil,
isOpening = false,
lastCheck = nil,
moneyCollected = 0,
}
local INBOX_SIZE = TSM.IsWowClassic() and 50 or 100
local MAIL_REFRESH_TIME = TSM.IsWowClassic() and 60 or 15
-- ============================================================================
-- Module Functions
-- ============================================================================
function Open.OnInitialize()
private.thread = Threading.New("MAIL_OPENING", private.OpenMailThread)
Event.Register("MAIL_SHOW", private.ScheduleCheck)
Event.Register("MAIL_CLOSED", private.MailClosedHandler)
end
function Open.KillThread()
Threading.Kill(private.thread)
private.PrintMoneyCollected()
private.isOpening = false
end
function Open.StartOpening(callback, autoRefresh, keepMoney, filterText, filterType)
Threading.Kill(private.thread)
private.isOpening = true
private.moneyCollected = 0
Threading.SetCallback(private.thread, callback)
Threading.Start(private.thread, autoRefresh, keepMoney, filterText, filterType)
end
function Open.GetLastCheckTime()
return private.lastCheck
end
-- ============================================================================
-- Mail Opening Thread
-- ============================================================================
function private.OpenMailThread(autoRefresh, keepMoney, filterText, filterType)
local isLastLoop = false
while true do
local query = TSM.Mailing.Inbox.CreateQuery()
query:ResetOrderBy()
:OrderBy("index", false)
:Or()
:Matches("itemList", filterText)
:Matches("subject", filterText)
:End()
:Select("index")
if filterType then
query:Equal("icon", filterType)
end
local mails = Threading.AcquireSafeTempTable()
for _, index in query:Iterator() do
tinsert(mails, index)
end
query:Release()
private.OpenMails(mails, keepMoney, filterType)
Threading.ReleaseSafeTempTable(mails)
if not autoRefresh or isLastLoop then
break
end
local numLeftMail, totalLeftMail = GetInboxNumItems()
if totalLeftMail == numLeftMail or numLeftMail == INBOX_SIZE then
isLastLoop = true
end
CheckInbox()
Threading.Sleep(1)
end
private.PrintMoneyCollected()
private.isOpening = false
end
function private.CanOpenMail()
return not C_Mail.IsCommandPending()
end
function private.OpenMails(mails, keepMoney, filterType)
for i = 1, #mails do
local index = mails[i]
Threading.WaitForFunction(private.CanOpenMail)
local mailType = MailTracking.GetMailType(index)
local matchesFilter = (not filterType and mailType) or (filterType and filterType == mailType)
local hasBagSpace = not MailTracking.GetInboxItemLink(index) or CalculateTotalNumberOfFreeBagSlots() > TSM.db.global.mailingOptions.keepMailSpace
if matchesFilter and hasBagSpace then
local _, _, _, _, money = GetInboxHeaderInfo(index)
if not keepMoney or (keepMoney and money <= 0) then
-- marks the mail as read
GetInboxText(index)
AutoLootMailItem(index)
private.moneyCollected = private.moneyCollected + money
if Threading.WaitForEvent("CLOSE_INBOX_ITEM", "MAIL_FAILED") ~= "MAIL_FAILED" then
if TSM.db.global.mailingOptions.inboxMessages then
private.PrintOpenMailMessage(index)
end
end
end
end
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.CheckInbox()
if private.isOpening then
private.ScheduleCheck()
return
end
if not TSM.UI.MailingUI.Inbox.IsMailOpened() then
CheckInbox()
end
private.ScheduleCheck()
end
function private.PrintMoneyCollected()
if TSM.db.global.mailingOptions.inboxMessages and private.moneyCollected > 0 then
Log.PrintfUser(L["Total Gold Collected: %s"], Money.ToString(private.moneyCollected))
end
private.moneyCollected = 0
end
function private.PrintOpenMailMessage(index)
local _, _, sender, subject, money, cod, _, hasItem = GetInboxHeaderInfo(index)
sender = sender or "?"
local _, _, _, _, isInvoice = GetInboxText(index)
if isInvoice then
-- it's an invoice
local invoiceType, itemName, playerName, bid, _, _, ahcut, _, _, _, quantity = GetInboxInvoiceInfo(index)
playerName = playerName or (invoiceType == "buyer" and AUCTION_HOUSE_MAIL_MULTIPLE_SELLERS or AUCTION_HOUSE_MAIL_MULTIPLE_BUYERS)
if invoiceType == "buyer" then
local itemLink = MailTracking.GetInboxItemLink(index) or "["..itemName.."]"
Log.PrintfUser(L["Bought %sx%d for %s from %s"], itemLink, quantity, Money.ToString(bid, Theme.GetFeedbackColor("RED"):GetTextColorPrefix()), playerName)
elseif invoiceType == "seller" then
Log.PrintfUser(L["Sold [%s]x%d for %s to %s"], itemName, quantity, Money.ToString(bid - ahcut, Theme.GetFeedbackColor("GREEN"):GetTextColorPrefix()), playerName)
end
elseif hasItem then
local itemLink
local quantity = 0
for i = 1, hasItem do
local link = GetInboxItemLink(index, i)
itemLink = itemLink or link
quantity = quantity + (select(4, GetInboxItem(index, i)) or 0)
if ItemString.Get(itemLink) ~= ItemString.Get(link) then
itemLink = L["Multiple Items"]
quantity = -1
break
end
end
if hasItem == 1 then
itemLink = MailTracking.GetInboxItemLink(index) or itemLink
end
local itemName = ItemInfo.GetName(itemLink) or "?"
local itemDesc = (quantity > 0 and format("%sx%d", itemLink, quantity)) or (quantity == -1 and "Multiple Items") or "?"
if hasItem == 1 and itemLink and strfind(subject, "^" .. String.Escape(format(AUCTION_EXPIRED_MAIL_SUBJECT, itemName))) then
Log.PrintfUser(L["Your auction of %s expired"], itemDesc)
elseif hasItem == 1 and quantity > 0 and (subject == format(AUCTION_REMOVED_MAIL_SUBJECT.."x%d", itemName, quantity) or subject == format(AUCTION_REMOVED_MAIL_SUBJECT, itemName)) then
Log.PrintfUser(L["Cancelled auction of %sx%d"], itemLink, quantity)
elseif cod > 0 then
Log.PrintfUser(L["%s sent you a COD of %s for %s"], sender, Money.ToString(cod, Theme.GetFeedbackColor("RED"):GetTextColorPrefix()), itemDesc)
elseif money > 0 then
Log.PrintfUser(L["%s sent you %s and %s"], sender, itemDesc, Money.ToString(money, Theme.GetFeedbackColor("GREEN"):GetTextColorPrefix()))
else
Log.PrintfUser(L["%s sent you %s"], sender, itemDesc)
end
elseif money > 0 then
Log.PrintfUser(L["%s sent you %s"], sender, Money.ToString(money, Theme.GetFeedbackColor("GREEN"):GetTextColorPrefix()))
elseif subject then
Log.PrintfUser(L["%s sent you a message: %s"], sender, subject)
end
end
-- ============================================================================
-- Event Handlers
-- ============================================================================
function private.ScheduleCheck()
if not private.lastCheck or time() - private.lastCheck > (MAIL_REFRESH_TIME - 1) then
private.lastCheck = time()
Delay.AfterTime("mailInboxCheck", MAIL_REFRESH_TIME, private.CheckInbox)
else
local nextUpdate = MAIL_REFRESH_TIME - (time() - private.lastCheck)
Delay.AfterTime("mailInboxCheck", nextUpdate, private.CheckInbox)
end
end
function private.MailClosedHandler()
Delay.Cancel("mailInboxCheck")
end

View File

@ -0,0 +1,287 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Send = TSM.Mailing:NewPackage("Send")
local L = TSM.Include("Locale").GetTable()
local Table = TSM.Include("Util.Table")
local Money = TSM.Include("Util.Money")
local SlotId = TSM.Include("Util.SlotId")
local Log = TSM.Include("Util.Log")
local ItemString = TSM.Include("Util.ItemString")
local Theme = TSM.Include("Util.Theme")
local Threading = TSM.Include("Service.Threading")
local ItemInfo = TSM.Include("Service.ItemInfo")
local InventoryInfo = TSM.Include("Service.InventoryInfo")
local BagTracking = TSM.Include("Service.BagTracking")
local private = {
thread = nil,
bagUpdate = nil,
}
local PLAYER_NAME = UnitName("player")
local PLAYER_NAME_REALM = string.gsub(PLAYER_NAME.."-"..GetRealmName(), "%s+", "")
-- ============================================================================
-- Module Functions
-- ============================================================================
function Send.OnInitialize()
private.thread = Threading.New("MAIL_SENDING", private.SendMailThread)
BagTracking.RegisterCallback(private.BagUpdate)
end
function Send.KillThread()
Threading.Kill(private.thread)
end
function Send.StartSending(callback, recipient, subject, body, money, items, isGroup, isDryRun)
Threading.Kill(private.thread)
Threading.SetCallback(private.thread, callback)
Threading.Start(private.thread, recipient, subject, body, money, items, isGroup, isDryRun)
end
-- ============================================================================
-- Mail Sending Thread
-- ============================================================================
function private.SendMailThread(recipient, subject, body, money, items, isGroup, isDryRun)
if recipient == "" or recipient == PLAYER_NAME or recipient == PLAYER_NAME_REALM then
return
end
private.PrintMailMessage(money, items, recipient, isGroup, isDryRun)
if isDryRun then
return
end
if not items then
private.SendMail(recipient, subject, body, money, true)
return
end
ClearSendMail()
local itemInfo = Threading.AcquireSafeTempTable()
local query = BagTracking.CreateQueryBags()
:OrderBy("slotId", true)
:Select("bag", "slot", "itemString", "quantity")
:Equal("isBoP", false)
for _, bag, slot, itemString, quantity in query:Iterator() do
if isGroup then
itemString = TSM.Groups.TranslateItemString(itemString)
end
if items[itemString] and not InventoryInfo.IsBagSlotLocked(bag, slot) then
if not itemInfo[itemString] then
itemInfo[itemString] = { locations = {} }
end
tinsert(itemInfo[itemString].locations, { bag = bag, slot = slot, quantity = quantity })
end
end
query:Release()
for itemString, quantity in pairs(items) do
if quantity > 0 and itemInfo[itemString] and #itemInfo[itemString].locations > 0 then
for i = 1, #itemInfo[itemString].locations do
local info = itemInfo[itemString].locations[i]
if info.quantity > 0 then
if quantity == info.quantity then
PickupContainerItem(info.bag, info.slot)
ClickSendMailItemButton()
if private.GetNumPendingAttachments() == ATTACHMENTS_MAX_SEND or (isGroup and TSM.db.global.mailingOptions.sendItemsIndividually) then
private.SendMail(recipient, subject, body, money)
end
items[itemString] = 0
info.quantity = 0
break
end
end
end
end
end
for itemString in pairs(items) do
if items[itemString] > 0 and itemInfo[itemString] and #itemInfo[itemString].locations > 0 then
local emptySlotIds = private.GetEmptyBagSlotsThreaded(ItemString.IsItem(itemString) and GetItemFamily(ItemString.ToId(itemString)) or 0)
for i = 1, #itemInfo[itemString].locations do
local info = itemInfo[itemString].locations[i]
if items[itemString] > 0 and info.quantity > 0 then
if items[itemString] < info.quantity then
if #emptySlotIds > 0 then
local splitBag, splitSlot = SlotId.Split(tremove(emptySlotIds, 1))
SplitContainerItem(info.bag, info.slot, items[itemString])
PickupContainerItem(splitBag, splitSlot)
Threading.WaitForFunction(private.BagSlotHasItem, splitBag, splitSlot)
PickupContainerItem(splitBag, splitSlot)
ClickSendMailItemButton()
if private.GetNumPendingAttachments() == ATTACHMENTS_MAX_SEND then
private.SendMail(recipient, subject, body, money)
end
items[itemString] = 0
info.quantity = 0
break
end
else
PickupContainerItem(info.bag, info.slot)
ClickSendMailItemButton()
if private.GetNumPendingAttachments() == ATTACHMENTS_MAX_SEND then
private.SendMail(recipient, subject, body, money)
end
items[itemString] = items[itemString] - info.quantity
info.quantity = 0
end
end
end
if isGroup and TSM.db.global.mailingOptions.sendItemsIndividually then
private.SendMail(recipient, subject, body, money)
end
Threading.ReleaseSafeTempTable(emptySlotIds)
end
end
if private.HasPendingAttachments() then
private.SendMail(recipient, subject, body, money)
end
Threading.ReleaseSafeTempTable(itemInfo)
end
function private.PrintMailMessage(money, items, target, isGroup, isDryRun)
if not TSM.db.global.mailingOptions.sendMessages and not isDryRun then
return
end
if money > 0 and not items then
Log.PrintfUser(L["Sending %s to %s"], Money.ToString(money), target)
return
end
if not items then
return
end
local itemList = ""
for k, v in pairs(items) do
local coloredItem = ItemInfo.GetLink(k)
itemList = itemList..coloredItem.."x"..v..", "
end
itemList = strtrim(itemList, ", ")
if next(items) and money < 0 then
if isDryRun then
Log.PrintfUser(L["Would send %s to %s with a COD of %s"], itemList, target, Money.ToString(money, Theme.GetFeedbackColor("RED"):GetTextColorPrefix()))
else
Log.PrintfUser(L["Sending %s to %s with a COD of %s"], itemList, target, Money.ToString(money, Theme.GetFeedbackColor("RED"):GetTextColorPrefix()))
end
elseif next(items) then
if isDryRun then
Log.PrintfUser(L["Would send %s to %s"], itemList, target)
else
Log.PrintfUser(L["Sending %s to %s"], itemList, target)
end
end
end
function private.SendMail(recipient, subject, body, money, noItem)
if subject == "" then
local text = SendMailSubjectEditBox:GetText()
subject = text ~= "" and text or "TSM Mailing"
end
if money > 0 then
SetSendMailMoney(money)
SetSendMailCOD(0)
elseif money < 0 then
SetSendMailCOD(abs(money))
SetSendMailMoney(0)
else
SetSendMailMoney(0)
SetSendMailCOD(0)
end
private.bagUpdate = false
SendMail(recipient, subject, body)
if Threading.WaitForEvent("MAIL_SUCCESS", "MAIL_FAILED") == "MAIL_SUCCESS" then
if noItem then
Threading.Sleep(0.5)
else
Threading.WaitForFunction(private.HasNewBagUpdate)
end
else
Threading.Sleep(0.5)
end
end
function private.BagUpdate()
private.bagUpdate = true
end
function private.HasNewBagUpdate()
return private.bagUpdate
end
function private.HasPendingAttachments()
for i = 1, ATTACHMENTS_MAX_SEND do
if GetSendMailItem(i) then
return true
end
end
return false
end
function private.GetNumPendingAttachments()
local totalAttached = 0
for i = 1, ATTACHMENTS_MAX_SEND do
if GetSendMailItem(i) then
totalAttached = totalAttached + 1
end
end
return totalAttached
end
function private.BagSlotHasItem(bag, slot)
return GetContainerItemInfo(bag, slot) and true or false
end
function private.GetEmptyBagSlotsThreaded(itemFamily)
local emptySlotIds = Threading.AcquireSafeTempTable()
local sortvalue = Threading.AcquireSafeTempTable()
for bag = 0, NUM_BAG_SLOTS do
-- make sure the item can go in this bag
local bagFamily = bag ~= 0 and GetItemFamily(GetInventoryItemLink("player", ContainerIDToInventoryID(bag))) or 0
if bagFamily == 0 or bit.band(itemFamily, bagFamily) > 0 then
for slot = 1, GetContainerNumSlots(bag) do
if not GetContainerItemInfo(bag, slot) then
local slotId = SlotId.Join(bag, slot)
tinsert(emptySlotIds, slotId)
-- use special bags first
sortvalue[slotId] = slotId + (bagFamily > 0 and 0 or 100000)
end
end
end
Threading.Yield()
end
Table.SortWithValueLookup(emptySlotIds, sortvalue)
Threading.ReleaseSafeTempTable(sortvalue)
return emptySlotIds
end

View File

@ -0,0 +1,263 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local MyAuctions = TSM:NewPackage("MyAuctions")
local L = TSM.Include("Locale").GetTable()
local Database = TSM.Include("Util.Database")
local Event = TSM.Include("Util.Event")
local TempTable = TSM.Include("Util.TempTable")
local Log = TSM.Include("Util.Log")
local AuctionTracking = TSM.Include("Service.AuctionTracking")
local ItemInfo = TSM.Include("Service.ItemInfo")
local AuctionHouseWrapper = TSM.Include("Service.AuctionHouseWrapper")
local private = {
pendingDB = nil,
ahOpen = false,
pendingHashes = {},
expectedCounts = {},
auctionInfo = { numPosted = 0, numSold = 0, postedGold = 0, soldGold = 0 },
dbHashFields = {},
pendingFuture = nil,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function MyAuctions.OnInitialize()
private.pendingDB = Database.NewSchema("MY_AUCTIONS_PENDING")
:AddUniqueNumberField("index")
:AddNumberField("hash")
:AddBooleanField("isPending")
:AddNumberField("pendingAuctionId")
:AddIndex("index")
:Commit()
for field in AuctionTracking.DatabaseFieldIterator() do
if field ~= "index" and field ~= "auctionId" then
tinsert(private.dbHashFields, field)
end
end
Event.Register("AUCTION_HOUSE_SHOW", private.AuctionHouseShowEventHandler)
Event.Register("AUCTION_HOUSE_CLOSED", private.AuctionHouseHideEventHandler)
Event.Register("CHAT_MSG_SYSTEM", private.ChatMsgSystemEventHandler)
Event.Register("UI_ERROR_MESSAGE", private.UIErrorMessageEventHandler)
AuctionTracking.RegisterCallback(private.OnAuctionsUpdated)
end
function MyAuctions.CreateQuery()
local query = AuctionTracking.CreateQuery()
:LeftJoin(private.pendingDB, "index")
:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
:VirtualField("group", "string", private.AuctionsGetGroupText, "itemString")
if TSM.IsWowClassic() then
query:OrderBy("index", false)
else
query:OrderBy("saleStatus", false)
query:OrderBy("name", true)
query:OrderBy("auctionId", true)
end
return query
end
function MyAuctions.CancelAuction(auctionId)
local row = private.pendingDB:NewQuery()
:Equal("pendingAuctionId", auctionId)
:GetFirstResultAndRelease()
local hash = row:GetField("hash")
assert(hash)
Log.Info("Canceling (auctionId=%d, hash=%d)", auctionId, hash)
if TSM.IsWowClassic() then
CancelAuction(auctionId)
else
private.pendingFuture = AuctionHouseWrapper.CancelAuction(auctionId)
if not private.pendingFuture then
Log.PrintUser(L["Failed to cancel auction due to the auction house being busy. Ensure no other addons are scanning the AH and try again."])
return
end
private.pendingFuture:SetScript("OnDone", private.PendingFutureOnDone)
end
if private.expectedCounts[hash] and private.expectedCounts[hash] > 0 then
private.expectedCounts[hash] = private.expectedCounts[hash] - 1
else
private.expectedCounts[hash] = private.GetNumRowsByHash(hash) - 1
end
assert(private.expectedCounts[hash] >= 0)
assert(not row:GetField("isPending"))
row:SetField("isPending", true)
:Update()
row:Release()
tinsert(private.pendingHashes, hash)
end
function MyAuctions.CanCancel(index)
if TSM.IsWowClassic() then
local numPending = private.pendingDB:NewQuery()
:Equal("isPending", true)
:LessThanOrEqual("index", index)
:CountAndRelease()
return numPending == 0
else
return not private.pendingFuture
end
end
function MyAuctions.GetNumPending()
if TSM.IsWowClassic() then
return private.pendingDB:NewQuery()
:Equal("isPending", true)
:CountAndRelease()
else
return private.pendingFuture and 1 or 0
end
end
function MyAuctions.GetAuctionInfo()
if not private.ahOpen then
return
end
return private.auctionInfo.numPosted, private.auctionInfo.numSold, private.auctionInfo.postedGold, private.auctionInfo.soldGold
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.AuctionHouseShowEventHandler()
private.ahOpen = true
end
function private.AuctionHouseHideEventHandler()
private.ahOpen = false
if private.pendingFuture then
private.pendingFuture:Cancel()
private.pendingFuture = nil
end
end
function private.ChatMsgSystemEventHandler(_, msg)
if msg == ERR_AUCTION_REMOVED and #private.pendingHashes > 0 and TSM.IsWowClassic() then
local hash = tremove(private.pendingHashes, 1)
assert(hash)
Log.Info("Confirmed (hash=%d)", hash)
end
end
function private.UIErrorMessageEventHandler(_, _, msg)
if (msg == ERR_ITEM_NOT_FOUND or msg == ERR_NOT_ENOUGH_MONEY) and #private.pendingHashes > 0 and TSM.IsWowClassic() then
local hash = tremove(private.pendingHashes, 1)
assert(hash)
Log.Info("Failed to cancel (hash=%d)", hash)
if private.expectedCounts[hash] then
private.expectedCounts[hash] = private.expectedCounts[hash] + 1
end
end
end
function private.PendingFutureOnDone()
local result = private.pendingFuture:GetValue()
private.pendingFuture = nil
local hash = tremove(private.pendingHashes, 1)
assert(hash)
if result then
Log.Info("Confirmed (hash=%d)", hash)
else
Log.Info("Failed to cancel (hash=%d)", hash)
if private.expectedCounts[hash] then
private.expectedCounts[hash] = private.expectedCounts[hash] + 1
end
private.OnAuctionsUpdated()
AuctionTracking.QueryOwnedAuctions()
end
end
function private.GetNumRowsByHash(hash)
return private.pendingDB:NewQuery()
:Equal("hash", hash)
:CountAndRelease()
end
function private.AuctionsGetGroupText(itemString)
local groupPath = TSM.Groups.GetPathByItem(itemString)
if not groupPath then
return ""
end
return groupPath
end
function private.OnAuctionsUpdated()
local minPendingIndexByHash = TempTable.Acquire()
local numByHash = TempTable.Acquire()
local query = AuctionTracking.CreateQuery()
:OrderBy("index", true)
for _, row in query:Iterator() do
local index = row:GetField("index")
local hash = row:CalculateHash(private.dbHashFields)
numByHash[hash] = (numByHash[hash] or 0) + 1
if not minPendingIndexByHash[hash] and private.pendingDB:GetUniqueRowField("index", index, "isPending") then
minPendingIndexByHash[hash] = index
end
end
local numUsed = TempTable.Acquire()
private.pendingDB:TruncateAndBulkInsertStart()
for _, row in query:Iterator() do
local hash = row:CalculateHash(private.dbHashFields)
assert(numByHash[hash] > 0)
local expectedCount = private.expectedCounts[hash]
local isPending = nil
if not expectedCount then
-- this was never pending
isPending = false
elseif numByHash[hash] <= expectedCount then
-- this is no longer pending
isPending = false
private.expectedCounts[hash] = nil
elseif row:GetField("index") >= (minPendingIndexByHash[hash] or math.huge) then
local numPending = numByHash[hash] - expectedCount
assert(numPending > 0)
numUsed[hash] = (numUsed[hash] or 0) + 1
isPending = numUsed[hash] <= numPending
else
-- it's a later auction which is pending
isPending = false
end
private.pendingDB:BulkInsertNewRow(row:GetField("index"), hash, isPending, row:GetField("auctionId"))
end
private.pendingDB:BulkInsertEnd()
TempTable.Release(numByHash)
TempTable.Release(numUsed)
TempTable.Release(minPendingIndexByHash)
-- update the player's auction status
private.auctionInfo.numPosted = 0
private.auctionInfo.numSold = 0
private.auctionInfo.postedGold = 0
private.auctionInfo.soldGold = 0
for _, row in query:Iterator() do
local itemString, saleStatus, buyout, currentBid, stackSize = row:GetFields("itemString", "saleStatus", "buyout", "currentBid", "stackSize")
if saleStatus == 1 then
private.auctionInfo.numSold = private.auctionInfo.numSold + 1
-- if somebody did a buyout, then bid will be equal to buyout, otherwise it'll be the winning bid
private.auctionInfo.soldGold = private.auctionInfo.soldGold + currentBid
else
private.auctionInfo.numPosted = private.auctionInfo.numPosted + 1
if ItemInfo.IsCommodity(itemString) then
private.auctionInfo.postedGold = private.auctionInfo.postedGold + (buyout * stackSize)
else
private.auctionInfo.postedGold = private.auctionInfo.postedGold + buyout
end
end
end
query:Release()
end

View File

@ -0,0 +1,152 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Auctioning = TSM.Operations:NewPackage("Auctioning")
local private = {}
local L = TSM.Include("Locale").GetTable()
local TempTable = TSM.Include("Util.TempTable")
local Money = TSM.Include("Util.Money")
local OPERATION_INFO = {
-- general
blacklist = { type = "string", default = "" },
ignoreLowDuration = { type = "number", default = 0 },
-- post
postCap = { type = "string", default = "5" },
keepQuantity = { type = "string", default = "0" },
maxExpires = { type = "string", default = "0" },
duration = { type = "number", default = 2, customSanitizeFunction = nil },
bidPercent = { type = "number", default = 1 },
undercut = { type = "string", default = "0c", customSanitizeFunction = nil },
minPrice = { type = "string", default = "check(first(crafting,dbmarket,dbregionmarketavg),max(0.25*avg(crafting,dbmarket,dbregionmarketavg),1.5*vendorsell))" },
maxPrice = { type = "string", default = "check(first(crafting,dbmarket,dbregionmarketavg),max(5*avg(crafting,dbmarket,dbregionmarketavg),30*vendorsell))" },
normalPrice = { type = "string", default = "check(first(crafting,dbmarket,dbregionmarketavg),max(2*avg(crafting,dbmarket,dbregionmarketavg),12*vendorsell))" },
priceReset = { type = "string", default = "none" },
aboveMax = { type = "string", default = "maxPrice" },
-- cancel
cancelUndercut = { type = "boolean", default = true },
cancelRepost = { type = "boolean", default = true },
cancelRepostThreshold = { type = "string", default = "1g" },
}
local OPERATION_VALUE_LIMITS = {
postCap = { min = 0, max = 50000 },
keepQuantity = { min = 0, max = 50000 },
maxExpires = { min = 0, max = 50000 },
}
if TSM.IsWowClassic() then
OPERATION_INFO.undercut.default = "1c"
OPERATION_INFO.matchStackSize = { type = "boolean", default = false }
OPERATION_INFO.stackSize = { type = "string", default = "1" }
OPERATION_INFO.stackSizeIsCap = { type = "boolean", default = false }
OPERATION_VALUE_LIMITS.stackSize = { min = 1, max = 200 }
OPERATION_VALUE_LIMITS.postCap.max = 200
end
-- ============================================================================
-- Module Functions
-- ============================================================================
function Auctioning.OnInitialize()
OPERATION_INFO.duration.customSanitizeFunction = private.SanitizeDuration
OPERATION_INFO.undercut.customSanitizeFunction = private.SanitizeUndercut
TSM.Operations.Register("Auctioning", L["Auctioning"], OPERATION_INFO, 20, private.GetOperationInfo, private.OperationSanitize)
end
function Auctioning.GetMinMaxValues(key)
local info = OPERATION_VALUE_LIMITS[key]
return info and info.min or -math.huge, info and info.max or math.huge
end
function Auctioning.GetMinPrice(itemString)
return private.GetOperationValueHelper(itemString, "minPrice")
end
function Auctioning.GetMaxPrice(itemString)
return private.GetOperationValueHelper(itemString, "maxPrice")
end
function Auctioning.GetNormalPrice(itemString)
return private.GetOperationValueHelper(itemString, "normalPrice")
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.OperationSanitize(operation)
if not TSM.IsWowClassic() then
if operation.stackSize then
operation.postCap = tonumber(operation.postCap) * tonumber(operation.stackSize)
end
if (type(operation.undercut) == "number" and operation.undercut or Money.FromString(operation.undercut) or math.huge) < COPPER_PER_SILVER then
operation.undercut = "0c"
end
end
end
function private.SanitizeDuration(value)
-- convert from 12/24/48 durations to 1/2/3 API values
if value == 12 then
return 1
elseif value == 24 then
return 2
elseif value == 48 then
return 3
else
return value
end
end
function private.SanitizeUndercut(value)
if not TSM.IsWowClassic() and (Money.FromString(Money.ToString(value) or value) or math.huge) < COPPER_PER_SILVER then
return "0c"
end
return value
end
function private.GetOperationValueHelper(itemString, key)
local origItemString = itemString
itemString = TSM.Groups.TranslateItemString(itemString)
local operationName, operationSettings = TSM.Operations.GetFirstOperationByItem("Auctioning", itemString)
if not operationName then
return
end
return TSM.Auctioning.Util.GetPrice(key, operationSettings, origItemString)
end
function private.GetOperationInfo(operationSettings)
local parts = TempTable.Acquire()
-- get the post string
if operationSettings.postCap == 0 then
tinsert(parts, L["No posting."])
else
if TSM.IsWowClassic() then
tinsert(parts, format(L["Posting %d stack(s) of %d for %s hours."], operationSettings.postCap, operationSettings.stackSize, strmatch(TSM.CONST.AUCTION_DURATIONS[operationSettings.duration], "%d+")))
else
tinsert(parts, format(L["Posting %d items for %s hours."], operationSettings.postCap, strmatch(TSM.CONST.AUCTION_DURATIONS[operationSettings.duration], "%d+")))
end
end
-- get the cancel string
if operationSettings.cancelUndercut and operationSettings.cancelRepost then
tinsert(parts, format(L["Canceling undercut auctions and to repost higher."]))
elseif operationSettings.cancelUndercut then
tinsert(parts, format(L["Canceling undercut auctions."]))
elseif operationSettings.cancelRepost then
tinsert(parts, format(L["Canceling to repost higher."]))
else
tinsert(parts, L["Not canceling."])
end
local result = table.concat(parts, " ")
TempTable.Release(parts)
return result
end

View File

@ -0,0 +1,466 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Operations = TSM:NewPackage("Operations")
local TempTable = TSM.Include("Util.TempTable")
local Log = TSM.Include("Util.Log")
local Database = TSM.Include("Util.Database")
local private = {
db = nil,
operations = nil,
operationInfo = {},
operationModules = {},
shouldCreateDefaultOperations = false,
ignoreProfileUpdate = false,
}
local COMMON_OPERATION_INFO = {
ignorePlayer = { type = "table", default = {} },
ignoreFactionrealm = { type = "table", default = {} },
relationships = { type = "table", default = {} },
}
local FACTION_REALM = UnitFactionGroup("player").." - "..GetRealmName()
local PLAYER_KEY = UnitName("player").." - "..FACTION_REALM
-- ============================================================================
-- Modules Functions
-- ============================================================================
function Operations.OnInitialize()
private.db = Database.NewSchema("OPERATIONS")
:AddStringField("moduleName")
:AddStringField("operationName")
:AddIndex("moduleName")
:Commit()
if TSM.db.global.coreOptions.globalOperations then
private.operations = TSM.db.global.userData.operations
else
private.operations = TSM.db.profile.userData.operations
end
private.RebuildDB()
private.shouldCreateDefaultOperations = not TSM.db.profile.internalData.createdDefaultOperations
TSM.db.profile.internalData.createdDefaultOperations = true
TSM.db:RegisterCallback("OnProfileUpdated", private.OnProfileUpdated)
end
function Operations.Register(moduleName, localizedName, operationInfo, maxOperations, infoCallback, customSanitizeFunction)
for key, info in pairs(operationInfo) do
assert(type(key) == "string" and type(info) == "table")
assert(info.type == type(info.default))
end
for key, info in pairs(COMMON_OPERATION_INFO) do
assert(not operationInfo[key])
operationInfo[key] = info
end
tinsert(private.operationModules, moduleName)
private.operationInfo[moduleName] = {
info = operationInfo,
localizedName = localizedName,
maxOperations = maxOperations,
infoCallback = infoCallback,
customSanitizeFunction = customSanitizeFunction,
}
local shouldCreateDefaultOperations = private.shouldCreateDefaultOperations or not private.operations[moduleName]
private.operations[moduleName] = private.operations[moduleName] or {}
if shouldCreateDefaultOperations and not private.operations[moduleName]["#Default"] then
-- create default operation
Operations.Create(moduleName, "#Default")
end
private.ValidateOperations(moduleName)
private.RebuildDB()
end
function Operations.IsCommonKey(key)
return COMMON_OPERATION_INFO[key] and true or false
end
function Operations.IsValidName(operationName)
return operationName == strtrim(operationName) and operationName ~= "" and not strmatch(operationName, TSM.CONST.OPERATION_SEP)
end
function Operations.ModuleIterator()
return ipairs(private.operationModules)
end
function Operations.ModuleExists(moduleName)
return private.operationInfo[moduleName] and true or false
end
function Operations.GetLocalizedName(moduleName)
return private.operationInfo[moduleName].localizedName
end
function Operations.GetMaxNumber(moduleName)
return private.operationInfo[moduleName].maxOperations
end
function Operations.GetSettingDefault(moduleName, key)
local info = private.operationInfo[moduleName].info[key]
return info.type == "table" and CopyTable(info.default) or info.default
end
function Operations.OperationIterator(moduleName)
local operations = TempTable.Acquire()
for operationName in pairs(private.operations[moduleName]) do
tinsert(operations, operationName)
end
sort(operations)
return TempTable.Iterator(operations)
end
function Operations.Exists(moduleName, operationName)
return private.operations[moduleName][operationName] and true or false
end
function Operations.GetSettings(moduleName, operationName)
return private.operations[moduleName][operationName]
end
function Operations.Create(moduleName, operationName)
assert(not private.operations[moduleName][operationName])
private.operations[moduleName][operationName] = {}
Operations.Reset(moduleName, operationName)
private.RebuildDB()
end
function Operations.BulkCreateFromImport(operations, replaceExisting)
for moduleName, moduleOperations in pairs(operations) do
for operationName, operationSettings in pairs(moduleOperations) do
assert(replaceExisting or not private.operations[moduleName][operationName])
private.operations[moduleName][operationName] = operationSettings
end
end
private.RebuildDB()
end
function Operations.Rename(moduleName, oldName, newName)
assert(private.operations[moduleName][oldName])
private.operations[moduleName][newName] = private.operations[moduleName][oldName]
private.operations[moduleName][oldName] = nil
-- redirect relationships
for _, operation in pairs(private.operations[moduleName]) do
for key, target in pairs(operation.relationships) do
if target == oldName then
operation.relationships[key] = newName
end
end
end
TSM.Groups.OperationRenamed(moduleName, oldName, newName)
private.RebuildDB()
end
function Operations.Copy(moduleName, operationName, sourceOperationName)
assert(private.operations[moduleName][operationName] and private.operations[moduleName][sourceOperationName])
for key, info in pairs(private.operationInfo[moduleName].info) do
local sourceValue = private.operations[moduleName][sourceOperationName][key]
private.operations[moduleName][operationName][key] = info.type == "table" and CopyTable(sourceValue) or sourceValue
end
private.RemoveDeadRelationships(moduleName)
private.RebuildDB()
end
function Operations.Delete(moduleName, operationName)
assert(private.operations[moduleName][operationName])
private.operations[moduleName][operationName] = nil
private.RemoveDeadRelationships(moduleName)
TSM.Groups.RemoveOperationFromAllGroups(moduleName, operationName)
private.RebuildDB()
end
function Operations.DeleteList(moduleName, operationNames)
for _, operationName in ipairs(operationNames) do
assert(private.operations[moduleName][operationName])
private.operations[moduleName][operationName] = nil
private.RemoveDeadRelationships(moduleName)
TSM.Groups.RemoveOperationFromAllGroups(moduleName, operationName)
end
private.RebuildDB()
end
function Operations.Reset(moduleName, operationName)
for key in pairs(private.operationInfo[moduleName].info) do
private.operations[moduleName][operationName][key] = Operations.GetSettingDefault(moduleName, key)
end
end
function Operations.Update(moduleName, operationName)
for key in pairs(private.operations[moduleName][operationName].relationships) do
local operation = private.operations[moduleName][operationName]
while operation.relationships[key] do
local newOperation = private.operations[moduleName][operation.relationships[key]]
if not newOperation then
break
end
operation = newOperation
end
private.operations[moduleName][operationName][key] = operation[key]
end
end
function Operations.IsCircularRelationship(moduleName, operationName, key)
local visited = TempTable.Acquire()
while operationName do
if visited[operationName] then
TempTable.Release(visited)
return true
end
visited[operationName] = true
operationName = private.operations[moduleName][operationName].relationships[key]
end
TempTable.Release(visited)
return false
end
function Operations.GetFirstOperationByItem(moduleName, itemString)
local groupPath = TSM.Groups.GetPathByItem(itemString)
for _, operationName in TSM.Groups.OperationIterator(groupPath, moduleName) do
Operations.Update(moduleName, operationName)
if not private.IsIgnored(moduleName, operationName) then
return operationName, private.operations[moduleName][operationName]
end
end
end
function Operations.GroupOperationIterator(moduleName, groupPath)
local operations = TempTable.Acquire()
operations.moduleName = moduleName
for _, operationName in TSM.Groups.OperationIterator(groupPath, moduleName) do
Operations.Update(moduleName, operationName)
if not private.IsIgnored(moduleName, operationName) then
tinsert(operations, operationName)
end
end
return private.GroupOperationIteratorHelper, operations, 0
end
function Operations.GroupHasOperation(moduleName, groupPath, targetOperationName)
for _, operationName in TSM.Groups.OperationIterator(groupPath, moduleName) do
if operationName == targetOperationName then
return true
end
end
return false
end
function Operations.GetDescription(moduleName, operationName)
local operationSettings = private.operations[moduleName][operationName]
assert(operationSettings)
Operations.Update(moduleName, operationName)
return private.operationInfo[moduleName].infoCallback(operationSettings)
end
function Operations.SanitizeSettings(moduleName, operationName, operationSettings, silentMissingCommonKeys, noRelationshipCheck)
local didReset = false
local operationInfo = private.operationInfo[moduleName].info
if private.operationInfo[moduleName].customSanitizeFunction then
private.operationInfo[moduleName].customSanitizeFunction(operationSettings)
end
for key, value in pairs(operationSettings) do
if not noRelationshipCheck and Operations.IsCircularRelationship(moduleName, operationName, key) then
Log.Err("Removing circular relationship (%s, %s, %s)", moduleName, operationName, key)
operationSettings.relationships[key] = nil
end
if not operationInfo[key] then
operationSettings[key] = nil
elseif type(value) ~= operationInfo[key].type then
if operationInfo[key].type == "string" and type(value) == "number" then
-- some custom price settings were potentially stored as numbers previously, so just convert them
operationSettings[key] = tostring(value)
else
didReset = true
Log.Err("Resetting operation setting %s,%s,%s (%s)", moduleName, operationName, tostring(key), tostring(value))
operationSettings[key] = operationInfo[key].type == "table" and CopyTable(operationInfo[key].default) or operationInfo[key].default
end
elseif operationInfo[key].customSanitizeFunction then
operationSettings[key] = operationInfo[key].customSanitizeFunction(value)
end
end
for key in pairs(operationInfo) do
if operationSettings[key] == nil then
-- this key was missing
if operationInfo[key].type == "boolean" then
-- we previously stored booleans as nil instead of false
operationSettings[key] = false
else
if not silentMissingCommonKeys or not Operations.IsCommonKey(key) then
didReset = true
Log.Err("Resetting missing operation setting %s,%s,%s", moduleName, operationName, tostring(key))
end
operationSettings[key] = operationInfo[key].type == "table" and CopyTable(operationInfo[key].default) or operationInfo[key].default
end
end
end
return didReset
end
function Operations.HasRelationship(moduleName, operationName, settingKey)
return Operations.GetRelationship(moduleName, operationName, settingKey) and true or false
end
function Operations.GetRelationship(moduleName, operationName, settingKey)
assert(private.operationInfo[moduleName].info[settingKey])
return private.operations[moduleName][operationName].relationships[settingKey]
end
function Operations.SetRelationship(moduleName, operationName, settingKey, targetOperationName)
assert(targetOperationName == nil or private.operations[moduleName][targetOperationName])
assert(private.operationInfo[moduleName].info[settingKey])
private.operations[moduleName][operationName].relationships[settingKey] = targetOperationName
end
function Operations.GetRelationshipColors(operationType, operationName, settingKey, value)
local relationshipSet = Operations.HasRelationship(operationType, operationName, settingKey)
local linkColor = nil
if not value and relationshipSet then
linkColor = "INDICATOR_DISABLED"
elseif not value then
linkColor = "TEXT_DISABLED"
elseif relationshipSet then
linkColor = "INDICATOR"
else
linkColor = "TEXT"
end
local linkTexture = TSM.UI.TexturePacks.GetColoredKey("iconPack.14x14/Link", linkColor)
return relationshipSet, linkTexture, value and not relationshipSet and "TEXT" or "TEXT_DISABLED"
end
function Operations.IsStoredGlobally()
return TSM.db.global.coreOptions.globalOperations
end
function Operations.SetStoredGlobally(storeGlobally)
TSM.db.global.coreOptions.globalOperations = storeGlobally
-- we shouldn't be running the OnProfileUpdated callback while switching profiles
private.ignoreProfileUpdate = true
if storeGlobally then
-- move current profile to global
TSM.db.global.userData.operations = CopyTable(TSM.db.profile.userData.operations)
-- clear out old operations
for _ in TSM.GetTSMProfileIterator() do
TSM.db.profile.userData.operations = nil
end
else
-- move global to all profiles
for _ in TSM.GetTSMProfileIterator() do
TSM.db.profile.userData.operations = CopyTable(TSM.db.global.userData.operations)
end
-- clear out old operations
TSM.db.global.userData.operations = nil
end
private.ignoreProfileUpdate = false
private.OnProfileUpdated()
end
function Operations.ReplaceProfileOperations(newOperations)
for k, v in pairs(newOperations) do
TSM.db.profile.userData.operations[k] = v
end
end
function Operations.CreateQuery()
return private.db:NewQuery()
end
function Operations.GroupIterator(moduleName, filterOperationName, overrideOnly)
local result = TempTable.Acquire()
-- check the base group
if Operations.GroupHasOperation(moduleName, TSM.CONST.ROOT_GROUP_PATH, filterOperationName) then
tinsert(result, TSM.CONST.ROOT_GROUP_PATH)
end
-- need to filter out the groups without operations
for _, groupPath in TSM.Groups.GroupIterator() do
if (not overrideOnly or TSM.Groups.HasOperationOverride(groupPath, moduleName)) and Operations.GroupHasOperation(moduleName, groupPath, filterOperationName) then
tinsert(result, groupPath)
end
end
return TempTable.Iterator(result)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.OnProfileUpdated()
if private.ignoreProfileUpdate then
return
end
if TSM.db.global.coreOptions.globalOperations then
private.operations = TSM.db.global.userData.operations
else
private.operations = TSM.db.profile.userData.operations
end
for _, moduleName in Operations.ModuleIterator() do
private.ValidateOperations(moduleName)
end
private.RebuildDB()
TSM.Groups.RebuildDatabase()
end
function private.ValidateOperations(moduleName)
if not private.operations[moduleName] then
-- this is a new profile
private.operations[moduleName] = {}
Operations.Create(moduleName, "#Default")
return
end
for operationName, operationSettings in pairs(private.operations[moduleName]) do
if type(operationName) ~= "string" or not Operations.IsValidName(operationName) then
Log.Err("Removing %s operation with invalid name: ", moduleName, tostring(operationName))
private.operations[moduleName][operationName] = nil
else
Operations.SanitizeSettings(moduleName, operationName, operationSettings)
for key, target in pairs(operationSettings.relationships) do
if not private.operations[moduleName][target] then
Log.Err("Removing invalid relationship %s,%s,%s -> %s", moduleName, operationName, tostring(key), tostring(target))
operationSettings.relationships[key] = nil
end
end
end
end
end
function private.IsIgnored(moduleName, operationName)
local operationSettings = private.operations[moduleName][operationName]
assert(operationSettings)
return operationSettings.ignorePlayer[PLAYER_KEY] or operationSettings.ignoreFactionrealm[FACTION_REALM]
end
function private.GroupOperationIteratorHelper(operations, index)
index = index + 1
if index > #operations then
TempTable.Release(operations)
return
end
local operationName = operations[index]
return index, operationName, private.operations[operations.moduleName][operationName]
end
function private.RemoveDeadRelationships(moduleName)
for _, operation in pairs(private.operations[moduleName]) do
for key, target in pairs(operation.relationships) do
if not private.operations[moduleName][target] then
operation.relationships[key] = nil
end
end
end
end
function private.RebuildDB()
private.db:TruncateAndBulkInsertStart()
for moduleName, operations in pairs(private.operations) do
for operationName in pairs(operations) do
private.db:BulkInsertNewRow(moduleName, operationName)
end
end
private.db:BulkInsertEnd()
end

View File

@ -0,0 +1,161 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Crafting = TSM.Operations:NewPackage("Crafting")
local L = TSM.Include("Locale").GetTable()
local Log = TSM.Include("Util.Log")
local CustomPrice = TSM.Include("Service.CustomPrice")
local ItemInfo = TSM.Include("Service.ItemInfo")
local private = {}
local OPERATION_INFO = {
minRestock = { type = "string", default = "10" },
maxRestock = { type = "string", default = "20" },
minProfit = { type = "string", default = "100g" },
craftPriceMethod = { type = "string", default = "" },
}
local MIN_RESTOCK_VALUE = 0
local MAX_RESTOCK_VALUE = 2000
local BAD_CRAFTING_PRICE_SOURCES = {
crafting = true,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Crafting.OnInitialize()
TSM.Operations.Register("Crafting", L["Crafting"], OPERATION_INFO, 1, private.GetOperationInfo)
for _, name in TSM.Operations.OperationIterator("Crafting") do
local operation = TSM.Operations.GetSettings("Crafting", name)
if operation.craftPriceMethod ~= "" then
local isValid, err = CustomPrice.Validate(operation.craftPriceMethod, BAD_CRAFTING_PRICE_SOURCES)
if not isValid then
Log.PrintfUser(L["Your craft value method for '%s' was invalid so it has been returned to the default. Details: %s"], name, err)
operation.craftPriceMethod = ""
end
end
end
end
function Crafting.HasOperation(itemString)
return private.GetOperationSettings(itemString) and true or false
end
function Crafting.GetRestockRange()
return MIN_RESTOCK_VALUE, MAX_RESTOCK_VALUE
end
function Crafting.IsValid(itemString)
local origItemString = itemString
itemString = TSM.Groups.TranslateItemString(itemString)
local operationName, operationSettings = TSM.Operations.GetFirstOperationByItem("Crafting", itemString)
if not operationSettings then
return false
end
local minRestock, maxRestock, errMsg = nil, nil, nil
minRestock, errMsg = private.GetMinRestock(operationSettings, origItemString)
if not minRestock then
return false, errMsg
end
maxRestock, errMsg = private.GetMaxRestock(operationSettings, origItemString)
if not maxRestock then
return false, errMsg
end
if minRestock > maxRestock then
-- invalid cause min > max restock quantity
return false, format(L["'%s' is an invalid operation. Min restock of %d is higher than max restock of %d for %s."], operationName, minRestock, maxRestock, ItemInfo.GetLink(origItemString))
end
return true
end
function Crafting.GetMinProfit(itemString)
local operationSettings = private.GetOperationSettings(itemString)
if not operationSettings then
return false
end
if operationSettings.minProfit == "" then
return false
end
return true, CustomPrice.GetValue(operationSettings.minProfit, itemString)
end
function Crafting.GetRestockQuantity(itemString, haveQuantity)
local operationSettings = private.GetOperationSettings(itemString)
if not operationSettings then
return 0
end
local minRestock = private.GetMinRestock(operationSettings, itemString)
local maxRestock = private.GetMaxRestock(operationSettings, itemString)
if not minRestock or not maxRestock or minRestock > maxRestock then
return 0
end
local neededQuantity = maxRestock - haveQuantity
if neededQuantity <= 0 then
-- don't need to queue any
return 0
elseif neededQuantity < minRestock then
-- we're below the min restock quantity
return 0
end
return neededQuantity
end
function Crafting.GetCraftedItemValue(itemString)
local operationSettings = private.GetOperationSettings(itemString)
if not operationSettings then
return false
end
if operationSettings.craftPriceMethod == "" then
return false
end
return true, CustomPrice.GetValue(operationSettings.craftPriceMethod, itemString)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetMinRestock(operationSettings, itemString)
local minRestock, errMsg = CustomPrice.GetValue(operationSettings.minRestock, itemString, true)
if not minRestock then
return nil, format(L["Your min restock (%s) is invalid for %s."], operationSettings.minRestock, ItemInfo.GetLink(itemString) or "?").." "..errMsg
elseif minRestock < MIN_RESTOCK_VALUE or minRestock > MAX_RESTOCK_VALUE then
return nil, format(L["Your min restock (%s) is invalid for %s."], operationSettings.minRestock, ItemInfo.GetLink(itemString) or "?").." "..format(L["Must be between %d and %s."], MIN_RESTOCK_VALUE, MAX_RESTOCK_VALUE)
end
return minRestock
end
function private.GetMaxRestock(operationSettings, itemString)
local maxRestock, errMsg = CustomPrice.GetValue(operationSettings.maxRestock, itemString, true)
if not maxRestock then
return nil, format(L["Your max restock (%s) is invalid for %s."], operationSettings.maxRestock, ItemInfo.GetLink(itemString) or "?").." "..errMsg
elseif maxRestock < MIN_RESTOCK_VALUE or maxRestock > MAX_RESTOCK_VALUE then
return nil, format(L["Your max restock (%s) is invalid for %s."], operationSettings.maxRestock, ItemInfo.GetLink(itemString) or "?").." "..format(L["Must be between %d and %s."], MIN_RESTOCK_VALUE, MAX_RESTOCK_VALUE)
end
return maxRestock
end
function private.GetOperationInfo(operationSettings)
if operationSettings.minProfit ~= "" then
return L["Restocking with a min profit."]
else
return L["Restocking with no min profit."]
end
end
function private.GetOperationSettings(itemString)
itemString = TSM.Groups.TranslateItemString(itemString)
local operationName, operationSettings = TSM.Operations.GetFirstOperationByItem("Crafting", itemString)
if not operationName then
return
end
return operationSettings
end

View File

@ -0,0 +1,46 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Mailing = TSM.Operations:NewPackage("Mailing")
local private = {}
local L = TSM.Include("Locale").GetTable()
local OPERATION_INFO = {
maxQtyEnabled = { type = "boolean", default = false },
maxQty = { type = "number", default = 10 },
target = { type = "string", default = "" },
restock = { type = "boolean", default = false },
restockSources = { type = "table", default = { guild = false, bank = false } },
keepQty = { type = "number", default = 0 },
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Mailing.OnInitialize()
TSM.Operations.Register("Mailing", L["Mailing"], OPERATION_INFO, 50, private.GetOperationInfo)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetOperationInfo(operationSettings)
if operationSettings.target == "" then
return
end
if operationSettings.maxQtyEnabled then
return format(L["Mailing up to %d to %s."], operationSettings.maxQty, operationSettings.target)
else
return format(L["Mailing all to %s."], operationSettings.target)
end
end

View File

@ -0,0 +1,132 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Shopping = TSM.Operations:NewPackage("Shopping")
local private = {}
local L = TSM.Include("Locale").GetTable()
local CustomPrice = TSM.Include("Service.CustomPrice")
local Inventory = TSM.Include("Service.Inventory")
local OPERATION_INFO = {
restockQuantity = { type = "string", default = "0" },
maxPrice = { type = "string", default = "dbmarket" },
showAboveMaxPrice = { type = "boolean", default = true },
restockSources = { type = "table", default = { alts = false, auctions = false, bank = false, guild = false } },
}
local MIN_RESTOCK_VALUE = 0
local MAX_RESTOCK_VALUE = 50000
-- ============================================================================
-- Module Functions
-- ============================================================================
function Shopping.OnInitialize()
TSM.Operations.Register("Shopping", L["Shopping"], OPERATION_INFO, 1, private.GetOperationInfo)
end
function Shopping.GetRestockRange()
return MIN_RESTOCK_VALUE, MAX_RESTOCK_VALUE
end
function Shopping.GetMaxPrice(itemString)
local operationSettings = private.GetOperationSettings(itemString)
if not operationSettings then
return
end
return CustomPrice.GetValue(operationSettings.maxPrice, itemString)
end
function Shopping.ShouldShowAboveMaxPrice(itemString)
local operationSettings = private.GetOperationSettings(itemString)
if not operationSettings then
return
end
return operationSettings.showAboveMaxPrice
end
function Shopping.IsFiltered(itemString, itemBuyout)
local operationSettings = private.GetOperationSettings(itemString)
if not operationSettings then
return true
end
if operationSettings.showAboveMaxPrice then
return false
end
local maxPrice = CustomPrice.GetValue(operationSettings.maxPrice, itemString)
if itemBuyout > (maxPrice or 0) then
return true, true
end
return false
end
function Shopping.ValidAndGetRestockQuantity(itemString)
local operationSettings = private.GetOperationSettings(itemString)
if not operationSettings then
return false, nil
end
local isValid, err = CustomPrice.Validate(operationSettings.maxPrice)
if not isValid then
return false, err
end
local maxQuantity, restockQuantity = nil, nil
restockQuantity, err = CustomPrice.GetValue(operationSettings.restockQuantity, itemString, true)
if not restockQuantity then
return false, err
elseif restockQuantity < MIN_RESTOCK_VALUE or restockQuantity > MAX_RESTOCK_VALUE then
return false, format(L["Your restock quantity is invalid. It must be between %d and %s."], MIN_RESTOCK_VALUE, MAX_RESTOCK_VALUE)
end
if restockQuantity > 0 then
-- include mail and bags
local numHave = Inventory.GetBagQuantity(itemString) + Inventory.GetMailQuantity(itemString)
if operationSettings.restockSources.bank then
numHave = numHave + Inventory.GetBankQuantity(itemString) + Inventory.GetReagentBankQuantity(itemString)
end
if operationSettings.restockSources.guild then
numHave = numHave + Inventory.GetGuildQuantity(itemString)
end
local _, numAlts, numAuctions = Inventory.GetPlayerTotals(itemString)
if operationSettings.restockSources.alts then
numHave = numHave + numAlts
end
if operationSettings.restockSources.auctions then
numHave = numHave + numAuctions
end
if numHave >= restockQuantity then
return false, nil
end
maxQuantity = restockQuantity - numHave
end
if not operationSettings.showAboveMaxPrice and not CustomPrice.GetValue(operationSettings.maxPrice, itemString) then
-- we're not showing auctions above the max price and the max price isn't valid for this item, so skip it
return false, nil
end
return true, maxQuantity
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetOperationInfo(operationSettings)
if operationSettings.showAboveMaxPrice then
return format(L["Shopping for auctions including those above the max price."])
else
return format(L["Shopping for auctions with a max price set."])
end
end
function private.GetOperationSettings(itemString)
itemString = TSM.Groups.TranslateItemString(itemString)
local operationName, operationSettings = TSM.Operations.GetFirstOperationByItem("Shopping", itemString)
if not operationName then
return
end
return operationSettings
end

View File

@ -0,0 +1,57 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Sniper = TSM.Operations:NewPackage("Sniper")
local private = {}
local L = TSM.Include("Locale").GetTable()
local CustomPrice = TSM.Include("Service.CustomPrice")
local OPERATION_INFO = {
belowPrice = { type = "string", default = "max(vendorsell, ifgt(DBRegionMarketAvg, 250000g, 0.8, ifgt(DBRegionMarketAvg, 100000g, 0.7, ifgt(DBRegionMarketAvg, 50000g, 0.6, ifgt(DBRegionMarketAvg, 25000g, 0.5, ifgt(DBRegionMarketAvg, 10000g, 0.4, ifgt(DBRegionMarketAvg, 5000g, 0.3, ifgt(DBRegionMarketAvg, 2000g, 0.2, ifgt(DBRegionMarketAvg, 1000g, 0.1, 0.05)))))))) * DBRegionMarketAvg)" },
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Sniper.OnInitialize()
TSM.Operations.Register("Sniper", L["Sniper"], OPERATION_INFO, 1, private.GetOperationInfo)
end
function Sniper.IsOperationValid(itemString)
local _, operationSettings = TSM.Operations.GetFirstOperationByItem("Sniper", itemString)
if not operationSettings then
return false
end
local isValid = CustomPrice.Validate(operationSettings.belowPrice)
return isValid
end
function Sniper.HasOperation(itemString)
itemString = TSM.Groups.TranslateItemString(itemString)
return TSM.Operations.GetFirstOperationByItem("Sniper", itemString) and true or false
end
function Sniper.GetBelowPrice(itemString)
itemString = TSM.Groups.TranslateItemString(itemString)
local operationName, operationSettings = TSM.Operations.GetFirstOperationByItem("Sniper", itemString)
if not operationName then
return
end
return CustomPrice.GetValue(operationSettings.belowPrice, itemString)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetOperationInfo(operationSettings)
return L["Sniping items below a max price"]
end

View File

@ -0,0 +1,60 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Vendoring = TSM.Operations:NewPackage("Vendoring")
local private = {}
local L = TSM.Include("Locale").GetTable()
local TempTable = TSM.Include("Util.TempTable")
local OPERATION_INFO = {
sellAfterExpired = { type = "number", default = 20 },
sellSoulbound = { type = "boolean", default = false },
keepQty = { type = "number", default = 0 },
restockQty = { type = "number", default = 0 },
restockSources = { type = "table", default = { alts = false, ah = false, bank = false, guild = false, alts_ah = false, mail = false } },
enableBuy = { type = "boolean", default = true },
enableSell = { type = "boolean", default = true },
vsMarketValue = { type = "string", default = "dbmarket" },
vsMaxMarketValue = { type = "string", default = "0c" },
vsDestroyValue = { type = "string", default = "destroy" },
vsMaxDestroyValue = { type = "string", default = "0c" },
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Vendoring.OnInitialize()
TSM.Operations.Register("Vendoring", L["Vendoring"], OPERATION_INFO, 1, private.GetOperationInfo)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetOperationInfo(operationSettings)
local parts = TempTable.Acquire()
if operationSettings.enableBuy and operationSettings.restockQty > 0 then
tinsert(parts, format(L["Restocking to %d."], operationSettings.restockQty))
end
if operationSettings.enableSell then
if operationSettings.keepQty > 0 then
tinsert(parts, format(L["Keeping %d."], operationSettings.keepQty))
end
if operationSettings.sellSoulbound then
tinsert(parts, L["Selling soulbound items."])
end
end
local result = table.concat(parts, " ")
TempTable.Release(parts)
return result
end

View File

@ -0,0 +1,95 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Warehousing = TSM.Operations:NewPackage("Warehousing")
local private = {}
local L = TSM.Include("Locale").GetTable()
local OPERATION_INFO = {
moveQuantity = { type = "number", default = 0 },
keepBagQuantity = { type = "number", default = 0 },
keepBankQuantity = { type = "number", default = 0 },
restockQuantity = { type = "number", default = 0 },
stackSize = { type = "number", default = 0 },
restockKeepBankQuantity = { type = "number", default = 0 },
restockStackSize = { type = "number", default = 0 },
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Warehousing.OnInitialize()
TSM.Operations.Register("Warehousing", L["Warehousing"], OPERATION_INFO, 12, private.GetOperationInfo)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetOperationInfo(operationSettings)
if (operationSettings.keepBagQuantity ~= 0 or operationSettings.keepBankQuantity ~= 0) and operationSettings.moveQuantity == 0 then
if operationSettings.keepBagQuantity ~= 0 then
if operationSettings.keepBankQuantity ~= 0 then
if operationSettings.restockQuantity ~= 0 then
return format(L["Warehousing will move all of the items in this group keeping %d of each item back when bags > bank/gbank, %d of each item back when bank/gbank > bags. Restock will maintain %d items in your bags."], operationSettings.keepBagQuantity, operationSettings.keepBankQuantity, operationSettings.restockQuantity)
else
return format(L["Warehousing will move all of the items in this group keeping %d of each item back when bags > bank/gbank, %d of each item back when bank/gbank > bags."], operationSettings.keepBagQuantity, operationSettings.keepBankQuantity)
end
else
if operationSettings.restockQuantity ~= 0 then
return format(L["Warehousing will move all of the items in this group keeping %d of each item back when bags > bank/gbank. Restock will maintain %d items in your bags."], operationSettings.keepBagQuantity, operationSettings.restockQuantity)
else
return format(L["Warehousing will move all of the items in this group keeping %d of each item back when bags > bank/gbank."], operationSettings.keepBagQuantity)
end
end
else
if operationSettings.restockQuantity ~= 0 then
return format(L["Warehousing will move all of the items in this group keeping %d of each item back when bank/gbank > bags. Restock will maintain %d items in your bags."], operationSettings.keepBankQuantity, operationSettings.restockQuantity)
else
return format(L["Warehousing will move all of the items in this group keeping %d of each item back when bank/gbank > bags."], operationSettings.keepBankQuantity)
end
end
elseif (operationSettings.keepBagQuantity ~= 0 or operationSettings.keepBankQuantity ~= 0) and operationSettings.moveQuantity ~= 0 then
if operationSettings.keepBagQuantity ~= 0 then
if operationSettings.keepBankQuantity ~= 0 then
if operationSettings.restockQuantity ~= 0 then
return format(L["Warehousing will move a max of %d of each item in this group keeping %d of each item back when bags > bank/gbank, %d of each item back when bank/gbank > bags. Restock will maintain %d items in your bags."], operationSettings.moveQuantity, operationSettings.keepBagQuantity, operationSettings.keepBankQuantity, operationSettings.restockQuantity)
else
return format(L["Warehousing will move a max of %d of each item in this group keeping %d of each item back when bags > bank/gbank, %d of each item back when bank/gbank > bags."], operationSettings.moveQuantity, operationSettings.keepBagQuantity, operationSettings.keepBankQuantity)
end
else
if operationSettings.restockQuantity ~= 0 then
return format(L["Warehousing will move a max of %d of each item in this group keeping %d of each item back when bags > bank/gbank. Restock will maintain %d items in your bags."], operationSettings.keepBankQuantity, operationSettings.restockQuantity)
else
return format(L["Warehousing will move a max of %d of each item in this group keeping %d of each item back when bags > bank/gbank."], operationSettings.keepBankQuantity)
end
end
else
if operationSettings.restockQuantity ~= 0 then
return format(L["Warehousing will move a max of %d of each item in this group keeping %d of each item back when bank/gbank > bags. Restock will maintain %d items in your bags."], operationSettings.moveQuantity, operationSettings.keepBankQuantity, operationSettings.restockQuantity)
else
return format(L["Warehousing will move a max of %d of each item in this group keeping %d of each item back when bank/gbank > bags."], operationSettings.moveQuantity, operationSettings.keepBankQuantity)
end
end
elseif operationSettings.moveQuantity ~= 0 then
if operationSettings.restockQuantity ~= 0 then
return format(L["Warehousing will move a max of %d of each item in this group. Restock will maintain %d items in your bags."], operationSettings.moveQuantity, operationSettings.restockQuantity)
else
return format(L["Warehousing will move a max of %d of each item in this group."], operationSettings.moveQuantity)
end
else
if operationSettings.restockQuantity ~= 0 then
return format(L["Warehousing will move all of the items in this group. Restock will maintain %d items in your bags."], operationSettings.restockQuantity)
else
return L["Warehousing will move all of the items in this group."]
end
end
end

View File

@ -0,0 +1,88 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
TSM:NewPackage("Shopping")
local Threading = TSM.Include("Service.Threading")
local ShoppingSearchContext = TSM.Include("LibTSMClass").DefineClass("ShoppingSearchContext")
TSM.Shopping.ShoppingSearchContext = ShoppingSearchContext
-- ============================================================================
-- ShoppingSearchContext - Public Class Methods
-- ============================================================================
function ShoppingSearchContext.__init(self, threadId, marketValueFunc)
assert(threadId and marketValueFunc)
self._threadId = threadId
self._marketValueFunc = marketValueFunc
self._name = nil
self._filterInfo = nil
self._postContext = nil
self._buyCallback = nil
self._stateCallback = nil
self._pctTooltip = nil
end
function ShoppingSearchContext.SetScanContext(self, name, filterInfo, postContext, pctTooltip)
assert(name)
self._name = name
self._filterInfo = filterInfo
self._postContext = postContext
-- clear the callbacks when the scan context changes
self._buyCallback = nil
self._stateCallback = nil
self._pctTooltip = pctTooltip
return self
end
function ShoppingSearchContext.SetCallbacks(self, buyCallback, stateCallback)
self._buyCallback = buyCallback
self._stateCallback = stateCallback
return self
end
function ShoppingSearchContext.StartThread(self, callback, auctionScan)
Threading.SetCallback(self._threadId, callback)
Threading.Start(self._threadId, auctionScan, self._filterInfo, self._postContext)
end
function ShoppingSearchContext.KillThread(self)
Threading.Kill(self._threadId)
end
function ShoppingSearchContext.GetMarketValueFunc(self)
return self._marketValueFunc
end
function ShoppingSearchContext.GetPctTooltip(self)
return self._pctTooltip
end
function ShoppingSearchContext.GetMaxCanBuy(self, itemString)
return nil
end
function ShoppingSearchContext.OnBuy(self, itemString, quantity)
if self._buyCallback then
self._buyCallback(itemString, quantity)
end
end
function ShoppingSearchContext.OnStateChanged(self, state)
if self._stateCallback then
self._stateCallback(state)
end
end
function ShoppingSearchContext.GetName(self)
return self._name
end
function ShoppingSearchContext.GetPostContext(self)
return self._postContext
end

View File

@ -0,0 +1,104 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local DisenchantSearch = TSM.Shopping:NewPackage("DisenchantSearch")
local L = TSM.Include("Locale").GetTable()
local Log = TSM.Include("Util.Log")
local Threading = TSM.Include("Service.Threading")
local ItemInfo = TSM.Include("Service.ItemInfo")
local CustomPrice = TSM.Include("Service.CustomPrice")
local private = {
itemList = {},
scanThreadId = nil,
searchContext = nil,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function DisenchantSearch.OnInitialize()
-- initialize thread
private.scanThreadId = Threading.New("DISENCHANT_SEARCH", private.ScanThread)
private.searchContext = TSM.Shopping.ShoppingSearchContext(private.scanThreadId, private.MarketValueFunction)
end
function DisenchantSearch.GetSearchContext()
return private.searchContext:SetScanContext(L["Disenchant Search"], nil, nil, L["Disenchant Value"])
end
-- ============================================================================
-- Scan Thread
-- ============================================================================
function private.ScanThread(auctionScan)
if (TSM.AuctionDB.GetLastCompleteScanTime() or 0) < time() - 60 * 60 * 12 then
Log.PrintUser(L["No recent AuctionDB scan data found."])
return false
end
-- create the list of items
wipe(private.itemList)
for _, itemString, _, minBuyout in TSM.AuctionDB.LastScanIteratorThreaded() do
if minBuyout and private.ShouldInclude(itemString, minBuyout) then
tinsert(private.itemList, itemString)
end
Threading.Yield()
end
-- run the scan
auctionScan:AddItemListQueriesThreaded(private.itemList)
for _, query in auctionScan:QueryIterator() do
query:AddCustomFilter(private.QueryFilter)
end
if not auctionScan:ScanQueriesThreaded() then
Log.PrintUser(L["TSM failed to scan some auctions. Please rerun the scan."])
end
end
function private.ShouldInclude(itemString, minBuyout)
if not ItemInfo.IsDisenchantable(itemString) then
return false
end
local itemLevel = ItemInfo.GetItemLevel(itemString) or -1
if itemLevel < TSM.db.global.shoppingOptions.minDeSearchLvl or itemLevel > TSM.db.global.shoppingOptions.maxDeSearchLvl then
return false
end
if private.IsItemBuyoutTooHigh(itemString, minBuyout) then
return false
end
return true
end
function private.QueryFilter(_, row)
local itemString = row:GetItemString()
if not itemString then
return false
end
local _, itemBuyout = row:GetBuyouts()
if not itemBuyout then
return false
end
return private.IsItemBuyoutTooHigh(itemString, itemBuyout)
end
function private.IsItemBuyoutTooHigh(itemString, itemBuyout)
local disenchantValue = CustomPrice.GetItemPrice(itemString, "Destroy")
return not disenchantValue or itemBuyout > TSM.db.global.shoppingOptions.maxDeSearchPercent / 100 * disenchantValue
end
function private.MarketValueFunction(row)
return CustomPrice.GetItemPrice(row:GetItemString() or row:GetBaseItemString(), "Destroy")
end

View File

@ -0,0 +1,405 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local FilterSearch = TSM.Shopping:NewPackage("FilterSearch")
local L = TSM.Include("Locale").GetTable()
local DisenchantInfo = TSM.Include("Data.DisenchantInfo")
local TempTable = TSM.Include("Util.TempTable")
local String = TSM.Include("Util.String")
local Log = TSM.Include("Util.Log")
local Math = TSM.Include("Util.Math")
local ItemString = TSM.Include("Util.ItemString")
local Threading = TSM.Include("Service.Threading")
local ItemFilter = TSM.Include("Service.ItemFilter")
local CustomPrice = TSM.Include("Service.CustomPrice")
local Conversions = TSM.Include("Service.Conversions")
local ItemInfo = TSM.Include("Service.ItemInfo")
local FilterSearchContext = TSM.Include("LibTSMClass").DefineClass("FilterSearchContext", TSM.Shopping.ShoppingSearchContext)
local private = {
scanThreadId = nil,
itemFilter = nil,
isSpecial = false,
marketValueSource = nil,
searchContext = nil,
gatheringSearchContext = nil,
targetItem = {},
itemList = {},
generalMaxQuantity = {},
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function FilterSearch.OnInitialize()
-- initialize thread
private.scanThreadId = Threading.New("FILTER_SEARCH", private.ScanThread)
private.itemFilter = ItemFilter.New()
private.searchContext = FilterSearchContext(private.scanThreadId, private.MarketValueFunction)
private.gatheringSearchContext = FilterSearchContext(private.scanThreadId, private.MarketValueFunction)
end
function FilterSearch.GetGreatDealsSearchContext(filterStr)
filterStr = private.ValidateFilterStr(filterStr, "NORMAL")
if not filterStr then
return
end
private.marketValueSource = TSM.db.global.shoppingOptions.pctSource
private.isSpecial = true
return private.searchContext:SetScanContext(L["Great Deals Search"], filterStr, nil, L["Market Value"])
end
function FilterSearch.GetSearchContext(filterStr, itemInfo)
local errMsg = nil
filterStr, errMsg = private.ValidateFilterStr(filterStr, "NORMAL")
if not filterStr then
return nil, errMsg
end
private.marketValueSource = TSM.db.global.shoppingOptions.pctSource
private.isSpecial = false
return private.searchContext:SetScanContext(filterStr, filterStr, itemInfo, L["Market Value"])
end
function FilterSearch.GetGatheringSearchContext(filterStr, mode)
filterStr = private.ValidateFilterStr(filterStr, mode)
if not filterStr then
return
end
private.marketValueSource = "matprice"
private.isSpecial = true
return private.gatheringSearchContext:SetScanContext(L["Gathering Search"], filterStr, nil, L["Material Cost"])
end
-- ============================================================================
-- Scan Thread
-- ============================================================================
function private.ScanThread(auctionScan, filterStr)
wipe(private.generalMaxQuantity)
if not TSM.IsWowClassic() and filterStr == "" then
auctionScan:NewQuery()
:SetStr("")
wipe(private.targetItem)
wipe(private.itemList)
else
local hasFilter, errMsg = false, nil
for filter in String.SplitIterator(filterStr, ";") do
filter = strtrim(filter)
if filter ~= "" then
local filterIsValid, filterErrMsg = private.itemFilter:ParseStr(filter)
if filterIsValid then
hasFilter = true
else
errMsg = errMsg or filterErrMsg
end
end
end
if not hasFilter then
Log.PrintUser(format(L["Invalid search filter (%s)."], filterStr).." "..errMsg)
return false
end
wipe(private.targetItem)
wipe(private.itemList)
local itemFilter = private.itemFilter
for filterPart in String.SplitIterator(filterStr, ";") do
filterPart = strtrim(filterPart)
if filterPart ~= "" and itemFilter:ParseStr(filterPart) then
if itemFilter:GetCrafting() then
wipe(private.itemList)
local targetItem = Conversions.GetTargetItemByName(private.itemFilter:GetStr())
assert(targetItem)
-- populate the list of items
private.targetItem[targetItem] = targetItem
tinsert(private.itemList, targetItem)
local conversionInfo = Conversions.GetSourceItems(targetItem)
for itemString in pairs(conversionInfo) do
if not private.targetItem[itemString] then
private.targetItem[itemString] = targetItem
tinsert(private.itemList, itemString)
end
end
-- generate the queries and add our filter
local queryOffset = auctionScan:GetNumQueries()
auctionScan:AddItemListQueriesThreaded(private.itemList)
local maxQuantity = itemFilter:GetMaxQuantity()
local firstQuery = nil
for _, query in auctionScan:QueryIterator(queryOffset) do
private.targetItem[query] = targetItem
query:AddCustomFilter(private.TargetItemQueryFilter)
if maxQuantity then
if firstQuery then
-- redirect to the first query so the max quantity spans them all
private.generalMaxQuantity[query] = firstQuery
else
private.generalMaxQuantity[query] = maxQuantity
firstQuery = query
end
end
end
auctionScan:AddResultsUpdateCallback(private.ResultsUpdated)
auctionScan:SetScript("OnQueryDone", private.OnQueryDone)
elseif itemFilter:GetDisenchant() then
local queryOffset = auctionScan:GetNumQueries()
local targetItem = Conversions.GetTargetItemByName(itemFilter:GetStr())
assert(targetItem)
-- generate queries for groups of items that d/e into the target item
local disenchantInfo = DisenchantInfo.GetInfo(targetItem)
for _, info in ipairs(disenchantInfo.sourceInfo) do
auctionScan:NewQuery()
:SetLevelRange(disenchantInfo.minLevel, disenchantInfo.maxLevel)
:SetQualityRange(info.quality, info.quality)
:SetClass(info.classId)
:SetItemLevelRange(info.minItemLevel, info.maxItemLevel)
end
-- add a query for the target item itself
wipe(private.itemList)
tinsert(private.itemList, targetItem)
private.targetItem[targetItem] = targetItem
auctionScan:AddItemListQueriesThreaded(private.itemList)
-- add our filter to each query and generate a lookup from query to target item
local maxQuantity = itemFilter:GetMaxQuantity()
local firstQuery = nil
for _, query in auctionScan:QueryIterator(queryOffset) do
private.targetItem[query] = targetItem
query:AddCustomFilter(private.TargetItemQueryFilter)
if maxQuantity then
if firstQuery then
-- redirect to the first query so the max quantity spans them all
private.generalMaxQuantity[query] = firstQuery
else
private.generalMaxQuantity[query] = maxQuantity
firstQuery = query
end
end
end
auctionScan:AddResultsUpdateCallback(private.ResultsUpdated)
auctionScan:SetScript("OnQueryDone", private.OnQueryDone)
else
local query = auctionScan:NewQuery()
query:SetStr(itemFilter:GetStr(), itemFilter:GetExactOnly())
query:SetQualityRange(itemFilter:GetMinQuality(), itemFilter:GetMaxQuality())
query:SetLevelRange(itemFilter:GetMinLevel(), itemFilter:GetMaxLevel())
query:SetItemLevelRange(itemFilter:GetMinItemLevel(), itemFilter:GetMaxItemLevel())
query:SetClass(itemFilter:GetClass(), itemFilter:GetSubClass(), itemFilter:GetInvSlotId())
query:SetUsable(itemFilter:GetUsableOnly())
query:SetUncollected(itemFilter:GetUncollected())
query:SetUpgrades(itemFilter:GetUpgrades())
query:SetPriceRange(itemFilter:GetMinPrice(), itemFilter:GetMaxPrice())
query:SetItems(itemFilter:GetItem())
query:SetCanLearn(itemFilter:GetCanLearn())
query:SetUnlearned(itemFilter:GetUnlearned())
private.generalMaxQuantity[query] = itemFilter:GetMaxQuantity()
end
end
end
if not private.isSpecial then
TSM.Shopping.SavedSearches.RecordFilterSearch(filterStr)
end
end
-- run the scan
if not auctionScan:ScanQueriesThreaded() then
Log.PrintUser(L["TSM failed to scan some auctions. Please rerun the scan."])
end
return true
end
-- ============================================================================
-- FilterSearchContext Class
-- ============================================================================
function FilterSearchContext.GetMaxCanBuy(self, itemString)
local targetItemString = private.targetItem[itemString]
local maxNum = nil
local itemQuery = private.GetMaxQuantityQuery(targetItemString or itemString)
if itemQuery then
maxNum = private.generalMaxQuantity[itemQuery]
if targetItemString then
local rate, chunkSize = private.GetTargetItemRate(targetItemString, itemString)
maxNum = Math.Ceil(maxNum / rate, chunkSize)
end
end
return maxNum
end
function FilterSearchContext.OnBuy(self, itemString, quantity)
local targetItemString = private.targetItem[itemString]
if targetItemString then
quantity = quantity * private.GetTargetItemRate(targetItemString, itemString)
itemString = targetItemString
end
self.__super:OnBuy(itemString, quantity)
local itemQuery = private.GetMaxQuantityQuery(itemString)
if itemQuery then
private.generalMaxQuantity[itemQuery] = private.generalMaxQuantity[itemQuery] - quantity
if private.generalMaxQuantity[itemQuery] <= 0 then
itemQuery:WipeBrowseResults()
for query, maxQuantity in pairs(private.generalMaxQuantity) do
if maxQuantity == itemQuery then
query:WipeBrowseResults()
end
end
end
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.ValidateFilterStr(filterStr, mode)
assert(mode == "NORMAL" or mode == "CRAFTING" or mode == "DISENCHANT")
filterStr = strtrim(filterStr)
if mode == "NORMAL" and not TSM.IsWowClassic() and filterStr == "" then
return filterStr
end
local isValid, errMsg = true, nil
local filters = TempTable.Acquire()
for filter in String.SplitIterator(filterStr, ";") do
filter = strtrim(filter)
if isValid and gsub(filter, "/", "") ~= "" then
local filterIsValid, filterErrMsg = private.itemFilter:ParseStr(filter)
if filterIsValid then
local str = private.itemFilter:GetStr()
if mode == "CRAFTING" and not strfind(strlower(filter), "/crafting") and str then
filter = filter.."/crafting"
elseif mode == "DISENCHANT" and not strfind(strlower(filter), "/disenchant") and str then
filter = filter.."/disenchant"
end
if strfind(strlower(filter), "/crafting") then
local craftingTargetItem = str and Conversions.GetTargetItemByName(str) or nil
if not craftingTargetItem or not Conversions.GetSourceItems(craftingTargetItem) then
isValid = false
errMsg = errMsg or L["The specified item is not supported for crafting searches."]
end
end
if strfind(strlower(filter), "/disenchant") then
local targetItemString = str and Conversions.GetTargetItemByName(str) or nil
if not DisenchantInfo.IsTargetItem(targetItemString) then
isValid = false
errMsg = errMsg or L["The specified item is not supported for disenchant searches."]
end
end
else
isValid = false
errMsg = errMsg or filterErrMsg
end
else
isValid = false
end
if isValid then
tinsert(filters, filter)
end
end
local result = table.concat(filters, ";")
TempTable.Release(filters)
result = isValid and result ~= "" and result or nil
errMsg = errMsg or L["The specified filter was empty."]
return result, errMsg
end
function private.MarketValueFunction(subRow)
local baseItemString = subRow:GetBaseItemString()
local itemString = subRow:GetItemString()
if next(private.targetItem) then
local targetItemString = private.targetItem[itemString]
if not itemString or not targetItemString then
return nil
end
local targetItemRate = private.GetTargetItemRate(targetItemString, itemString)
return Math.Round(targetItemRate * CustomPrice.GetValue(private.marketValueSource, targetItemString))
else
return CustomPrice.GetValue(private.marketValueSource, itemString or baseItemString)
end
end
function private.GetTargetItemRate(targetItemString, itemString)
if itemString == targetItemString then
return 1, 1
end
if DisenchantInfo.IsTargetItem(targetItemString) then
local classId = ItemInfo.GetClassId(itemString)
local ilvl = ItemInfo.GetItemLevel(ItemString.GetBaseFast(itemString))
local quality = ItemInfo.GetQuality(itemString)
local amountOfMats = DisenchantInfo.GetTargetItemSourceInfo(targetItemString, classId, quality, ilvl)
if amountOfMats then
return amountOfMats, 1
end
end
local conversionInfo = Conversions.GetSourceItems(targetItemString)
local conversionChunkSize = 1
for _ in Conversions.TargetItemsByMethodIterator(itemString, Conversions.METHOD.MILL) do
conversionChunkSize = 5
end
for _ in Conversions.TargetItemsByMethodIterator(itemString, Conversions.METHOD.PROSPECT) do
conversionChunkSize = 5
end
return conversionInfo and conversionInfo[itemString] or 0, conversionChunkSize
end
function private.TargetItemQueryFilter(query, row)
local itemString = row:GetItemString()
local targetItemString = private.targetItem[itemString] or private.targetItem[query]
return itemString and targetItemString and private.GetTargetItemRate(targetItemString, itemString) == 0
end
function private.ResultsUpdated(_, query)
local targetItemString = private.targetItem[query]
if not targetItemString then
return
end
-- populate the targetItem table for each item in the results
for _, row in query:BrowseResultsIterator() do
if row:HasItemInfo() then
for _, subRow in row:SubRowIterator() do
local itemString = subRow:GetItemString()
if itemString then
private.targetItem[itemString] = targetItemString
end
end
end
end
end
function private.OnQueryDone(_, query)
private.ResultsUpdated(nil, query)
private.targetItem[query] = nil
end
function private.GetMaxQuantityQuery(itemString)
if not next(private.generalMaxQuantity) then
return
end
-- find the query this item belongs to
local itemQuery = nil
for query, value in pairs(private.generalMaxQuantity) do
local containsItem = false
for _ in query:ItemSubRowIterator(itemString) do
containsItem = true
end
if containsItem then
-- resolve any potential redirection to the base query
itemQuery = type(value) == "number" and query or value
break
end
end
if not itemQuery or not private.generalMaxQuantity[itemQuery] then
return
end
return itemQuery
end

View File

@ -0,0 +1,45 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local GreatDealsSearch = TSM.Shopping:NewPackage("GreatDealsSearch")
local Vararg = TSM.Include("Util.Vararg")
local ItemInfo = TSM.Include("Service.ItemInfo")
local private = {
filter = nil,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function GreatDealsSearch.OnEnable()
local appData = TSMAPI.AppHelper and TSMAPI.AppHelper:FetchData("SHOPPING_SEARCHES")
if not appData then
return
end
for _, info in pairs(appData) do
local realmName, data = unpack(info)
if TSMAPI.AppHelper:IsCurrentRealm(realmName) then
private.filter = assert(loadstring(data))().greatDeals
if private.filter == "" then
break
end
-- populate item info cache
for _, item in Vararg.Iterator(strsplit(";", private.filter)) do
item = strsplit("/", item)
ItemInfo.FetchInfo(item)
end
break
end
end
end
function GreatDealsSearch.GetFilter()
return private.filter
end

View File

@ -0,0 +1,172 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local GroupSearch = TSM.Shopping:NewPackage("GroupSearch")
local L = TSM.Include("Locale").GetTable()
local Log = TSM.Include("Util.Log")
local TempTable = TSM.Include("Util.TempTable")
local ItemString = TSM.Include("Util.ItemString")
local Threading = TSM.Include("Service.Threading")
local ItemInfo = TSM.Include("Service.ItemInfo")
local GroupSearchContext = TSM.Include("LibTSMClass").DefineClass("GroupSearchContext", TSM.Shopping.ShoppingSearchContext)
local private = {
groups = {},
itemList = {},
maxQuantity = {},
scanThreadId = nil,
seenMaxPrice = {},
searchContext = nil,
queries = {},
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function GroupSearch.OnInitialize()
-- initialize thread
private.scanThreadId = Threading.New("GROUP_SEARCH", private.ScanThread)
private.searchContext = GroupSearchContext(private.scanThreadId, private.MarketValueFunction)
end
function GroupSearch.GetSearchContext(groupList)
return private.searchContext:SetScanContext(L["Group Search"], groupList, nil, L["Max Price"])
end
-- ============================================================================
-- Scan Thread
-- ============================================================================
function private.ScanThread(auctionScan, groupList)
wipe(private.seenMaxPrice)
-- create the list of items, and add filters for them
wipe(private.itemList)
wipe(private.maxQuantity)
wipe(private.queries)
for _, groupPath in ipairs(groupList) do
private.groups[groupPath] = true
for _, itemString in TSM.Groups.ItemIterator(groupPath) do
local isValid, maxQuantityOrErr = TSM.Operations.Shopping.ValidAndGetRestockQuantity(itemString)
if isValid then
private.maxQuantity[itemString] = maxQuantityOrErr
tinsert(private.itemList, itemString)
elseif maxQuantityOrErr then
Log.PrintfUser(L["Invalid custom price source for %s. %s"], ItemInfo.GetLink(itemString), maxQuantityOrErr)
end
end
end
if #private.itemList == 0 then
return false
end
auctionScan:AddItemListQueriesThreaded(private.itemList)
for _, query in auctionScan:QueryIterator() do
query:SetIsBrowseDoneFunction(private.QueryIsBrowseDoneFunction)
query:AddCustomFilter(private.QueryFilter)
tinsert(private.queries, query)
end
-- run the scan
if not auctionScan:ScanQueriesThreaded() then
Log.PrintUser(L["TSM failed to scan some auctions. Please rerun the scan."])
end
return true
end
-- ============================================================================
-- GroupSearchContext Class
-- ============================================================================
function GroupSearchContext.GetMaxCanBuy(self, itemString)
return private.maxQuantity[itemString]
end
function GroupSearchContext.OnBuy(self, itemString, quantity)
self.__super:OnBuy(itemString, quantity)
if not private.maxQuantity[itemString] then
return
end
private.maxQuantity[itemString] = private.maxQuantity[itemString] - quantity
if private.maxQuantity[itemString] <= 0 then
private.maxQuantity[itemString] = nil
local toRemove = TempTable.Acquire()
for _, query in ipairs(private.queries) do
for _, row in query:BrowseResultsIterator() do
if row:HasItemInfo() then
for _, subRow in row:SubRowIterator() do
if subRow:GetItemString() == itemString then
tinsert(toRemove, subRow)
end
end
for _, subRow in ipairs(toRemove) do
row:RemoveSubRow(subRow)
end
wipe(toRemove)
end
end
end
TempTable.Release(toRemove)
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.QueryIsBrowseDoneFunction(query)
local isDone = true
for itemString in query:ItemIterator() do
if TSM.Operations.Shopping.ShouldShowAboveMaxPrice(itemString) then
-- need to scan all the auctions
isDone = false
elseif not private.seenMaxPrice[itemString] then
-- we haven't seen any auctions above the max price, so need to keep scanning
isDone = false
end
end
return isDone
end
function private.QueryFilter(query, row)
local baseItemString = row:GetBaseItemString()
local itemString = row:GetItemString()
local _, itemBuyout, minItemBuyout = row:GetBuyouts()
itemBuyout = itemBuyout or minItemBuyout
if not itemBuyout then
return false
elseif itemBuyout == 0 then
return true
end
if itemString then
local isFiltered, aboveMax = TSM.Operations.Shopping.IsFiltered(itemString, itemBuyout)
private.seenMaxPrice[itemString] = private.seenMaxPrice[itemString] or aboveMax
return isFiltered
else
local allFiltered = true
for queryItemString in query:ItemIterator() do
if ItemString.GetBaseFast(queryItemString) == baseItemString and not TSM.Operations.Shopping.IsFiltered(queryItemString, itemBuyout) then
allFiltered = false
end
end
return allFiltered
end
end
function private.MarketValueFunction(row)
local itemString = row:GetItemString()
return itemString and TSM.Operations.Shopping.GetMaxPrice(itemString) or nil
end

View File

@ -0,0 +1,143 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local SavedSearches = TSM.Shopping:NewPackage("SavedSearches")
local Log = TSM.Include("Util.Log")
local Database = TSM.Include("Util.Database")
local TempTable = TSM.Include("Util.TempTable")
local Settings = TSM.Include("Service.Settings")
local private = {
settings = nil,
db = nil,
}
local MAX_RECENT_SEARCHES = 2000
-- ============================================================================
-- Module Functions
-- ============================================================================
function SavedSearches.OnInitialize()
private.settings = Settings.NewView()
:AddKey("global", "userData", "savedShoppingSearches")
-- remove duplicates
local seen = TempTable.Acquire()
for i = #private.settings.savedShoppingSearches.filters, 1, -1 do
local filter = private.settings.savedShoppingSearches.filters[i]
local filterLower = strlower(private.settings.savedShoppingSearches.filters[i])
if seen[filterLower] then
tremove(private.settings.savedShoppingSearches.filters, i)
private.settings.savedShoppingSearches.name[filter] = nil
private.settings.savedShoppingSearches.isFavorite[filter] = nil
else
seen[filterLower] = true
end
end
TempTable.Release(seen)
-- remove old recent searches
local remainingRecentSearches = MAX_RECENT_SEARCHES
local numRemoved = 0
for i = #private.settings.savedShoppingSearches.filters, 1, -1 do
local filter = private.settings.savedShoppingSearches.filters
if not private.settings.savedShoppingSearches.isFavorite[filter] then
if remainingRecentSearches > 0 then
remainingRecentSearches = remainingRecentSearches - 1
else
tremove(private.settings.savedShoppingSearches.filters, i)
private.settings.savedShoppingSearches.name[filter] = nil
numRemoved = numRemoved + 1
end
end
end
if numRemoved > 0 then
Log.Info("Removed %d old recent searches", numRemoved)
end
private.db = Database.NewSchema("SHOPPING_SAVED_SEARCHES")
:AddUniqueNumberField("index")
:AddStringField("name")
:AddBooleanField("isFavorite")
:AddStringField("filter")
:AddIndex("index")
:AddIndex("name")
:Commit()
private.RebuildDB()
end
function SavedSearches.CreateRecentSearchesQuery()
return private.db:NewQuery()
:OrderBy("index", false)
end
function SavedSearches.CreateFavoriteSearchesQuery()
return private.db:NewQuery()
:Equal("isFavorite", true)
:OrderBy("name", true)
end
function SavedSearches.SetSearchIsFavorite(dbRow, isFavorite)
local filter = dbRow:GetField("filter")
private.settings.savedShoppingSearches.isFavorite[filter] = isFavorite or nil
dbRow:SetField("isFavorite", isFavorite)
:Update()
end
function SavedSearches.RenameSearch(dbRow, newName)
local filter = dbRow:GetField("filter")
private.settings.savedShoppingSearches.name[filter] = newName ~= filter and newName or nil
dbRow:SetField("name", newName)
:Update()
end
function SavedSearches.DeleteSearch(dbRow)
local index, filter = dbRow:GetFields("index", "filter")
tremove(private.settings.savedShoppingSearches.filters, index)
private.settings.savedShoppingSearches.name[filter] = nil
private.settings.savedShoppingSearches.isFavorite[filter] = nil
private.RebuildDB()
end
function SavedSearches.RecordFilterSearch(filter)
for i, existingFilter in ipairs(private.settings.savedShoppingSearches.filters) do
if strlower(existingFilter) == strlower(filter) then
-- move this to the end of the list and rebuild the DB
-- insert the existing filter so we don't need to update the isFavorite and name tables
tremove(private.settings.savedShoppingSearches.filters, i)
tinsert(private.settings.savedShoppingSearches.filters, existingFilter)
private.RebuildDB()
return
end
end
-- didn't find an existing entry, so add a new one
tinsert(private.settings.savedShoppingSearches.filters, filter)
private.db:NewRow()
:SetField("index", #private.settings.savedShoppingSearches.filters)
:SetField("name", filter)
:SetField("isFavorite", false)
:SetField("filter", filter)
:Create()
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.RebuildDB()
private.db:TruncateAndBulkInsertStart()
for index, filter in ipairs(private.settings.savedShoppingSearches.filters) do
local name = private.settings.savedShoppingSearches.name[filter] or filter
local isFavorite = private.settings.savedShoppingSearches.isFavorite[filter] and true or false
private.db:BulkInsertNewRow(index, name, isFavorite, filter)
end
private.db:BulkInsertEnd()
end

View File

@ -0,0 +1,77 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local SearchCommon = TSM.Shopping:NewPackage("SearchCommon")
local Delay = TSM.Include("Util.Delay")
local Threading = TSM.Include("Service.Threading")
local private = {
findThreadId = nil,
callback = nil,
isRunning = false,
pendingStartArgs = {},
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function SearchCommon.OnInitialize()
-- initialize threads
private.findThreadId = Threading.New("FIND_SEARCH", private.FindThread)
Threading.SetCallback(private.findThreadId, private.ThreadCallback)
end
function SearchCommon.StartFindAuction(auctionScan, auction, callback, noSeller)
wipe(private.pendingStartArgs)
private.pendingStartArgs.auctionScan = auctionScan
private.pendingStartArgs.auction = auction
private.pendingStartArgs.callback = callback
private.pendingStartArgs.noSeller = noSeller
Delay.AfterTime("SEARCH_COMMON_THREAD_START", 0, private.StartThread)
end
function SearchCommon.StopFindAuction(noKill)
wipe(private.pendingStartArgs)
private.callback = nil
if not noKill then
Threading.Kill(private.findThreadId)
end
end
-- ============================================================================
-- Find Thread
-- ============================================================================
function private.FindThread(auctionScan, row, noSeller)
return auctionScan:FindAuctionThreaded(row, noSeller)
end
function private.StartThread()
if not private.pendingStartArgs.auctionScan then
return
end
if private.isRunning then
Delay.AfterTime("SEARCH_COMMON_THREAD_START", 0.1, private.StartThread)
return
end
private.isRunning = true
private.callback = private.pendingStartArgs.callback
Threading.Start(private.findThreadId, private.pendingStartArgs.auctionScan, private.pendingStartArgs.auction, private.pendingStartArgs.noSeller)
wipe(private.pendingStartArgs)
end
function private.ThreadCallback(...)
private.isRunning = false
if private.callback then
private.callback(...)
end
end

View File

@ -0,0 +1,83 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local VendorSearch = TSM.Shopping:NewPackage("VendorSearch")
local L = TSM.Include("Locale").GetTable()
local Log = TSM.Include("Util.Log")
local Threading = TSM.Include("Service.Threading")
local ItemInfo = TSM.Include("Service.ItemInfo")
local private = {
itemList = {},
scanThreadId = nil,
searchContext = nil,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function VendorSearch.OnInitialize()
-- initialize thread
private.scanThreadId = Threading.New("VENDOR_SEARCH", private.ScanThread)
private.searchContext = TSM.Shopping.ShoppingSearchContext(private.scanThreadId, private.MarketValueFunction)
end
function VendorSearch.GetSearchContext()
return private.searchContext:SetScanContext(L["Vendor Search"], nil, nil, L["Vendor Sell Price"])
end
-- ============================================================================
-- Scan Thread
-- ============================================================================
function private.ScanThread(auctionScan)
if (TSM.AuctionDB.GetLastCompleteScanTime() or 0) < time() - 60 * 60 * 12 then
Log.PrintUser(L["No recent AuctionDB scan data found."])
return false
end
-- create the list of items
wipe(private.itemList)
for _, itemString, _, minBuyout in TSM.AuctionDB.LastScanIteratorThreaded() do
local vendorSell = ItemInfo.GetVendorSell(itemString) or 0
if vendorSell and minBuyout and minBuyout < vendorSell then
tinsert(private.itemList, itemString)
end
Threading.Yield()
end
-- run the scan
auctionScan:AddItemListQueriesThreaded(private.itemList)
for _, query in auctionScan:QueryIterator() do
query:AddCustomFilter(private.QueryFilter)
end
if not auctionScan:ScanQueriesThreaded() then
Log.PrintUser(L["TSM failed to scan some auctions. Please rerun the scan."])
end
return true
end
function private.QueryFilter(_, row)
local itemString = row:GetItemString()
if not itemString then
return false
end
local _, itemBuyout = row:GetBuyouts()
if not itemBuyout then
return false
end
local vendorSell = ItemInfo.GetVendorSell(itemString)
return not vendorSell or itemBuyout == 0 or itemBuyout >= vendorSell
end
function private.MarketValueFunction(row)
return ItemInfo.GetVendorSell(row:GetItemString() or row:GetBaseItemString())
end

View File

@ -0,0 +1,71 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local BidSearch = TSM.Sniper:NewPackage("BidSearch")
local Threading = TSM.Include("Service.Threading")
local private = {
scanThreadId = nil,
searchContext = nil,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function BidSearch.OnInitialize()
private.scanThreadId = Threading.New("SNIPER_BID_SEARCH", private.ScanThread)
private.searchContext = TSM.Sniper.SniperSearchContext(private.scanThreadId, private.MarketValueFunction, "BID")
end
function BidSearch.GetSearchContext()
assert(TSM.IsWowClassic())
return private.searchContext
end
-- ============================================================================
-- Scan Thread
-- ============================================================================
function private.ScanThread(auctionScan)
assert(TSM.IsWowClassic())
local numQueries = auctionScan:GetNumQueries()
if numQueries == 0 then
auctionScan:NewQuery()
:AddCustomFilter(private.QueryFilter)
:SetPage("FIRST")
else
assert(numQueries == 1)
end
-- don't care if the scan fails for sniper since it's rerun constantly
auctionScan:ScanQueriesThreaded()
return true
end
function private.QueryFilter(_, subRow)
local itemString = subRow:GetItemString()
if not itemString or not subRow:IsSubRow() or not subRow:HasRawData() then
-- can only filter complete subRows
return false
end
local maxPrice = TSM.Operations.Sniper.GetBelowPrice(itemString) or nil
if not maxPrice then
-- no Shopping operation applies to this item, so filter it out
return true
end
local _, itemDisplayedBid = subRow:GetDisplayedBids()
return itemDisplayedBid > maxPrice
end
function private.MarketValueFunction(row)
local itemString = row:GetItemString()
return itemString and TSM.Operations.Sniper.GetBelowPrice(itemString) or nil
end

View File

@ -0,0 +1,111 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local BuyoutSearch = TSM.Sniper:NewPackage("BuyoutSearch")
local L = TSM.Include("Locale").GetTable()
local Log = TSM.Include("Util.Log")
local Threading = TSM.Include("Service.Threading")
local ItemInfo = TSM.Include("Service.ItemInfo")
local private = {
scanThreadId = nil,
searchContext = nil,
itemList = {},
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function BuyoutSearch.OnInitialize()
private.scanThreadId = Threading.New("SNIPER_BUYOUT_SEARCH", private.ScanThread)
private.searchContext = TSM.Sniper.SniperSearchContext(private.scanThreadId, private.MarketValueFunction, "BUYOUT")
end
function BuyoutSearch.GetSearchContext()
return private.searchContext
end
-- ============================================================================
-- Scan Thread
-- ============================================================================
function private.ScanThread(auctionScan)
local numQueries = auctionScan:GetNumQueries()
if numQueries == 0 then
if TSM.IsWowClassic() then
auctionScan:NewQuery()
:AddCustomFilter(private.QueryFilter)
:SetPage("LAST")
else
wipe(private.itemList)
if not TSM.Sniper.PopulateItemList(private.itemList) then
-- scan the entire AH
auctionScan:NewQuery()
:AddCustomFilter(private.QueryFilter)
elseif #private.itemList == 0 then
Log.PrintUser(L["Failed to start sniper. No groups have a Sniper operation applied."])
return false
else
-- scan for the list of items
auctionScan:AddItemListQueriesThreaded(private.itemList)
for _, query in auctionScan:QueryIterator() do
query:AddCustomFilter(private.QueryFilter)
end
end
end
end
-- don't care if the scan fails for sniper since it's rerun constantly
auctionScan:ScanQueriesThreaded()
return true
end
function private.QueryFilter(_, subRow)
local baseItemString = subRow:GetBaseItemString()
local itemString = subRow:GetItemString()
local maxPrice = itemString and TSM.Operations.Sniper.GetBelowPrice(itemString) or nil
if itemString and not maxPrice then
-- no Shopping operation applies to this item, so filter it out
return true
end
local auctionBuyout, itemBuyout, minItemBuyout = subRow:GetBuyouts()
itemBuyout = itemBuyout or minItemBuyout
if not itemBuyout then
-- don't have buyout info yet, so don't filter
return false
elseif auctionBuyout == 0 then
-- no buyout, so filter it out
return true
elseif itemString then
-- filter if the buyout is too high
return itemBuyout > maxPrice
elseif not ItemInfo.CanHaveVariations(baseItemString) then
-- check the buyout against the base item
return itemBuyout > (TSM.Operations.Sniper.GetBelowPrice(baseItemString) or 0)
end
-- check if any variant of this item is in a group and could potentially be worth scnaning
local hasPotentialItem = false
for _, groupItemString in TSM.Groups.ItemByBaseItemStringIterator(baseItemString) do
hasPotentialItem = hasPotentialItem or itemBuyout < (TSM.Operations.Sniper.GetBelowPrice(groupItemString) or 0)
end
if hasPotentialItem then
return false
elseif not TSM.Operations.Sniper.HasOperation(baseItemString) then
-- no potential other variants we care about
return true
end
return false
end
function private.MarketValueFunction(row)
local itemString = row:GetItemString()
return itemString and TSM.Operations.Sniper.GetBelowPrice(itemString) or nil
end

View File

@ -0,0 +1,77 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Sniper = TSM:NewPackage("Sniper")
local Threading = TSM.Include("Service.Threading")
local SniperSearchContext = TSM.Include("LibTSMClass").DefineClass("SniperSearchContext")
TSM.Sniper.SniperSearchContext = SniperSearchContext
-- ============================================================================
-- Module Methods
-- ============================================================================
function Sniper.PopulateItemList(itemList)
local baseHasOperation = false
for _ in TSM.Operations.GroupOperationIterator("Sniper", TSM.CONST.ROOT_GROUP_PATH) do
baseHasOperation = true
end
if baseHasOperation and TSM.IsWowClassic() then
return false
end
-- add all the items from groups with Sniper operations
for _, groupPath in TSM.Groups.GroupIterator() do
local hasOperations = false
for _ in TSM.Operations.GroupOperationIterator("Sniper", groupPath) do
hasOperations = true
end
if hasOperations then
for _, itemString in TSM.Groups.ItemIterator(groupPath) do
if TSM.Operations.Sniper.IsOperationValid(itemString) then
tinsert(itemList, itemString)
end
end
end
end
return true
end
-- ============================================================================
-- SniperSearchContext - Public Class Methods
-- ============================================================================
function SniperSearchContext.__init(self, threadId, marketValueFunc, scanType)
assert(threadId and marketValueFunc and (scanType == "BUYOUT" or scanType == "BID"))
self._threadId = threadId
self._marketValueFunc = marketValueFunc
self._scanType = scanType
end
function SniperSearchContext.StartThread(self, callback, auctionScan)
Threading.SetCallback(self._threadId, callback)
Threading.Start(self._threadId, auctionScan)
end
function SniperSearchContext.KillThread(self)
Threading.Kill(self._threadId)
end
function SniperSearchContext.GetMarketValueFunc(self)
return self._marketValueFunc
end
function SniperSearchContext.IsBuyoutScan(self)
return self._scanType == "BUYOUT"
end
function SniperSearchContext.IsBidScan(self)
return self._scanType == "BID"
end

View File

@ -0,0 +1,116 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Cooldowns = TSM.TaskList:NewPackage("Cooldowns")
local L = TSM.Include("Locale").GetTable()
local Delay = TSM.Include("Util.Delay")
local ObjectPool = TSM.Include("Util.ObjectPool")
local Table = TSM.Include("Util.Table")
local String = TSM.Include("Util.String")
local private = {
query = nil,
taskPool = ObjectPool.New("COOLDOWN_TASK", TSM.TaskList.CooldownCraftingTask, 0),
activeTasks = {},
activeTaskByProfession = {},
ignoredQuery = nil, -- luacheck: ignore 1004 - just stored for GC reasons
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Cooldowns.OnEnable()
TSM.TaskList.RegisterTaskPool(private.ActiveTaskIterator)
private.query = TSM.Crafting.CreateCooldownSpellsQuery()
:Select("profession", "spellId")
:Custom(private.QueryPlayerFilter, UnitName("player"))
:SetUpdateCallback(private.PopulateTasks)
private.ignoredQuery = TSM.Crafting.CreateIgnoredCooldownQuery()
:SetUpdateCallback(private.PopulateTasks)
private.PopulateTasks()
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.ActiveTaskIterator()
return ipairs(private.activeTasks)
end
function private.QueryPlayerFilter(row, player)
return String.SeparatedContains(row:GetField("players"), ",", player)
end
function private.PopulateTasks()
-- clean DB entries with expired times
for spellId, expireTime in pairs(TSM.db.char.internalData.craftingCooldowns) do
if expireTime <= time() then
TSM.db.char.internalData.craftingCooldowns[spellId] = nil
end
end
-- clear out the existing tasks
for _, task in pairs(private.activeTaskByProfession) do
task:WipeSpellIds()
end
local minPendingCooldown = math.huge
for _, profession, spellId in private.query:Iterator() do
if TSM.Crafting.IsCooldownIgnored(spellId) then
-- this is ignored
elseif TSM.db.char.internalData.craftingCooldowns[spellId] then
-- this is on CD
minPendingCooldown = min(minPendingCooldown, TSM.db.char.internalData.craftingCooldowns[spellId] - time())
else
-- this is a new CD task
local task = private.activeTaskByProfession[profession]
if not task then
task = private.taskPool:Get()
task:Acquire(private.RemoveTask, L["Cooldowns"], profession)
private.activeTaskByProfession[profession] = task
end
if not task:HasSpellId(spellId) then
task:AddSpellId(spellId, 1)
end
end
end
-- update our tasks
wipe(private.activeTasks)
for profession, task in pairs(private.activeTaskByProfession) do
if task:HasSpellIds() then
tinsert(private.activeTasks, task)
task:Update()
else
private.activeTaskByProfession[profession] = nil
task:Release()
private.taskPool:Recycle(task)
end
end
TSM.TaskList.OnTaskUpdated()
if minPendingCooldown ~= math.huge then
Delay.AfterTime("COOLDOWN_UPDATE", minPendingCooldown, private.PopulateTasks)
else
Delay.Cancel("COOLDOWN_UPDATE")
end
end
function private.RemoveTask(task)
local profession = task:GetProfession()
assert(Table.RemoveByValue(private.activeTasks, task) == 1)
assert(private.activeTaskByProfession[profession] == task)
private.activeTaskByProfession[profession] = nil
task:Release()
private.taskPool:Recycle(task)
TSM.TaskList.OnTaskUpdated()
end

View File

@ -0,0 +1,54 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local TaskList = TSM:NewPackage("TaskList")
local TempTable = TSM.Include("Util.TempTable")
local Task = TSM.Include("LibTSMClass").DefineClass("TASK", nil, "ABSTRACT")
TaskList.Task = Task
local private = {
updateCallback = nil,
iterFuncs = {},
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function TaskList.RegisterTaskPool(iterFunc)
tinsert(private.iterFuncs, iterFunc)
end
function TaskList.SetUpdateCallback(func)
assert(func and not private.updateCallback)
private.updateCallback = func
end
function TaskList.GetNumTasks()
local num = 0
for _, iterFunc in ipairs(private.iterFuncs) do
for _ in iterFunc() do
num = num + 1
end
end
return num
end
function TaskList.Iterator()
local tasks = TempTable.Acquire()
for _, iterFunc in ipairs(private.iterFuncs) do
for _, task in iterFunc() do
tinsert(tasks, task)
end
end
return TempTable.Iterator(tasks)
end
function TaskList.OnTaskUpdated()
private.updateCallback()
end

View File

@ -0,0 +1,154 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Expirations = TSM.TaskList:NewPackage("Expirations")
local L = TSM.Include("Locale").GetTable()
local Delay = TSM.Include("Util.Delay")
local Table = TSM.Include("Util.Table")
local ObjectPool = TSM.Include("Util.ObjectPool")
local AuctionTracking = TSM.Include("Service.AuctionTracking")
local MailTracking = TSM.Include("Service.MailTracking")
local private = {
mailTaskPool = ObjectPool.New("EXPIRING_MAIL_TASK", TSM.TaskList.ExpiringMailTask, 0),
auctionTaskPool = ObjectPool.New("EXPIRED_AUCTION_TASK", TSM.TaskList.ExpiredAuctionTask, 0),
activeTasks = {},
expiringMailTasks = {},
expiredAuctionTasks = {},
}
local PLAYER_NAME = UnitName("player")
local DAYS_LEFT_LIMIT = 1
-- ============================================================================
-- Module Functions
-- ============================================================================
function Expirations.OnEnable()
AuctionTracking.RegisterExpiresCallback(Expirations.Update)
MailTracking.RegisterExpiresCallback(private.UpdateDelayed)
TSM.TaskList.RegisterTaskPool(private.ActiveTaskIterator)
private.PopulateTasks()
end
function Expirations.Update()
private.PopulateTasks()
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.UpdateDelayed()
Delay.AfterTime("EXPIRATION_UPDATE_DELAYED", 0.5, private.PopulateTasks)
end
function private.ActiveTaskIterator()
return ipairs(private.activeTasks)
end
function private.PopulateTasks()
local minPendingCooldown = math.huge
wipe(private.activeTasks)
for _, task in pairs(private.expiringMailTasks) do
task:WipeCharacters()
end
for _, task in pairs(private.expiredAuctionTasks) do
task:WipeCharacters()
end
-- expiring mails
for k, v in pairs(TSM.db.factionrealm.internalData.expiringMail) do
local task = private.expiringMailTasks["ExpiringMails"]
if not task then
task = private.mailTaskPool:Get()
task:Acquire(private.RemoveMailTask, L["Expirations"])
private.expiringMailTasks["ExpiringMails"] = task
end
local expiration = (v - time()) / 24 / 60 / 60
if expiration <= DAYS_LEFT_LIMIT * -1 then
TSM.db.factionrealm.internalData.expiringMail[PLAYER_NAME] = nil
else
if not task:HasCharacter(k) and expiration <= DAYS_LEFT_LIMIT then
task:AddCharacter(k, expiration)
end
if expiration > 0 and expiration <= DAYS_LEFT_LIMIT then
minPendingCooldown = min(minPendingCooldown, expiration * 24 * 60 * 60)
else
minPendingCooldown = min(minPendingCooldown, (expiration + DAYS_LEFT_LIMIT) * 24 * 60 * 60)
end
end
end
for character, task in pairs(private.expiringMailTasks) do
if task:HasCharacters() then
tinsert(private.activeTasks, task)
task:Update()
else
private.expiringMailTasks[character] = nil
task:Release()
private.mailTaskPool:Recycle(task)
end
end
-- expired auctions
for k, v in pairs(TSM.db.factionrealm.internalData.expiringAuction) do
local task = private.expiredAuctionTasks["ExpiredAuctions"]
if not task then
task = private.auctionTaskPool:Get()
task:Acquire(private.RemoveAuctionTask, L["Expirations"])
private.expiredAuctionTasks["ExpiredAuctions"] = task
end
local expiration = (v - time()) / 24 / 60 / 60
if expiration > 0 and expiration <= DAYS_LEFT_LIMIT then
minPendingCooldown = min(minPendingCooldown, expiration * 24 * 60 * 60)
else
if not task:HasCharacter(k) then
task:AddCharacter(k, expiration)
end
end
end
for character, task in pairs(private.expiredAuctionTasks) do
if task:HasCharacters() then
tinsert(private.activeTasks, task)
task:Update()
else
private.expiredAuctionTasks[character] = nil
task:Release()
private.auctionTaskPool:Recycle(task)
end
end
TSM.TaskList.OnTaskUpdated()
if minPendingCooldown ~= math.huge and minPendingCooldown < DAYS_LEFT_LIMIT then
Delay.AfterTime("EXPIRATION_UPDATE", minPendingCooldown, private.PopulateTasks)
else
Delay.Cancel("EXPIRATION_UPDATE")
end
end
function private.RemoveMailTask(task)
assert(Table.RemoveByValue(private.activeTasks, task) == 1)
task:Release()
private.mailTaskPool:Recycle(task)
TSM.TaskList.OnTaskUpdated()
end
function private.RemoveAuctionTask(task)
assert(Table.RemoveByValue(private.activeTasks, task) == 1)
task:Release()
private.auctionTaskPool:Recycle(task)
TSM.TaskList.OnTaskUpdated()
end

View File

@ -0,0 +1,161 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Gathering = TSM.TaskList:NewPackage("Gathering")
local L = TSM.Include("Locale").GetTable()
local TempTable = TSM.Include("Util.TempTable")
local ObjectPool = TSM.Include("Util.ObjectPool")
local private = {
activeTasks = {},
query = nil,
sourceTasks = {},
altTaskPool = ObjectPool.New("GATHERING_ALT_TASK", TSM.TaskList.AltTask, 0),
professionTasks = {},
prevHash = nil,
}
local ITEM_SOURCES = {
"auction",
"auctionDE",
"auctionCrafting",
"vendor",
"bank",
"guildBank",
"sendMail",
"openMail",
}
local SOURCE_CLASS_CONSTRUCTORS = {
auction = function() return TSM.TaskList.ShoppingTask("NORMAL") end,
auctionDE = function() return TSM.TaskList.ShoppingTask("DISENCHANT") end,
auctionCrafting = function() return TSM.TaskList.ShoppingTask("CRAFTING") end,
vendor = TSM.TaskList.VendoringTask,
bank = function() return TSM.TaskList.BankingTask(false) end,
guildBank = function() return TSM.TaskList.BankingTask(true) end,
sendMail = TSM.TaskList.SendMailTask,
openMail = TSM.TaskList.OpenMailTask,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Gathering.OnInitialize()
for _, source in ipairs(ITEM_SOURCES) do
private.sourceTasks[source] = SOURCE_CLASS_CONSTRUCTORS[source]()
private.sourceTasks[source]:Acquire(private.SourceProfessionTaskDone, L["Gathering"])
end
end
function Gathering.OnEnable()
TSM.TaskList.RegisterTaskPool(private.ActiveTaskIterator)
private.query = TSM.Crafting.Gathering.CreateQuery()
:Select("itemString", "sourcesStr")
:GreaterThan("numNeed", 0)
:SetUpdateCallback(private.PopulateTasks)
private.PopulateTasks()
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.PopulateTasks()
local hash = private.query:Hash()
if hash == private.prevHash then
-- nothing changed
return
end
private.prevHash = hash
for task in pairs(private.activeTasks) do
if task:__isa(TSM.TaskList.AltTask) then
private.RemoveAltTask(task)
end
end
wipe(private.activeTasks)
for _, task in pairs(private.sourceTasks) do
task:WipeItems()
end
for _, task in pairs(private.professionTasks) do
task:WipeSpellIds()
end
local alts = TempTable.Acquire()
local sourceInfo = TempTable.Acquire()
for _, itemString, sourcesStr in private.query:Iterator() do
TSM.Crafting.Gathering.SourcesStrToTable(sourcesStr, sourceInfo, alts)
sourceInfo.alt = nil
sourceInfo.altGuildBank = nil
for _, source in ipairs(ITEM_SOURCES) do
if sourceInfo[source] then
private.sourceTasks[source]:AddItem(itemString, sourceInfo[source])
sourceInfo[source] = nil
end
end
if sourceInfo.craftProfit or sourceInfo.craftNoProfit then
local spellId = TSM.Crafting.GetMostProfitableSpellIdByItem(itemString, TSM.db.factionrealm.gatheringContext.crafter)
assert(spellId)
local profession = TSM.Crafting.GetProfession(spellId)
if not private.professionTasks[profession] then
private.professionTasks[profession] = TSM.TaskList.CraftingTask()
private.professionTasks[profession]:Acquire(private.SourceProfessionTaskDone, L["Gathering"], profession)
end
private.professionTasks[profession]:AddSpellId(spellId, sourceInfo.craftProfit or sourceInfo.craftNoProfit)
sourceInfo.craftProfit = nil
sourceInfo.craftNoProfit = nil
end
-- make sure we processed everything from the sourceInfo table
assert(not next(sourceInfo))
end
TempTable.Release(sourceInfo)
for character in pairs(alts) do
local task = private.altTaskPool:Get()
task:Acquire(private.RemoveAltTask, L["Gathering"], character)
private.activeTasks[task] = task
task:Update()
end
TempTable.Release(alts)
if TSM.db.factionrealm.gatheringContext.crafter ~= "" then
private.sourceTasks.sendMail:SetTarget(TSM.db.factionrealm.gatheringContext.crafter)
end
for _, task in pairs(private.sourceTasks) do
if task:HasItems() then
private.activeTasks[task] = task
task:Update()
end
end
for _, task in pairs(private.professionTasks) do
if task:HasSpellIds() then
private.activeTasks[task] = task
task:Update()
end
end
TSM.TaskList.OnTaskUpdated()
end
function private.ActiveTaskIterator()
return pairs(private.activeTasks)
end
function private.RemoveAltTask(task)
assert(private.activeTasks[task])
private.activeTasks[task] = nil
task:Release()
private.altTaskPool:Recycle(task)
end
function private.SourceProfessionTaskDone(task)
assert(private.activeTasks[task])
private.activeTasks[task] = nil
TSM.TaskList.OnTaskUpdated()
end

View File

@ -0,0 +1,44 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local AltTask = TSM.Include("LibTSMClass").DefineClass("AltTask", TSM.TaskList.Task)
local L = TSM.Include("Locale").GetTable()
TSM.TaskList.AltTask = AltTask
-- ============================================================================
-- Class Meta Methods
-- ============================================================================
function AltTask.Acquire(self, doneHandler, category, character)
self.__super:Acquire(doneHandler, category, format(L["Switch to %s"], character))
end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function AltTask.IsSecureMacro(self)
return true
end
function AltTask.GetSecureMacroText(self)
return "/logout"
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function AltTask._UpdateState(self)
return self:_SetButtonState(true, strupper(LOGOUT))
end

View File

@ -0,0 +1,132 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local BankingTask = TSM.Include("LibTSMClass").DefineClass("BankingTask", TSM.TaskList.ItemTask)
local L = TSM.Include("Locale").GetTable()
local Inventory = TSM.Include("Service.Inventory")
TSM.TaskList.BankingTask = BankingTask
local private = {
registeredCallbacks = false,
currentlyMoving = nil,
activeTasks = {},
}
-- ============================================================================
-- Class Meta Methods
-- ============================================================================
function BankingTask.__init(self, isGuildBank)
self.__super:__init()
self._isMoving = false
self._isGuildBank = isGuildBank
if not private.registeredCallbacks then
TSM.Banking.RegisterFrameCallback(private.FrameCallback)
private.registeredCallbacks = true
end
end
function BankingTask.Acquire(self, doneHandler, category)
self.__super:Acquire(doneHandler, category, self._isGuildBank and L["Get from Guild Bank"] or L["Get from Bank"])
private.activeTasks[self] = true
end
function BankingTask.Release(self)
self.__super:Release()
self._isMoving = false
private.activeTasks[self] = nil
end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function BankingTask.OnButtonClick(self)
private.currentlyMoving = self
self._isMoving = true
TSM.Banking.MoveToBag(self:GetItems(), private.MoveCallback)
self:_UpdateState()
TSM.TaskList.OnTaskUpdated()
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function BankingTask._UpdateState(self)
local isOpen = nil
if self._isGuildBank then
isOpen = TSM.Banking.IsGuildBankOpen()
else
isOpen = TSM.Banking.IsBankOpen()
end
if not isOpen then
return self:_SetButtonState(false, L["NOT OPEN"])
end
local canMove = false
for itemString in pairs(self:GetItems()) do
if self._isGuildBank and Inventory.GetGuildQuantity(itemString) > 0 then
canMove = true
break
elseif not self._isGuildBank and Inventory.GetBankQuantity(itemString) + Inventory.GetReagentBankQuantity(itemString) > 0 then
canMove = true
break
end
end
if self._isMoving then
return self:_SetButtonState(false, L["MOVING"])
elseif private.currentlyMoving then
return self:_SetButtonState(false, L["BUSY"])
elseif not canMove then
return self:_SetButtonState(false, L["NO ITEMS"])
else
return self:_SetButtonState(true, L["MOVE"])
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.FrameCallback()
for task in pairs(private.activeTasks) do
task:Update()
end
end
function private.MoveCallback(event, ...)
local self = private.currentlyMoving
if not self then
return
end
assert(self._isMoving)
if event == "MOVED" then
local itemString, quantity = ...
if self:_RemoveItem(itemString, quantity) then
TSM.TaskList.OnTaskUpdated()
end
if not private.activeTasks[self] then
-- this task finished
private.currentlyMoving = nil
end
elseif event == "DONE" then
self._isMoving = false
private.currentlyMoving = nil
elseif event == "PROGRESS" then
-- pass
else
error("Unexpected event: "..tostring(event))
end
end

View File

@ -0,0 +1,94 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local CooldownCraftingTask = TSM.Include("LibTSMClass").DefineClass("CooldownCraftingTask", TSM.TaskList.CraftingTask)
local Math = TSM.Include("Util.Math")
TSM.TaskList.CooldownCraftingTask = CooldownCraftingTask
local private = {
registeredCallbacks = false,
activeTasks = {},
}
-- ============================================================================
-- Class Meta Methods
-- ============================================================================
function CooldownCraftingTask.__init(self)
self.__super:__init()
if not private.registeredCallbacks then
TSM.Crafting.CreateIgnoredCooldownQuery()
:SetUpdateCallback(private.UpdateTasks)
private.registeredCallbacks = true
end
end
function CooldownCraftingTask.Acquire(self, ...)
self.__super:Acquire(...)
private.activeTasks[self] = true
end
function CooldownCraftingTask.Release(self)
self.__super:Release()
private.activeTasks[self] = nil
end
function CooldownCraftingTask.CanHideSubTasks(self)
return true
end
function CooldownCraftingTask.HideSubTask(self, index)
TSM.Crafting.IgnoreCooldown(self._spellIds[index])
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function CooldownCraftingTask._UpdateState(self)
local result = self.__super:_UpdateState()
if not self:HasSpellIds() then
return result
end
for i = #self._spellIds, 1, -1 do
if self:_IsOnCooldown(self._spellIds[i]) or TSM.Crafting.IsCooldownIgnored(self._spellIds[i]) then
self:_RemoveSpellId(self._spellIds[i])
end
end
if not self:HasSpellIds() then
self:_doneHandler()
return true
end
return result
end
function CooldownCraftingTask._IsOnCooldown(self, spellId)
assert(not TSM.db.char.internalData.craftingCooldowns[spellId])
local remainingCooldown = TSM.Crafting.ProfessionUtil.GetRemainingCooldown(spellId)
if remainingCooldown then
TSM.db.char.internalData.craftingCooldowns[spellId] = time() + Math.Round(remainingCooldown)
return true
end
return false
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.UpdateTasks()
for task in pairs(private.activeTasks) do
if task:HasSpellIds() then
task:Update()
end
end
end

View File

@ -0,0 +1,212 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local CraftingTask = TSM.Include("LibTSMClass").DefineClass("CraftingTask", TSM.TaskList.Task)
local L = TSM.Include("Locale").GetTable()
local Table = TSM.Include("Util.Table")
local Log = TSM.Include("Util.Log")
local BagTracking = TSM.Include("Service.BagTracking")
TSM.TaskList.CraftingTask = CraftingTask
local private = {
currentlyCrafting = nil,
registeredCallbacks = false,
activeTasks = {},
}
-- ============================================================================
-- Class Meta Methods
-- ============================================================================
function CraftingTask.__init(self)
self.__super:__init()
self._profession = nil
self._skillId = nil
self._spellIds = {}
self._spellQuantity = {}
if not private.registeredCallbacks then
TSM.Crafting.ProfessionState.RegisterUpdateCallback(private.UpdateTasks)
TSM.Crafting.ProfessionScanner.RegisterHasScannedCallback(private.UpdateTasks)
BagTracking.RegisterCallback(private.UpdateTasks)
private.registeredCallbacks = true
end
end
function CraftingTask.Acquire(self, doneHandler, category, profession)
self.__super:Acquire(doneHandler, category, format(L["%s Crafts"], profession))
self._profession = profession
for _, _, prof, skillId in TSM.Crafting.PlayerProfessions.Iterator() do
if prof == profession and (not self._skillId or (self._skillId == -1 and skillId ~= -1)) then
self._skillId = skillId
end
end
private.activeTasks[self] = true
end
function CraftingTask.Release(self)
self.__super:Release()
self._profession = nil
self._skillId = nil
wipe(self._spellIds)
wipe(self._spellQuantity)
private.activeTasks[self] = nil
end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function CraftingTask.WipeSpellIds(self)
wipe(self._spellIds)
wipe(self._spellQuantity)
end
function CraftingTask.HasSpellIds(self)
return #self._spellIds > 0
end
function CraftingTask.GetProfession(self)
return self._profession
end
function CraftingTask.HasSpellId(self, spellId)
return self._spellQuantity[spellId] and true or false
end
function CraftingTask.AddSpellId(self, spellId, quantity)
tinsert(self._spellIds, spellId)
self._spellQuantity[spellId] = quantity
end
function CraftingTask.OnMouseDown(self)
if self._buttonText == L["CRAFT"] then
local spellId = self._spellIds[1]
local quantity = self._spellQuantity[spellId]
Log.Info("Preparing %d (%d)", spellId, quantity)
TSM.Crafting.ProfessionUtil.PrepareToCraft(spellId, quantity)
end
end
function CraftingTask.OnButtonClick(self)
if self._buttonText == L["CRAFT"] then
local spellId = self._spellIds[1]
local quantity = self._spellQuantity[spellId]
Log.Info("Crafting %d (%d)", spellId, quantity)
private.currentlyCrafting = self
local numCrafted = TSM.Crafting.ProfessionUtil.Craft(spellId, quantity, true, private.CraftCompleteCallback)
if numCrafted == 0 then
-- we're probably crafting something else already - so just bail
Log.Err("Failed to craft")
private.currentlyCrafting = nil
end
elseif self._buttonText == L["OPEN"] then
TSM.Crafting.ProfessionUtil.OpenProfession(self._profession, self._skillId)
else
error("Invalid state: "..tostring(self._buttonText))
end
self:Update()
end
function CraftingTask.HasSubTasks(self)
assert(self:HasSpellIds())
return true
end
function CraftingTask.SubTaskIterator(self)
assert(self:HasSpellIds())
sort(self._spellIds, private.SpellIdSort)
return private.SubTaskIterator, self, 0
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function CraftingTask._UpdateState(self)
sort(self._spellIds, private.SpellIdSort)
if TSM.Crafting.ProfessionUtil.GetNumCraftableFromDB(self._spellIds[1]) == 0 then
-- don't have the mats to craft this
return self:_SetButtonState(false, L["NEED MATS"])
elseif self._profession ~= TSM.Crafting.ProfessionState.GetCurrentProfession() then
-- the profession isn't opened
return self:_SetButtonState(true, L["OPEN"])
elseif not TSM.Crafting.ProfessionScanner.HasScanned() then
-- the profession is opened, but we haven't yet fully scanned it
return self:_SetButtonState(false, strupper(OPENING))
elseif private.currentlyCrafting == self then
return self:_SetButtonState(false, L["CRAFTING"])
elseif private.currentlyCrafting then
return self:_SetButtonState(false, L["BUSY"])
else
-- ready to craft
return self:_SetButtonState(true, L["CRAFT"])
end
end
function CraftingTask._RemoveSpellId(self, spellId)
assert(Table.RemoveByValue(self._spellIds, spellId) == 1)
self._spellQuantity[spellId] = nil
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.SubTaskIterator(self, index)
index = index + 1
local spellId = self._spellIds[index]
if not spellId then
return
end
return index, TSM.Crafting.GetName(spellId).." ("..self._spellQuantity[spellId]..")"
end
function private.CraftCompleteCallback(success, isDone)
local self = private.currentlyCrafting
assert(self)
local spellId = self._spellIds[1]
if isDone then
private.currentlyCrafting = nil
if success then
self:_RemoveSpellId(spellId)
if not self:HasSpellIds() then
self:_doneHandler()
end
end
elseif success then
self._spellQuantity[spellId] = self._spellQuantity[spellId] - 1
assert(self._spellQuantity[spellId] > 0)
end
if self:HasSpellIds() then
self:Update()
end
end
function private.UpdateTasks()
for task in pairs(private.activeTasks) do
if task:HasSpellIds() then
task:Update()
end
end
end
function private.SpellIdSort(a, b)
local aNumCraftable = TSM.Crafting.ProfessionUtil.GetNumCraftableFromDB(a)
local bNumCraftable = TSM.Crafting.ProfessionUtil.GetNumCraftableFromDB(b)
if aNumCraftable == bNumCraftable then
return a < b
end
return aNumCraftable > bNumCraftable
end

View File

@ -0,0 +1,124 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local ExpiredAuctionTask = TSM.Include("LibTSMClass").DefineClass("ExpiredAuctionTask", TSM.TaskList.Task)
local L = TSM.Include("Locale").GetTable()
TSM.TaskList.ExpiredAuctionTask = ExpiredAuctionTask
local private = {}
-- ============================================================================
-- Class Meta Methods
-- ============================================================================
function ExpiredAuctionTask.__init(self)
self.__super:__init()
self._characters = {}
self._daysLeft = {}
end
function ExpiredAuctionTask.Acquire(self, doneHandler, category)
self.__super:Acquire(doneHandler, category, L["Expired Auctions"])
end
function ExpiredAuctionTask.Release(self)
self.__super:Release()
wipe(self._characters)
wipe(self._daysLeft)
end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function ExpiredAuctionTask.IsSecureMacro(self)
return true
end
function ExpiredAuctionTask.GetSecureMacroText(self)
return "/logout"
end
function ExpiredAuctionTask.GetDaysLeft(self, character)
return self._daysLeft[character] or false
end
function ExpiredAuctionTask.WipeCharacters(self)
wipe(self._characters)
wipe(self._daysLeft)
end
function ExpiredAuctionTask.HasCharacters(self)
return #self._characters > 0
end
function ExpiredAuctionTask.HasCharacter(self, character)
return self._daysLeft[character] and true or false
end
function ExpiredAuctionTask.AddCharacter(self, character, days)
tinsert(self._characters, character)
self._daysLeft[character] = days
end
function ExpiredAuctionTask.CanHideSubTasks(self)
return true
end
function ExpiredAuctionTask.HideSubTask(self, index)
local character = self._characters[index]
if not character then
return
end
TSM.db.factionrealm.internalData.expiringAuction[character] = nil
TSM.TaskList.Expirations.Update()
end
function ExpiredAuctionTask.HasSubTasks(self)
assert(self:HasCharacters())
return true
end
function ExpiredAuctionTask.SubTaskIterator(self)
assert(self:HasCharacters())
sort(self._characters)
return private.SubTaskIterator, self, 0
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function ExpiredAuctionTask._UpdateState(self)
return self:_SetButtonState(true, strupper(LOGOUT))
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.SubTaskIterator(self, index)
index = index + 1
local character = self._characters[index]
if not character then
return
end
local charColored = character
local classColor = RAID_CLASS_COLORS[TSM.db:Get("sync", TSM.db:GetSyncScopeKeyByCharacter(character), "internalData", "classKey")]
if classColor then
charColored = "|c"..classColor.colorStr..charColored.."|r"
end
return index, charColored
end

View File

@ -0,0 +1,140 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local ExpiringMailTask = TSM.Include("LibTSMClass").DefineClass("ExpiringMailTask", TSM.TaskList.Task)
local L = TSM.Include("Locale").GetTable()
TSM.TaskList.ExpiringMailTask = ExpiringMailTask
local private = {}
-- ============================================================================
-- Class Meta Methods
-- ============================================================================
function ExpiringMailTask.__init(self)
self.__super:__init()
self._characters = {}
self._daysLeft = {}
end
function ExpiringMailTask.Acquire(self, doneHandler, category)
self.__super:Acquire(doneHandler, category, L["Expiring Mails"])
end
function ExpiringMailTask.Release(self)
self.__super:Release()
wipe(self._characters)
wipe(self._daysLeft)
end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function ExpiringMailTask.IsSecureMacro(self)
return true
end
function ExpiringMailTask.GetSecureMacroText(self)
return "/logout"
end
function ExpiringMailTask.GetDaysLeft(self, character)
return self._daysLeft[character] or false
end
function ExpiringMailTask.WipeCharacters(self)
wipe(self._characters)
wipe(self._daysLeft)
end
function ExpiringMailTask.HasCharacters(self)
return #self._characters > 0
end
function ExpiringMailTask.HasCharacter(self, character)
return self._daysLeft[character] and true or false
end
function ExpiringMailTask.AddCharacter(self, character, days)
tinsert(self._characters, character)
self._daysLeft[character] = days
end
function ExpiringMailTask.CanHideSubTasks(self)
return true
end
function ExpiringMailTask.HideSubTask(self, index)
local character = self._characters[index]
if not character then
return
end
TSM.db.factionrealm.internalData.expiringMail[character] = nil
TSM.TaskList.Expirations.Update()
end
function ExpiringMailTask.HasSubTasks(self)
assert(self:HasCharacters())
return true
end
function ExpiringMailTask.SubTaskIterator(self)
assert(self:HasCharacters())
sort(self._characters)
return private.SubTaskIterator, self, 0
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function ExpiringMailTask._UpdateState(self)
return self:_SetButtonState(true, strupper(LOGOUT))
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.SubTaskIterator(self, index)
index = index + 1
local character = self._characters[index]
if not character then
return
end
local timeLeft = self._daysLeft[character]
if timeLeft < 0 then
timeLeft = L["Expired"]
elseif timeLeft >= 1 then
timeLeft = floor(timeLeft).." "..DAYS
else
local hoursLeft = floor(timeLeft * 24)
if hoursLeft > 1 then
timeLeft = hoursLeft.." "..L["Hrs"]
elseif hoursLeft == 1 then
timeLeft = hoursLeft.." "..L["Hr"]
else
timeLeft = floor(hoursLeft / 60).." "..L["Min"]
end
end
local charColored = character
local classColor = RAID_CLASS_COLORS[TSM.db:Get("sync", TSM.db:GetSyncScopeKeyByCharacter(character), "internalData", "classKey")]
if classColor then
charColored = "|c"..classColor.colorStr..charColored.."|r"
end
return index, charColored.." ("..timeLeft..")"
end

View File

@ -0,0 +1,118 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local ItemTask = TSM.Include("LibTSMClass").DefineClass("ItemTask", TSM.TaskList.Task, "ABSTRACT")
local Table = TSM.Include("Util.Table")
local Math = TSM.Include("Util.Math")
local ItemInfo = TSM.Include("Service.ItemInfo")
TSM.TaskList.ItemTask = ItemTask
local private = {}
-- ============================================================================
-- Class Meta Methods
-- ============================================================================
function ItemTask.__init(self)
self.__super:__init()
self._itemList = {}
self._itemNum = {}
end
function ItemTask.Release(self)
self.__super:Release()
self:WipeItems()
end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function ItemTask.WipeItems(self)
wipe(self._itemList)
wipe(self._itemNum)
end
function ItemTask.AddItem(self, itemString, quantity)
if not self._itemNum[itemString] then
tinsert(self._itemList, itemString)
self._itemNum[itemString] = 0
end
self._itemNum[itemString] = self._itemNum[itemString] + quantity
end
function ItemTask.GetItems(self)
return self._itemNum
end
function ItemTask.HasItems(self)
return next(self._itemNum) and true or false
end
function ItemTask.HasSubTasks(self)
assert(#self._itemList > 0)
return true
end
function ItemTask.SubTaskIterator(self)
assert(#self._itemList > 0)
Table.Sort(self._itemList, private.ItemSortHelper)
return private.SubTaskIterator, self, 0
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function ItemTask._RemoveItem(self, itemString, quantity)
if not self._itemNum[itemString] then
return false
end
self._itemNum[itemString] = Math.Round(self._itemNum[itemString] - quantity, 0.01)
if self._itemNum[itemString] <= 0.01 then
self._itemNum[itemString] = nil
assert(Table.RemoveByValue(self._itemList, itemString) == 1)
end
if #self._itemList == 0 then
self:_doneHandler()
end
return true
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.SubTaskIterator(self, index)
index = index + 1
local itemString = self._itemList[index]
if not itemString then
return
end
return index, format("%s (%d)", ItemInfo.GetLink(itemString), self._itemNum[itemString])
end
function private.ItemSortHelper(a, b)
local aName = ItemInfo.GetName(a)
local bName = ItemInfo.GetName(b)
if aName == bName then
return a < b
end
if not aName then
return false
elseif not bName then
return true
end
return aName < bName
end

View File

@ -0,0 +1,74 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local OpenMailTask = TSM.Include("LibTSMClass").DefineClass("OpenMailTask", TSM.TaskList.ItemTask)
local L = TSM.Include("Locale").GetTable()
TSM.TaskList.OpenMailTask = OpenMailTask
local private = {
activeTasks = {},
registeredCallbacks = false,
}
-- ============================================================================
-- Class Meta Methods
-- ============================================================================
function OpenMailTask.__init(self)
self.__super:__init()
if not private.registeredCallbacks then
TSM.Mailing.RegisterFrameCallback(private.FrameCallback)
private.registeredCallbacks = true
end
end
function OpenMailTask.Acquire(self, doneHandler, category)
self.__super:Acquire(doneHandler, category, L["Open Mail"])
private.activeTasks[self] = true
end
function OpenMailTask.Release(self)
self.__super:Release()
private.activeTasks[self] = nil
end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function OpenMailTask.OnButtonClick(self)
-- TODO
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function OpenMailTask._UpdateState(self)
if not TSM.Mailing.IsOpen() then
return self:_SetButtonState(false, L["NOT OPEN"])
else
return self:_SetButtonState(false, L["OPEN"])
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.FrameCallback()
for task in pairs(private.activeTasks) do
task:Update()
end
end

View File

@ -0,0 +1,105 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local SendMailTask = TSM.Include("LibTSMClass").DefineClass("SendMailTask", TSM.TaskList.ItemTask)
local L = TSM.Include("Locale").GetTable()
TSM.TaskList.SendMailTask = SendMailTask
local private = {
registeredCallbacks = false,
currentlySending = nil,
activeTasks = {},
}
-- ============================================================================
-- Class Meta Methods
-- ============================================================================
function SendMailTask.__init(self)
self.__super:__init()
self._target = nil
self._isSending = false
if not private.registeredCallbacks then
TSM.Mailing.RegisterFrameCallback(private.FrameCallback)
private.registeredCallbacks = true
end
end
function SendMailTask.Acquire(self, doneHandler, category)
self.__super:Acquire(doneHandler, category, "")
private.activeTasks[self] = true
end
function SendMailTask.Release(self)
self.__super:Release()
self._target = nil
self._isSending = false
private.activeTasks[self] = nil
end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function SendMailTask.SetTarget(self, target)
self._target = target
self._desc = format(L["Mail to %s"], target)
end
function SendMailTask.OnButtonClick(self)
private.currentlySending = self
self._isSending = true
TSM.Mailing.Send.StartSending(private.SendCallback, self._target, "", "", 0, self:GetItems())
self:_UpdateState()
TSM.TaskList.OnTaskUpdated()
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function SendMailTask._UpdateState(self)
if not TSM.Mailing.IsOpen() then
return self:_SetButtonState(false, L["NOT OPEN"])
elseif self._isSending then
return self:_SetButtonState(false, L["SENDING"])
elseif private.currentlySending then
return self:_SetButtonState(false, L["BUSY"])
else
return self:_SetButtonState(true, strupper(L["Send"]))
end
end
-- ============================================================================
-- Private Helper Methods
-- ============================================================================
function private.FrameCallback()
for task in pairs(private.activeTasks) do
task:Update()
end
end
function private.SendCallback()
local self = private.currentlySending
if not self then
return
end
assert(self._isSending)
self._isSending = false
private.currentlySending = nil
for itemString, quantity in pairs(self:GetItems()) do
self:_RemoveItem(itemString, quantity)
end
end

View File

@ -0,0 +1,141 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local ShoppingTask = TSM.Include("LibTSMClass").DefineClass("ShoppingTask", TSM.TaskList.ItemTask)
local L = TSM.Include("Locale").GetTable()
local Delay = TSM.Include("Util.Delay")
local Log = TSM.Include("Util.Log")
TSM.TaskList.ShoppingTask = ShoppingTask
local private = {
registeredCallbacks = false,
currentlyScanning = nil,
activeTasks = {},
}
-- ============================================================================
-- Class Meta Methods
-- ============================================================================
function ShoppingTask.__init(self, searchType)
self.__super:__init()
self._isScanning = false
self._isShowingResults = false
assert(searchType == "NORMAL" or searchType == "DISENCHANT" or searchType == "CRAFTING")
self._searchType = searchType
if not private.registeredCallbacks then
TSM.UI.AuctionUI.RegisterUpdateCallback(private.UIUpdateCallback)
TSM.UI.AuctionUI.Shopping.RegisterUpdateCallback(private.UIUpdateCallback)
private.registeredCallbacks = true
end
end
function ShoppingTask.Acquire(self, doneHandler, category)
local name = nil
if self._searchType == "NORMAL" then
name = L["Buy from AH"]
elseif self._searchType == "DISENCHANT" then
name = L["Buy from AH (Disenchant)"]
elseif self._searchType == "CRAFTING" then
name = L["Buy from AH (Crafting)"]
else
error("Invalid searchType: "..tostring(self._searchType))
end
self.__super:Acquire(doneHandler, category, name)
private.activeTasks[self] = true
end
function ShoppingTask.Release(self)
self.__super:Release()
self._isScanning = false
self._isShowingResults = false
private.activeTasks[self] = nil
end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function ShoppingTask.OnButtonClick(self)
private.currentlyScanning = self
TSM.UI.AuctionUI.Shopping.StartGatheringSearch(self:GetItems(), private.StateCallback, private.BuyCallback, self._searchType)
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function ShoppingTask._UpdateState(self)
if not TSM.UI.AuctionUI.Shopping.IsVisible() then
return self:_SetButtonState(false, L["NOT OPEN"])
elseif self._isScanning then
return self:_SetButtonState(false, L["SCANNING"])
elseif self._isShowingResults then
return self:_SetButtonState(false, L["BUY"])
elseif TSM.UI.AuctionUI.IsScanning() or private.currentlyScanning then
return self:_SetButtonState(false, L["AH BUSY"])
else
return self:_SetButtonState(true, L["SCAN ALL"])
end
end
function ShoppingTask._OnSearchStateChanged(self, state)
if state == "SCANNING" then
self._isScanning = true
self._isShowingResults = false
elseif state == "RESULTS" then
self._isScanning = false
self._isShowingResults = true
elseif state == "DONE" then
assert(private.currentlyScanning == self)
private.currentlyScanning = nil
self._isScanning = false
self._isShowingResults = false
else
error("Unexpected state: "..tostring(state))
end
self:Update()
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.UIUpdateCallback()
Delay.AfterFrame("SHOPPING_TASK_UPDATE_CALLBACK", 1, private.UIUpdateCallbackDelayed)
end
function private.UIUpdateCallbackDelayed()
for task in pairs(private.activeTasks) do
task:Update()
end
end
function private.StateCallback(state)
Log.Info("State changed (%s)", state)
local self = private.currentlyScanning
assert(self)
self:_OnSearchStateChanged(state)
private.UIUpdateCallback()
end
function private.BuyCallback(itemString, quantity)
Log.Info("Bought item (%s,%d)", itemString, quantity)
local self = private.currentlyScanning
assert(self)
if self:_RemoveItem(itemString, quantity) then
TSM.TaskList.OnTaskUpdated()
end
end

View File

@ -0,0 +1,112 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Task = TSM.Include("LibTSMClass").DefineClass("TASK", nil, "ABSTRACT")
TSM.TaskList.Task = Task
-- ============================================================================
-- Task - Class Meta Methods
-- ============================================================================
function Task.__init(self)
self._category = nil
self._desc = nil
self._buttonEnabled = nil
self._buttonText = nil
self._doneHandler = nil
end
function Task.Acquire(self, doneHandler, category, desc)
self._doneHandler = doneHandler
self._category = category
self._desc = desc
end
function Task.Release(self)
self._category = nil
self._desc = nil
self._buttonEnabled = nil
self._buttonText = nil
self._doneHandler = nil
end
-- ============================================================================
-- Task - Public Methods
-- ============================================================================
function Task.GetCategory(self)
return self._category
end
function Task.GetTaskDesc(self)
return self._desc
end
function Task.HasSubTasks(self)
return false
end
function Task.SubTaskIterator(self)
error("Must be implemented by the subclass")
end
function Task.IsSecureMacro(self)
return false
end
function Task.GetSecureMacroText(self)
error("Must be implemented by the subclass")
end
function Task.GetButtonState(self)
return self._buttonEnabled, self._buttonText
end
function Task.Update(self)
if self:_UpdateState() then
TSM.TaskList.OnTaskUpdated()
end
end
function Task.OnMouseDown(self)
end
function Task.OnButtonClick(self)
error("Must be implemented by the subclass")
end
function Task.CanHideSubTasks(self)
return false
end
function Task.HideSubTask(self)
error("Must be implemented by the subclass")
end
-- ============================================================================
-- Task - Private Methods
-- ============================================================================
function Task._UpdateState(self)
error("Must be implemented by the subclass")
end
function Task._SetButtonState(self, buttonEnabled, buttonText)
if buttonEnabled == self._buttonEnabled and buttonText == self._buttonText then
-- nothing changed
return false
end
self._buttonEnabled = buttonEnabled
self._buttonText = buttonText
return true
end

View File

@ -0,0 +1,104 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local VendoringTask = TSM.Include("LibTSMClass").DefineClass("VendoringTask", TSM.TaskList.ItemTask)
local L = TSM.Include("Locale").GetTable()
local TempTable = TSM.Include("Util.TempTable")
TSM.TaskList.VendoringTask = VendoringTask
local private = {
query = nil,
activeTasks = {},
}
-- ============================================================================
-- Class Meta Methods
-- ============================================================================
function VendoringTask.__init(self)
self.__super:__init()
if not private.query then
private.query = TSM.Vendoring.Buy.CreateMerchantQuery()
:SetUpdateCallback(private.QueryUpdateCallback)
end
end
function VendoringTask.Acquire(self, doneHandler, category)
self.__super:Acquire(doneHandler, category, L["Buy from Vendor"])
private.activeTasks[self] = true
end
function VendoringTask.Release(self)
self.__super:Release()
private.activeTasks[self] = nil
end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function VendoringTask.OnButtonClick(self)
local itemsToBuy = TempTable.Acquire()
local query = TSM.Vendoring.Buy.CreateMerchantQuery()
:Select("itemString")
for _, itemString in query:Iterator() do
itemsToBuy[itemString] = self:GetItems()[itemString]
end
query:Release()
local didBuy = false
for itemString, quantity in pairs(itemsToBuy) do
TSM.Vendoring.Buy.BuyItem(itemString, quantity)
self:_RemoveItem(itemString, quantity)
didBuy = true
end
TempTable.Release(itemsToBuy)
if didBuy then
TSM.TaskList.OnTaskUpdated(self)
end
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function VendoringTask._UpdateState(self)
if not TSM.UI.VendoringUI.IsVisible() then
return self:_SetButtonState(false, L["NOT OPEN"])
end
local canBuy = false
for itemString in pairs(self:GetItems()) do
if TSM.Vendoring.Buy.CanBuyItem(itemString) then
canBuy = true
break
end
end
if not canBuy then
return self:_SetButtonState(false, L["NO ITEMS"])
else
return self:_SetButtonState(true, L["BUY"])
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.QueryUpdateCallback()
for task in pairs(private.activeTasks) do
task:Update()
end
end

View File

@ -0,0 +1,153 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Accounting = TSM.Tooltip:NewPackage("Accounting")
local L = TSM.Include("Locale").GetTable()
local Math = TSM.Include("Util.Math")
local ItemString = TSM.Include("Util.ItemString")
local CustomPrice = TSM.Include("Service.CustomPrice")
local private = {}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Accounting.OnInitialize()
TSM.Tooltip.Register(TSM.Tooltip.CreateInfo()
:SetHeadings(L["TSM Accounting"])
:SetSettingsModule("Accounting")
:AddSettingEntry("purchase", true, private.PopulatePurchaseLines)
:AddSettingEntry("sale", true, private.PopulateSaleLines)
:AddSettingEntry("saleRate", false, private.PopulateSaleRateLine)
:AddSettingEntry("expiredAuctions", false, private.PopulateExpireLine)
:AddSettingEntry("cancelledAuctions", false, private.PopulateCancelLine)
)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.PopulateSaleLines(tooltip, itemString)
local showTotals = itemString ~= ItemString.GetPlaceholder() and IsShiftKeyDown()
local avgSalePrice, totalSaleNum, lastSaleTime, minSellPrice, maxSellPrice = nil, nil, nil, nil, nil
if itemString == ItemString.GetPlaceholder() then
-- example tooltip
avgSalePrice = 20
totalSaleNum = 5
lastSaleTime = time() - 60
minSellPrice = 10
maxSellPrice = 50
else
local totalPrice = nil
totalPrice, totalSaleNum = TSM.Accounting.Transactions.GetSaleStats(itemString)
if not totalSaleNum then
return
end
avgSalePrice = totalPrice and Math.Round(totalPrice / totalSaleNum) or nil
lastSaleTime = TSM.Accounting.Transactions.GetLastSaleTime(itemString)
if not showTotals then
minSellPrice = CustomPrice.GetItemPrice(itemString, "MinSell") or 0
maxSellPrice = CustomPrice.GetItemPrice(itemString, "MaxSell") or 0
end
end
if showTotals then
tooltip:AddQuantityValueLine(L["Sold (Total Price)"], totalSaleNum, avgSalePrice * totalSaleNum)
else
assert(minSellPrice and maxSellPrice)
tooltip:AddQuantityValueLine(L["Sold (Min/Avg/Max Price)"], totalSaleNum, minSellPrice, avgSalePrice, maxSellPrice)
end
tooltip:AddTextLine(L["Last Sold"], format(L["%s ago"], SecondsToTime(time() - lastSaleTime)))
end
function private.PopulateExpireLine(tooltip, itemString)
local expiredNum = nil
if itemString == ItemString.GetPlaceholder() then
-- example tooltip
expiredNum = 2
else
local lastSaleTime = TSM.Accounting.Transactions.GetLastSaleTime(itemString)
expiredNum = select(2, TSM.Accounting.Auctions.GetStats(itemString, lastSaleTime))
if expiredNum == 0 then
expiredNum = nil
end
end
if expiredNum then
tooltip:AddTextLine(L["Expired Since Last Sale"], expiredNum)
end
end
function private.PopulateCancelLine(tooltip, itemString)
local cancelledNum = nil
if itemString == ItemString.GetPlaceholder() then
-- example tooltip
cancelledNum = 2
else
local lastSaleTime = TSM.Accounting.Transactions.GetLastSaleTime(itemString)
cancelledNum = TSM.Accounting.Auctions.GetStats(itemString, lastSaleTime)
if cancelledNum == 0 then
cancelledNum = nil
end
end
if cancelledNum then
tooltip:AddTextLine(L["Cancelled Since Last Sale"], cancelledNum)
end
end
function private.PopulateSaleRateLine(tooltip, itemString)
local saleRate = nil
if itemString == ItemString.GetPlaceholder() then
-- example tooltip
saleRate = 0.7
else
saleRate = CustomPrice.GetItemPrice(itemString, "SaleRate")
if not saleRate then
return
end
end
tooltip:AddTextLine(L["Sale Rate"], saleRate)
end
function private.PopulatePurchaseLines(tooltip, itemString)
local showTotals = itemString ~= ItemString.GetPlaceholder() and IsShiftKeyDown()
local smartAvgPrice, totalPrice, totalNum, minPrice, maxPrice, lastBuyTime = nil, nil, nil, nil, nil, nil
if itemString == ItemString.GetPlaceholder() then
-- example tooltip
smartAvgPrice = 25
totalPrice = 78
totalNum = 3
minPrice = 15
maxPrice = 55
lastBuyTime = time() - 3600
else
smartAvgPrice = CustomPrice.GetItemPrice(itemString, "SmartAvgBuy")
totalPrice, totalNum = TSM.Accounting.Transactions.GetBuyStats(itemString, false)
if not totalPrice then
return
end
if not showTotals then
minPrice = CustomPrice.GetItemPrice(itemString, "MinBuy") or 0
maxPrice = CustomPrice.GetItemPrice(itemString, "MaxBuy") or 0
end
lastBuyTime = TSM.Accounting.Transactions.GetLastBuyTime(itemString)
end
if showTotals then
tooltip:AddQuantityValueLine(L["Purchased (Total Price)"], totalNum, totalPrice)
else
assert(minPrice and maxPrice)
tooltip:AddQuantityValueLine(L["Purchased (Min/Avg/Max Price)"], totalNum, minPrice, Math.Round(totalPrice / totalNum), maxPrice)
end
tooltip:AddValueLine(L["Smart Avg Buy Price"], smartAvgPrice)
tooltip:AddTextLine(L["Last Purchased"], format(L["%s ago"], SecondsToTime(time() - lastBuyTime)))
end

View File

@ -0,0 +1,84 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local AuctionDB = TSM.Tooltip:NewPackage("AuctionDB")
local L = TSM.Include("Locale").GetTable()
local ItemString = TSM.Include("Util.ItemString")
local Theme = TSM.Include("Util.Theme")
local private = {}
local INFO = {
{ key = "minBuyout", default = true, label = L["Min Buyout"] },
{ key = "marketValue", default = true, label = L["Market Value"] },
{ key = "historical", default = false, label = L["Historical Price"] },
{ key = "regionMinBuyout", default = false, label = L["Region Min Buyout Avg"] },
{ key = "regionMarketValue", default = true, label = L["Region Market Value Avg"] },
{ key = "regionHistorical", default = false, label = L["Region Historical Price"] },
{ key = "regionSale", default = true, label = L["Region Sale Avg"] },
{ key = "regionSalePercent", default = true, label = L["Region Sale Rate"] },
{ key = "regionSoldPerDay", default = true, label = L["Region Avg Daily Sold"] },
}
local DATA_OLD_THRESHOLD_SECONDS = 60 * 60 * 3
-- ============================================================================
-- Module Functions
-- ============================================================================
function AuctionDB.OnInitialize()
local tooltipInfo = TSM.Tooltip.CreateInfo()
:SetHeadings(L["TSM AuctionDB"], private.PopulateRightText)
:SetSettingsModule("AuctionDB")
for _, info in ipairs(INFO) do
tooltipInfo:AddSettingEntry(info.key, info.default, private.PopulateLine, info)
end
TSM.Tooltip.Register(tooltipInfo)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.PopulateLine(tooltip, itemString, info)
local value = nil
if itemString == ItemString.GetPlaceholder() then
-- example tooltip
value = 11
elseif strmatch(info.key, "^region") then
value = TSM.AuctionDB.GetRegionItemData(itemString, info.key)
else
value = TSM.AuctionDB.GetRealmItemData(itemString, info.key)
end
if value then
if info.key == "regionSalePercent" or info.key == "regionSoldPerDay" then
tooltip:AddTextLine(info.label, format("%0.2f", value/100))
else
tooltip:AddItemValueLine(info.label, value)
end
end
end
function private.PopulateRightText(tooltip, itemString)
local lastScan, numAuctions = nil
if itemString == ItemString.GetPlaceholder() then
-- example tooltip
lastScan = time() - 120
numAuctions = 5
else
lastScan = TSM.AuctionDB.GetRealmItemData(itemString, "lastScan")
numAuctions = TSM.AuctionDB.GetRealmItemData(itemString, "numAuctions") or 0
end
if lastScan then
local timeColor = (time() - lastScan) > DATA_OLD_THRESHOLD_SECONDS and Theme.GetFeedbackColor("RED") or Theme.GetFeedbackColor("GREEN")
local timeDiff = SecondsToTime(time() - lastScan)
return tooltip:ApplyValueColor(format(L["%d auctions"], numAuctions)).." ("..timeColor:ColorText(format(L["%s ago"], timeDiff))..")"
else
return tooltip:ApplyValueColor(L["Not Scanned"])
end
end

View File

@ -0,0 +1,79 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Auctioning = TSM.Tooltip:NewPackage("Auctioning")
local L = TSM.Include("Locale").GetTable()
local ItemString = TSM.Include("Util.ItemString")
local ItemInfo = TSM.Include("Service.ItemInfo")
local private = {}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Auctioning.OnInitialize()
TSM.Tooltip.Register(TSM.Tooltip.CreateInfo()
:SetHeadings(L["TSM Auctioning"])
:SetSettingsModule("Auctioning")
:AddSettingEntry("postQuantity", false, private.PopulatePostQuantityLine)
:AddSettingEntry("operationPrices", false, private.PopulatePricesLine)
)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.PopulatePostQuantityLine(tooltip, itemString)
local postCap, stackSize = nil, nil
if itemString == ItemString.GetPlaceholder() then
postCap = 5
stackSize = TSM.IsWowClassic() and 200
elseif ItemInfo.IsSoulbound(itemString) then
return
else
itemString = TSM.Groups.TranslateItemString(itemString)
local _, operation = TSM.Operations.GetFirstOperationByItem("Auctioning", itemString)
if not operation then
return
end
postCap = TSM.Auctioning.Util.GetPrice("postCap", operation, itemString)
stackSize = TSM.IsWowClassic() and TSM.Auctioning.Util.GetPrice("stackSize", operation, itemString)
end
if TSM.IsWowClassic() then
tooltip:AddTextLine(L["Post Quantity"], postCap and stackSize and postCap.."x"..stackSize or "---")
else
tooltip:AddTextLine(L["Post Quantity"], postCap or "---")
end
end
function private.PopulatePricesLine(tooltip, itemString)
local minPrice, normalPrice, maxPrice = nil, nil, nil
if itemString == ItemString.GetPlaceholder() then
minPrice = 20
normalPrice = 24
maxPrice = 29
elseif ItemInfo.IsSoulbound(itemString) then
return
else
itemString = TSM.Groups.TranslateItemString(itemString)
local _, operation = TSM.Operations.GetFirstOperationByItem("Auctioning", itemString)
if not operation then
return
end
minPrice = TSM.Auctioning.Util.GetPrice("minPrice", operation, itemString)
normalPrice = TSM.Auctioning.Util.GetPrice("normalPrice", operation, itemString)
maxPrice = TSM.Auctioning.Util.GetPrice("maxPrice", operation, itemString)
end
tooltip:AddValueLine(L["Min/Normal/Max Prices"], minPrice, normalPrice, maxPrice)
end

View File

@ -0,0 +1,275 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Tooltip = TSM:NewPackage("Tooltip")
local ItemString = TSM.Include("Util.ItemString")
local ObjectPool = TSM.Include("Util.ObjectPool")
local ItemTooltip = TSM.Include("Service.ItemTooltip")
local Settings = TSM.Include("Service.Settings")
local LibTSMClass = TSM.Include("LibTSMClass")
local TooltipInfo = LibTSMClass.DefineClass("TooltipInfo")
local TooltipEntry = LibTSMClass.DefineClass("TooltipEntry")
local private = {
entryObjPool = ObjectPool.New("TOOLTIP_ENTRY", TooltipEntry),
settings = nil,
registeredInfo = {},
settingsBuilder = nil,
}
local ITER_INDEX_PART_MULTIPLE = 1000
-- ============================================================================
-- Module Functions
-- ============================================================================
function Tooltip.OnInitialize()
private.settings = Settings.NewView()
:AddKey("global", "tooltipOptions", "moduleTooltips")
:AddKey("global", "tooltipOptions", "customPriceTooltips")
:AddKey("global", "tooltipOptions", "vendorBuyTooltip")
:AddKey("global", "tooltipOptions", "vendorSellTooltip")
:AddKey("global", "tooltipOptions", "groupNameTooltip")
:AddKey("global", "tooltipOptions", "detailedDestroyTooltip")
:AddKey("global", "tooltipOptions", "millTooltip")
:AddKey("global", "tooltipOptions", "prospectTooltip")
:AddKey("global", "tooltipOptions", "deTooltip")
:AddKey("global", "tooltipOptions", "transformTooltip")
:AddKey("global", "tooltipOptions", "operationTooltips")
:AddKey("global", "tooltipOptions", "inventoryTooltipFormat")
ItemTooltip.SetWrapperPopulateFunction(private.PopulateTooltip)
private.settingsBuilder = ItemTooltip.CreateBuilder()
end
function Tooltip.CreateInfo()
return TooltipInfo()
end
function Tooltip.Register(info)
info:_CheckDefaults()
tinsert(private.registeredInfo, info)
end
function Tooltip.SettingsLineIterator()
assert(not private.settingsBuilder:_Prepare(ItemString.GetPlaceholder(), 1))
return private.SettingsLineIteratorHelper, nil, 0
end
-- ============================================================================
-- TooltipInfo Class
-- ============================================================================
function TooltipInfo.__init(self)
self._headingLeft = nil
self._headingRight = nil
self._settingsModule = nil
self._entries = {}
end
function TooltipInfo.SetHeadings(self, left, right)
self._headingLeft = left
self._headingRight = right
return self
end
function TooltipInfo.SetSettingsModule(self, settingsModule)
self._settingsModule = settingsModule
return self
end
function TooltipInfo.AddSettingEntry(self, key, defaultValue, populateFunc, populateArg)
if defaultValue == nil then
defaultValue = private.settings:GetDefaultReadOnly(key)
end
assert(type(defaultValue) == "boolean")
local entry = private.entryObjPool:Get()
entry:_Acquire(self, key, true, false, defaultValue, populateFunc, populateArg)
tinsert(self._entries, entry)
return self
end
function TooltipInfo.AddSettingValueEntry(self, key, setValue, clearValue, populateFunc, populateArg)
local entry = private.entryObjPool:Get()
entry:_Acquire(self, key, setValue, clearValue, nil, populateFunc, populateArg)
tinsert(self._entries, entry)
return self
end
function TooltipInfo.DeleteSettingsByKeyMatch(self, matchStr)
for i = #self._entries, 1, -1 do
local entry = self._entries[i]
if entry:KeyMatches(matchStr) then
tremove(self._entries, i)
entry:_Release()
private.entryObjPool:Recycle(entry)
end
end
end
function TooltipInfo._CheckDefaults(self)
if self._settingsModule and not private.settings.moduleTooltips[self._settingsModule] then
-- populate all the default values
private.settings.moduleTooltips[self._settingsModule] = {}
for _, entry in ipairs(self._entries) do
entry:_ResetSetting()
end
end
end
function TooltipInfo._GetSettingsTable(self)
if self._settingsModule then
return private.settings.moduleTooltips[self._settingsModule]
else
return private.settings
end
end
function TooltipInfo._Populate(self, tooltip, itemString)
local headingRightText = self._headingRight and self._headingRight(tooltip, itemString) or nil
tooltip:StartSection(self._headingLeft, headingRightText)
for _, entry in ipairs(self._entries) do
if entry:IsEnabled() then
entry:_Populate(tooltip, itemString)
end
end
tooltip:EndSection()
end
function TooltipInfo._GetEntry(self, index)
return self._entries[index]
end
-- ============================================================================
-- TooltipEntry Class
-- ============================================================================
function TooltipEntry.__init(self)
self._info = nil
self._settingKey = nil
self._settingSetValue = nil
self._settingClearValue = nil
self._settingDefaultValue = nil
self._populateFunc = nil
self._populateArg = nil
end
function TooltipEntry._Acquire(self, info, key, setValue, clearValue, defaultValue, populateFunc, populateArg)
assert(setValue == nil or setValue, "'setValue' must be truthy")
assert(info and key and populateFunc)
assert(clearValue ~= nil or defaultValue ~= nil)
self._info = info
self._settingKey = key
self._settingSetValue = setValue or true
self._settingClearValue = clearValue or false
self._settingDefaultValue = defaultValue
self._populateFunc = populateFunc
self._populateArg = populateArg
end
function TooltipEntry._Release(self)
self._info = nil
self._settingKey = nil
self._settingSetValue = nil
self._settingClearValue = nil
self._settingDefaultValue = nil
self._populateFunc = nil
self._populateArg = nil
end
function TooltipEntry.GetSettingInfo(self)
local tbl = self._info:_GetSettingsTable()
local key, key2, extra = strsplit(".", self._settingKey)
assert(key and not extra)
if key2 then
tbl = tbl[key]
key = key2
end
assert(type(tbl) == "table")
return tbl, key
end
function TooltipEntry.IsEnabled(self)
local settingTbl, settingKey = self:GetSettingInfo()
local settingValue = settingTbl[settingKey]
if settingValue == nil then
assert(self._settingDefaultValue ~= nil)
settingTbl[settingKey] = self._settingDefaultValue
settingValue = settingTbl[settingKey]
end
return settingValue == self._settingSetValue
end
function TooltipEntry.KeyMatches(self, matchStr)
return strmatch(self._settingKey, matchStr) and true or false
end
function TooltipEntry._ResetSetting(self, value)
local tbl = self._info:_GetSettingsTable()
if self._settingDefaultValue ~= nil then
tbl[self._settingKey] = self._settingDefaultValue
elseif self._settingClearValue ~= nil then
tbl[self._settingKey] = self._settingClearValue
else
error("Invalid setting info")
end
end
function TooltipEntry._Populate(self, tooltip, itemString)
self._populateFunc(tooltip, itemString, self._populateArg)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.PopulateTooltip(tooltip, itemString)
for _, info in ipairs(private.registeredInfo) do
info:_Populate(tooltip, itemString)
end
end
function private.SettingsLineIteratorHelper(_, index)
local infoIndex = floor(index / (ITER_INDEX_PART_MULTIPLE ^ 2))
local entryIndex = floor(index / ITER_INDEX_PART_MULTIPLE) % ITER_INDEX_PART_MULTIPLE
local lineIndex = index % ITER_INDEX_PART_MULTIPLE
local info, entry = nil, nil
while lineIndex >= private.settingsBuilder:GetNumLines() do
-- move to the next entry
info = private.registeredInfo[infoIndex]
entryIndex = entryIndex + 1
entry = info and info:_GetEntry(entryIndex)
if entry then
private.settingsBuilder:SetDisabled(not entry:IsEnabled())
entry:_Populate(private.settingsBuilder, ItemString.GetPlaceholder())
private.settingsBuilder:SetDisabled(false)
else
-- move to the next info
if infoIndex > 0 then
private.settingsBuilder:EndSection()
end
infoIndex = infoIndex + 1
info = private.registeredInfo[infoIndex]
if info then
private.settingsBuilder:StartSection(info._headingLeft)
entryIndex = 0
else
return
end
end
end
lineIndex = lineIndex + 1
index = infoIndex * ITER_INDEX_PART_MULTIPLE ^ 2 + entryIndex * ITER_INDEX_PART_MULTIPLE + lineIndex
local leftText, rightText, lineColor = private.settingsBuilder:GetLine(lineIndex)
assert(infoIndex < ITER_INDEX_PART_MULTIPLE and entryIndex < ITER_INDEX_PART_MULTIPLE and lineIndex < ITER_INDEX_PART_MULTIPLE)
return index, leftText, rightText, lineColor
end

View File

@ -0,0 +1,95 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Crafting = TSM.Tooltip:NewPackage("Crafting")
local L = TSM.Include("Locale").GetTable()
local ItemString = TSM.Include("Util.ItemString")
local Theme = TSM.Include("Util.Theme")
local private = {}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Crafting.OnInitialize()
TSM.Tooltip.Register(TSM.Tooltip.CreateInfo()
:SetHeadings(L["TSM Crafting"])
:SetSettingsModule("Crafting")
:AddSettingEntry("craftingCost", true, private.PopulateCostLine)
:AddSettingEntry("detailedMats", false, private.PopulateDetailedMatsLines)
:AddSettingEntry("matPrice", false, private.PopulateMatPriceLine)
)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.PopulateCostLine(tooltip, itemString)
itemString = itemString and ItemString.GetBaseFast(itemString)
assert(itemString)
local cost, profit = nil, nil
if itemString == ItemString.GetPlaceholder() then
-- example tooltip
cost = 55
profit = 20
elseif not TSM.Crafting.CanCraftItem(itemString) then
return
else
cost = TSM.Crafting.Cost.GetLowestCostByItem(itemString)
local buyout = cost and TSM.Crafting.Cost.GetCraftedItemValue(itemString) or nil
profit = buyout and (buyout - cost) or nil
end
local costText = tooltip:FormatMoney(cost)
local profitText = tooltip:FormatMoney(profit, profit and Theme.GetFeedbackColor(profit >= 0 and "GREEN" or "RED") or nil)
tooltip:AddLine(L["Crafting Cost"], format(L["%s (%s profit)"], costText, profitText))
end
function private.PopulateDetailedMatsLines(tooltip, itemString)
itemString = itemString and ItemString.GetBaseFast(itemString)
assert(itemString)
if itemString == ItemString.GetPlaceholder() then
-- example tooltip
tooltip:StartSection()
tooltip:AddSubItemValueLine(ItemString.GetPlaceholder(), 11, 5)
tooltip:EndSection()
return
elseif not TSM.Crafting.CanCraftItem(itemString) then
return
end
local _, spellId = TSM.Crafting.Cost.GetLowestCostByItem(itemString)
if not spellId then
return
end
tooltip:StartSection()
local numResult = TSM.Crafting.GetNumResult(spellId)
for _, matItemString, matQuantity in TSM.Crafting.MatIterator(spellId) do
tooltip:AddSubItemValueLine(matItemString, TSM.Crafting.Cost.GetMatCost(matItemString), matQuantity / numResult)
end
tooltip:EndSection()
end
function private.PopulateMatPriceLine(tooltip, itemString)
itemString = itemString and ItemString.GetBase(itemString) or nil
local matCost = nil
if itemString == ItemString.GetPlaceholder() then
-- example tooltip
matCost = 17
else
matCost = TSM.Crafting.Cost.GetMatCost(itemString)
end
if matCost then
tooltip:AddItemValueLine(L["Material Cost"], matCost)
end
end

View File

@ -0,0 +1,389 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local General = TSM.Tooltip:NewPackage("General")
local L = TSM.Include("Locale").GetTable()
local DisenchantInfo = TSM.Include("Data.DisenchantInfo")
local TempTable = TSM.Include("Util.TempTable")
local String = TSM.Include("Util.String")
local ItemString = TSM.Include("Util.ItemString")
local ItemInfo = TSM.Include("Service.ItemInfo")
local CustomPrice = TSM.Include("Service.CustomPrice")
local Conversions = TSM.Include("Service.Conversions")
local Inventory = TSM.Include("Service.Inventory")
local private = {
tooltipInfo = nil,
}
local DESTROY_INFO = {
{ key = "deTooltip", method = Conversions.METHOD.DISENCHANT },
{ key = "millTooltip", method = Conversions.METHOD.MILL },
{ key = "prospectTooltip", method = Conversions.METHOD.PROSPECT },
{ key = "transformTooltip", method = Conversions.METHOD.TRANSFORM },
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function General.OnInitialize()
local tooltipInfo = TSM.Tooltip.CreateInfo()
:SetHeadings(L["TSM General Info"])
private.tooltipInfo = tooltipInfo
CustomPrice.RegisterCustomSourceCallback(private.UpdateCustomSources)
-- group name
tooltipInfo:AddSettingEntry("groupNameTooltip", nil, private.PopulateGroupLine)
-- operations
for _, moduleName in TSM.Operations.ModuleIterator() do
tooltipInfo:AddSettingEntry("operationTooltips."..moduleName, false, private.PopulateOperationLine, moduleName)
end
-- destroy info
for _, info in ipairs(DESTROY_INFO) do
tooltipInfo:AddSettingEntry(info.key, nil, private.PopulateDestroyValueLine, info.method)
tooltipInfo:AddSettingEntry("detailedDestroyTooltip", nil, private.PopulateDetailLines, info.method)
end
-- vendor prices
tooltipInfo:AddSettingEntry("vendorBuyTooltip", nil, private.PopulateVendorBuyLine)
tooltipInfo:AddSettingEntry("vendorSellTooltip", nil, private.PopulateVendorSellLine)
-- custom sources
private.UpdateCustomSources()
-- inventory info
tooltipInfo:AddSettingValueEntry("inventoryTooltipFormat", "full", "none", private.PopulateFullInventoryLines)
tooltipInfo:AddSettingValueEntry("inventoryTooltipFormat", "simple", "none", private.PopulateSimpleInventoryLine)
TSM.Tooltip.Register(tooltipInfo)
end
function private.UpdateCustomSources()
private.tooltipInfo:DeleteSettingsByKeyMatch("^customPriceTooltips%.")
local customPriceSources = TempTable.Acquire()
for name in pairs(TSM.db.global.userData.customPriceSources) do
tinsert(customPriceSources, name)
end
sort(customPriceSources)
for _, name in ipairs(customPriceSources) do
private.tooltipInfo:AddSettingEntry("customPriceTooltips."..name, false, private.PopulateCustomPriceLine, name)
end
TempTable.Release(customPriceSources)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.PopulateGroupLine(tooltip, itemString)
-- add group / operation info
local groupPath, itemInGroup = nil, nil
if itemString == ItemString.GetPlaceholder() then
-- example tooltip
groupPath = L["Example"]
itemInGroup = true
else
groupPath = TSM.Groups.GetPathByItem(itemString)
if groupPath == TSM.CONST.ROOT_GROUP_PATH then
groupPath = nil
else
itemInGroup = TSM.Groups.IsItemInGroup(itemString)
end
end
if groupPath then
local leftText = itemInGroup and GROUP or (GROUP.." ("..L["Base Item"]..")")
tooltip:AddTextLine(leftText, TSM.Groups.Path.Format(groupPath))
end
end
function private.PopulateOperationLine(tooltip, itemString, moduleName)
assert(moduleName)
local operations = TempTable.Acquire()
if itemString == ItemString.GetPlaceholder() then
-- example tooltip
tinsert(operations, L["Example"])
else
local groupPath = TSM.Groups.GetPathByItem(itemString)
if groupPath == TSM.CONST.ROOT_GROUP_PATH then
groupPath = nil
end
if not groupPath then
TempTable.Release(operations)
return
end
for _, operationName in TSM.Operations.GroupOperationIterator(moduleName, groupPath) do
tinsert(operations, operationName)
end
end
if #operations > 0 then
tooltip:AddLine(format(#operations == 1 and L["%s operation"] or L["%s operations"], TSM.Operations.GetLocalizedName(moduleName)), tooltip:ApplyValueColor(table.concat(operations, ", ")))
end
TempTable.Release(operations)
end
function private.PopulateDestroyValueLine(tooltip, itemString, method)
local value = nil
if itemString == ItemString.GetPlaceholder() then
-- example tooltip
if method == Conversions.METHOD.DISENCHANT then
value = 10
elseif method == Conversions.METHOD.MILL then
value = 50
elseif method == Conversions.METHOD.PROSPECT then
value = 20
elseif method == Conversions.METHOD.TRANSFORM then
value = 30
else
error("Invalid method: "..tostring(method))
end
else
value = CustomPrice.GetConversionsValue(itemString, TSM.db.global.coreOptions.destroyValueSource, method)
end
if not value then
return
end
local label = nil
if method == Conversions.METHOD.DISENCHANT then
label = L["Disenchant Value"]
elseif method == Conversions.METHOD.MILL then
label = L["Mill Value"]
elseif method == Conversions.METHOD.PROSPECT then
label = L["Prospect Value"]
elseif method == Conversions.METHOD.TRANSFORM then
label = L["Transform Value"]
else
error("Invalid method: "..tostring(method))
end
tooltip:AddItemValueLine(label, value)
end
function private.PopulateDetailLines(tooltip, itemString, method)
if itemString == ItemString.GetPlaceholder() then
-- example tooltip
tooltip:StartSection()
if method == Conversions.METHOD.DISENCHANT then
tooltip:AddSubItemValueLine(ItemString.GetPlaceholder(), 1, 10, 1, 1, 20)
elseif method == Conversions.METHOD.MILL then
tooltip:AddSubItemValueLine(ItemString.GetPlaceholder(), 5, 10, 1)
elseif method == Conversions.METHOD.PROSPECT then
tooltip:AddSubItemValueLine(ItemString.GetPlaceholder(), 2, 10, 1, 1, 20)
elseif method == Conversions.METHOD.TRANSFORM then
tooltip:AddSubItemValueLine(ItemString.GetPlaceholder(), 3, 10, 1)
else
error("Invalid method: "..tostring(method))
end
tooltip:EndSection()
return
elseif not CustomPrice.GetConversionsValue(itemString, TSM.db.global.coreOptions.destroyValueSource, method) then
return
end
tooltip:StartSection()
if method == Conversions.METHOD.DISENCHANT then
local quality = ItemInfo.GetQuality(itemString)
local ilvl = ItemInfo.GetItemLevel(ItemString.GetBase(itemString))
local classId = ItemInfo.GetClassId(itemString)
for targetItemString in DisenchantInfo.TargetItemIterator() do
local amountOfMats, matRate, minAmount, maxAmount = DisenchantInfo.GetTargetItemSourceInfo(targetItemString, classId, quality, ilvl)
if amountOfMats then
local matValue = CustomPrice.GetValue(TSM.db.global.coreOptions.destroyValueSource, targetItemString) or 0
if matValue > 0 then
tooltip:AddSubItemValueLine(targetItemString, matValue, amountOfMats, matRate, minAmount, maxAmount)
end
end
end
else
for targetItemString, amountOfMats, matRate, minAmount, maxAmount in Conversions.TargetItemsByMethodIterator(itemString, method) do
local matValue = CustomPrice.GetValue(TSM.db.global.coreOptions.destroyValueSource, targetItemString) or 0
if matValue > 0 then
tooltip:AddSubItemValueLine(targetItemString, matValue, amountOfMats, matRate, minAmount, maxAmount)
end
end
end
tooltip:EndSection()
end
function private.PopulateVendorBuyLine(tooltip, itemString)
local value = nil
if itemString == ItemString.GetPlaceholder() then
-- example item
value = 50
else
value = ItemInfo.GetVendorBuy(itemString) or 0
end
if value > 0 then
tooltip:AddItemValueLine(L["Vendor Buy Price"], value)
end
end
function private.PopulateVendorSellLine(tooltip, itemString)
local value = nil
if itemString == ItemString.GetPlaceholder() then
-- example item
value = 8
else
value = ItemInfo.GetVendorSell(itemString) or 0
end
if value > 0 then
tooltip:AddItemValueLine(L["Vendor Sell Price"], value)
end
end
function private.PopulateCustomPriceLine(tooltip, itemString, name)
assert(name)
if not TSM.db.global.userData.customPriceSources[name] then
-- TODO: this custom price source has been removed (ideally shouldn't get here)
return
end
local value = nil
if itemString == ItemString.GetPlaceholder() then
-- example tooltip
value = 10
else
value = CustomPrice.GetValue(name, itemString) or 0
end
if value > 0 then
tooltip:AddItemValueLine(L["Custom Source"].." ("..name..")", value)
end
end
function private.PopulateFullInventoryLines(tooltip, itemString)
if itemString == ItemString.GetPlaceholder() then
-- example tooltip
local totalNum = 0
local playerName = UnitName("player")
local bag, bank, auction, mail, guildQuantity = 5, 4, 4, 9, 1
local playerTotal = bag + bank + auction + mail
totalNum = totalNum + playerTotal
tooltip:StartSection(L["Inventory"], format(L["%s total"], tooltip:ApplyValueColor(totalNum)))
local classColor = RAID_CLASS_COLORS[TSM.db:Get("sync", TSM.db:GetSyncScopeKeyByCharacter(UnitName("player")), "internalData", "classKey")]
local rightText = private.RightTextFormatHelper(tooltip, L["%s (%s bags, %s bank, %s AH, %s mail)"], playerTotal, bag, bank, auction, mail)
if classColor then
tooltip:AddLine("|c"..classColor.colorStr..playerName.."|r", rightText)
else
tooltip:AddLine(playerName, rightText)
end
totalNum = totalNum + guildQuantity
tooltip:AddLine(L["Example"], format(L["%s in guild vault"], tooltip:ApplyValueColor(guildQuantity)))
tooltip:EndSection()
return
end
-- calculate the total number
local totalNum = 0
for factionrealm in TSM.db:GetConnectedRealmIterator("factionrealm") do
for _, character in TSM.db:FactionrealmCharacterIterator(factionrealm) do
local bag = Inventory.GetBagQuantity(itemString, character, factionrealm)
local bank = Inventory.GetBankQuantity(itemString, character, factionrealm)
local reagentBank = Inventory.GetReagentBankQuantity(itemString, character, factionrealm)
local auction = Inventory.GetAuctionQuantity(itemString, character, factionrealm)
local mail = Inventory.GetMailQuantity(itemString, character, factionrealm)
totalNum = totalNum + bag + bank + reagentBank + auction + mail
end
end
for guildName in pairs(TSM.db.factionrealm.internalData.guildVaults) do
local guildQuantity = Inventory.GetGuildQuantity(itemString, guildName)
totalNum = totalNum + guildQuantity
end
tooltip:StartSection(L["Inventory"], format(L["%s total"], tooltip:ApplyValueColor(totalNum)))
-- add the lines
for factionrealm in TSM.db:GetConnectedRealmIterator("factionrealm") do
for _, character in TSM.db:FactionrealmCharacterIterator(factionrealm) do
local realm = strmatch(factionrealm, "^.* "..String.Escape("-").." (.*)")
if realm == GetRealmName() then
realm = ""
else
realm = " - "..realm
end
local bag = Inventory.GetBagQuantity(itemString, character, factionrealm)
local bank = Inventory.GetBankQuantity(itemString, character, factionrealm)
local reagentBank = Inventory.GetReagentBankQuantity(itemString, character, factionrealm)
local auction = Inventory.GetAuctionQuantity(itemString, character, factionrealm)
local mail = Inventory.GetMailQuantity(itemString, character, factionrealm)
local playerTotal = bag + bank + reagentBank + auction + mail
if playerTotal > 0 then
local classColor = RAID_CLASS_COLORS[TSM.db:Get("sync", TSM.db:GetSyncScopeKeyByCharacter(character, factionrealm), "internalData", "classKey")]
local rightText = private.RightTextFormatHelper(tooltip, L["%s (%s bags, %s bank, %s AH, %s mail)"], playerTotal, bag, bank + reagentBank, auction, mail)
if classColor then
tooltip:AddLine("|c"..classColor.colorStr..character..realm.."|r", rightText)
else
tooltip:AddLine(character..realm, rightText)
end
end
end
end
for guildName in pairs(TSM.db.factionrealm.internalData.guildVaults) do
local guildQuantity = Inventory.GetGuildQuantity(itemString, guildName)
if guildQuantity > 0 then
tooltip:AddLine(guildName, format(L["%s in guild vault"], tooltip:ApplyValueColor(guildQuantity)))
end
end
tooltip:EndSection()
end
function private.PopulateSimpleInventoryLine(tooltip, itemString)
if itemString == ItemString.GetPlaceholder() then
-- example tooltip
local totalPlayer, totalAlt, totalGuild, totalAuction = 18, 0, 1, 4
local totalNum2 = totalPlayer + totalAlt + totalGuild + totalAuction
local rightText2 = nil
if not TSM.IsWowClassic() then
rightText2 = private.RightTextFormatHelper(tooltip, L["%s (%s player, %s alts, %s guild, %s AH)"], totalNum2, totalPlayer, totalAlt, totalGuild, totalAuction)
else
rightText2 = private.RightTextFormatHelper(tooltip, L["%s (%s player, %s alts, %s AH)"], totalNum2, totalPlayer, totalAlt, totalAuction)
end
tooltip:AddLine(L["Inventory"], rightText2)
end
local totalPlayer, totalAlt, totalGuild, totalAuction = 0, 0, 0, 0
for factionrealm in TSM.db:GetConnectedRealmIterator("factionrealm") do
for _, character in TSM.db:FactionrealmCharacterIterator(factionrealm) do
local bag = Inventory.GetBagQuantity(itemString, character, factionrealm)
local bank = Inventory.GetBankQuantity(itemString, character, factionrealm)
local reagentBank = Inventory.GetReagentBankQuantity(itemString, character, factionrealm)
local auction = Inventory.GetAuctionQuantity(itemString, character, factionrealm)
local mail = Inventory.GetMailQuantity(itemString, character, factionrealm)
if character == UnitName("player") then
totalPlayer = totalPlayer + bag + bank + reagentBank + mail
totalAuction = totalAuction + auction
else
totalAlt = totalAlt + bag + bank + reagentBank + mail
totalAuction = totalAuction + auction
end
end
end
for guildName in pairs(TSM.db.factionrealm.internalData.guildVaults) do
totalGuild = totalGuild + Inventory.GetGuildQuantity(itemString, guildName)
end
local totalNum = totalPlayer + totalAlt + totalGuild + totalAuction
if totalNum > 0 then
local rightText = nil
if not TSM.IsWowClassic() then
rightText = private.RightTextFormatHelper(tooltip, L["%s (%s player, %s alts, %s guild, %s AH)"], totalNum, totalPlayer, totalAlt, totalGuild, totalAuction)
else
rightText = private.RightTextFormatHelper(tooltip, L["%s (%s player, %s alts, %s AH)"], totalNum, totalPlayer, totalAlt, totalAuction)
end
tooltip:AddLine(L["Inventory"], rightText)
end
end
function private.RightTextFormatHelper(tooltip, fmtStr, ...)
local parts = TempTable.Acquire(...)
for i = 1, #parts do
parts[i] = tooltip:ApplyValueColor(parts[i])
end
local result = format(fmtStr, unpack(parts))
TempTable.Release(parts)
return result
end

View File

@ -0,0 +1,44 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Shopping = TSM.Tooltip:NewPackage("Shopping")
local L = TSM.Include("Locale").GetTable()
local ItemString = TSM.Include("Util.ItemString")
local private = {}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Shopping.OnInitialize()
TSM.Tooltip.Register(TSM.Tooltip.CreateInfo()
:SetHeadings(L["TSM Shopping"])
:SetSettingsModule("Shopping")
:AddSettingEntry("maxPrice", false, private.PopulateMaxPriceLine)
)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.PopulateMaxPriceLine(tooltip, itemString)
local maxPrice = nil
if itemString == ItemString.GetPlaceholder() then
-- example tooltip
maxPrice = 37
else
maxPrice = TSM.Operations.Shopping.GetMaxPrice(itemString)
end
if maxPrice then
tooltip:AddItemValueLine(L["Max Shopping Price"], maxPrice)
end
end

View File

@ -0,0 +1,44 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Sniper = TSM.Tooltip:NewPackage("Sniper")
local L = TSM.Include("Locale").GetTable()
local ItemString = TSM.Include("Util.ItemString")
local private = {}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Sniper.OnInitialize()
TSM.Tooltip.Register(TSM.Tooltip.CreateInfo()
:SetHeadings(L["TSM Sniper"])
:SetSettingsModule("Sniper")
:AddSettingEntry("belowPrice", false, private.PopulateBelowPriceLine)
)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.PopulateBelowPriceLine(tooltip, itemString)
local belowPrice = nil
if itemString == ItemString.GetPlaceholder() then
-- example tooltip
belowPrice = 35
else
belowPrice = TSM.Operations.Sniper.GetBelowPrice(itemString)
end
if belowPrice then
tooltip:AddItemValueLine(L["Sniper Below Price"], belowPrice)
end
end

View File

@ -0,0 +1,300 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Buy = TSM.Vendoring:NewPackage("Buy")
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 TempTable = TSM.Include("Util.TempTable")
local Theme = TSM.Include("Util.Theme")
local ItemString = TSM.Include("Util.ItemString")
local ItemInfo = TSM.Include("Service.ItemInfo")
local Inventory = TSM.Include("Service.Inventory")
local private = {
merchantDB = nil,
pendingIndex = nil,
pendingQuantity = 0,
}
local FIRST_BUY_TIMEOUT = 5
local FIRST_BUY_TIMEOUT_PER_STACK = 1
local CONSECUTIVE_BUY_TIMEOUT = 5
-- ============================================================================
-- Module Functions
-- ============================================================================
function Buy.OnInitialize()
private.merchantDB = Database.NewSchema("MERCHANT")
:AddUniqueNumberField("index")
:AddStringField("itemString")
:AddSmartMapField("baseItemString", ItemString.GetBaseMap(), "itemString")
:AddNumberField("price")
:AddStringField("costItemsText")
:AddStringField("firstCostItemString")
:AddNumberField("stackSize")
:AddNumberField("numAvailable")
:Commit()
Event.Register("MERCHANT_SHOW", private.MerchantShowEventHandler)
Event.Register("MERCHANT_CLOSED", private.MerchantClosedEventHandler)
Event.Register("MERCHANT_UPDATE", private.MerchantUpdateEventHandler)
Event.Register("CHAT_MSG_LOOT", private.ChatMsgLootEventHandler)
end
function Buy.CreateMerchantQuery()
return private.merchantDB:NewQuery()
end
function Buy.NeedsRepair()
local _, needsRepair = GetRepairAllCost()
return needsRepair
end
function Buy.CanGuildRepair()
return Buy.NeedsRepair() and not TSM.IsWowClassic() and CanGuildBankRepair()
end
function Buy.DoGuildRepair()
RepairAllItems(true)
end
function Buy.DoRepair()
RepairAllItems()
end
function Buy.GetMaxCanAfford(index)
local maxCanAfford = math.huge
local _, _, price, stackSize, _, _, _, extendedCost = GetMerchantItemInfo(index)
local numAltCurrencies = GetMerchantItemCostInfo(index)
-- bug with big keech vendor returning extendedCost = true for gold only items
if numAltCurrencies == 0 then
extendedCost = false
end
-- check the price
if price > 0 then
maxCanAfford = min(floor(GetMoney() / price), maxCanAfford)
end
-- check the extended cost
if extendedCost then
assert(numAltCurrencies > 0)
for i = 1, numAltCurrencies do
local _, costNum, costItemLink, currencyName = GetMerchantItemCostItem(index, i)
local costItemString = ItemString.Get(costItemLink)
local costNumHave = nil
if costItemString then
costNumHave = Inventory.GetBagQuantity(costItemString) + Inventory.GetBankQuantity(costItemString) + Inventory.GetReagentBankQuantity(costItemString)
elseif currencyName then
if TSM.IsShadowlands() then
for j = 1, C_CurrencyInfo.GetCurrencyListSize() do
local info = C_CurrencyInfo.GetCurrencyListInfo(j)
if not info.isHeader and info.name == currencyName then
costNumHave = info.quantity
break
end
end
else
for j = 1, GetCurrencyListSize() do
local name, isHeader, _, _, _, count = GetCurrencyListInfo(j)
if not isHeader and name == currencyName then
costNumHave = count
break
end
end
end
end
if costNumHave then
maxCanAfford = min(floor(costNumHave / costNum), maxCanAfford)
end
end
end
return maxCanAfford * stackSize
end
function Buy.BuyItem(itemString, quantity)
local index = private.GetFirstIndex(itemString)
if not index then
return
end
private.BuyIndex(index, quantity)
end
function Buy.BuyItemIndex(index, quantity)
private.BuyIndex(index, quantity)
end
function Buy.CanBuyItem(itemString)
local index = private.GetFirstIndex(itemString)
return index and true or false
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.MerchantShowEventHandler()
Delay.AfterFrame("UPDATE_MERCHANT_DB", 1, private.UpdateMerchantDB)
end
function private.MerchantClosedEventHandler()
private.ClearPendingContext()
Delay.Cancel("UPDATE_MERCHANT_DB")
Delay.Cancel("RESCAN_MERCHANT_DB")
private.merchantDB:Truncate()
end
function private.MerchantUpdateEventHandler()
Delay.AfterFrame("UPDATE_MERCHANT_DB", 1, private.UpdateMerchantDB)
end
function private.UpdateMerchantDB()
local needsRetry = false
private.merchantDB:TruncateAndBulkInsertStart()
for i = 1, GetMerchantNumItems() do
local itemLink = GetMerchantItemLink(i)
local itemString = ItemString.Get(itemLink)
if itemString then
ItemInfo.StoreItemInfoByLink(itemLink)
local _, _, price, stackSize, numAvailable, _, _, extendedCost = GetMerchantItemInfo(i)
local numAltCurrencies = GetMerchantItemCostInfo(i)
-- bug with big keech vendor returning extendedCost = true for gold only items
if numAltCurrencies == 0 then
extendedCost = false
end
local costItemsText, firstCostItemString = "", ""
if extendedCost then
assert(numAltCurrencies > 0)
local costItems = TempTable.Acquire()
for j = 1, numAltCurrencies do
local _, costNum, costItemLink = GetMerchantItemCostItem(i, j)
local costItemString = ItemString.Get(costItemLink)
local texture = nil
if not costItemLink then
needsRetry = true
elseif costItemString then
firstCostItemString = firstCostItemString ~= "" and firstCostItemString or costItemString
texture = ItemInfo.GetTexture(costItemString)
elseif strmatch(costItemLink, "currency:") then
if TSM.IsShadowlands() then
texture = C_CurrencyInfo.GetCurrencyInfoFromLink(costItemLink).iconFileID
else
_, _, texture = GetCurrencyInfo(costItemLink)
end
firstCostItemString = strmatch(costItemLink, "(currency:%d+)")
else
error(format("Unknown item cost (%d, %d, %s)", i, costNum, tostring(costItemLink)))
end
if TSM.Vendoring.Buy.GetMaxCanAfford(i) < stackSize then
costNum = Theme.GetFeedbackColor("RED"):ColorText(costNum)
end
tinsert(costItems, costNum.." |T"..(texture or "")..":12|t")
end
costItemsText = table.concat(costItems, " ")
TempTable.Release(costItems)
end
private.merchantDB:BulkInsertNewRow(i, itemString, price, costItemsText, firstCostItemString, stackSize, numAvailable)
end
end
private.merchantDB:BulkInsertEnd()
if needsRetry then
Log.Err("Failed to scan merchant")
Delay.AfterTime("RESCAN_MERCHANT_DB", 0.2, private.UpdateMerchantDB)
else
Delay.Cancel("RESCAN_MERCHANT_DB")
end
end
function private.GetFirstIndex(itemString)
local index = Buy.CreateMerchantQuery()
:Equal("itemString", itemString)
:OrderBy("index", true)
:Select("index")
:GetFirstResultAndRelease()
if not index and ItemString.GetBaseFast(itemString) == itemString then
index = Buy.CreateMerchantQuery()
:Equal("baseItemString", itemString)
:OrderBy("index", true)
:Select("index")
:GetFirstResultAndRelease()
end
return index
end
function private.BuyIndex(index, quantity)
local maxStack = GetMerchantItemMaxStack(index)
quantity = min(quantity, Buy.GetMaxCanAfford(index))
if quantity == 0 then
return
end
private.ClearPendingContext()
private.pendingIndex = index
local numStacks = 0
while quantity > 0 do
local buyQuantity = min(quantity, maxStack)
BuyMerchantItem(index, buyQuantity)
private.pendingQuantity = private.pendingQuantity + buyQuantity
quantity = quantity - buyQuantity
numStacks = numStacks + 1
end
Log.Info("Buying %d of %d (%d stacks)", private.pendingQuantity, index, numStacks)
Delay.AfterTime("VENDORING_BUY_TIMEOUT", numStacks * FIRST_BUY_TIMEOUT_PER_STACK + FIRST_BUY_TIMEOUT, private.BuyTimeout)
end
function private.ChatMsgLootEventHandler(_, msg)
if not private.pendingIndex then
return
end
local link = GetMerchantItemLink(private.pendingIndex)
if not link then
Log.Err("Failed to get link (%s)", private.pendingIndex)
private.ClearPendingContext()
return
end
local quantity = nil
if msg == format(LOOT_ITEM_PUSHED_SELF, link) then
quantity = 1
else
for i = 1, GetMerchantItemMaxStack(private.pendingIndex) do
if msg == format(LOOT_ITEM_PUSHED_SELF_MULTIPLE, link, i) then
quantity = i
break
end
end
end
Log.Info("Got CHAT_MSG_LOOT(%s) with a quantity of %s (%d pending)", msg, tostring(quantity), private.pendingQuantity)
if not quantity then
return
end
private.pendingQuantity = private.pendingQuantity - quantity
if private.pendingQuantity <= 0 then
-- we're done
private.ClearPendingContext()
return
end
-- reset the timeout
Delay.Cancel("VENDORING_BUY_TIMEOUT")
Delay.AfterTime("VENDORING_BUY_TIMEOUT", CONSECUTIVE_BUY_TIMEOUT, private.BuyTimeout)
end
function private.BuyTimeout()
Log.Warn("Retrying buying (%d, %d)", private.pendingIndex, private.pendingQuantity)
Buy.BuyItemIndex(private.pendingIndex, private.pendingQuantity)
end
function private.ClearPendingContext()
private.pendingIndex = nil
private.pendingQuantity = 0
Delay.Cancel("VENDORING_BUY_TIMEOUT")
end

View File

@ -0,0 +1,74 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Buyback = TSM.Vendoring:NewPackage("Buyback")
local Database = TSM.Include("Util.Database")
local Delay = TSM.Include("Util.Delay")
local Event = TSM.Include("Util.Event")
local ItemString = TSM.Include("Util.ItemString")
local ItemInfo = TSM.Include("Service.ItemInfo")
local private = {
buybackDB = nil,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Buyback.OnInitialize()
private.buybackDB = Database.NewSchema("BUYBACK")
:AddUniqueNumberField("index")
:AddStringField("itemString")
:AddNumberField("price")
:AddNumberField("quantity")
:Commit()
Event.Register("MERCHANT_SHOW", private.MerchantShowEventHandler)
Event.Register("MERCHANT_CLOSED", private.MerchantClosedEventHandler)
Event.Register("MERCHANT_UPDATE", private.MerchantUpdateEventHandler)
end
function Buyback.CreateQuery()
return private.buybackDB:NewQuery()
:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
end
function Buyback.BuybackItem(index)
BuybackItem(index)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.MerchantShowEventHandler()
Delay.AfterFrame("UPDATE_BUYBACK_DB", 1, private.UpdateBuybackDB)
end
function private.MerchantClosedEventHandler()
Delay.Cancel("UPDATE_BUYBACK_DB")
private.buybackDB:Truncate()
end
function private.MerchantUpdateEventHandler()
Delay.AfterFrame("UPDATE_BUYBACK_DB", 1, private.UpdateBuybackDB)
end
function private.UpdateBuybackDB()
private.buybackDB:TruncateAndBulkInsertStart()
for i = 1, GetNumBuybackItems() do
local itemString = ItemString.Get(GetBuybackItemLink(i))
if itemString then
local _, _, price, quantity = GetBuybackItemInfo(i)
private.buybackDB:BulkInsertNewRow(i, itemString, price, quantity)
end
end
private.buybackDB:BulkInsertEnd()
end

View File

@ -0,0 +1,8 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
TSM:NewPackage("Vendoring")

View File

@ -0,0 +1,278 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Groups = TSM.Vendoring:NewPackage("Groups")
local L = TSM.Include("Locale").GetTable()
local Table = TSM.Include("Util.Table")
local Money = TSM.Include("Util.Money")
local SlotId = TSM.Include("Util.SlotId")
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 CustomPrice = TSM.Include("Service.CustomPrice")
local BagTracking = TSM.Include("Service.BagTracking")
local Inventory = TSM.Include("Service.Inventory")
local private = {
buyThreadId = nil,
sellThreadId = nil,
tempGroups = {},
printedBagsFullMsg = false,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Groups.OnInitialize()
private.buyThreadId = Threading.New("VENDORING_GROUP_BUY", private.BuyThread)
private.sellThreadId = Threading.New("VENDORING_GROUP_SELL", private.SellThread)
end
function Groups.BuyGroups(groups, callback)
Groups.StopBuySell()
wipe(private.tempGroups)
for _, groupPath in ipairs(groups) do
tinsert(private.tempGroups, groupPath)
end
Threading.SetCallback(private.buyThreadId, callback)
Threading.Start(private.buyThreadId, private.tempGroups)
end
function Groups.SellGroups(groups, callback)
Groups.StopBuySell()
wipe(private.tempGroups)
for _, groupPath in ipairs(groups) do
tinsert(private.tempGroups, groupPath)
end
Threading.SetCallback(private.sellThreadId, callback)
Threading.Start(private.sellThreadId, private.tempGroups)
end
function Groups.StopBuySell()
Threading.Kill(private.buyThreadId)
Threading.Kill(private.sellThreadId)
end
-- ============================================================================
-- Buy Thread
-- ============================================================================
function private.BuyThread(groups)
for _, groupPath in ipairs(groups) do
groups[groupPath] = true
end
local itemsToBuy = Threading.AcquireSafeTempTable()
local itemBuyQuantity = Threading.AcquireSafeTempTable()
local query = TSM.Vendoring.Buy.CreateMerchantQuery()
:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
:InnerJoin(TSM.Groups.GetItemDBForJoin(), "itemString")
:Select("itemString", "groupPath", "numAvailable")
for _, itemString, groupPath, numAvailable in query:Iterator() do
if groups[groupPath] then
local _, operationSettings = TSM.Operations.GetFirstOperationByItem("Vendoring", itemString)
if operationSettings.enableBuy then
local numToBuy = private.GetNumToBuy(itemString, operationSettings)
if numAvailable ~= -1 then
numToBuy = min(numToBuy, numAvailable)
end
if numToBuy > 0 then
assert(not itemBuyQuantity[itemString])
tinsert(itemsToBuy, itemString)
itemBuyQuantity[itemString] = numToBuy
end
end
end
end
query:Release()
for _, itemString in ipairs(itemsToBuy) do
local numToBuy = itemBuyQuantity[itemString]
TSM.Vendoring.Buy.BuyItem(itemString, numToBuy)
Threading.Yield(true)
end
Threading.ReleaseSafeTempTable(itemsToBuy)
Threading.ReleaseSafeTempTable(itemBuyQuantity)
end
function private.GetNumToBuy(itemString, operationSettings)
local numHave = BagTracking.CreateQueryBagsItem(itemString)
:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
:Equal("autoBaseItemString", itemString)
:Equal("isBoA", false)
:SumAndRelease("quantity") or 0
if operationSettings.restockSources.bank then
numHave = numHave + Inventory.GetBankQuantity(itemString) + Inventory.GetReagentBankQuantity(itemString)
end
if operationSettings.restockSources.guild then
numHave = numHave + Inventory.GetGuildQuantity(itemString)
end
if operationSettings.restockSources.ah then
numHave = numHave + Inventory.GetAuctionQuantity(itemString)
end
if operationSettings.restockSources.mail then
numHave = numHave + Inventory.GetMailQuantity(itemString)
end
if operationSettings.restockSources.alts or operationSettings.restockSources.alts_ah then
local _, alts, _, altsAH = Inventory.GetPlayerTotals(itemString)
numHave = numHave + (operationSettings.restockSources.alts and alts or 0) + (operationSettings.restockSources.alts_ah and altsAH or 0)
end
return max(operationSettings.restockQty - numHave, 0)
end
-- ============================================================================
-- Sell Thread
-- ============================================================================
function private.SellThread(groups)
private.printedBagsFullMsg = false
local totalValue = 0
local operationsTemp = Threading.AcquireSafeTempTable()
for _, groupPath in ipairs(groups) do
if groupPath ~= TSM.CONST.ROOT_GROUP_PATH then
wipe(operationsTemp)
for _, operationName, operationSettings in TSM.Operations.GroupOperationIterator("Vendoring", groupPath) do
if operationSettings.enableSell then
tinsert(operationsTemp, operationName)
end
end
for _, operationName in ipairs(operationsTemp) do
for _, itemString in TSM.Groups.ItemIterator(groupPath) do
totalValue = totalValue + private.SellItemThreaded(itemString, TSM.Operations.GetSettings("Vendoring", operationName))
end
end
end
end
Threading.ReleaseSafeTempTable(operationsTemp)
if TSM.db.global.vendoringOptions.displayMoneyCollected then
Log.PrintfUser(L["Sold %s worth of items."], Money.ToString(totalValue))
end
end
function private.SellItemThreaded(itemString, operationSettings)
-- calculate the number to sell
local numHave = BagTracking.CreateQueryBagsItem(itemString)
:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
:Equal("autoBaseItemString", itemString)
:Equal("isBoA", false)
:SumAndRelease("quantity") or 0
local numToSell = numHave - operationSettings.keepQty
if numToSell <= 0 then
return 0
end
-- check the expires
if operationSettings.sellAfterExpired > 0 and TSM.Accounting.Auctions.GetNumExpiresSinceSale(itemString) < operationSettings.sellAfterExpired then
return 0
end
-- check the destroy value
local destroyValue = CustomPrice.GetValue(operationSettings.vsDestroyValue, itemString) or 0
local maxDestroyValue = CustomPrice.GetValue(operationSettings.vsMaxDestroyValue, itemString) or 0
if maxDestroyValue > 0 and destroyValue >= maxDestroyValue then
return 0
end
-- check the market value
local marketValue = CustomPrice.GetValue(operationSettings.vsMarketValue, itemString) or 0
local maxMarketValue = CustomPrice.GetValue(operationSettings.vsMaxMarketValue, itemString) or 0
if maxMarketValue > 0 and marketValue >= maxMarketValue then
return 0
end
-- get a list of empty slots which we can use to split items into
local emptySlotIds = private.GetEmptyBagSlotsThreaded(ItemString.IsItem(itemString) and GetItemFamily(ItemString.ToId(itemString)) or 0)
-- get a list of slots containing the item we want to sell
local slotIds = Threading.AcquireSafeTempTable()
local bagQuery = BagTracking.CreateQueryBagsItem(itemString)
:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
:Equal("autoBaseItemString", itemString)
:Select("slotId", "quantity")
:Equal("isBoA", false)
:OrderBy("quantity", true)
if not operationSettings.sellSoulbound then
bagQuery:Equal("isBoP", false)
end
for _, slotId in bagQuery:Iterator() do
tinsert(slotIds, slotId)
end
bagQuery:Release()
local totalValue = 0
for _, slotId in ipairs(slotIds) do
local bag, slot = SlotId.Split(slotId)
local quantity = BagTracking.GetQuantityBySlotId(slotId)
if quantity <= numToSell then
UseContainerItem(bag, slot)
totalValue = totalValue + ((ItemInfo.GetVendorSell(itemString) or 0) * quantity)
numToSell = numToSell - quantity
else
if #emptySlotIds > 0 then
local splitBag, splitSlot = SlotId.Split(tremove(emptySlotIds, 1))
SplitContainerItem(bag, slot, numToSell)
PickupContainerItem(splitBag, splitSlot)
-- wait for the stack to be split
Threading.WaitForFunction(private.BagSlotHasItem, splitBag, splitSlot)
PickupContainerItem(splitBag, splitSlot)
UseContainerItem(splitBag, splitSlot)
totalValue = totalValue + ((ItemInfo.GetVendorSell(itemString) or 0) * quantity)
elseif not private.printedBagsFullMsg then
Log.PrintUser(L["Could not sell items due to not having free bag space available to split a stack of items."])
private.printedBagsFullMsg = true
end
-- we're done
numToSell = 0
end
if numToSell == 0 then
break
end
Threading.Yield(true)
end
Threading.ReleaseSafeTempTable(slotIds)
Threading.ReleaseSafeTempTable(emptySlotIds)
return totalValue
end
function private.GetEmptyBagSlotsThreaded(itemFamily)
local emptySlotIds = Threading.AcquireSafeTempTable()
local sortvalue = Threading.AcquireSafeTempTable()
for bag = 0, NUM_BAG_SLOTS do
-- make sure the item can go in this bag
local bagFamily = bag ~= 0 and GetItemFamily(GetInventoryItemLink("player", ContainerIDToInventoryID(bag))) or 0
if bagFamily == 0 or bit.band(itemFamily, bagFamily) > 0 then
for slot = 1, GetContainerNumSlots(bag) do
if not GetContainerItemInfo(bag, slot) then
local slotId = SlotId.Join(bag, slot)
tinsert(emptySlotIds, slotId)
-- use special bags first
sortvalue[slotId] = slotId + (bagFamily > 0 and 0 or 100000)
end
end
end
Threading.Yield()
end
Table.SortWithValueLookup(emptySlotIds, sortvalue)
Threading.ReleaseSafeTempTable(sortvalue)
return emptySlotIds
end
function private.BagSlotHasItem(bag, slot)
return GetContainerItemInfo(bag, slot) and true or false
end

View File

@ -0,0 +1,168 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Sell = TSM.Vendoring:NewPackage("Sell")
local Database = TSM.Include("Util.Database")
local TempTable = TSM.Include("Util.TempTable")
local ItemString = TSM.Include("Util.ItemString")
local ItemInfo = TSM.Include("Service.ItemInfo")
local CustomPrice = TSM.Include("Service.CustomPrice")
local BagTracking = TSM.Include("Service.BagTracking")
local private = {
ignoreDB = nil,
potentialValueDB = nil,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function Sell.OnInitialize()
local used = TempTable.Acquire()
private.ignoreDB = Database.NewSchema("VENDORING_IGNORE")
:AddUniqueStringField("itemString")
:AddBooleanField("ignoreSession")
:AddBooleanField("ignorePermanent")
:Commit()
private.ignoreDB:BulkInsertStart()
for itemString in pairs(TSM.db.global.userData.vendoringIgnore) do
itemString = ItemString.Get(itemString)
if not used[itemString] then
used[itemString] = true
private.ignoreDB:BulkInsertNewRow(itemString, false, true)
end
end
private.ignoreDB:BulkInsertEnd()
TempTable.Release(used)
private.potentialValueDB = Database.NewSchema("VENDORING_POTENTIAL_VALUE")
:AddUniqueStringField("itemString")
:AddNumberField("potentialValue")
:Commit()
BagTracking.RegisterCallback(private.UpdatePotentialValueDB)
end
function Sell.IgnoreItemSession(itemString)
local row = private.ignoreDB:GetUniqueRow("itemString", itemString)
if row then
assert(not row:GetField("ignoreSession"))
row:SetField("ignoreSession", true)
row:Update()
row:Release()
else
private.ignoreDB:NewRow()
:SetField("itemString", itemString)
:SetField("ignoreSession", true)
:SetField("ignorePermanent", false)
:Create()
end
end
function Sell.IgnoreItemPermanent(itemString)
assert(not TSM.db.global.userData.vendoringIgnore[itemString])
TSM.db.global.userData.vendoringIgnore[itemString] = true
local row = private.ignoreDB:GetUniqueRow("itemString", itemString)
if row then
assert(not row:GetField("ignorePermanent"))
row:SetField("ignorePermanent", true)
row:Update()
row:Release()
else
private.ignoreDB:NewRow()
:SetField("itemString", itemString)
:SetField("ignoreSession", false)
:SetField("ignorePermanent", true)
:Create()
end
end
function Sell.ForgetIgnoreItemPermanent(itemString)
assert(TSM.db.global.userData.vendoringIgnore[itemString])
TSM.db.global.userData.vendoringIgnore[itemString] = nil
local row = private.ignoreDB:GetUniqueRow("itemString", itemString)
assert(row and row:GetField("ignorePermanent"))
if row:GetField("ignoreSession") then
row:SetField("ignorePermanent")
row:Update()
else
private.ignoreDB:DeleteRow(row)
end
row:Release()
end
function Sell.CreateIgnoreQuery()
return private.ignoreDB:NewQuery()
:Equal("ignorePermanent", true)
:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
:OrderBy("name", true)
end
function Sell.CreateBagsQuery()
local query = BagTracking.CreateQueryBags()
:Distinct("itemString")
:LeftJoin(private.ignoreDB, "itemString")
:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
:LeftJoin(private.potentialValueDB, "itemString")
:Equal("isBoP", false)
:Equal("isBoA", false)
Sell.ResetBagsQuery(query)
return query
end
function Sell.ResetBagsQuery(query)
query:ResetOrderBy()
query:ResetFilters()
BagTracking.FilterQueryBags(query)
query:NotEqual("ignoreSession", true)
:NotEqual("ignorePermanent", true)
:Equal("isBoP", false)
:Equal("isBoA", false)
:GreaterThan("vendorSell", 0)
:OrderBy("name", true)
end
function Sell.SellItem(itemString, includeSoulbound)
local query = BagTracking.CreateQueryBags()
:OrderBy("slotId", true)
:Select("bag", "slot", "itemString")
:Equal("isBoP", false)
:Equal("isBoA", false)
for _, bag, slot, bagItemString in query:Iterator() do
if itemString == bagItemString and ItemString.Get(GetContainerItemLink(bag, slot)) == itemString then
UseContainerItem(bag, slot)
end
end
query:Release()
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.UpdatePotentialValueDB()
private.potentialValueDB:TruncateAndBulkInsertStart()
local query = BagTracking.CreateQueryBags()
:OrderBy("slotId", true)
:Select("itemString")
:Distinct("itemString")
:Equal("isBoP", false)
:Equal("isBoA", false)
for _, itemString in query:Iterator() do
local value = CustomPrice.GetValue(TSM.db.global.vendoringOptions.qsMarketValue, itemString)
if value then
private.potentialValueDB:BulkInsertNewRow(itemString, value)
end
end
query:Release()
private.potentialValueDB:BulkInsertEnd()
end

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,441 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local BuyUtil = TSM.UI.AuctionUI:NewPackage("BuyUtil")
local L = TSM.Include("Locale").GetTable()
local Money = TSM.Include("Util.Money")
local Log = TSM.Include("Util.Log")
local Math = TSM.Include("Util.Math")
local Theme = TSM.Include("Util.Theme")
local ItemInfo = TSM.Include("Service.ItemInfo")
local CustomPrice = TSM.Include("Service.CustomPrice")
local UIElements = TSM.Include("UI.UIElements")
local private = {
totalBuyout = nil,
isBuy = nil,
auctionScan = nil,
subRow = nil,
index = nil,
noSeller = nil,
baseFrame = nil,
dialogFrame = nil,
future = nil,
prepareQuantity = nil,
prepareSuccess = false,
marketValueFunc = nil,
}
-- ============================================================================
-- Module Functions
-- ============================================================================
function BuyUtil.ShowConfirmation(baseFrame, subRow, isBuy, auctionNum, numFound, maxQuantity, callback, auctionScan, index, noSeller, marketValueFunc)
auctionNum = min(auctionNum, numFound)
local buyout = subRow:GetBuyouts()
if not isBuy then
buyout = subRow:GetRequiredBid(subRow)
end
local quantity = subRow:GetQuantities()
local itemString = subRow:GetItemString()
local _, _, _, isHighBidder = subRow:GetBidInfo()
local isCommodity = not TSM.IsWowClassic() and subRow:IsCommodity()
local shouldConfirm = false
if isCommodity then
shouldConfirm = true
elseif isBuy and isHighBidder then
shouldConfirm = true
elseif TSM.db.global.shoppingOptions.buyoutConfirm then
shouldConfirm = ceil(buyout / quantity) >= (CustomPrice.GetValue(TSM.db.global.shoppingOptions.buyoutAlertSource, itemString) or 0)
end
if not shouldConfirm then
return false
end
baseFrame = baseFrame:GetBaseElement()
private.isBuy = isBuy
private.auctionScan = auctionScan
private.subRow = subRow
private.index = index
private.noSeller = noSeller
private.baseFrame = baseFrame
private.marketValueFunc = marketValueFunc
if private.dialogFrame then
return true
end
local defaultQuantity = isCommodity and numFound or 1
assert(not isCommodity or isBuy)
local displayItemBuyout, displayTotalBuyout = nil, nil
if isCommodity then
displayTotalBuyout = private.CommodityResultsByQuantity(itemString, defaultQuantity)
displayItemBuyout = Math.Ceil(displayTotalBuyout / defaultQuantity, COPPER_PER_SILVER)
else
displayItemBuyout = ceil(buyout / quantity)
displayTotalBuyout = TSM.IsWowClassic() and buyout or ceil(buyout / quantity)
end
private.dialogFrame = UIElements.New("Frame", "frame")
:SetLayout("VERTICAL")
:SetSize(isCommodity and 600 or 326, isCommodity and 272 or 262)
:SetPadding(12)
:AddAnchor("CENTER")
:SetContext(callback)
:SetMouseEnabled(true)
:SetBackgroundColor("FRAME_BG", true)
:AddChild(UIElements.New("Frame", "header")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(0, 0, -4, 10)
:AddChild(UIElements.New("Spacer", "spacer")
:SetWidth(20)
)
:AddChild(UIElements.New("Text", "title")
:SetJustifyH("CENTER")
:SetFont("BODY_BODY1_BOLD")
:SetText(isCommodity and L["Order Confirmation"] or L["Buyout"])
)
:AddChild(UIElements.New("Button", "closeBtn")
:SetMargin(0, -4, 0, 0)
:SetBackgroundAndSize("iconPack.24x24/Close/Default")
:SetScript("OnClick", private.BuyoutConfirmCloseBtnOnClick)
)
)
:AddChild(UIElements.New("Frame", "content")
:SetLayout("HORIZONTAL")
:AddChild(UIElements.New("Frame", "left")
:SetLayout("VERTICAL")
:AddChild(UIElements.New("Frame", "item")
:SetLayout("HORIZONTAL")
:SetPadding(6)
:SetMargin(0, 0, 0, 16)
:SetBackgroundColor("PRIMARY_BG_ALT", true)
:AddChild(UIElements.New("Button", "icon")
:SetSize(36, 36)
:SetMargin(0, 8, 0, 0)
:SetBackground(ItemInfo.GetTexture(itemString))
:SetTooltip(itemString)
)
:AddChild(UIElements.New("Text", "name")
:SetHeight(36)
:SetFont("ITEM_BODY1")
:SetText(TSM.UI.GetColoredItemName(itemString))
)
)
:AddChildIf(isCommodity, UIElements.New("Frame", "quantity")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("Text", "desc")
:SetFont("BODY_BODY2")
:SetText(L["Quantity"]..":")
)
:AddChild(UIElements.New("Input", "input")
:SetWidth(140)
:SetJustifyH("RIGHT")
:SetBackgroundColor("PRIMARY_BG_ALT")
:SetValidateFunc("NUMBER", "1:"..maxQuantity)
:SetValue(tostring(defaultQuantity))
:SetContext(maxQuantity)
:SetScript("OnValueChanged", private.InputQtyOnValueChanged)
)
)
:AddChildIf(not isCommodity, UIElements.New("Frame", "stacks")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 0, 10)
:AddChild(UIElements.New("Text", "desc")
:SetWidth("AUTO")
:SetFont("BODY_BODY2")
:SetText(isBuy and L["Purchasing Auction"]..":" or L["Bidding Auction"]..":")
)
:AddChild(UIElements.New("Text", "number")
:SetJustifyH("RIGHT")
:SetFont("TABLE_TABLE1")
:SetText(auctionNum.."/"..numFound)
)
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Frame", "price")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 0, 10)
:AddChild(UIElements.New("Text", "desc")
:SetWidth("AUTO")
:SetFont("BODY_BODY2")
:SetText(L["Unit Price"]..":")
)
:AddChild(UIElements.New("Text", "money")
:SetJustifyH("RIGHT")
:SetFont("TABLE_TABLE1")
:SetContext(displayItemBuyout)
:SetText(private.GetUnitPriceMoneyStr(displayItemBuyout))
)
)
:AddChild(UIElements.New("Frame", "total")
:SetLayout("HORIZONTAL")
:SetHeight(20)
:SetMargin(0, 0, 0, 10)
:AddChild(UIElements.New("Text", "desc")
:SetWidth("AUTO")
:SetFont("BODY_BODY2")
:SetText(L["Total Price"]..":")
)
:AddChild(UIElements.New("Text", "money")
:SetJustifyH("RIGHT")
:SetFont("TABLE_TABLE1")
:SetText(Money.ToString(displayTotalBuyout, nil, "OPT_83_NO_COPPER"))
)
)
:AddChild(UIElements.New("Text", "warning")
:SetHeight(20)
:SetMargin(0, 0, 0, 10)
:SetFont("BODY_BODY3")
:SetJustifyH("CENTER")
:SetTextColor(Theme.GetFeedbackColor("YELLOW"))
:SetText("")
)
:AddChild(UIElements.NewNamed("ActionButton", "confirmBtn", "TSMBidBuyConfirmBtn")
:SetHeight(24)
:SetText(isCommodity and L["Buy Commodity"] or (isBuy and L["Buy Auction"] or L["Bid Auction"]))
:SetContext(not isCommodity and quantity or nil)
:SetDisabled(private.future and true or false)
:SetScript("OnClick", private.ConfirmBtnOnClick)
)
)
:AddChildIf(isCommodity, UIElements.New("Frame", "item")
:SetLayout("HORIZONTAL")
:SetWidth(266)
:SetMargin(12, 0, 0, 0)
:SetPadding(4, 4, 8, 8)
:SetBackgroundColor("PRIMARY_BG_ALT", true)
:AddChild(UIElements.New("CommodityList", "items")
:SetBackgroundColor("PRIMARY_BG_ALT")
:SetData(subRow:GetResultRow())
:SetMarketValueFunction(marketValueFunc)
:SetAlertThreshold(TSM.db.global.shoppingOptions.buyoutConfirm and (CustomPrice.GetValue(TSM.db.global.shoppingOptions.buyoutAlertSource, itemString) or 0) or nil)
:SelectQuantity(defaultQuantity)
:SetScript("OnRowClick", private.CommodityOnRowClick)
)
)
)
:SetScript("OnHide", private.DialogOnHide)
baseFrame:ShowDialogFrame(private.dialogFrame)
private.prepareQuantity = nil
private.Prepare(defaultQuantity)
return true
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.DialogOnHide()
if private.future then
private.future:Cancel()
private.future = nil
end
private.baseFrame = nil
private.dialogFrame = nil
private.isBuy = nil
private.auctionScan = nil
private.subRow = nil
private.index = nil
private.noSeller = nil
end
function private.BuyoutConfirmCloseBtnOnClick(button)
button:GetBaseElement():HideDialog()
end
function private.EnableFutureOnDone()
local result = private.future:GetValue()
private.future = nil
if not result or result ~= (private.subRow:IsCommodity() and private.totalBuyout or select(2, private.subRow:GetBuyouts())) then
-- the unit price changed
Log.PrintUser(L["Failed to buy auction."])
private.baseFrame:HideDialog()
return
end
local input = private.dialogFrame:GetElement("content.left.quantity.input")
private.prepareSuccess = tonumber(input:GetValue()) == private.prepareQuantity
private.UpdateConfirmButton()
end
function private.GetUnitPriceMoneyStr(itemBuyout)
local priceStr = Money.ToString(itemBuyout, nil, "OPT_83_NO_COPPER")
local marketValueStr = nil
local marketValue = private.marketValueFunc(private.subRow)
local pct = marketValue and marketValue > 0 and itemBuyout > 0 and Math.Round(100 * itemBuyout / marketValue) or nil
if pct then
local pctColor = Theme.GetAuctionPercentColor(pct)
if pct > 999 then
marketValueStr = pctColor:ColorText(">999%")
else
marketValueStr = pctColor:ColorText(pct.."%")
end
else
marketValueStr = "---"
end
return format("%s (%s)", priceStr, marketValueStr)
end
function private.CommodityResultsByQuantity(itemString, quantity)
local remainingQuantity = quantity
local totalPrice, maxPrice = 0, 0
for _, query in private.auctionScan:QueryIterator() do
for _, subRow in query:ItemSubRowIterator(itemString) do
if remainingQuantity > 0 then
local _, itemBuyout = subRow:GetBuyouts()
local _, numOwnerItems = subRow:GetOwnerInfo()
local quantityAvailable = subRow:GetQuantities() - numOwnerItems
local quantityToBuy = min(quantityAvailable, remainingQuantity)
totalPrice = totalPrice + (itemBuyout * quantityToBuy)
remainingQuantity = remainingQuantity - quantityToBuy
maxPrice = max(maxPrice, itemBuyout)
end
end
end
return totalPrice, maxPrice
end
function private.InputQtyOnValueChanged(input, noListUpdate)
local quantity = tonumber(input:GetValue())
input:SetValue(quantity)
local totalBuyout = private.subRow:IsCommodity() and private.CommodityResultsByQuantity(private.subRow:GetItemString(), quantity) or input:GetElement("__parent.__parent.price.money"):GetContext() * quantity
local totalQuantity = quantity
local itemBuyout = totalQuantity > 0 and Math.Ceil(totalBuyout / totalQuantity, COPPER_PER_SILVER) or 0
input:GetElement("__parent.__parent.price.money")
:SetContext(itemBuyout)
:SetText(private.GetUnitPriceMoneyStr(itemBuyout))
:Draw()
input:GetElement("__parent.__parent.total.money")
:SetText(Money.ToString(totalBuyout, nil, "OPT_83_NO_COPPER"))
:Draw()
if not noListUpdate then
input:GetElement("__parent.__parent.__parent.item.items")
:SelectQuantity(quantity)
end
if quantity ~= private.prepareQuantity then
private.prepareSuccess = false
private.prepareQuantity = nil
end
private.UpdateConfirmButton()
end
function private.CommodityOnRowClick(list, index)
local input = list:GetElement("__parent.__parent.left.quantity.input")
input:SetValue(list:GetTotalQuantity(index))
:Draw()
private.Prepare(tonumber(input:GetValue()))
private.InputQtyOnValueChanged(input, true)
end
function private.ConfirmBtnOnClick(button)
local inputQuantity = nil
if not TSM.IsWowClassic() and private.subRow:IsCommodity() then
local input = private.dialogFrame:GetElement("content.left.quantity.input")
inputQuantity = tonumber(input:GetValue())
if not private.prepareSuccess and not TSM.IsWowClassic() then
-- this is a prepare click
private.Prepare(inputQuantity)
return
end
assert(private.prepareQuantity == inputQuantity)
end
local isBuy = private.isBuy
local callbackQuantity = button:GetContext()
if callbackQuantity == nil then
assert(inputQuantity)
callbackQuantity = inputQuantity
end
local callback = button:GetElement("__parent.__parent.__parent"):GetContext()
button:GetBaseElement():HideDialog()
callback(isBuy, callbackQuantity)
end
function private.UpdateConfirmButton()
local confirmBtn = private.dialogFrame:GetElement("content.left.confirmBtn")
local text, disabled, requireManualClick = nil, false, false
if not TSM.IsWowClassic() and private.subRow:IsCommodity() then
local input = confirmBtn:GetElement("__parent.quantity.input")
local inputQuantity = tonumber(input:GetValue())
local minQuantity = 1
local maxQuantity = confirmBtn:GetElement("__parent.quantity.input"):GetContext()
local itemString = private.subRow:GetItemString()
local totalCost, maxCost = private.CommodityResultsByQuantity(itemString, inputQuantity)
local alertThreshold = TSM.db.global.shoppingOptions.buyoutConfirm and (CustomPrice.GetValue(TSM.db.global.shoppingOptions.buyoutAlertSource, itemString) or 0) or math.huge
if maxCost >= alertThreshold then
requireManualClick = true
confirmBtn:GetElement("__parent.warning")
:SetText(L["Contains auctions above your alert threshold!"])
:Draw()
else
confirmBtn:GetElement("__parent.warning")
:SetText("")
:Draw()
end
if GetMoney() < totalCost then
text = L["Not Enough Money"]
disabled = true
elseif totalCost <= 0 or inputQuantity < minQuantity or inputQuantity > maxQuantity then
text = L["Invalid Quantity"]
disabled = true
elseif private.prepareSuccess or TSM.IsWowClassic() then
text = L["Buy Commodity"]
disabled = false
elseif private.prepareQuantity then
text = L["Preparing..."]
disabled = true
else
text = private.isBuy and L["Prepare Buy"] or L["Prepare Bid"]
disabled = false
end
else
if GetMoney() < confirmBtn:GetElement("__parent.price.money"):GetContext() then
text = L["Not Enough Money"]
disabled = true
else
text = private.isBuy and L["Buy Auction"] or L["Bid Auction"]
disabled = false
end
end
confirmBtn:SetText(text)
:SetDisabled(disabled)
:SetRequireManualClick(requireManualClick)
:Draw()
end
function private.Prepare(quantity)
if quantity == private.prepareQuantity then
return
end
if private.future then
private.future:Cancel()
private.future = nil
end
private.prepareQuantity = quantity
private.prepareSuccess = false
local totalBuyout = not TSM.IsWowClassic() and private.subRow:IsCommodity() and private.CommodityResultsByQuantity(private.subRow:GetItemString(), quantity) or (select(2, private.subRow:GetBuyouts()))
local totalQuantity = quantity
private.totalBuyout = totalBuyout
local itemBuyout = totalQuantity > 0 and Math.Ceil(totalBuyout / totalQuantity, COPPER_PER_SILVER)
local result, future = private.auctionScan:PrepareForBidOrBuyout(private.index, private.subRow, private.noSeller, quantity, itemBuyout)
if not result then
private.prepareQuantity = nil
return
elseif future then
private.future = future
future:SetScript("OnDone", private.EnableFutureOnDone)
end
private.UpdateConfirmButton()
end

Some files were not shown because too many files have changed in this diff Show More