TradeSkillMaster/Core/Service/Auctioning/CancelScan.lua

462 lines
19 KiB
Lua
Raw Normal View History

2020-11-13 14:13:12 -05:00
-- ------------------------------------------------------------------------------ --
-- 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