966 lines
37 KiB
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
|