initial commit
This commit is contained in:
461
Core/Service/Auctioning/CancelScan.lua
Normal file
461
Core/Service/Auctioning/CancelScan.lua
Normal file
@@ -0,0 +1,461 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local CancelScan = TSM.Auctioning:NewPackage("CancelScan")
|
||||
local L = TSM.Include("Locale").GetTable()
|
||||
local Database = TSM.Include("Util.Database")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local Threading = TSM.Include("Service.Threading")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local AuctionTracking = TSM.Include("Service.AuctionTracking")
|
||||
local AuctionHouseWrapper = TSM.Include("Service.AuctionHouseWrapper")
|
||||
local private = {
|
||||
scanThreadId = nil,
|
||||
queueDB = nil,
|
||||
itemList = {},
|
||||
usedAuctionIndex = {},
|
||||
subRowsTemp = {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function CancelScan.OnInitialize()
|
||||
-- initialize thread
|
||||
private.scanThreadId = Threading.New("CANCEL_SCAN", private.ScanThread)
|
||||
private.queueDB = Database.NewSchema("AUCTIONING_CANCEL_QUEUE")
|
||||
:AddNumberField("auctionId")
|
||||
:AddStringField("itemString")
|
||||
:AddStringField("operationName")
|
||||
:AddNumberField("bid")
|
||||
:AddNumberField("buyout")
|
||||
:AddNumberField("itemBid")
|
||||
:AddNumberField("itemBuyout")
|
||||
:AddNumberField("stackSize")
|
||||
:AddNumberField("duration")
|
||||
:AddNumberField("numStacks")
|
||||
:AddNumberField("numProcessed")
|
||||
:AddNumberField("numConfirmed")
|
||||
:AddNumberField("numFailed")
|
||||
:AddIndex("auctionId")
|
||||
:AddIndex("itemString")
|
||||
:Commit()
|
||||
end
|
||||
|
||||
function CancelScan.Prepare()
|
||||
return private.scanThreadId
|
||||
end
|
||||
|
||||
function CancelScan.GetCurrentRow()
|
||||
return private.queueDB:NewQuery()
|
||||
:Custom(private.NextProcessRowQueryHelper)
|
||||
:OrderBy("auctionId", false)
|
||||
:GetFirstResultAndRelease()
|
||||
end
|
||||
|
||||
function CancelScan.GetStatus()
|
||||
return TSM.Auctioning.Util.GetQueueStatus(private.queueDB:NewQuery())
|
||||
end
|
||||
|
||||
function CancelScan.DoProcess()
|
||||
local cancelRow = CancelScan.GetCurrentRow()
|
||||
local cancelItemString = cancelRow:GetField("itemString")
|
||||
local query = AuctionTracking.CreateQueryUnsoldItem(cancelItemString)
|
||||
:Equal("stackSize", cancelRow:GetField("stackSize"))
|
||||
:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
|
||||
:Equal("autoBaseItemString", cancelItemString)
|
||||
:Custom(private.ProcessQueryHelper, cancelRow)
|
||||
:OrderBy("auctionId", false)
|
||||
:Select("auctionId", "autoBaseItemString", "currentBid", "buyout")
|
||||
if not TSM.db.global.auctioningOptions.cancelWithBid then
|
||||
query:Equal("highBidder", "")
|
||||
end
|
||||
local auctionId, itemString, currentBid, buyout = query:GetFirstResultAndRelease()
|
||||
if auctionId then
|
||||
local result = nil
|
||||
if TSM.IsWowClassic() then
|
||||
private.usedAuctionIndex[itemString..buyout..currentBid..auctionId] = true
|
||||
CancelAuction(auctionId)
|
||||
result = true
|
||||
else
|
||||
private.usedAuctionIndex[auctionId] = true
|
||||
result = AuctionHouseWrapper.CancelAuction(auctionId)
|
||||
end
|
||||
local isRowDone = cancelRow:GetField("numProcessed") + 1 == cancelRow:GetField("numStacks")
|
||||
cancelRow:SetField("numProcessed", cancelRow:GetField("numProcessed") + 1)
|
||||
:Update()
|
||||
cancelRow:Release()
|
||||
if result and isRowDone then
|
||||
-- update the log
|
||||
TSM.Auctioning.Log.UpdateRowByIndex(auctionId, "state", "CANCELLED")
|
||||
end
|
||||
return result, false
|
||||
end
|
||||
|
||||
-- we couldn't find this item, so mark this cancel as failed and we'll try again later
|
||||
cancelRow:SetField("numProcessed", cancelRow:GetField("numProcessed") + 1)
|
||||
:Update()
|
||||
cancelRow:Release()
|
||||
return false, false
|
||||
end
|
||||
|
||||
function CancelScan.DoSkip()
|
||||
local cancelRow = CancelScan.GetCurrentRow()
|
||||
local auctionId = cancelRow:GetField("auctionId")
|
||||
cancelRow:SetField("numProcessed", cancelRow:GetField("numProcessed") + 1)
|
||||
:SetField("numConfirmed", cancelRow:GetField("numConfirmed") + 1)
|
||||
:Update()
|
||||
cancelRow:Release()
|
||||
-- update the log
|
||||
TSM.Auctioning.Log.UpdateRowByIndex(auctionId, "state", "SKIPPED")
|
||||
end
|
||||
|
||||
function CancelScan.HandleConfirm(success, canRetry)
|
||||
local confirmRow = private.queueDB:NewQuery()
|
||||
:Custom(private.ConfirmRowQueryHelper)
|
||||
:OrderBy("auctionId", true)
|
||||
:GetFirstResultAndRelease()
|
||||
if not confirmRow then
|
||||
-- we may have cancelled something outside of TSM
|
||||
return
|
||||
end
|
||||
|
||||
if canRetry then
|
||||
assert(not success)
|
||||
confirmRow:SetField("numFailed", confirmRow:GetField("numFailed") + 1)
|
||||
end
|
||||
confirmRow:SetField("numConfirmed", confirmRow:GetField("numConfirmed") + 1)
|
||||
:Update()
|
||||
confirmRow:Release()
|
||||
end
|
||||
|
||||
function CancelScan.PrepareFailedCancels()
|
||||
wipe(private.usedAuctionIndex)
|
||||
private.queueDB:SetQueryUpdatesPaused(true)
|
||||
local query = private.queueDB:NewQuery()
|
||||
:GreaterThan("numFailed", 0)
|
||||
for _, row in query:Iterator() do
|
||||
local numFailed, numProcessed, numConfirmed = row:GetFields("numFailed", "numProcessed", "numConfirmed")
|
||||
assert(numProcessed >= numFailed and numConfirmed >= numFailed)
|
||||
row:SetField("numFailed", 0)
|
||||
:SetField("numProcessed", numProcessed - numFailed)
|
||||
:SetField("numConfirmed", numConfirmed - numFailed)
|
||||
:Update()
|
||||
end
|
||||
query:Release()
|
||||
private.queueDB:SetQueryUpdatesPaused(false)
|
||||
end
|
||||
|
||||
function CancelScan.Reset()
|
||||
private.queueDB:Truncate()
|
||||
wipe(private.usedAuctionIndex)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Scan Thread
|
||||
-- ============================================================================
|
||||
|
||||
function private.ScanThread(auctionScan, groupList)
|
||||
auctionScan:SetScript("OnQueryDone", private.AuctionScanOnQueryDone)
|
||||
|
||||
-- generate the list of items we want to scan for
|
||||
wipe(private.itemList)
|
||||
local processedItems = TempTable.Acquire()
|
||||
local query = AuctionTracking.CreateQueryUnsold()
|
||||
:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
|
||||
:Select("autoBaseItemString")
|
||||
if not TSM.db.global.auctioningOptions.cancelWithBid then
|
||||
query:Equal("highBidder", "")
|
||||
end
|
||||
for _, itemString in query:Iterator() do
|
||||
if not processedItems[itemString] and private.CanCancelItem(itemString, groupList) then
|
||||
tinsert(private.itemList, itemString)
|
||||
end
|
||||
processedItems[itemString] = true
|
||||
end
|
||||
query:Release()
|
||||
TempTable.Release(processedItems)
|
||||
|
||||
if #private.itemList == 0 then
|
||||
return
|
||||
end
|
||||
TSM.Auctioning.SavedSearches.RecordSearch(groupList, "cancelGroups")
|
||||
|
||||
-- run the scan
|
||||
auctionScan:AddItemListQueriesThreaded(private.itemList)
|
||||
for _, query2 in auctionScan:QueryIterator() do
|
||||
query2:AddCustomFilter(private.QueryBuyoutFilter)
|
||||
end
|
||||
if not auctionScan:ScanQueriesThreaded() then
|
||||
Log.PrintUser(L["TSM failed to scan some auctions. Please rerun the scan."])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.CanCancelItem(itemString, groupList)
|
||||
local groupPath = TSM.Groups.GetPathByItem(itemString)
|
||||
if not groupPath or not tContains(groupList, groupPath) then
|
||||
return false
|
||||
end
|
||||
|
||||
local hasValidOperation, hasInvalidOperation = false, false
|
||||
for _, operationName, operationSettings in TSM.Operations.GroupOperationIterator("Auctioning", groupPath) do
|
||||
local isValid = private.IsOperationValid(itemString, operationName, operationSettings)
|
||||
if isValid == true then
|
||||
hasValidOperation = true
|
||||
elseif isValid == false then
|
||||
hasInvalidOperation = true
|
||||
else
|
||||
-- we are ignoring this operation
|
||||
assert(isValid == nil, "Invalid return value")
|
||||
end
|
||||
end
|
||||
return hasValidOperation and not hasInvalidOperation, itemString
|
||||
end
|
||||
|
||||
function private.IsOperationValid(itemString, operationName, operationSettings)
|
||||
if not operationSettings.cancelUndercut and not operationSettings.cancelRepost then
|
||||
-- canceling is disabled, so ignore this operation
|
||||
TSM.Auctioning.Log.AddEntry(itemString, operationName, "cancelDisabled", "", 0, 0)
|
||||
return nil
|
||||
end
|
||||
|
||||
local errMsg = nil
|
||||
local minPrice = TSM.Auctioning.Util.GetPrice("minPrice", operationSettings, itemString)
|
||||
local normalPrice = TSM.Auctioning.Util.GetPrice("normalPrice", operationSettings, itemString)
|
||||
local maxPrice = TSM.Auctioning.Util.GetPrice("maxPrice", operationSettings, itemString)
|
||||
local undercut = TSM.Auctioning.Util.GetPrice("undercut", operationSettings, itemString)
|
||||
local cancelRepostThreshold = TSM.Auctioning.Util.GetPrice("cancelRepostThreshold", operationSettings, itemString)
|
||||
if not minPrice then
|
||||
errMsg = format(L["Did not cancel %s because your minimum price (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.minPrice)
|
||||
elseif not maxPrice then
|
||||
errMsg = format(L["Did not cancel %s because your maximum price (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.maxPrice)
|
||||
elseif not normalPrice then
|
||||
errMsg = format(L["Did not cancel %s because your normal price (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.normalPrice)
|
||||
elseif operationSettings.cancelRepost and not cancelRepostThreshold then
|
||||
errMsg = format(L["Did not cancel %s because your cancel to repost threshold (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.cancelRepostThreshold)
|
||||
elseif not undercut then
|
||||
errMsg = format(L["Did not cancel %s because your undercut (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.undercut)
|
||||
elseif maxPrice < minPrice then
|
||||
errMsg = format(L["Did not cancel %s because your maximum price (%s) is lower than your minimum price (%s). Check your settings."], ItemInfo.GetLink(itemString), operationSettings.maxPrice, operationSettings.minPrice)
|
||||
elseif normalPrice < minPrice then
|
||||
errMsg = format(L["Did not cancel %s because your normal price (%s) is lower than your minimum price (%s). Check your settings."], ItemInfo.GetLink(itemString), operationSettings.normalPrice, operationSettings.minPrice)
|
||||
end
|
||||
|
||||
if errMsg then
|
||||
if not TSM.db.global.auctioningOptions.disableInvalidMsg then
|
||||
Log.PrintUser(errMsg)
|
||||
end
|
||||
TSM.Auctioning.Log.AddEntry(itemString, operationName, "invalidItemGroup", "", 0, 0)
|
||||
return false
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function private.QueryBuyoutFilter(_, row)
|
||||
local _, itemBuyout, minItemBuyout = row:GetBuyouts()
|
||||
return (itemBuyout and itemBuyout == 0) or (minItemBuyout and minItemBuyout == 0)
|
||||
end
|
||||
|
||||
function private.AuctionScanOnQueryDone(_, query)
|
||||
TSM.Auctioning.Log.SetQueryUpdatesPaused(true)
|
||||
for itemString in query:ItemIterator() do
|
||||
local groupPath = TSM.Groups.GetPathByItem(itemString)
|
||||
if groupPath then
|
||||
local auctionsDBQuery = AuctionTracking.CreateQueryUnsoldItem(itemString)
|
||||
:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
|
||||
:Equal("autoBaseItemString", itemString)
|
||||
:OrderBy("auctionId", false)
|
||||
for _, auctionsDBRow in auctionsDBQuery:IteratorAndRelease() do
|
||||
private.GenerateCancels(auctionsDBRow, itemString, groupPath, query)
|
||||
end
|
||||
else
|
||||
Log.Warn("Item removed from group since start of scan: %s", itemString)
|
||||
end
|
||||
end
|
||||
TSM.Auctioning.Log.SetQueryUpdatesPaused(false)
|
||||
end
|
||||
|
||||
function private.GenerateCancels(auctionsDBRow, itemString, groupPath, query)
|
||||
local isHandled = false
|
||||
for _, operationName, operationSettings in TSM.Operations.GroupOperationIterator("Auctioning", groupPath) do
|
||||
if not isHandled and private.IsOperationValid(itemString, operationName, operationSettings) then
|
||||
assert(not next(private.subRowsTemp))
|
||||
TSM.Auctioning.Util.GetFilteredSubRows(query, itemString, operationSettings, private.subRowsTemp)
|
||||
local handled, logReason, itemBuyout, seller, auctionId = private.GenerateCancel(auctionsDBRow, itemString, operationName, operationSettings, private.subRowsTemp)
|
||||
wipe(private.subRowsTemp)
|
||||
if logReason then
|
||||
seller = seller or ""
|
||||
auctionId = auctionId or 0
|
||||
TSM.Auctioning.Log.AddEntry(itemString, operationName, logReason, seller, itemBuyout, auctionId)
|
||||
end
|
||||
isHandled = isHandled or handled
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function private.GenerateCancel(auctionsDBRow, itemString, operationName, operationSettings, subRows)
|
||||
local auctionId, stackSize, currentBid, buyout, highBidder, duration = auctionsDBRow:GetFields("auctionId", "stackSize", "currentBid", "buyout", "highBidder", "duration")
|
||||
local itemBuyout = TSM.IsWowClassic() and floor(buyout / stackSize) or buyout
|
||||
local itemBid = TSM.IsWowClassic() and floor(currentBid / stackSize) or currentBid
|
||||
if TSM.IsWowClassic() and operationSettings.matchStackSize and stackSize ~= TSM.Auctioning.Util.GetPrice("stackSize", operationSettings, itemString) then
|
||||
return false
|
||||
elseif not TSM.db.global.auctioningOptions.cancelWithBid and highBidder ~= "" then
|
||||
-- Don't cancel an auction if it has a bid and we're set to not cancel those
|
||||
return true, "cancelBid", itemBuyout, nil, auctionId
|
||||
elseif not TSM.IsWowClassic() and C_AuctionHouse.GetCancelCost(auctionId) > GetMoney() then
|
||||
return true, "cancelNoMoney", itemBuyout, nil, auctionId
|
||||
end
|
||||
|
||||
local lowestAuction = TempTable.Acquire()
|
||||
if not TSM.Auctioning.Util.GetLowestAuction(subRows, itemString, operationSettings, lowestAuction) then
|
||||
TempTable.Release(lowestAuction)
|
||||
lowestAuction = nil
|
||||
end
|
||||
local minPrice = TSM.Auctioning.Util.GetPrice("minPrice", operationSettings, itemString)
|
||||
local normalPrice = TSM.Auctioning.Util.GetPrice("normalPrice", operationSettings, itemString)
|
||||
local maxPrice = TSM.Auctioning.Util.GetPrice("maxPrice", operationSettings, itemString)
|
||||
local resetPrice = TSM.Auctioning.Util.GetPrice("priceReset", operationSettings, itemString)
|
||||
local cancelRepostThreshold = TSM.Auctioning.Util.GetPrice("cancelRepostThreshold", operationSettings, itemString)
|
||||
local undercut = TSM.Auctioning.Util.GetPrice("undercut", operationSettings, itemString)
|
||||
local aboveMax = TSM.Auctioning.Util.GetPrice("aboveMax", operationSettings, itemString)
|
||||
|
||||
if not lowestAuction then
|
||||
-- all auctions which are posted (including ours) have been ignored, so check if we should cancel to repost higher
|
||||
if operationSettings.cancelRepost and normalPrice - itemBuyout > cancelRepostThreshold then
|
||||
private.AddToQueue(itemString, operationName, itemBid, itemBuyout, stackSize, duration, auctionId)
|
||||
return true, "cancelRepost", itemBuyout, nil, auctionId
|
||||
else
|
||||
return false, "cancelNotUndercut", itemBuyout
|
||||
end
|
||||
elseif lowestAuction.hasInvalidSeller then
|
||||
Log.PrintfUser(L["The seller name of the lowest auction for %s was not given by the server. Skipping this item."], ItemInfo.GetLink(itemString))
|
||||
TempTable.Release(lowestAuction)
|
||||
return false, "invalidSeller", itemBuyout
|
||||
end
|
||||
|
||||
local shouldCancel, logReason = false, nil
|
||||
local playerLowestItemBuyout, playerLowestAuctionId = TSM.Auctioning.Util.GetPlayerLowestBuyout(subRows, itemString, operationSettings)
|
||||
local secondLowestBuyout = TSM.Auctioning.Util.GetNextLowestItemBuyout(subRows, itemString, lowestAuction, operationSettings)
|
||||
local nonPlayerLowestAuctionId = not TSM.IsWowClassic() and playerLowestItemBuyout and TSM.Auctioning.Util.GetLowestNonPlayerAuctionId(subRows, itemString, operationSettings, playerLowestItemBuyout)
|
||||
if itemBuyout < minPrice and not lowestAuction.isBlacklist then
|
||||
-- this auction is below the min price
|
||||
if operationSettings.cancelRepost and resetPrice and itemBuyout < (resetPrice - cancelRepostThreshold) then
|
||||
-- canceling to post at reset price
|
||||
shouldCancel = true
|
||||
logReason = "cancelReset"
|
||||
else
|
||||
logReason = "cancelBelowMin"
|
||||
end
|
||||
elseif lowestAuction.buyout < minPrice and not lowestAuction.isBlacklist then
|
||||
-- lowest buyout is below min price, so do nothing
|
||||
logReason = "cancelBelowMin"
|
||||
elseif operationSettings.cancelUndercut and playerLowestItemBuyout and ((itemBuyout - undercut) > playerLowestItemBuyout or (not TSM.IsWowClassic() and (itemBuyout - undercut) == playerLowestItemBuyout and auctionId ~= playerLowestAuctionId and auctionId < (nonPlayerLowestAuctionId or 0))) then
|
||||
-- we've undercut this auction
|
||||
shouldCancel = true
|
||||
logReason = "cancelPlayerUndercut"
|
||||
elseif TSM.Auctioning.Util.IsPlayerOnlySeller(subRows, itemString, operationSettings) then
|
||||
-- we are the only auction
|
||||
if operationSettings.cancelRepost and (normalPrice - itemBuyout) > cancelRepostThreshold then
|
||||
-- we can repost higher
|
||||
shouldCancel = true
|
||||
logReason = "cancelRepost"
|
||||
else
|
||||
logReason = "cancelAtNormal"
|
||||
end
|
||||
elseif lowestAuction.isPlayer and secondLowestBuyout and secondLowestBuyout > maxPrice then
|
||||
-- we are posted at the aboveMax price with no competition under our max price
|
||||
if operationSettings.cancelRepost and operationSettings.aboveMax ~= "none" and (aboveMax - itemBuyout) > cancelRepostThreshold then
|
||||
-- we can repost higher
|
||||
shouldCancel = true
|
||||
logReason = "cancelRepost"
|
||||
else
|
||||
logReason = "cancelAtAboveMax"
|
||||
end
|
||||
elseif lowestAuction.isPlayer then
|
||||
-- we are the loewst auction
|
||||
if operationSettings.cancelRepost and secondLowestBuyout and ((secondLowestBuyout - undercut) - lowestAuction.buyout) > cancelRepostThreshold then
|
||||
-- we can repost higher
|
||||
shouldCancel = true
|
||||
logReason = "cancelRepost"
|
||||
else
|
||||
logReason = "cancelNotUndercut"
|
||||
end
|
||||
elseif not operationSettings.cancelUndercut then
|
||||
-- we're undercut but not canceling undercut auctions
|
||||
elseif lowestAuction.isWhitelist and itemBuyout == lowestAuction.buyout then
|
||||
-- at whitelisted player price
|
||||
logReason = "cancelAtWhitelist"
|
||||
elseif not lowestAuction.isWhitelist then
|
||||
-- we've been undercut by somebody not on our whitelist
|
||||
shouldCancel = true
|
||||
logReason = "cancelUndercut"
|
||||
elseif itemBuyout ~= lowestAuction.buyout or itemBid ~= lowestAuction.bid then
|
||||
-- somebody on our whitelist undercut us (or their bid is lower)
|
||||
shouldCancel = true
|
||||
logReason = "cancelWhitelistUndercut"
|
||||
else
|
||||
error("Should not get here")
|
||||
end
|
||||
|
||||
local seller = lowestAuction.seller
|
||||
TempTable.Release(lowestAuction)
|
||||
if shouldCancel then
|
||||
private.AddToQueue(itemString, operationName, itemBid, itemBuyout, stackSize, duration, auctionId)
|
||||
end
|
||||
return shouldCancel, logReason, itemBuyout, seller, shouldCancel and auctionId or nil
|
||||
end
|
||||
|
||||
function private.AddToQueue(itemString, operationName, itemBid, itemBuyout, stackSize, duration, auctionId)
|
||||
private.queueDB:NewRow()
|
||||
:SetField("auctionId", auctionId)
|
||||
:SetField("itemString", itemString)
|
||||
:SetField("operationName", operationName)
|
||||
:SetField("bid", itemBid * stackSize)
|
||||
:SetField("buyout", itemBuyout * stackSize)
|
||||
:SetField("itemBid", itemBid)
|
||||
:SetField("itemBuyout", itemBuyout)
|
||||
:SetField("stackSize", stackSize)
|
||||
:SetField("duration", duration)
|
||||
:SetField("numStacks", 1)
|
||||
:SetField("numProcessed", 0)
|
||||
:SetField("numConfirmed", 0)
|
||||
:SetField("numFailed", 0)
|
||||
:Create()
|
||||
end
|
||||
|
||||
function private.ProcessQueryHelper(row, cancelRow)
|
||||
if TSM.IsWowClassic() then
|
||||
local auctionId, itemString, stackSize, currentBid, buyout = row:GetFields("auctionId", "autoBaseItemString", "stackSize", "currentBid", "buyout")
|
||||
local itemBid = floor(currentBid / stackSize)
|
||||
local itemBuyout = floor(buyout / stackSize)
|
||||
return not private.usedAuctionIndex[itemString..buyout..currentBid..auctionId] and cancelRow:GetField("itemBid") == itemBid and cancelRow:GetField("itemBuyout") == itemBuyout
|
||||
else
|
||||
local auctionId = row:GetField("auctionId")
|
||||
return not private.usedAuctionIndex[auctionId] and cancelRow:GetField("auctionId") == auctionId
|
||||
end
|
||||
end
|
||||
|
||||
function private.ConfirmRowQueryHelper(row)
|
||||
return row:GetField("numConfirmed") < row:GetField("numProcessed")
|
||||
end
|
||||
|
||||
function private.NextProcessRowQueryHelper(row)
|
||||
return row:GetField("numProcessed") < row:GetField("numStacks")
|
||||
end
|
||||
8
Core/Service/Auctioning/Core.lua
Normal file
8
Core/Service/Auctioning/Core.lua
Normal file
@@ -0,0 +1,8 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
TSM:NewPackage("Auctioning")
|
||||
142
Core/Service/Auctioning/Log.lua
Normal file
142
Core/Service/Auctioning/Log.lua
Normal file
@@ -0,0 +1,142 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Log = TSM.Auctioning:NewPackage("Log")
|
||||
local L = TSM.Include("Locale").GetTable()
|
||||
local Database = TSM.Include("Util.Database")
|
||||
local Theme = TSM.Include("Util.Theme")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local private = {
|
||||
db = nil,
|
||||
}
|
||||
local REASON_INFO = {
|
||||
-- general
|
||||
invalidItemGroup = { color = "RED", str = L["Item/Group is invalid (see chat)."] },
|
||||
invalidSeller = { color = "RED", str = L["Invalid seller data returned by server."] },
|
||||
-- post scan
|
||||
postDisabled = { color = "ORANGE", str = L["Posting disabled."] },
|
||||
postNotEnough = { color = "ORANGE", str = L["Not enough items in bags."] },
|
||||
postMaxExpires = { color = "ORANGE", str = L["Above max expires."] },
|
||||
postBelowMin = { color = "ORANGE", str = L["Cheapest auction below min price."] },
|
||||
postTooMany = { color = "BLUE", str = L["Maximum amount already posted."] },
|
||||
postNormal = { color = "GREEN", str = L["Posting at normal price."] },
|
||||
postResetMin = { color = "GREEN", str = L["Below min price. Posting at min."] },
|
||||
postResetMax = { color = "GREEN", str = L["Below min price. Posting at max."] },
|
||||
postResetNormal = { color = "GREEN", str = L["Below min price. Posting at normal."] },
|
||||
postAboveMaxMin = { color = "GREEN", str = L["Above max price. Posting at min."] },
|
||||
postAboveMaxMax = { color = "GREEN", str = L["Above max price. Posting at max."] },
|
||||
postAboveMaxNormal = { color = "GREEN", str = L["Above max price. Posting at normal."] },
|
||||
postAboveMaxNoPost = { color = "ORANGE", str = L["Above max price. Not posting."] },
|
||||
postUndercut = { color = "GREEN", str = L["Undercutting competition."] },
|
||||
postPlayer = { color = "GREEN", str = L["Posting at your current price."] },
|
||||
postWhitelist = { color = "GREEN", str = L["Posting at whitelisted player's price."] },
|
||||
postWhitelistNoPost = { color = "ORANGE", str = L["Lowest auction by whitelisted player."] },
|
||||
postBlacklist = { color = "GREEN", str = L["Undercutting blacklisted player."] },
|
||||
-- cancel scan
|
||||
cancelDisabled = { color = "ORANGE", str = L["Canceling disabled."] },
|
||||
cancelNotUndercut = { color = "GREEN", str = L["Your auction has not been undercut."] },
|
||||
cancelBid = { color = "BLUE", str = L["Auction has been bid on."] },
|
||||
cancelNoMoney = { color = "BLUE", str = L["Not enough money to cancel."] },
|
||||
cancelKeepPosted = { color = "BLUE", str = L["Keeping undercut auctions posted."] },
|
||||
cancelBelowMin = { color = "ORANGE", str = L["Not canceling auction below min price."] },
|
||||
cancelAtReset = { color = "GREEN", str = L["Not canceling auction at reset price."] },
|
||||
cancelAtNormal = { color = "GREEN", str = L["At normal price and not undercut."] },
|
||||
cancelAtAboveMax = { color = "GREEN", str = L["At above max price and not undercut."] },
|
||||
cancelAtWhitelist = { color = "GREEN", str = L["Posted at whitelisted player's price."] },
|
||||
cancelUndercut = { color = "RED", str = L["You've been undercut."] },
|
||||
cancelRepost = { color = "BLUE", str = L["Canceling to repost at higher price."] },
|
||||
cancelReset = { color = "BLUE", str = L["Canceling to repost at reset price."] },
|
||||
cancelWhitelistUndercut = { color = "RED", str = L["Undercut by whitelisted player."] },
|
||||
cancelPlayerUndercut = { color = "BLUE", str = L["Canceling auction you've undercut."] },
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Log.OnInitialize()
|
||||
private.db = Database.NewSchema("AUCTIONING_LOG")
|
||||
:AddNumberField("index")
|
||||
:AddStringField("itemString")
|
||||
:AddStringField("seller")
|
||||
:AddNumberField("buyout")
|
||||
:AddStringField("operation")
|
||||
:AddStringField("reasonStr")
|
||||
:AddStringField("reasonKey")
|
||||
:AddStringField("state")
|
||||
:AddIndex("index")
|
||||
:Commit()
|
||||
end
|
||||
|
||||
function Log.Truncate()
|
||||
private.db:Truncate()
|
||||
end
|
||||
|
||||
function Log.CreateQuery()
|
||||
return private.db:NewQuery()
|
||||
:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
|
||||
:OrderBy("index", true)
|
||||
end
|
||||
|
||||
function Log.UpdateRowByIndex(index, field, value)
|
||||
local row = private.db:NewQuery()
|
||||
:Equal("index", index)
|
||||
:GetFirstResultAndRelease()
|
||||
|
||||
if field == "state" then
|
||||
assert(value == "POSTED" or value == "CANCELLED" or value == "SKIPPED")
|
||||
if not row then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
row:SetField(field, value)
|
||||
:Update()
|
||||
|
||||
row:Release()
|
||||
end
|
||||
|
||||
function Log.SetQueryUpdatesPaused(paused)
|
||||
private.db:SetQueryUpdatesPaused(paused)
|
||||
end
|
||||
|
||||
function Log.AddEntry(itemString, operationName, reasonKey, seller, buyout, index)
|
||||
private.db:NewRow()
|
||||
:SetField("itemString", itemString)
|
||||
:SetField("seller", seller)
|
||||
:SetField("buyout", buyout)
|
||||
:SetField("operation", operationName)
|
||||
:SetField("reasonStr", REASON_INFO[reasonKey].str)
|
||||
:SetField("reasonKey", reasonKey)
|
||||
:SetField("index", index)
|
||||
:SetField("state", "PENDING")
|
||||
:Create()
|
||||
end
|
||||
|
||||
function Log.GetColorFromReasonKey(reasonKey)
|
||||
return Theme.GetFeedbackColor(REASON_INFO[reasonKey].color)
|
||||
end
|
||||
|
||||
function Log.GetInfoStr(row)
|
||||
local state, reasonKey = row:GetFields("state", "reasonKey")
|
||||
local reasonInfo = REASON_INFO[reasonKey]
|
||||
local color = nil
|
||||
if state == "PENDING" then
|
||||
return Theme.GetFeedbackColor(reasonInfo.color):ColorText(reasonInfo.str)
|
||||
elseif state == "POSTED" then
|
||||
return Theme.GetColor("INDICATOR"):ColorText(L["Posted:"]).." "..reasonInfo.str
|
||||
elseif state == "CANCELLED" then
|
||||
return Theme.GetColor("INDICATOR"):ColorText(L["Cancelled:"]).." "..reasonInfo.str
|
||||
elseif state == "SKIPPED" then
|
||||
return Theme.GetColor("INDICATOR"):ColorText(L["Skipped:"]).." "..reasonInfo.str
|
||||
else
|
||||
error("Invalid state: "..tostring(state))
|
||||
end
|
||||
return color:ColorText(reasonInfo.str)
|
||||
end
|
||||
965
Core/Service/Auctioning/PostScan.lua
Normal file
965
Core/Service/Auctioning/PostScan.lua
Normal file
@@ -0,0 +1,965 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local PostScan = TSM.Auctioning:NewPackage("PostScan")
|
||||
local L = TSM.Include("Locale").GetTable()
|
||||
local Database = TSM.Include("Util.Database")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local SlotId = TSM.Include("Util.SlotId")
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local Threading = TSM.Include("Service.Threading")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local BagTracking = TSM.Include("Service.BagTracking")
|
||||
local AuctionHouseWrapper = TSM.Include("Service.AuctionHouseWrapper")
|
||||
local private = {
|
||||
scanThreadId = nil,
|
||||
queueDB = nil,
|
||||
nextQueueIndex = 1,
|
||||
bagDB = nil,
|
||||
itemList = {},
|
||||
operationDB = nil,
|
||||
debugLog = {},
|
||||
itemLocation = ItemLocation:CreateEmpty(),
|
||||
subRowsTemp = {},
|
||||
groupsQuery = nil, --luacheck: ignore 1004 - just stored for GC reasons
|
||||
operationsQuery = nil, --luacheck: ignore 1004 - just stored for GC reasons
|
||||
isAHOpen = false,
|
||||
}
|
||||
local RESET_REASON_LOOKUP = {
|
||||
minPrice = "postResetMin",
|
||||
maxPrice = "postResetMax",
|
||||
normalPrice = "postResetNormal"
|
||||
}
|
||||
local ABOVE_MAX_REASON_LOOKUP = {
|
||||
minPrice = "postAboveMaxMin",
|
||||
maxPrice = "postAboveMaxMax",
|
||||
normalPrice = "postAboveMaxNormal",
|
||||
none = "postAboveMaxNoPost"
|
||||
}
|
||||
local MAX_COMMODITY_STACKS_PER_AUCTION = 40
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function PostScan.OnInitialize()
|
||||
BagTracking.RegisterCallback(private.UpdateOperationDB)
|
||||
Event.Register("AUCTION_HOUSE_SHOW", private.AuctionHouseShowHandler)
|
||||
Event.Register("AUCTION_HOUSE_CLOSED", private.AuctionHouseClosedHandler)
|
||||
private.operationDB = Database.NewSchema("AUCTIONING_OPERATIONS")
|
||||
:AddUniqueStringField("autoBaseItemString")
|
||||
:AddStringField("firstOperation")
|
||||
:Commit()
|
||||
private.scanThreadId = Threading.New("POST_SCAN", private.ScanThread)
|
||||
private.queueDB = Database.NewSchema("AUCTIONING_POST_QUEUE")
|
||||
:AddNumberField("auctionId")
|
||||
:AddStringField("itemString")
|
||||
:AddStringField("operationName")
|
||||
:AddNumberField("bid")
|
||||
:AddNumberField("buyout")
|
||||
:AddNumberField("itemBuyout")
|
||||
:AddNumberField("stackSize")
|
||||
:AddNumberField("numStacks")
|
||||
:AddNumberField("postTime")
|
||||
:AddNumberField("numProcessed")
|
||||
:AddNumberField("numConfirmed")
|
||||
:AddNumberField("numFailed")
|
||||
:AddIndex("auctionId")
|
||||
:AddIndex("itemString")
|
||||
:Commit()
|
||||
-- We maintain our own bag database rather than using the one in BagTracking since we need to be able to remove items
|
||||
-- as they are posted, without waiting for bag update events, and control when our DB updates.
|
||||
private.bagDB = Database.NewSchema("AUCTIONING_POST_BAGS")
|
||||
:AddStringField("itemString")
|
||||
:AddNumberField("bag")
|
||||
:AddNumberField("slot")
|
||||
:AddNumberField("quantity")
|
||||
:AddUniqueNumberField("slotId")
|
||||
:AddIndex("itemString")
|
||||
:AddIndex("slotId")
|
||||
:Commit()
|
||||
-- create a groups and operations query just to register for updates
|
||||
private.groupsQuery = TSM.Groups.CreateQuery()
|
||||
:SetUpdateCallback(private.OnGroupsOperationsChanged)
|
||||
private.operationsQuery = TSM.Operations.CreateQuery()
|
||||
:SetUpdateCallback(private.OnGroupsOperationsChanged)
|
||||
end
|
||||
|
||||
function PostScan.CreateBagsQuery()
|
||||
return BagTracking.CreateQueryBagsAuctionable()
|
||||
:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
|
||||
:Distinct("autoBaseItemString")
|
||||
:LeftJoin(private.operationDB, "autoBaseItemString")
|
||||
:InnerJoin(ItemInfo.GetDBForJoin(), "itemString")
|
||||
:OrderBy("name", true)
|
||||
end
|
||||
|
||||
function PostScan.Prepare()
|
||||
return private.scanThreadId
|
||||
end
|
||||
|
||||
function PostScan.GetCurrentRow()
|
||||
return private.queueDB:NewQuery()
|
||||
:Custom(private.NextProcessRowQueryHelper)
|
||||
:OrderBy("auctionId", true)
|
||||
:GetFirstResultAndRelease()
|
||||
end
|
||||
|
||||
function PostScan.GetStatus()
|
||||
return TSM.Auctioning.Util.GetQueueStatus(private.queueDB:NewQuery())
|
||||
end
|
||||
|
||||
function PostScan.DoProcess()
|
||||
local result, noRetry = nil, false
|
||||
local postRow = PostScan.GetCurrentRow()
|
||||
local itemString, stackSize, bid, buyout, itemBuyout, postTime = postRow:GetFields("itemString", "stackSize", "bid", "buyout", "itemBuyout", "postTime")
|
||||
local bag, slot = private.GetPostBagSlot(itemString, stackSize)
|
||||
if bag then
|
||||
local _, bagQuantity = GetContainerItemInfo(bag, slot)
|
||||
Log.Info("Posting %s x %d from %d,%d (%d)", itemString, stackSize, bag, slot, bagQuantity or -1)
|
||||
if TSM.IsWowClassic() then
|
||||
-- need to set the duration in the default UI to avoid Blizzard errors
|
||||
AuctionFrameAuctions.duration = postTime
|
||||
ClearCursor()
|
||||
PickupContainerItem(bag, slot)
|
||||
ClickAuctionSellItemButton(AuctionsItemButton, "LeftButton")
|
||||
PostAuction(bid, buyout, postTime, stackSize, 1)
|
||||
ClearCursor()
|
||||
result = true
|
||||
else
|
||||
bid = Math.Round(bid / stackSize, COPPER_PER_SILVER)
|
||||
buyout = Math.Round(buyout / stackSize, COPPER_PER_SILVER)
|
||||
itemBuyout = Math.Round(itemBuyout, COPPER_PER_SILVER)
|
||||
private.itemLocation:Clear()
|
||||
private.itemLocation:SetBagAndSlot(bag, slot)
|
||||
local commodityStatus = C_AuctionHouse.GetItemCommodityStatus(private.itemLocation)
|
||||
if commodityStatus == Enum.ItemCommodityStatus.Item then
|
||||
result = AuctionHouseWrapper.PostItem(private.itemLocation, postTime, stackSize, bid < buyout and bid or nil, buyout)
|
||||
elseif commodityStatus == Enum.ItemCommodityStatus.Commodity then
|
||||
result = AuctionHouseWrapper.PostCommodity(private.itemLocation, postTime, stackSize, itemBuyout)
|
||||
else
|
||||
error("Unknown commodity status: "..tostring(itemString))
|
||||
end
|
||||
if not result then
|
||||
Log.Err("Failed to post (%s, %s, %s)", itemString, bag, slot)
|
||||
end
|
||||
end
|
||||
else
|
||||
-- we couldn't find this item, so mark this post as failed and we'll try again later
|
||||
result = false
|
||||
noRetry = slot
|
||||
if noRetry then
|
||||
Log.PrintfUser(L["Failed to post %sx%d as the item no longer exists in your bags."], ItemInfo.GetLink(itemString), stackSize)
|
||||
end
|
||||
end
|
||||
if result then
|
||||
private.DebugLogInsert(itemString, "Posting %d from %d, %d", stackSize, bag, slot)
|
||||
if postRow:GetField("numProcessed") + 1 == postRow:GetField("numStacks") then
|
||||
-- update the log
|
||||
local auctionId = postRow:GetField("auctionId")
|
||||
TSM.Auctioning.Log.UpdateRowByIndex(auctionId, "state", "POSTED")
|
||||
end
|
||||
end
|
||||
postRow:SetField("numProcessed", postRow:GetField("numProcessed") + 1)
|
||||
:Update()
|
||||
postRow:Release()
|
||||
return result, noRetry
|
||||
end
|
||||
|
||||
function PostScan.DoSkip()
|
||||
local postRow = PostScan.GetCurrentRow()
|
||||
local auctionId = postRow:GetField("auctionId")
|
||||
local numStacks = postRow:GetField("numStacks")
|
||||
postRow:SetField("numProcessed", numStacks)
|
||||
:SetField("numConfirmed", numStacks)
|
||||
:Update()
|
||||
postRow:Release()
|
||||
-- update the log
|
||||
TSM.Auctioning.Log.UpdateRowByIndex(auctionId, "state", "SKIPPED")
|
||||
end
|
||||
|
||||
function PostScan.HandleConfirm(success, canRetry)
|
||||
if not success then
|
||||
ClearCursor()
|
||||
end
|
||||
|
||||
local confirmRow = private.queueDB:NewQuery()
|
||||
:Custom(private.ConfirmRowQueryHelper)
|
||||
:OrderBy("auctionId", true)
|
||||
:GetFirstResultAndRelease()
|
||||
if not confirmRow then
|
||||
-- we may have posted something outside of TSM
|
||||
return
|
||||
end
|
||||
|
||||
private.DebugLogInsert(confirmRow:GetField("itemString"), "HandleConfirm(success=%s) x %d", tostring(success), confirmRow:GetField("stackSize"))
|
||||
if canRetry then
|
||||
assert(not success)
|
||||
confirmRow:SetField("numFailed", confirmRow:GetField("numFailed") + 1)
|
||||
end
|
||||
confirmRow:SetField("numConfirmed", confirmRow:GetField("numConfirmed") + 1)
|
||||
:Update()
|
||||
confirmRow:Release()
|
||||
end
|
||||
|
||||
function PostScan.PrepareFailedPosts()
|
||||
private.queueDB:SetQueryUpdatesPaused(true)
|
||||
local query = private.queueDB:NewQuery()
|
||||
:GreaterThan("numFailed", 0)
|
||||
:OrderBy("auctionId", true)
|
||||
for _, row in query:Iterator() do
|
||||
local numFailed, numProcessed, numConfirmed = row:GetFields("numFailed", "numProcessed", "numConfirmed")
|
||||
assert(numProcessed >= numFailed and numConfirmed >= numFailed)
|
||||
private.DebugLogInsert(row:GetField("itemString"), "Preparing failed (%d, %d, %d)", numFailed, numProcessed, numConfirmed)
|
||||
row:SetField("numFailed", 0)
|
||||
:SetField("numProcessed", numProcessed - numFailed)
|
||||
:SetField("numConfirmed", numConfirmed - numFailed)
|
||||
:Update()
|
||||
end
|
||||
query:Release()
|
||||
private.queueDB:SetQueryUpdatesPaused(false)
|
||||
private.UpdateBagDB()
|
||||
end
|
||||
|
||||
function PostScan.Reset()
|
||||
private.queueDB:Truncate()
|
||||
private.nextQueueIndex = 1
|
||||
private.bagDB:Truncate()
|
||||
end
|
||||
|
||||
function PostScan.ChangePostDetail(field, value)
|
||||
local postRow = PostScan.GetCurrentRow()
|
||||
local isCommodity = ItemInfo.IsCommodity(postRow:GetField("itemString"))
|
||||
if field == "bid" then
|
||||
assert(not isCommodity)
|
||||
value = min(max(value, 1), postRow:GetField("buyout"))
|
||||
elseif field == "buyout" then
|
||||
if not isCommodity and value < postRow:GetField("bid") then
|
||||
postRow:SetField("bid", value)
|
||||
end
|
||||
TSM.Auctioning.Log.UpdateRowByIndex(postRow:GetField("auctionId"), field, value)
|
||||
end
|
||||
postRow:SetField((field == "buyout" and isCommodity) and "itemBuyout" or field, value)
|
||||
:Update()
|
||||
postRow:Release()
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions (General)
|
||||
-- ============================================================================
|
||||
|
||||
function private.AuctionHouseShowHandler()
|
||||
private.isAHOpen = true
|
||||
private.UpdateOperationDB()
|
||||
end
|
||||
|
||||
function private.AuctionHouseClosedHandler()
|
||||
private.isAHOpen = false
|
||||
end
|
||||
|
||||
function private.OnGroupsOperationsChanged()
|
||||
Delay.AfterFrame("POST_GROUP_OPERATIONS_CHANGED", 1, private.UpdateOperationDB)
|
||||
end
|
||||
|
||||
function private.UpdateOperationDB()
|
||||
if not private.isAHOpen then
|
||||
return
|
||||
end
|
||||
private.operationDB:TruncateAndBulkInsertStart()
|
||||
local query = BagTracking.CreateQueryBagsAuctionable()
|
||||
:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
|
||||
:Select("autoBaseItemString")
|
||||
:Distinct("autoBaseItemString")
|
||||
for _, itemString in query:Iterator() do
|
||||
local firstOperation = TSM.Operations.GetFirstOperationByItem("Auctioning", itemString)
|
||||
if firstOperation then
|
||||
private.operationDB:BulkInsertNewRow(itemString, firstOperation)
|
||||
end
|
||||
end
|
||||
query:Release()
|
||||
private.operationDB:BulkInsertEnd()
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Scan Thread
|
||||
-- ============================================================================
|
||||
|
||||
function private.ScanThread(auctionScan, scanContext)
|
||||
wipe(private.debugLog)
|
||||
auctionScan:SetScript("OnQueryDone", private.AuctionScanOnQueryDone)
|
||||
private.UpdateBagDB()
|
||||
|
||||
-- get the state of the player's bags
|
||||
local bagCounts = TempTable.Acquire()
|
||||
local bagQuery = private.bagDB:NewQuery()
|
||||
:Select("itemString", "quantity")
|
||||
for _, itemString, quantity in bagQuery:Iterator() do
|
||||
bagCounts[itemString] = (bagCounts[itemString] or 0) + quantity
|
||||
end
|
||||
bagQuery:Release()
|
||||
|
||||
-- generate the list of items we want to scan for
|
||||
wipe(private.itemList)
|
||||
for itemString, numHave in pairs(bagCounts) do
|
||||
private.DebugLogInsert(itemString, "Scan thread has %d", numHave)
|
||||
local groupPath = TSM.Groups.GetPathByItem(itemString)
|
||||
local contextFilter = scanContext.isItems and itemString or groupPath
|
||||
if groupPath and tContains(scanContext, contextFilter) and private.CanPostItem(itemString, groupPath, numHave) then
|
||||
tinsert(private.itemList, itemString)
|
||||
end
|
||||
end
|
||||
TempTable.Release(bagCounts)
|
||||
if #private.itemList == 0 then
|
||||
return
|
||||
end
|
||||
-- record this search
|
||||
TSM.Auctioning.SavedSearches.RecordSearch(scanContext, scanContext.isItems and "postItems" or "postGroups")
|
||||
|
||||
-- run the scan
|
||||
auctionScan:AddItemListQueriesThreaded(private.itemList)
|
||||
for _, query in auctionScan:QueryIterator() do
|
||||
query:SetIsBrowseDoneFunction(private.QueryIsBrowseDoneFunction)
|
||||
query:AddCustomFilter(private.QueryBuyoutFilter)
|
||||
end
|
||||
if not auctionScan:ScanQueriesThreaded() then
|
||||
Log.PrintUser(L["TSM failed to scan some auctions. Please rerun the scan."])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions for Scanning
|
||||
-- ============================================================================
|
||||
|
||||
function private.UpdateBagDB()
|
||||
private.bagDB:TruncateAndBulkInsertStart()
|
||||
local query = BagTracking.CreateQueryBagsAuctionable()
|
||||
:OrderBy("slotId", true)
|
||||
:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
|
||||
:Select("slotId", "bag", "slot", "autoBaseItemString", "quantity")
|
||||
for _, slotId, bag, slot, itemString, quantity in query:Iterator() do
|
||||
private.DebugLogInsert(itemString, "Updating bag DB with %d in %d, %d", quantity, bag, slot)
|
||||
private.bagDB:BulkInsertNewRow(itemString, bag, slot, quantity, slotId)
|
||||
end
|
||||
query:Release()
|
||||
private.bagDB:BulkInsertEnd()
|
||||
end
|
||||
|
||||
function private.CanPostItem(itemString, groupPath, numHave)
|
||||
local hasValidOperation, hasInvalidOperation = false, false
|
||||
for _, operationName, operationSettings in TSM.Operations.GroupOperationIterator("Auctioning", groupPath) do
|
||||
local isValid, numUsed = private.IsOperationValid(itemString, numHave, operationName, operationSettings)
|
||||
if isValid == true then
|
||||
assert(numUsed and numUsed > 0)
|
||||
numHave = numHave - numUsed
|
||||
hasValidOperation = true
|
||||
elseif isValid == false then
|
||||
hasInvalidOperation = true
|
||||
else
|
||||
-- we are ignoring this operation
|
||||
assert(isValid == nil, "Invalid return value")
|
||||
end
|
||||
end
|
||||
|
||||
return hasValidOperation and not hasInvalidOperation
|
||||
end
|
||||
|
||||
function private.IsOperationValid(itemString, num, operationName, operationSettings)
|
||||
local postCap = TSM.Auctioning.Util.GetPrice("postCap", operationSettings, itemString)
|
||||
if not postCap then
|
||||
-- invalid postCap setting
|
||||
if not TSM.db.global.auctioningOptions.disableInvalidMsg then
|
||||
Log.PrintfUser(L["Did not post %s because your post cap (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.postCap)
|
||||
end
|
||||
TSM.Auctioning.Log.AddEntry(itemString, operationName, "invalidItemGroup", "", 0, math.huge)
|
||||
return nil
|
||||
elseif postCap == 0 then
|
||||
-- posting is disabled, so ignore this operation
|
||||
TSM.Auctioning.Log.AddEntry(itemString, operationName, "postDisabled", "", 0, math.huge)
|
||||
return nil
|
||||
end
|
||||
|
||||
local stackSize = nil
|
||||
local minPostQuantity = nil
|
||||
if not TSM.IsWowClassic() then
|
||||
minPostQuantity = 1
|
||||
else
|
||||
-- check the stack size
|
||||
stackSize = TSM.Auctioning.Util.GetPrice("stackSize", operationSettings, itemString)
|
||||
if not stackSize then
|
||||
-- invalid stackSize setting
|
||||
if not TSM.db.global.auctioningOptions.disableInvalidMsg then
|
||||
Log.PrintfUser(L["Did not post %s because your stack size (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.stackSize)
|
||||
end
|
||||
TSM.Auctioning.Log.AddEntry(itemString, operationName, "invalidItemGroup", "", 0, math.huge)
|
||||
return nil
|
||||
end
|
||||
local maxStackSize = ItemInfo.GetMaxStack(itemString)
|
||||
minPostQuantity = operationSettings.stackSizeIsCap and 1 or stackSize
|
||||
if not maxStackSize then
|
||||
-- couldn't lookup item info for this item (shouldn't happen)
|
||||
if not TSM.db.global.auctioningOptions.disableInvalidMsg then
|
||||
Log.PrintfUser(L["Did not post %s because Blizzard didn't provide all necessary information for it. Try again later."], ItemInfo.GetLink(itemString))
|
||||
end
|
||||
TSM.Auctioning.Log.AddEntry(itemString, operationName, "invalidItemGroup", "", 0, math.huge)
|
||||
return false
|
||||
elseif maxStackSize < minPostQuantity then
|
||||
-- invalid stack size
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
-- check that we have enough to post
|
||||
local keepQuantity = TSM.Auctioning.Util.GetPrice("keepQuantity", operationSettings, itemString)
|
||||
if not keepQuantity then
|
||||
-- invalid keepQuantity setting
|
||||
if not TSM.db.global.auctioningOptions.disableInvalidMsg then
|
||||
Log.PrintfUser(L["Did not post %s because your keep quantity (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.keepQuantity)
|
||||
end
|
||||
TSM.Auctioning.Log.AddEntry(itemString, operationName, "invalidItemGroup", "", 0, math.huge)
|
||||
return nil
|
||||
end
|
||||
num = num - keepQuantity
|
||||
if num < minPostQuantity then
|
||||
-- not enough items to post for this operation
|
||||
TSM.Auctioning.Log.AddEntry(itemString, operationName, "postNotEnough", "", 0, math.huge)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- check the max expires
|
||||
local maxExpires = TSM.Auctioning.Util.GetPrice("maxExpires", operationSettings, itemString)
|
||||
if not maxExpires then
|
||||
-- invalid maxExpires setting
|
||||
if not TSM.db.global.auctioningOptions.disableInvalidMsg then
|
||||
Log.PrintfUser(L["Did not post %s because your max expires (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.maxExpires)
|
||||
end
|
||||
TSM.Auctioning.Log.AddEntry(itemString, operationName, "invalidItemGroup", "", 0, math.huge)
|
||||
return nil
|
||||
end
|
||||
if maxExpires > 0 then
|
||||
local numExpires = TSM.Accounting.Auctions.GetNumExpiresSinceSale(itemString)
|
||||
if numExpires and numExpires > maxExpires then
|
||||
-- too many expires, so ignore this operation
|
||||
TSM.Auctioning.Log.AddEntry(itemString, operationName, "postMaxExpires", "", 0, math.huge)
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
local errMsg = nil
|
||||
local minPrice = TSM.Auctioning.Util.GetPrice("minPrice", operationSettings, itemString)
|
||||
local normalPrice = TSM.Auctioning.Util.GetPrice("normalPrice", operationSettings, itemString)
|
||||
local maxPrice = TSM.Auctioning.Util.GetPrice("maxPrice", operationSettings, itemString)
|
||||
local undercut = TSM.Auctioning.Util.GetPrice("undercut", operationSettings, itemString)
|
||||
if not minPrice then
|
||||
errMsg = format(L["Did not post %s because your minimum price (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.minPrice)
|
||||
elseif not maxPrice then
|
||||
errMsg = format(L["Did not post %s because your maximum price (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.maxPrice)
|
||||
elseif not normalPrice then
|
||||
errMsg = format(L["Did not post %s because your normal price (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.normalPrice)
|
||||
elseif not undercut then
|
||||
errMsg = format(L["Did not post %s because your undercut (%s) is invalid. Check your settings."], ItemInfo.GetLink(itemString), operationSettings.undercut)
|
||||
elseif normalPrice < minPrice then
|
||||
errMsg = format(L["Did not post %s because your normal price (%s) is lower than your minimum price (%s). Check your settings."], ItemInfo.GetLink(itemString), operationSettings.normalPrice, operationSettings.minPrice)
|
||||
elseif maxPrice < minPrice then
|
||||
errMsg = format(L["Did not post %s because your maximum price (%s) is lower than your minimum price (%s). Check your settings."], ItemInfo.GetLink(itemString), operationSettings.maxPrice, operationSettings.minPrice)
|
||||
end
|
||||
|
||||
if errMsg then
|
||||
if not TSM.db.global.auctioningOptions.disableInvalidMsg then
|
||||
Log.PrintUser(errMsg)
|
||||
end
|
||||
TSM.Auctioning.Log.AddEntry(itemString, operationName, "invalidItemGroup", "", 0, math.huge)
|
||||
return false
|
||||
else
|
||||
local vendorSellPrice = ItemInfo.GetVendorSell(itemString) or 0
|
||||
if vendorSellPrice > 0 and minPrice <= vendorSellPrice / 0.95 then
|
||||
-- just a warning, not an error
|
||||
Log.PrintfUser(L["WARNING: Your minimum price for %s is below its vendorsell price (with AH cut taken into account). Consider raising your minimum price, or vendoring the item."], ItemInfo.GetLink(itemString))
|
||||
end
|
||||
return true, (TSM.IsWowClassic() and stackSize or 1) * postCap
|
||||
end
|
||||
end
|
||||
|
||||
function private.QueryBuyoutFilter(_, row)
|
||||
local _, itemBuyout, minItemBuyout = row:GetBuyouts()
|
||||
return (itemBuyout and itemBuyout == 0) or (minItemBuyout and minItemBuyout == 0)
|
||||
end
|
||||
|
||||
function private.QueryIsBrowseDoneFunction(query)
|
||||
if not TSM.IsWowClassic() then
|
||||
return false
|
||||
end
|
||||
local isDone = true
|
||||
for itemString in query:ItemIterator() do
|
||||
isDone = isDone and private.QueryIsBrowseDoneForItem(query, itemString)
|
||||
end
|
||||
return isDone
|
||||
end
|
||||
|
||||
function private.QueryIsBrowseDoneForItem(query, itemString)
|
||||
local groupPath = TSM.Groups.GetPathByItem(itemString)
|
||||
if not groupPath then
|
||||
return true
|
||||
end
|
||||
local isFilterDone = true
|
||||
for _, _, operationSettings in TSM.Operations.GroupOperationIterator("Auctioning", groupPath) do
|
||||
if isFilterDone then
|
||||
local numBuyouts, minItemBuyout, maxItemBuyout = 0, nil, nil
|
||||
for _, subRow in query:ItemSubRowIterator(itemString) do
|
||||
local _, itemBuyout = subRow:GetBuyouts()
|
||||
local timeLeft = subRow:GetListingInfo()
|
||||
if itemBuyout > 0 and timeLeft > operationSettings.ignoreLowDuration then
|
||||
numBuyouts = numBuyouts + 1
|
||||
minItemBuyout = min(minItemBuyout or math.huge, itemBuyout)
|
||||
maxItemBuyout = max(maxItemBuyout or 0, itemBuyout)
|
||||
end
|
||||
end
|
||||
if numBuyouts <= 1 then
|
||||
-- there is only one distinct item buyout, so can't stop yet
|
||||
isFilterDone = false
|
||||
else
|
||||
local minPrice = TSM.Auctioning.Util.GetPrice("minPrice", operationSettings, itemString)
|
||||
local undercut = TSM.Auctioning.Util.GetPrice("undercut", operationSettings, itemString)
|
||||
if not minPrice or not undercut then
|
||||
-- the min price or undercut is not valid, so just keep scanning
|
||||
isFilterDone = false
|
||||
elseif minItemBuyout - undercut <= minPrice then
|
||||
local resetPrice = TSM.Auctioning.Util.GetPrice("priceReset", operationSettings, itemString)
|
||||
if operationSettings.priceReset == "ignore" or (resetPrice and maxItemBuyout <= resetPrice) then
|
||||
-- we need to keep scanning to handle the reset price (always keep scanning for "ignore")
|
||||
isFilterDone = false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return isFilterDone
|
||||
end
|
||||
|
||||
function private.AuctionScanOnQueryDone(_, query)
|
||||
for itemString in query:ItemIterator() do
|
||||
local groupPath = TSM.Groups.GetPathByItem(itemString)
|
||||
if groupPath then
|
||||
local numHave = 0
|
||||
local bagQuery = private.bagDB:NewQuery()
|
||||
:Select("quantity", "bag", "slot")
|
||||
:Equal("itemString", itemString)
|
||||
for _, quantity, bag, slot in bagQuery:Iterator() do
|
||||
numHave = numHave + quantity
|
||||
private.DebugLogInsert(itemString, "Filter done and have %d in %d, %d", numHave, bag, slot)
|
||||
end
|
||||
bagQuery:Release()
|
||||
|
||||
for _, operationName, operationSettings in TSM.Operations.GroupOperationIterator("Auctioning", groupPath) do
|
||||
if private.IsOperationValid(itemString, numHave, operationName, operationSettings) then
|
||||
local keepQuantity = TSM.Auctioning.Util.GetPrice("keepQuantity", operationSettings, itemString)
|
||||
assert(keepQuantity)
|
||||
local operationNumHave = numHave - keepQuantity
|
||||
if operationNumHave > 0 then
|
||||
assert(not next(private.subRowsTemp))
|
||||
TSM.Auctioning.Util.GetFilteredSubRows(query, itemString, operationSettings, private.subRowsTemp)
|
||||
local reason, numUsed, itemBuyout, seller, auctionId = private.GeneratePosts(itemString, operationName, operationSettings, operationNumHave, private.subRowsTemp)
|
||||
wipe(private.subRowsTemp)
|
||||
numHave = numHave - (numUsed or 0)
|
||||
seller = seller or ""
|
||||
auctionId = auctionId or math.huge
|
||||
TSM.Auctioning.Log.AddEntry(itemString, operationName, reason, seller, itemBuyout or 0, auctionId)
|
||||
end
|
||||
end
|
||||
end
|
||||
assert(numHave >= 0)
|
||||
else
|
||||
Log.Warn("Item removed from group since start of scan: %s", itemString)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function private.GeneratePosts(itemString, operationName, operationSettings, numHave, subRows)
|
||||
if numHave == 0 then
|
||||
return "postNotEnough"
|
||||
end
|
||||
|
||||
local perAuction, maxCanPost = nil, nil
|
||||
local postCap = TSM.Auctioning.Util.GetPrice("postCap", operationSettings, itemString)
|
||||
if not TSM.IsWowClassic() then
|
||||
perAuction = min(postCap, numHave)
|
||||
maxCanPost = 1
|
||||
else
|
||||
local stackSize = TSM.Auctioning.Util.GetPrice("stackSize", operationSettings, itemString)
|
||||
local maxStackSize = ItemInfo.GetMaxStack(itemString)
|
||||
if stackSize > maxStackSize and not operationSettings.stackSizeIsCap then
|
||||
return "postNotEnough"
|
||||
end
|
||||
|
||||
perAuction = min(stackSize, maxStackSize)
|
||||
maxCanPost = min(floor(numHave / perAuction), postCap)
|
||||
if maxCanPost == 0 then
|
||||
if operationSettings.stackSizeIsCap then
|
||||
perAuction = numHave
|
||||
maxCanPost = 1
|
||||
else
|
||||
-- not enough for single post
|
||||
return "postNotEnough"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local lowestAuction = TempTable.Acquire()
|
||||
if not TSM.Auctioning.Util.GetLowestAuction(subRows, itemString, operationSettings, lowestAuction) then
|
||||
TempTable.Release(lowestAuction)
|
||||
lowestAuction = nil
|
||||
end
|
||||
local minPrice = TSM.Auctioning.Util.GetPrice("minPrice", operationSettings, itemString)
|
||||
local normalPrice = TSM.Auctioning.Util.GetPrice("normalPrice", operationSettings, itemString)
|
||||
local maxPrice = TSM.Auctioning.Util.GetPrice("maxPrice", operationSettings, itemString)
|
||||
local undercut = TSM.Auctioning.Util.GetPrice("undercut", operationSettings, itemString)
|
||||
local resetPrice = TSM.Auctioning.Util.GetPrice("priceReset", operationSettings, itemString)
|
||||
local aboveMax = TSM.Auctioning.Util.GetPrice("aboveMax", operationSettings, itemString)
|
||||
|
||||
local reason, bid, buyout, seller, activeAuctions = nil, nil, nil, nil, 0
|
||||
if not lowestAuction then
|
||||
-- post as many as we can at the normal price
|
||||
reason = "postNormal"
|
||||
buyout = normalPrice
|
||||
elseif lowestAuction.hasInvalidSeller then
|
||||
-- we didn't get all the necessary seller info
|
||||
Log.PrintfUser(L["The seller name of the lowest auction for %s was not given by the server. Skipping this item."], ItemInfo.GetLink(itemString))
|
||||
TempTable.Release(lowestAuction)
|
||||
return "invalidSeller"
|
||||
elseif lowestAuction.isBlacklist and lowestAuction.isPlayer then
|
||||
Log.PrintfUser(L["Did not post %s because you or one of your alts (%s) is on the blacklist which is not allowed. Remove this character from your blacklist."], ItemInfo.GetLink(itemString), lowestAuction.seller)
|
||||
TempTable.Release(lowestAuction)
|
||||
return "invalidItemGroup"
|
||||
elseif lowestAuction.isBlacklist and lowestAuction.isWhitelist then
|
||||
Log.PrintfUser(L["Did not post %s because the owner of the lowest auction (%s) is on both the blacklist and whitelist which is not allowed. Adjust your settings to correct this issue."], ItemInfo.GetLink(itemString), lowestAuction.seller)
|
||||
TempTable.Release(lowestAuction)
|
||||
return "invalidItemGroup"
|
||||
elseif lowestAuction.buyout - undercut < minPrice then
|
||||
seller = lowestAuction.seller
|
||||
if resetPrice then
|
||||
-- lowest is below the min price, but there is a reset price
|
||||
assert(RESET_REASON_LOOKUP[operationSettings.priceReset], "Unexpected 'below minimum price' setting: "..tostring(operationSettings.priceReset))
|
||||
reason = RESET_REASON_LOOKUP[operationSettings.priceReset]
|
||||
buyout = resetPrice
|
||||
bid = max(bid or buyout * operationSettings.bidPercent, minPrice)
|
||||
activeAuctions = TSM.Auctioning.Util.GetPlayerAuctionCount(subRows, itemString, operationSettings, floor(bid), buyout, perAuction)
|
||||
elseif lowestAuction.isBlacklist then
|
||||
-- undercut the blacklisted player
|
||||
reason = "postBlacklist"
|
||||
buyout = lowestAuction.buyout - undercut
|
||||
else
|
||||
-- don't post this item
|
||||
TempTable.Release(lowestAuction)
|
||||
return "postBelowMin", nil, nil, seller
|
||||
end
|
||||
elseif lowestAuction.isPlayer or (lowestAuction.isWhitelist and TSM.db.global.auctioningOptions.matchWhitelist) then
|
||||
-- we (or a whitelisted play we should match) are lowest, so match the current price and post as many as we can
|
||||
activeAuctions = TSM.Auctioning.Util.GetPlayerAuctionCount(subRows, itemString, operationSettings, lowestAuction.bid, lowestAuction.buyout, perAuction)
|
||||
if lowestAuction.isPlayer then
|
||||
reason = "postPlayer"
|
||||
else
|
||||
reason = "postWhitelist"
|
||||
end
|
||||
bid = lowestAuction.bid
|
||||
buyout = lowestAuction.buyout
|
||||
seller = lowestAuction.seller
|
||||
elseif lowestAuction.isWhitelist then
|
||||
-- don't undercut a whitelisted player
|
||||
seller = lowestAuction.seller
|
||||
TempTable.Release(lowestAuction)
|
||||
return "postWhitelistNoPost", nil, nil, seller
|
||||
elseif (lowestAuction.buyout - undercut) > maxPrice then
|
||||
-- we'd be posting above the max price, so resort to the aboveMax setting
|
||||
seller = lowestAuction.seller
|
||||
if operationSettings.aboveMax == "none" then
|
||||
TempTable.Release(lowestAuction)
|
||||
return "postAboveMaxNoPost", nil, nil, seller
|
||||
end
|
||||
assert(ABOVE_MAX_REASON_LOOKUP[operationSettings.aboveMax], "Unexpected 'above max price' setting: "..tostring(operationSettings.aboveMax))
|
||||
reason = ABOVE_MAX_REASON_LOOKUP[operationSettings.aboveMax]
|
||||
buyout = aboveMax
|
||||
else
|
||||
-- we just need to do a normal undercut of the lowest auction
|
||||
reason = "postUndercut"
|
||||
buyout = lowestAuction.buyout - undercut
|
||||
seller = lowestAuction.seller
|
||||
end
|
||||
if reason == "postBlacklist" then
|
||||
bid = bid or buyout * operationSettings.bidPercent
|
||||
else
|
||||
buyout = max(buyout, minPrice)
|
||||
bid = max(bid or buyout * operationSettings.bidPercent, minPrice)
|
||||
end
|
||||
if lowestAuction then
|
||||
TempTable.Release(lowestAuction)
|
||||
end
|
||||
if TSM.IsWowClassic() then
|
||||
bid = floor(bid)
|
||||
else
|
||||
bid = max(Math.Round(bid, COPPER_PER_SILVER), COPPER_PER_SILVER)
|
||||
buyout = max(Math.Round(buyout, COPPER_PER_SILVER), COPPER_PER_SILVER)
|
||||
end
|
||||
|
||||
bid = min(bid, TSM.IsWowClassic() and MAXIMUM_BID_PRICE or MAXIMUM_BID_PRICE - 99)
|
||||
buyout = min(buyout, TSM.IsWowClassic() and MAXIMUM_BID_PRICE or MAXIMUM_BID_PRICE - 99)
|
||||
|
||||
-- check if we can't post anymore
|
||||
local queueQuery = private.queueDB:NewQuery()
|
||||
:Select("numStacks")
|
||||
:Equal("itemString", itemString)
|
||||
:Equal("stackSize", perAuction)
|
||||
:Equal("itemBuyout", buyout)
|
||||
for _, numStacks in queueQuery:Iterator() do
|
||||
activeAuctions = activeAuctions + numStacks
|
||||
end
|
||||
queueQuery:Release()
|
||||
if TSM.IsWowClassic() then
|
||||
maxCanPost = min(postCap - activeAuctions, maxCanPost)
|
||||
else
|
||||
perAuction = min(postCap - activeAuctions, perAuction)
|
||||
end
|
||||
if maxCanPost <= 0 or perAuction <= 0 then
|
||||
return "postTooMany"
|
||||
end
|
||||
|
||||
if TSM.IsWowClassic() and (bid * perAuction > MAXIMUM_BID_PRICE or buyout * perAuction > MAXIMUM_BID_PRICE) then
|
||||
Log.PrintfUser(L["The buyout price for %s would be above the maximum allowed price. Skipping this item."], ItemInfo.GetLink(itemString))
|
||||
return "invalidItemGroup"
|
||||
end
|
||||
|
||||
-- insert the posts into our DB
|
||||
local auctionId = private.nextQueueIndex
|
||||
local postTime = operationSettings.duration
|
||||
local extraStack = 0
|
||||
if TSM.IsWowClassic() then
|
||||
private.AddToQueue(itemString, operationName, bid, buyout, perAuction, maxCanPost, postTime)
|
||||
-- check if we can post an extra partial stack
|
||||
extraStack = (maxCanPost < postCap and operationSettings.stackSizeIsCap and (numHave % perAuction)) or 0
|
||||
else
|
||||
assert(maxCanPost == 1)
|
||||
if ItemInfo.IsCommodity(itemString) then
|
||||
local maxPerAuction = ItemInfo.GetMaxStack(itemString) * MAX_COMMODITY_STACKS_PER_AUCTION
|
||||
maxCanPost = floor(perAuction / maxPerAuction)
|
||||
-- check if we can post an extra partial stack
|
||||
extraStack = perAuction % maxPerAuction
|
||||
perAuction = min(perAuction, maxPerAuction)
|
||||
else
|
||||
-- post non-commodities as single stacks
|
||||
maxCanPost = perAuction
|
||||
perAuction = 1
|
||||
end
|
||||
assert(maxCanPost > 0 or extraStack > 0)
|
||||
if maxCanPost > 0 then
|
||||
private.AddToQueue(itemString, operationName, bid, buyout, perAuction, maxCanPost, postTime)
|
||||
end
|
||||
end
|
||||
if extraStack > 0 then
|
||||
private.AddToQueue(itemString, operationName, bid, buyout, extraStack, 1, postTime)
|
||||
end
|
||||
return reason, (perAuction * maxCanPost) + extraStack, buyout, seller, auctionId
|
||||
end
|
||||
|
||||
function private.AddToQueue(itemString, operationName, itemBid, itemBuyout, stackSize, numStacks, postTime)
|
||||
private.DebugLogInsert(itemString, "Queued %d stacks of %d", stackSize, numStacks)
|
||||
private.queueDB:NewRow()
|
||||
:SetField("auctionId", private.nextQueueIndex)
|
||||
:SetField("itemString", itemString)
|
||||
:SetField("operationName", operationName)
|
||||
:SetField("bid", itemBid * stackSize)
|
||||
:SetField("buyout", itemBuyout * stackSize)
|
||||
:SetField("itemBuyout", itemBuyout)
|
||||
:SetField("stackSize", stackSize)
|
||||
:SetField("numStacks", numStacks)
|
||||
:SetField("postTime", postTime)
|
||||
:SetField("numProcessed", 0)
|
||||
:SetField("numConfirmed", 0)
|
||||
:SetField("numFailed", 0)
|
||||
:Create()
|
||||
private.nextQueueIndex = private.nextQueueIndex + 1
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions for Posting
|
||||
-- ============================================================================
|
||||
|
||||
function private.GetPostBagSlot(itemString, quantity)
|
||||
-- start with the slot which is closest to the desired stack size
|
||||
local bag, slot = private.bagDB:NewQuery()
|
||||
:Select("bag", "slot")
|
||||
:Equal("itemString", itemString)
|
||||
:GreaterThanOrEqual("quantity", quantity)
|
||||
:OrderBy("quantity", true)
|
||||
:GetFirstResultAndRelease()
|
||||
if not bag then
|
||||
bag, slot = private.bagDB:NewQuery()
|
||||
:Select("bag", "slot")
|
||||
:Equal("itemString", itemString)
|
||||
:LessThanOrEqual("quantity", quantity)
|
||||
:OrderBy("quantity", false)
|
||||
:GetFirstResultAndRelease()
|
||||
end
|
||||
if not bag or not slot then
|
||||
-- this item was likely removed from the player's bags, so just give up
|
||||
Log.Err("Failed to find initial bag / slot (%s, %d)", itemString, quantity)
|
||||
return nil, true
|
||||
end
|
||||
local removeContext = TempTable.Acquire()
|
||||
bag, slot = private.ItemBagSlotHelper(itemString, bag, slot, quantity, removeContext)
|
||||
|
||||
local bagItemString = ItemString.Get(GetContainerItemLink(bag, slot))
|
||||
if not bagItemString or TSM.Groups.TranslateItemString(bagItemString) ~= itemString then
|
||||
-- something changed with the player's bags so we can't post the item right now
|
||||
TempTable.Release(removeContext)
|
||||
private.DebugLogInsert(itemString, "Bags changed")
|
||||
return nil, nil
|
||||
end
|
||||
local _, _, _, quality = GetContainerItemInfo(bag, slot)
|
||||
assert(quality)
|
||||
if quality == -1 then
|
||||
-- the game client doesn't have item info cached for this item, so we can't post it yet
|
||||
TempTable.Release(removeContext)
|
||||
private.DebugLogInsert(itemString, "No item info")
|
||||
return nil, nil
|
||||
end
|
||||
for slotId, removeQuantity in pairs(removeContext) do
|
||||
private.RemoveBagQuantity(slotId, removeQuantity)
|
||||
end
|
||||
TempTable.Release(removeContext)
|
||||
private.DebugLogInsert(itemString, "GetPostBagSlot(%d) -> %d, %d", quantity, bag, slot)
|
||||
return bag, slot
|
||||
end
|
||||
|
||||
function private.ItemBagSlotHelper(itemString, bag, slot, quantity, removeContext)
|
||||
local slotId = SlotId.Join(bag, slot)
|
||||
|
||||
-- try to post completely from the selected slot
|
||||
local found = private.bagDB:NewQuery()
|
||||
:Select("slotId")
|
||||
:Equal("slotId", slotId)
|
||||
:GreaterThanOrEqual("quantity", quantity)
|
||||
:GetFirstResultAndRelease()
|
||||
if found then
|
||||
removeContext[slotId] = quantity
|
||||
return bag, slot
|
||||
end
|
||||
|
||||
-- try to find a stack at a lower slot which has enough to post from
|
||||
local foundSlotId, foundBag, foundSlot = private.bagDB:NewQuery()
|
||||
:Select("slotId", "bag", "slot")
|
||||
:Equal("itemString", itemString)
|
||||
:LessThan("slotId", slotId)
|
||||
:GreaterThanOrEqual("quantity", quantity)
|
||||
:OrderBy("slotId", true)
|
||||
:GetFirstResultAndRelease()
|
||||
if foundSlotId then
|
||||
removeContext[foundSlotId] = quantity
|
||||
return foundBag, foundSlot
|
||||
end
|
||||
|
||||
-- try to post using the selected slot and the lower slots
|
||||
local selectedQuantity = private.bagDB:NewQuery()
|
||||
:Select("quantity")
|
||||
:Equal("slotId", slotId)
|
||||
:GetFirstResultAndRelease()
|
||||
local query = private.bagDB:NewQuery()
|
||||
:Select("slotId", "quantity")
|
||||
:Equal("itemString", itemString)
|
||||
:LessThan("slotId", slotId)
|
||||
:OrderBy("slotId", true)
|
||||
local numNeeded = quantity - selectedQuantity
|
||||
local numUsed = 0
|
||||
local usedSlotIds = TempTable.Acquire()
|
||||
for _, rowSlotId, rowQuantity in query:Iterator() do
|
||||
if numNeeded ~= numUsed then
|
||||
numUsed = min(numUsed + rowQuantity, numNeeded)
|
||||
tinsert(usedSlotIds, rowSlotId)
|
||||
end
|
||||
end
|
||||
query:Release()
|
||||
if numNeeded == numUsed then
|
||||
removeContext[slotId] = selectedQuantity
|
||||
for _, rowSlotId in TempTable.Iterator(usedSlotIds) do
|
||||
local rowQuantity = private.bagDB:GetUniqueRowField("slotId", rowSlotId, "quantity")
|
||||
local rowNumUsed = min(numUsed, rowQuantity)
|
||||
numUsed = numUsed - rowNumUsed
|
||||
removeContext[rowSlotId] = (removeContext[rowSlotId] or 0) + rowNumUsed
|
||||
end
|
||||
return bag, slot
|
||||
else
|
||||
TempTable.Release(usedSlotIds)
|
||||
end
|
||||
|
||||
-- try posting from the next highest slot
|
||||
local rowBag, rowSlot = private.bagDB:NewQuery()
|
||||
:Select("bag", "slot")
|
||||
:Equal("itemString", itemString)
|
||||
:GreaterThan("slotId", slotId)
|
||||
:OrderBy("slotId", true)
|
||||
:GetFirstResultAndRelease()
|
||||
if not rowBag or not rowSlot then
|
||||
private.ErrorForItem(itemString, "Failed to find next highest bag / slot")
|
||||
end
|
||||
return private.ItemBagSlotHelper(itemString, rowBag, rowSlot, quantity, removeContext)
|
||||
end
|
||||
|
||||
function private.RemoveBagQuantity(slotId, quantity)
|
||||
local row = private.bagDB:GetUniqueRow("slotId", slotId)
|
||||
local remainingQuantity = row:GetField("quantity") - quantity
|
||||
private.DebugLogInsert(row:GetField("itemString"), "Removing %d (%d remain) from %d", quantity, remainingQuantity, slotId)
|
||||
if remainingQuantity > 0 then
|
||||
row:SetField("quantity", remainingQuantity)
|
||||
:Update()
|
||||
else
|
||||
assert(remainingQuantity == 0)
|
||||
private.bagDB:DeleteRow(row)
|
||||
end
|
||||
row:Release()
|
||||
end
|
||||
|
||||
function private.ConfirmRowQueryHelper(row)
|
||||
return row:GetField("numConfirmed") < row:GetField("numProcessed")
|
||||
end
|
||||
|
||||
function private.NextProcessRowQueryHelper(row)
|
||||
return row:GetField("numProcessed") < row:GetField("numStacks")
|
||||
end
|
||||
|
||||
function private.DebugLogInsert(itemString, ...)
|
||||
tinsert(private.debugLog, itemString)
|
||||
tinsert(private.debugLog, format(...))
|
||||
end
|
||||
|
||||
function private.ErrorForItem(itemString, errorStr)
|
||||
for i = 1, #private.debugLog, 2 do
|
||||
if private.debugLog[i] == itemString then
|
||||
Log.Info(private.debugLog[i + 1])
|
||||
end
|
||||
end
|
||||
Log.Info("Bag state:")
|
||||
for b = 0, NUM_BAG_SLOTS do
|
||||
for s = 1, GetContainerNumSlots(b) do
|
||||
if ItemString.GetBase(GetContainerItemLink(b, s)) == itemString then
|
||||
local _, q = GetContainerItemInfo(b, s)
|
||||
Log.Info("%d in %d, %d", q, b, s)
|
||||
end
|
||||
end
|
||||
end
|
||||
error(errorStr, 2)
|
||||
end
|
||||
204
Core/Service/Auctioning/SavedSearches.lua
Normal file
204
Core/Service/Auctioning/SavedSearches.lua
Normal file
@@ -0,0 +1,204 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local SavedSearches = TSM.Auctioning:NewPackage("SavedSearches")
|
||||
local L = TSM.Include("Locale").GetTable()
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local String = TSM.Include("Util.String")
|
||||
local Database = TSM.Include("Util.Database")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Theme = TSM.Include("Util.Theme")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local private = {
|
||||
settings = nil,
|
||||
db = nil,
|
||||
}
|
||||
local FILTER_SEP = "\001"
|
||||
local MAX_RECENT_SEARCHES = 500
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function SavedSearches.OnInitialize()
|
||||
private.settings = Settings.NewView()
|
||||
:AddKey("global", "userData", "savedAuctioningSearches")
|
||||
|
||||
-- remove duplicates
|
||||
local seen = TempTable.Acquire()
|
||||
for i = #private.settings.savedAuctioningSearches.filters, 1, -1 do
|
||||
local filter = private.settings.savedAuctioningSearches.filters[i]
|
||||
if seen[filter] then
|
||||
tremove(private.settings.savedAuctioningSearches.filters, i)
|
||||
tremove(private.settings.savedAuctioningSearches.searchTypes, i)
|
||||
private.settings.savedAuctioningSearches.name[filter] = nil
|
||||
private.settings.savedAuctioningSearches.isFavorite[filter] = nil
|
||||
else
|
||||
seen[filter] = true
|
||||
end
|
||||
end
|
||||
TempTable.Release(seen)
|
||||
|
||||
-- remove old recent searches
|
||||
local remainingRecentSearches = MAX_RECENT_SEARCHES
|
||||
local numRemoved = 0
|
||||
for i = #private.settings.savedAuctioningSearches.filters, 1, -1 do
|
||||
local filter = private.settings.savedAuctioningSearches.filters
|
||||
if not private.settings.savedAuctioningSearches.isFavorite[filter] then
|
||||
if remainingRecentSearches > 0 then
|
||||
remainingRecentSearches = remainingRecentSearches - 1
|
||||
else
|
||||
tremove(private.settings.savedAuctioningSearches.filters, i)
|
||||
tremove(private.settings.savedAuctioningSearches.searchTypes, i)
|
||||
private.settings.savedAuctioningSearches.name[filter] = nil
|
||||
private.settings.savedAuctioningSearches.isFavorite[filter] = nil
|
||||
numRemoved = numRemoved + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
if numRemoved > 0 then
|
||||
Log.Info("Removed %d old recent searches", numRemoved)
|
||||
end
|
||||
|
||||
private.db = Database.NewSchema("AUCTIONING_SAVED_SEARCHES")
|
||||
:AddUniqueNumberField("index")
|
||||
:AddBooleanField("isFavorite")
|
||||
:AddStringField("searchType")
|
||||
:AddStringField("filter")
|
||||
:AddStringField("name")
|
||||
:AddIndex("index")
|
||||
:Commit()
|
||||
private.RebuildDB()
|
||||
end
|
||||
|
||||
function SavedSearches.CreateRecentSearchesQuery()
|
||||
return private.db:NewQuery()
|
||||
:OrderBy("index", false)
|
||||
end
|
||||
|
||||
function SavedSearches.CreateFavoriteSearchesQuery()
|
||||
return private.db:NewQuery()
|
||||
:Equal("isFavorite", true)
|
||||
:OrderBy("name", true)
|
||||
end
|
||||
|
||||
function SavedSearches.SetSearchIsFavorite(dbRow, isFavorite)
|
||||
local filter = dbRow:GetField("filter")
|
||||
private.settings.savedAuctioningSearches.isFavorite[filter] = isFavorite or nil
|
||||
dbRow:SetField("isFavorite", isFavorite)
|
||||
:Update()
|
||||
end
|
||||
|
||||
function SavedSearches.RenameSearch(dbRow, newName)
|
||||
local filter = dbRow:GetField("filter")
|
||||
private.settings.savedAuctioningSearches.name[filter] = newName
|
||||
dbRow:SetField("name", newName)
|
||||
:Update()
|
||||
end
|
||||
|
||||
function SavedSearches.DeleteSearch(dbRow)
|
||||
local index, filter = dbRow:GetFields("index", "filter")
|
||||
tremove(private.settings.savedAuctioningSearches.filters, index)
|
||||
tremove(private.settings.savedAuctioningSearches.searchTypes, index)
|
||||
private.settings.savedAuctioningSearches.name[filter] = nil
|
||||
private.settings.savedAuctioningSearches.isFavorite[filter] = nil
|
||||
private.RebuildDB()
|
||||
end
|
||||
|
||||
function SavedSearches.RecordSearch(searchList, searchType)
|
||||
assert(searchType == "postItems" or searchType == "postGroups" or searchType == "cancelGroups")
|
||||
local filter = table.concat(searchList, FILTER_SEP)
|
||||
for i, existingFilter in ipairs(private.settings.savedAuctioningSearches.filters) do
|
||||
local existingSearchType = private.settings.savedAuctioningSearches.searchTypes[i]
|
||||
if filter == existingFilter and searchType == existingSearchType then
|
||||
-- move this to the end of the list and rebuild the DB
|
||||
-- insert the existing filter so we don't need to update the isFavorite and name tables
|
||||
tremove(private.settings.savedAuctioningSearches.filters, i)
|
||||
tinsert(private.settings.savedAuctioningSearches.filters, existingFilter)
|
||||
tremove(private.settings.savedAuctioningSearches.searchTypes, i)
|
||||
tinsert(private.settings.savedAuctioningSearches.searchTypes, existingSearchType)
|
||||
private.RebuildDB()
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- didn't find an existing entry, so add a new one
|
||||
tinsert(private.settings.savedAuctioningSearches.filters, filter)
|
||||
tinsert(private.settings.savedAuctioningSearches.searchTypes, searchType)
|
||||
assert(#private.settings.savedAuctioningSearches.filters == #private.settings.savedAuctioningSearches.searchTypes)
|
||||
private.db:NewRow()
|
||||
:SetField("index", #private.settings.savedAuctioningSearches.filters)
|
||||
:SetField("isFavorite", false)
|
||||
:SetField("searchType", searchType)
|
||||
:SetField("filter", filter)
|
||||
:SetField("name", private.GetSearchName(filter, searchType))
|
||||
:Create()
|
||||
end
|
||||
|
||||
function SavedSearches.FiltersToTable(dbRow, tbl)
|
||||
String.SafeSplit(dbRow:GetField("filter"), FILTER_SEP, tbl)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.RebuildDB()
|
||||
assert(#private.settings.savedAuctioningSearches.filters == #private.settings.savedAuctioningSearches.searchTypes)
|
||||
private.db:TruncateAndBulkInsertStart()
|
||||
for index, filter in ipairs(private.settings.savedAuctioningSearches.filters) do
|
||||
local searchType = private.settings.savedAuctioningSearches.searchTypes[index]
|
||||
assert(searchType == "postItems" or searchType == "postGroups" or searchType == "cancelGroups")
|
||||
local name = private.settings.savedAuctioningSearches.name[filter] or private.GetSearchName(filter, searchType)
|
||||
local isFavorite = private.settings.savedAuctioningSearches.isFavorite[filter] and true or false
|
||||
private.db:BulkInsertNewRow(index, isFavorite, searchType, filter, name)
|
||||
end
|
||||
private.db:BulkInsertEnd()
|
||||
end
|
||||
|
||||
function private.GetSearchName(filter, searchType)
|
||||
local filters = TempTable.Acquire()
|
||||
local searchTypeStr, numFiltersStr = nil, nil
|
||||
if filter == "" or string.sub(filter, 1, 1) == FILTER_SEP then
|
||||
tinsert(filters, L["Base Group"])
|
||||
end
|
||||
if searchType == "postGroups" or searchType == "cancelGroups" then
|
||||
for groupPath in gmatch(filter, "[^"..FILTER_SEP.."]+") do
|
||||
local groupName = TSM.Groups.Path.GetName(groupPath)
|
||||
local level = select('#', strsplit(TSM.CONST.GROUP_SEP, groupPath))
|
||||
local color = Theme.GetGroupColor(level)
|
||||
tinsert(filters, color:ColorText(groupName))
|
||||
end
|
||||
searchTypeStr = searchType == "postGroups" and L["Post Scan"] or L["Cancel Scan"]
|
||||
numFiltersStr = #filters == 1 and L["1 Group"] or format(L["%d Groups"], #filters)
|
||||
elseif searchType == "postItems" then
|
||||
local numItems = 0
|
||||
for itemString in gmatch(filter, "[^"..FILTER_SEP.."]+") do
|
||||
numItems = numItems + 1
|
||||
local coloredName = TSM.UI.GetColoredItemName(itemString)
|
||||
if coloredName then
|
||||
tinsert(filters, coloredName)
|
||||
end
|
||||
end
|
||||
searchTypeStr = L["Post Scan"]
|
||||
numFiltersStr = numItems == 1 and L["1 Item"] or format(L["%d Items"], numItems)
|
||||
else
|
||||
error("Unknown searchType: "..tostring(searchType))
|
||||
end
|
||||
local groupList = nil
|
||||
if #filters > 10 then
|
||||
groupList = table.concat(filters, ", ", 1, 10)..",..."
|
||||
TempTable.Release(filters)
|
||||
else
|
||||
groupList = strjoin(", ", TempTable.UnpackAndRelease(filters))
|
||||
end
|
||||
return format("%s (%s): %s", searchTypeStr, numFiltersStr, groupList)
|
||||
end
|
||||
326
Core/Service/Auctioning/Util.lua
Normal file
326
Core/Service/Auctioning/Util.lua
Normal file
@@ -0,0 +1,326 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Util = TSM.Auctioning:NewPackage("Util")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Vararg = TSM.Include("Util.Vararg")
|
||||
local String = TSM.Include("Util.String")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
local CustomPrice = TSM.Include("Service.CustomPrice")
|
||||
local PlayerInfo = TSM.Include("Service.PlayerInfo")
|
||||
local private = {
|
||||
priceCache = {},
|
||||
}
|
||||
local INVALID_PRICE = {}
|
||||
local VALID_PRICE_KEYS = {
|
||||
minPrice = true,
|
||||
normalPrice = true,
|
||||
maxPrice = true,
|
||||
undercut = true,
|
||||
cancelRepostThreshold = true,
|
||||
priceReset = true,
|
||||
aboveMax = true,
|
||||
postCap = true,
|
||||
stackSize = true,
|
||||
keepQuantity = true,
|
||||
maxExpires = true,
|
||||
}
|
||||
local IS_GOLD_PRICE_KEY = {
|
||||
minPrice = true,
|
||||
normalPrice = true,
|
||||
maxPrice = true,
|
||||
undercut = TSM.IsWowClassic(),
|
||||
priceReset = true,
|
||||
aboveMax = true,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Util.GetPrice(key, operation, itemString)
|
||||
assert(VALID_PRICE_KEYS[key])
|
||||
local cacheKey = key..tostring(operation)..itemString
|
||||
if private.priceCache.updateTime ~= GetTime() then
|
||||
wipe(private.priceCache)
|
||||
private.priceCache.updateTime = GetTime()
|
||||
end
|
||||
if not private.priceCache[cacheKey] then
|
||||
local value = nil
|
||||
if key == "aboveMax" or key == "priceReset" then
|
||||
-- redirect to the selected price (if applicable)
|
||||
local priceKey = operation[key]
|
||||
if VALID_PRICE_KEYS[priceKey] then
|
||||
value = Util.GetPrice(priceKey, operation, itemString)
|
||||
end
|
||||
else
|
||||
value = CustomPrice.GetValue(operation[key], itemString, not IS_GOLD_PRICE_KEY[key])
|
||||
end
|
||||
if not TSM.IsWowClassic() and IS_GOLD_PRICE_KEY[key] then
|
||||
value = value and Math.Ceil(value, COPPER_PER_SILVER) or nil
|
||||
else
|
||||
value = value and Math.Round(value) or nil
|
||||
end
|
||||
local minValue, maxValue = TSM.Operations.Auctioning.GetMinMaxValues(key)
|
||||
private.priceCache[cacheKey] = (value and value >= minValue and value <= maxValue) and value or INVALID_PRICE
|
||||
end
|
||||
if private.priceCache[cacheKey] == INVALID_PRICE then
|
||||
return nil
|
||||
end
|
||||
return private.priceCache[cacheKey]
|
||||
end
|
||||
|
||||
function Util.GetLowestAuction(subRows, itemString, operationSettings, resultTbl)
|
||||
if not TSM.IsWowClassic() then
|
||||
local foundLowest = false
|
||||
for _, subRow in ipairs(subRows) do
|
||||
local _, itemBuyout = subRow:GetBuyouts()
|
||||
local quantity = subRow:GetQuantities()
|
||||
local timeLeft = subRow:GetListingInfo()
|
||||
if not foundLowest and not Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft) then
|
||||
local ownerStr = subRow:GetOwnerInfo()
|
||||
local _, auctionId = subRow:GetListingInfo()
|
||||
local _, itemMinBid = subRow:GetBidInfo()
|
||||
local firstSeller = strsplit(",", ownerStr)
|
||||
resultTbl.buyout = itemBuyout
|
||||
resultTbl.bid = itemMinBid
|
||||
resultTbl.seller = firstSeller
|
||||
resultTbl.auctionId = auctionId
|
||||
resultTbl.isWhitelist = TSM.db.factionrealm.auctioningOptions.whitelist[strlower(firstSeller)] and true or false
|
||||
resultTbl.isPlayer = PlayerInfo.IsPlayer(firstSeller, true, true, true)
|
||||
if not subRow:HasOwners() then
|
||||
resultTbl.hasInvalidSeller = true
|
||||
end
|
||||
foundLowest = true
|
||||
end
|
||||
end
|
||||
return foundLowest
|
||||
else
|
||||
local hasInvalidSeller = nil
|
||||
local ignoreWhitelist = nil
|
||||
local lowestItemBuyout = nil
|
||||
local lowestAuction = nil
|
||||
for _, subRow in ipairs(subRows) do
|
||||
local _, itemBuyout = subRow:GetBuyouts()
|
||||
local quantity = subRow:GetQuantities()
|
||||
local timeLeft = subRow:GetListingInfo()
|
||||
if not Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft) then
|
||||
assert(itemBuyout and itemBuyout > 0)
|
||||
lowestItemBuyout = lowestItemBuyout or itemBuyout
|
||||
if itemBuyout == lowestItemBuyout then
|
||||
local ownerStr = subRow:GetOwnerInfo()
|
||||
local _, auctionId = subRow:GetListingInfo()
|
||||
local _, itemMinBid = subRow:GetBidInfo()
|
||||
local temp = TempTable.Acquire()
|
||||
temp.buyout = itemBuyout
|
||||
temp.bid = itemMinBid
|
||||
temp.seller = ownerStr
|
||||
temp.auctionId = auctionId
|
||||
temp.isWhitelist = TSM.db.factionrealm.auctioningOptions.whitelist[strlower(ownerStr)] and true or false
|
||||
temp.isPlayer = PlayerInfo.IsPlayer(ownerStr, true, true, true)
|
||||
if not temp.isWhitelist and not temp.isPlayer then
|
||||
-- there is a non-whitelisted competitor, so we don't care if a whitelisted competitor also posts at this price
|
||||
ignoreWhitelist = true
|
||||
end
|
||||
if not subRow:HasOwners() and next(TSM.db.factionrealm.auctioningOptions.whitelist) then
|
||||
hasInvalidSeller = true
|
||||
end
|
||||
if operationSettings.blacklist then
|
||||
for _, player in Vararg.Iterator(strsplit(",", operationSettings.blacklist)) do
|
||||
if String.SeparatedContains(strlower(ownerStr), ",", strlower(strtrim(player))) then
|
||||
temp.isBlacklist = true
|
||||
end
|
||||
end
|
||||
end
|
||||
if not lowestAuction then
|
||||
lowestAuction = temp
|
||||
elseif private.LowestAuctionCompare(temp, lowestAuction) then
|
||||
TempTable.Release(lowestAuction)
|
||||
lowestAuction = temp
|
||||
else
|
||||
TempTable.Release(temp)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if not lowestAuction then
|
||||
return false
|
||||
end
|
||||
for k, v in pairs(lowestAuction) do
|
||||
resultTbl[k] = v
|
||||
end
|
||||
TempTable.Release(lowestAuction)
|
||||
if resultTbl.isWhitelist and ignoreWhitelist then
|
||||
resultTbl.isWhitelist = false
|
||||
end
|
||||
resultTbl.hasInvalidSeller = hasInvalidSeller
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function Util.GetPlayerAuctionCount(subRows, itemString, operationSettings, findBid, findBuyout, findStackSize)
|
||||
local playerQuantity = 0
|
||||
for _, subRow in ipairs(subRows) do
|
||||
local _, itemBuyout = subRow:GetBuyouts()
|
||||
local quantity = subRow:GetQuantities()
|
||||
local timeLeft = subRow:GetListingInfo()
|
||||
if not Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft) then
|
||||
local _, itemMinBid = subRow:GetBidInfo()
|
||||
if itemMinBid == findBid and itemBuyout == findBuyout and (not TSM.IsWowClassic() or quantity == findStackSize) then
|
||||
local count = private.GetPlayerAuctionCount(subRow)
|
||||
if not TSM.IsWowClassic() and count == 0 and playerQuantity > 0 then
|
||||
-- there's another player's auction after ours, so stop counting
|
||||
break
|
||||
end
|
||||
playerQuantity = playerQuantity + count
|
||||
end
|
||||
end
|
||||
end
|
||||
return playerQuantity
|
||||
end
|
||||
|
||||
function Util.GetPlayerLowestBuyout(subRows, itemString, operationSettings)
|
||||
local lowestItemBuyout, lowestItemAuctionId = nil, nil
|
||||
for _, subRow in ipairs(subRows) do
|
||||
local _, itemBuyout = subRow:GetBuyouts()
|
||||
local quantity = subRow:GetQuantities()
|
||||
local timeLeft, auctionId = subRow:GetListingInfo()
|
||||
if not lowestItemBuyout and not Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft) and private.GetPlayerAuctionCount(subRow) > 0 then
|
||||
lowestItemBuyout = itemBuyout
|
||||
lowestItemAuctionId = auctionId
|
||||
end
|
||||
end
|
||||
return lowestItemBuyout, lowestItemAuctionId
|
||||
end
|
||||
|
||||
function Util.GetLowestNonPlayerAuctionId(subRows, itemString, operationSettings, lowestItemBuyout)
|
||||
local lowestItemAuctionId = nil
|
||||
for _, subRow in ipairs(subRows) do
|
||||
local _, itemBuyout = subRow:GetBuyouts()
|
||||
local quantity = subRow:GetQuantities()
|
||||
local timeLeft, auctionId = subRow:GetListingInfo()
|
||||
if not lowestItemAuctionId and not Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft) and private.GetPlayerAuctionCount(subRow) == 0 and itemBuyout == lowestItemBuyout then
|
||||
lowestItemAuctionId = auctionId
|
||||
end
|
||||
end
|
||||
return lowestItemAuctionId
|
||||
end
|
||||
|
||||
function Util.IsPlayerOnlySeller(subRows, itemString, operationSettings)
|
||||
local isOnly = true
|
||||
for _, subRow in ipairs(subRows) do
|
||||
local _, itemBuyout = subRow:GetBuyouts()
|
||||
local quantity = subRow:GetQuantities()
|
||||
local timeLeft = subRow:GetListingInfo()
|
||||
if isOnly and not Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft) and private.GetPlayerAuctionCount(subRow) < (TSM.IsWowClassic() and 1 or quantity) then
|
||||
isOnly = false
|
||||
end
|
||||
end
|
||||
return isOnly
|
||||
end
|
||||
|
||||
function Util.GetNextLowestItemBuyout(subRows, itemString, lowestAuction, operationSettings)
|
||||
local nextLowestItemBuyout = nil
|
||||
for _, subRow in ipairs(subRows) do
|
||||
local _, itemBuyout = subRow:GetBuyouts()
|
||||
local quantity = subRow:GetQuantities()
|
||||
local timeLeft, auctionId = subRow:GetListingInfo()
|
||||
local isLower = itemBuyout > lowestAuction.buyout or (itemBuyout == lowestAuction.buyout and auctionId < lowestAuction.auctionId)
|
||||
if not nextLowestItemBuyout and not Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft) and isLower then
|
||||
nextLowestItemBuyout = itemBuyout
|
||||
end
|
||||
end
|
||||
return nextLowestItemBuyout
|
||||
end
|
||||
|
||||
function Util.GetQueueStatus(query)
|
||||
local numProcessed, numConfirmed, numFailed, totalNum = 0, 0, 0, 0
|
||||
query:OrderBy("auctionId", true)
|
||||
for _, row in query:Iterator() do
|
||||
local rowNumStacks, rowNumProcessed, rowNumConfirmed, rowNumFailed = row:GetFields("numStacks", "numProcessed", "numConfirmed", "numFailed")
|
||||
totalNum = totalNum + rowNumStacks
|
||||
numProcessed = numProcessed + rowNumProcessed
|
||||
numConfirmed = numConfirmed + rowNumConfirmed
|
||||
numFailed = numFailed + rowNumFailed
|
||||
end
|
||||
query:Release()
|
||||
return numProcessed, numConfirmed, numFailed, totalNum
|
||||
end
|
||||
|
||||
function Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft)
|
||||
if timeLeft <= operationSettings.ignoreLowDuration then
|
||||
-- ignoring low duration
|
||||
return true
|
||||
elseif TSM.IsWowClassic() and operationSettings.matchStackSize and quantity ~= Util.GetPrice("stackSize", operationSettings, itemString) then
|
||||
-- matching stack size
|
||||
return true
|
||||
elseif operationSettings.priceReset == "ignore" then
|
||||
local minPrice = Util.GetPrice("minPrice", operationSettings, itemString)
|
||||
local undercut = Util.GetPrice("undercut", operationSettings, itemString)
|
||||
if minPrice and itemBuyout - undercut < minPrice then
|
||||
-- ignoring auctions below threshold
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function Util.GetFilteredSubRows(query, itemString, operationSettings, result)
|
||||
for _, subRow in query:ItemSubRowIterator(itemString) do
|
||||
local _, itemBuyout = subRow:GetBuyouts()
|
||||
local quantity = subRow:GetQuantities()
|
||||
local timeLeft = subRow:GetListingInfo()
|
||||
if not Util.IsFiltered(itemString, operationSettings, itemBuyout, quantity, timeLeft) then
|
||||
tinsert(result, subRow)
|
||||
end
|
||||
end
|
||||
sort(result, private.SubRowSortHelper)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.SubRowSortHelper(a, b)
|
||||
local _, aItemBuyout = a:GetBuyouts()
|
||||
local _, bItemBuyout = b:GetBuyouts()
|
||||
if aItemBuyout ~= bItemBuyout then
|
||||
return aItemBuyout < bItemBuyout
|
||||
end
|
||||
local _, aAuctionId = a:GetListingInfo()
|
||||
local _, bAuctionId = b:GetListingInfo()
|
||||
return aAuctionId > bAuctionId
|
||||
end
|
||||
|
||||
function private.LowestAuctionCompare(a, b)
|
||||
if a.isBlacklist ~= b.isBlacklist then
|
||||
return a.isBlacklist
|
||||
end
|
||||
if a.isWhitelist ~= b.isWhitelist then
|
||||
return a.isWhitelist
|
||||
end
|
||||
if a.auctionId ~= b.auctionId then
|
||||
return a.auctionId > b.auctionId
|
||||
end
|
||||
if a.isPlayer ~= b.isPlayer then
|
||||
return b.isPlayer
|
||||
end
|
||||
return tostring(a) < tostring(b)
|
||||
end
|
||||
|
||||
function private.GetPlayerAuctionCount(subRow)
|
||||
local ownerStr, numOwnerItems = subRow:GetOwnerInfo()
|
||||
if TSM.IsWowClassic() then
|
||||
return PlayerInfo.IsPlayer(ownerStr, true, true, true) and select(2, subRow:GetQuantities()) or 0
|
||||
else
|
||||
return numOwnerItems
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user