TradeSkillMaster/Core/Service/Auctioning/PostScan.lua

966 lines
37 KiB
Lua

-- ------------------------------------------------------------------------------ --
-- 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