initial commit
This commit is contained in:
commit
05df49ff60
13
ChangeLog.md
Normal file
13
ChangeLog.md
Normal 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
420
Core/API.lua
Normal 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
20
Core/Const/__init.lua
Normal 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
18
Core/Development/Core.lua
Normal 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
|
||||
163
Core/Development/Profiling.lua
Normal file
163
Core/Development/Profiling.lua
Normal 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
204
Core/Lib/Addon.lua
Normal 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
188
Core/Lib/Exporter.lua
Normal 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
|
||||
235
Core/Service/Accounting/Auctions.lua
Normal file
235
Core/Service/Accounting/Auctions.lua
Normal 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
|
||||
54
Core/Service/Accounting/Core.lua
Normal file
54
Core/Service/Accounting/Core.lua
Normal 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
|
||||
56
Core/Service/Accounting/Garrison.lua
Normal file
56
Core/Service/Accounting/Garrison.lua
Normal 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
|
||||
291
Core/Service/Accounting/GoldTracker.lua
Normal file
291
Core/Service/Accounting/GoldTracker.lua
Normal 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
|
||||
387
Core/Service/Accounting/Mail.lua
Normal file
387
Core/Service/Accounting/Mail.lua
Normal 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
|
||||
133
Core/Service/Accounting/Merchant.lua
Normal file
133
Core/Service/Accounting/Merchant.lua
Normal 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
|
||||
171
Core/Service/Accounting/Money.lua
Normal file
171
Core/Service/Accounting/Money.lua
Normal 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
|
||||
287
Core/Service/Accounting/Sync.lua
Normal file
287
Core/Service/Accounting/Sync.lua
Normal 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
|
||||
165
Core/Service/Accounting/Trade.lua
Normal file
165
Core/Service/Accounting/Trade.lua
Normal 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
|
||||
825
Core/Service/Accounting/Transactions.lua
Normal file
825
Core/Service/Accounting/Transactions.lua
Normal 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
|
||||
571
Core/Service/AuctionDB/Core.lua
Normal file
571
Core/Service/AuctionDB/Core.lua
Normal 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
|
||||
461
Core/Service/Auctioning/CancelScan.lua
Normal file
461
Core/Service/Auctioning/CancelScan.lua
Normal 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
|
||||
8
Core/Service/Auctioning/Core.lua
Normal file
8
Core/Service/Auctioning/Core.lua
Normal file
@ -0,0 +1,8 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
TSM:NewPackage("Auctioning")
|
||||
142
Core/Service/Auctioning/Log.lua
Normal file
142
Core/Service/Auctioning/Log.lua
Normal 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
|
||||
965
Core/Service/Auctioning/PostScan.lua
Normal file
965
Core/Service/Auctioning/PostScan.lua
Normal 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
|
||||
204
Core/Service/Auctioning/SavedSearches.lua
Normal file
204
Core/Service/Auctioning/SavedSearches.lua
Normal 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
|
||||
326
Core/Service/Auctioning/Util.lua
Normal file
326
Core/Service/Auctioning/Util.lua
Normal 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
|
||||
123
Core/Service/Banking/Auctioning.lua
Normal file
123
Core/Service/Banking/Auctioning.lua
Normal 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
|
||||
321
Core/Service/Banking/Core.lua
Normal file
321
Core/Service/Banking/Core.lua
Normal 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
|
||||
105
Core/Service/Banking/Mailing.lua
Normal file
105
Core/Service/Banking/Mailing.lua
Normal 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
|
||||
292
Core/Service/Banking/MoveContext.lua
Normal file
292
Core/Service/Banking/MoveContext.lua
Normal 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
|
||||
115
Core/Service/Banking/Util.lua
Normal file
115
Core/Service/Banking/Util.lua
Normal 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
|
||||
92
Core/Service/Banking/Warehousing.lua
Normal file
92
Core/Service/Banking/Warehousing.lua
Normal 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
|
||||
776
Core/Service/Crafting/Core.lua
Normal file
776
Core/Service/Crafting/Core.lua
Normal 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
|
||||
156
Core/Service/Crafting/Cost.lua
Normal file
156
Core/Service/Crafting/Cost.lua
Normal 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
|
||||
525
Core/Service/Crafting/Gathering.lua
Normal file
525
Core/Service/Crafting/Gathering.lua
Normal 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
|
||||
303
Core/Service/Crafting/PlayerProfessions.lua
Normal file
303
Core/Service/Crafting/PlayerProfessions.lua
Normal 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
|
||||
554
Core/Service/Crafting/ProfessionScanner.lua
Normal file
554
Core/Service/Crafting/ProfessionScanner.lua
Normal 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
|
||||
191
Core/Service/Crafting/ProfessionState.lua
Normal file
191
Core/Service/Crafting/ProfessionState.lua
Normal 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
|
||||
480
Core/Service/Crafting/ProfessionUtil.lua
Normal file
480
Core/Service/Crafting/ProfessionUtil.lua
Normal 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
|
||||
186
Core/Service/Crafting/Queue.lua
Normal file
186
Core/Service/Crafting/Queue.lua
Normal 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
|
||||
227
Core/Service/Crafting/Sync.lua
Normal file
227
Core/Service/Crafting/Sync.lua
Normal 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
|
||||
442
Core/Service/Destroying/Core.lua
Normal file
442
Core/Service/Destroying/Core.lua
Normal 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
|
||||
623
Core/Service/Groups/Core.lua
Normal file
623
Core/Service/Groups/Core.lua
Normal 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
|
||||
869
Core/Service/Groups/ImportExport.lua
Normal file
869
Core/Service/Groups/ImportExport.lua
Normal 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
|
||||
81
Core/Service/Groups/Path.lua
Normal file
81
Core/Service/Groups/Path.lua
Normal 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
|
||||
99
Core/Service/Groups/Sync.lua
Normal file
99
Core/Service/Groups/Sync.lua
Normal 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
|
||||
55
Core/Service/Mailing/Core.lua
Normal file
55
Core/Service/Mailing/Core.lua
Normal 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
|
||||
161
Core/Service/Mailing/Groups.lua
Normal file
161
Core/Service/Mailing/Groups.lua
Normal 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
|
||||
53
Core/Service/Mailing/Inbox.lua
Normal file
53
Core/Service/Mailing/Inbox.lua
Normal 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
|
||||
233
Core/Service/Mailing/Open.lua
Normal file
233
Core/Service/Mailing/Open.lua
Normal 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
|
||||
287
Core/Service/Mailing/Send.lua
Normal file
287
Core/Service/Mailing/Send.lua
Normal 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
|
||||
263
Core/Service/MyAuctions/Core.lua
Normal file
263
Core/Service/MyAuctions/Core.lua
Normal 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
|
||||
152
Core/Service/Operations/Auctioning.lua
Normal file
152
Core/Service/Operations/Auctioning.lua
Normal 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
|
||||
466
Core/Service/Operations/Core.lua
Normal file
466
Core/Service/Operations/Core.lua
Normal 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
|
||||
161
Core/Service/Operations/Crafting.lua
Normal file
161
Core/Service/Operations/Crafting.lua
Normal 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
|
||||
46
Core/Service/Operations/Mailing.lua
Normal file
46
Core/Service/Operations/Mailing.lua
Normal 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
|
||||
132
Core/Service/Operations/Shopping.lua
Normal file
132
Core/Service/Operations/Shopping.lua
Normal 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
|
||||
57
Core/Service/Operations/Sniper.lua
Normal file
57
Core/Service/Operations/Sniper.lua
Normal 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
|
||||
60
Core/Service/Operations/Vendoring.lua
Normal file
60
Core/Service/Operations/Vendoring.lua
Normal 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
|
||||
95
Core/Service/Operations/Warehousing.lua
Normal file
95
Core/Service/Operations/Warehousing.lua
Normal 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
|
||||
88
Core/Service/Shopping/Core.lua
Normal file
88
Core/Service/Shopping/Core.lua
Normal 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
|
||||
104
Core/Service/Shopping/DisenchantSearch.lua
Normal file
104
Core/Service/Shopping/DisenchantSearch.lua
Normal 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
|
||||
405
Core/Service/Shopping/FilterSearch.lua
Normal file
405
Core/Service/Shopping/FilterSearch.lua
Normal 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
|
||||
45
Core/Service/Shopping/GreatDealsSearch.lua
Normal file
45
Core/Service/Shopping/GreatDealsSearch.lua
Normal 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
|
||||
172
Core/Service/Shopping/GroupSearch.lua
Normal file
172
Core/Service/Shopping/GroupSearch.lua
Normal 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
|
||||
143
Core/Service/Shopping/SavedSearches.lua
Normal file
143
Core/Service/Shopping/SavedSearches.lua
Normal 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
|
||||
77
Core/Service/Shopping/SearchCommon.lua
Normal file
77
Core/Service/Shopping/SearchCommon.lua
Normal 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
|
||||
83
Core/Service/Shopping/VendorSearch.lua
Normal file
83
Core/Service/Shopping/VendorSearch.lua
Normal 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
|
||||
71
Core/Service/Sniper/BidSearch.lua
Normal file
71
Core/Service/Sniper/BidSearch.lua
Normal 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
|
||||
111
Core/Service/Sniper/BuyoutSearch.lua
Normal file
111
Core/Service/Sniper/BuyoutSearch.lua
Normal 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
|
||||
77
Core/Service/Sniper/Core.lua
Normal file
77
Core/Service/Sniper/Core.lua
Normal 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
|
||||
116
Core/Service/TaskList/Cooldowns.lua
Normal file
116
Core/Service/TaskList/Cooldowns.lua
Normal 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
|
||||
54
Core/Service/TaskList/Core.lua
Normal file
54
Core/Service/TaskList/Core.lua
Normal 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
|
||||
154
Core/Service/TaskList/Expirations.lua
Normal file
154
Core/Service/TaskList/Expirations.lua
Normal 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
|
||||
161
Core/Service/TaskList/Gathering.lua
Normal file
161
Core/Service/TaskList/Gathering.lua
Normal 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
|
||||
44
Core/Service/TaskList/Tasks/AltTask.lua
Normal file
44
Core/Service/TaskList/Tasks/AltTask.lua
Normal 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
|
||||
132
Core/Service/TaskList/Tasks/BankingTask.lua
Normal file
132
Core/Service/TaskList/Tasks/BankingTask.lua
Normal 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
|
||||
94
Core/Service/TaskList/Tasks/CooldownCraftingTask.lua
Normal file
94
Core/Service/TaskList/Tasks/CooldownCraftingTask.lua
Normal 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
|
||||
212
Core/Service/TaskList/Tasks/CraftingTask.lua
Normal file
212
Core/Service/TaskList/Tasks/CraftingTask.lua
Normal 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
|
||||
124
Core/Service/TaskList/Tasks/ExpiredAuctionTask.lua
Normal file
124
Core/Service/TaskList/Tasks/ExpiredAuctionTask.lua
Normal 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
|
||||
140
Core/Service/TaskList/Tasks/ExpiringMailTask.lua
Normal file
140
Core/Service/TaskList/Tasks/ExpiringMailTask.lua
Normal 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
|
||||
118
Core/Service/TaskList/Tasks/ItemTask.lua
Normal file
118
Core/Service/TaskList/Tasks/ItemTask.lua
Normal 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
|
||||
74
Core/Service/TaskList/Tasks/OpenMailTask.lua
Normal file
74
Core/Service/TaskList/Tasks/OpenMailTask.lua
Normal 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
|
||||
105
Core/Service/TaskList/Tasks/SendMailTask.lua
Normal file
105
Core/Service/TaskList/Tasks/SendMailTask.lua
Normal 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
|
||||
141
Core/Service/TaskList/Tasks/ShoppingTask.lua
Normal file
141
Core/Service/TaskList/Tasks/ShoppingTask.lua
Normal 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
|
||||
112
Core/Service/TaskList/Tasks/Task.lua
Normal file
112
Core/Service/TaskList/Tasks/Task.lua
Normal 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
|
||||
104
Core/Service/TaskList/Tasks/VendoringTask.lua
Normal file
104
Core/Service/TaskList/Tasks/VendoringTask.lua
Normal 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
|
||||
153
Core/Service/Tooltip/Accounting.lua
Normal file
153
Core/Service/Tooltip/Accounting.lua
Normal 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
|
||||
84
Core/Service/Tooltip/AuctionDB.lua
Normal file
84
Core/Service/Tooltip/AuctionDB.lua
Normal 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
|
||||
79
Core/Service/Tooltip/Auctioning.lua
Normal file
79
Core/Service/Tooltip/Auctioning.lua
Normal 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
|
||||
275
Core/Service/Tooltip/Core.lua
Normal file
275
Core/Service/Tooltip/Core.lua
Normal 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
|
||||
95
Core/Service/Tooltip/Crafting.lua
Normal file
95
Core/Service/Tooltip/Crafting.lua
Normal 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
|
||||
389
Core/Service/Tooltip/General.lua
Normal file
389
Core/Service/Tooltip/General.lua
Normal 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
|
||||
44
Core/Service/Tooltip/Shopping.lua
Normal file
44
Core/Service/Tooltip/Shopping.lua
Normal 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
|
||||
44
Core/Service/Tooltip/Sniper.lua
Normal file
44
Core/Service/Tooltip/Sniper.lua
Normal 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
|
||||
300
Core/Service/Vendoring/Buy.lua
Normal file
300
Core/Service/Vendoring/Buy.lua
Normal 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
|
||||
74
Core/Service/Vendoring/Buyback.lua
Normal file
74
Core/Service/Vendoring/Buyback.lua
Normal 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
|
||||
8
Core/Service/Vendoring/Core.lua
Normal file
8
Core/Service/Vendoring/Core.lua
Normal file
@ -0,0 +1,8 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
TSM:NewPackage("Vendoring")
|
||||
278
Core/Service/Vendoring/Groups.lua
Normal file
278
Core/Service/Vendoring/Groups.lua
Normal 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
|
||||
168
Core/Service/Vendoring/Sell.lua
Normal file
168
Core/Service/Vendoring/Sell.lua
Normal 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
|
||||
1758
Core/UI/AuctionUI/Auctioning.lua
Normal file
1758
Core/UI/AuctionUI/Auctioning.lua
Normal file
File diff suppressed because it is too large
Load Diff
441
Core/UI/AuctionUI/BuyUtil.lua
Normal file
441
Core/UI/AuctionUI/BuyUtil.lua
Normal 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
Loading…
x
Reference in New Issue
Block a user