initial commit

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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