You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

543 lines
19 KiB

-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster_Auctioning --
-- http://www.curse.com/addons/wow/tradeskillmaster_auctioning --
-- --
-- A TradeSkillMaster Addon (http://tradeskillmaster.com) --
-- All Rights Reserved* - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local TSM = select(2, ...)
local Post = TSM:NewModule("Post", "AceEvent-3.0")
local L = LibStub("AceLocale-3.0"):GetLocale("TradeSkillMaster_Auctioning") -- loads the localization table
local bagInfo, bagState = {}, {}
local bagInfoUpdate = 0
local postQueue, currentItem, itemLocations = {}, {}, {}
local scanItems
local totalToPost, totalPosted, count = 0, 0, 0
local isScanning, GUI
local lastProcessedAuctionDataHash
local lastProcessedAuctionDataCount
function Post:ValidateOperation(itemString, operation)
local itemLink, salePrice = TSMAPI:Select({2, 11}, TSMAPI:GetSafeItemInfo(itemString))
local prices = TSM.Util:GetItemPrices(operation, itemString)
-- don't post this item if their settings are invalid
if operation.postCap == 0 then
return -- posting is disabled
elseif not prices.minPrice then
if not TSM.db.global.disableInvalidMsg then
TSM:Printf(L["Did not post %s because your minimum price (%s) is invalid. Check your settings."], itemLink or itemString, operation.minPrice)
end
TSM.Log:AddLogRecord(itemString, "post", "Skip", "invalid", operation)
elseif not prices.maxPrice then
if not TSM.db.global.disableInvalidMsg then
TSM:Printf(L["Did not post %s because your maximum price (%s) is invalid. Check your settings."], itemLink or itemString, operation.maxPrice)
end
TSM.Log:AddLogRecord(itemString, "post", "Skip", "invalid", operation)
elseif not prices.normalPrice then
if not TSM.db.global.disableInvalidMsg then
TSM:Printf(L["Did not post %s because your normal price (%s) is invalid. Check your settings."], itemLink or itemString, operation.normalPrice)
end
TSM.Log:AddLogRecord(itemString, "post", "Skip", "invalid", operation)
elseif not prices.undercut then
if not TSM.db.global.disableInvalidMsg then
TSM:Printf(L["Did not post %s because your undercut (%s) is invalid. Check your settings."], itemLink or itemString, operation.undercut)
end
TSM.Log:AddLogRecord(itemString, "post", "Skip", "invalid")
elseif prices.normalPrice < prices.minPrice then
if not TSM.db.global.disableInvalidMsg then
TSM:Printf(L["Did not post %s because your normal price (%s) is lower than your minimum price (%s). Check your settings."], itemLink or itemString, operation.normalPrice, operation.minPrice)
end
TSM.Log:AddLogRecord(itemString, "post", "Skip", "invalid", operation)
elseif prices.maxPrice < prices.minPrice then
if not TSM.db.global.disableInvalidMsg then
TSM:Printf(L["Did not post %s because your maximum price (%s) is lower than your minimum price (%s). Check your settings."], itemLink or itemString, operation.maxPrice, operation.minPrice)
end
TSM.Log:AddLogRecord(itemString, "post", "Skip", "invalid", operation)
elseif salePrice > 0 and prices.minPrice <= salePrice*1.05 then
TSM:Printf(L["WARNING: You minimum price for %s is below its vendorsell price (with AH cut taken into account). Consider raising your minimum price, or vendoring the item."], itemLink or itemString)
return true -- just a warning, doesn't make this invalid
else
return true
end
end
function Post:UpdateBagState()
if time() == bagInfoUpdate then return end
wipe(bagInfo)
wipe(bagState)
for bag, slot, itemString, quantity in TSMAPI:GetBagIterator(true) do
tinsert(bagInfo, {bag, slot, itemString, quantity})
bagState[itemString] = (bagState[itemString] or 0) + quantity
end
bagInfoUpdate = time()
end
function Post:GetScanListAndSetup(GUIRef, options)
-- setup stuff
GUI = GUIRef
isScanning = true
wipe(postQueue)
wipe(currentItem)
wipe(itemLocations)
wipe(TSM.operationLookup)
totalToPost, totalPosted, count = 0, 0, 0
local tempList, scanList = {}, {}
Post:UpdateBagState()
local function HasEnoughToPost(operation, itemString)
local maxStackSize = select(8, TSMAPI:GetSafeItemInfo(itemString)) or 1
local perAuction = min(maxStackSize, operation.stackSize)
local perAuctionIsCap = operation.stackSizeIsCap
local num = (bagState[itemString] or 0) - operation.keepQuantity
return num >= perAuction or (perAuctionIsCap and num > 0)
end
for itemString in pairs(bagState) do
local operations = options.itemOperations[itemString]
if not tempList[itemString] and operations then
for _, operation in ipairs(operations) do
if operation.postCap > 0 and HasEnoughToPost(operation, itemString) then
tempList[itemString] = tempList[itemString] or {}
tinsert(tempList[itemString], operation)
end
end
end
end
for itemString, operations in pairs(tempList) do
TSM.operationLookup[itemString] = operations
local isValid = true
for _, operation in ipairs(operations) do
if not Post:ValidateOperation(itemString, operation) then
isValid = nil
break
end
end
if #options.itemOperations[itemString] ~= #operations then
local j = 1
for i=1, #options.itemOperations[itemString] do
if options.itemOperations[itemString][i] ~= operations[j] then
TSM.Log:AddLogRecord(itemString, "post", "Skip", "notEnough", options.itemOperations[itemString][i])
else
j = j + 1
end
end
end
if isValid then
tinsert(scanList, itemString)
end
end
scanItems = {}
for _, itemString in ipairs(scanList) do
tinsert(scanItems, itemString)
end
TSMAPI:FireEvent("AUCTIONING:POST:START", {numItems=#scanList, isGroup=true})
return scanList
end
function Post:ProcessItem(itemString)
local operations = TSM.operationLookup[itemString]
if not operations then return end
local numInBags = Post:GetNumInBags(itemString)
for _, operation in ipairs(operations) do
local toPost, reason, buyout
numInBags = numInBags - operation.keepQuantity
toPost, reason, numInBags = Post:ShouldPost(itemString, operation, numInBags)
numInBags = numInBags + operation.keepQuantity -- restore for the next operation if there are multiple
local data = {}
if toPost then
local bid
bid, buyout, reason = Post:GetPostPrice(itemString, operation)
local postTime = (operation.duration == 48 and 3) or (operation.duration == 24 and 2) or 1
for i = 1, #toPost do
local stackSize, numStacks = unpack(toPost[i])
-- Increase the bid/buyout based on how many items we're posting
local stackBid, stackBuyout = floor(bid * stackSize), floor(buyout * stackSize)
Post:QueueItemToPost(itemString, numStacks, stackSize, stackBid, stackBuyout, postTime, operation)
tinsert(data, { numStacks = numStacks, stackSize = stackSize, buyout = buyout, postTime = postTime })
end
end
TSM.Log:AddLogRecord(itemString, "post", (toPost and L["Post"] or L["Skip"]), reason, operation, buyout)
if #postQueue > 0 and not currentItem.bag then
Post:SetupForAction()
end
if numInBags < 0 then error("less than 0") end
if numInBags == 0 then break end
end
end
function Post:ShouldPost(itemString, operation, numInBags)
local maxStackSize = select(8, TSMAPI:GetSafeItemInfo(itemString))
local perAuction = min(maxStackSize, operation.stackSize)
local maxCanPost = floor(numInBags / perAuction)
local perAuctionIsCap = operation.stackSizeIsCap
if maxCanPost == 0 then
if perAuctionIsCap then
perAuction = numInBags
maxCanPost = 1
else
return nil, "notEnough", numInBags -- not enough for single post
end
end
local activeAuctions = 0
local extraStack
local buyout, bid, _, isWhitelist, isPlayer, isInvalidSeller = TSM.Scan:GetLowestAuction(itemString, operation)
if isInvalidSeller then
TSM:Printf(L["Seller name of lowest auction for item %s was not returned from server. Skipping this item."], select(2, TSMAPI:GetSafeItemInfo(itemString)))
return nil, "invalidSeller", numInBags
end
local prices = TSM.Util:GetItemPrices(operation, itemString)
if buyout and buyout <= prices.minPrice then
-- lowest is below min price
if not prices.resetPrice then
-- lowest is below the min price and there's no reset price
return nil, "belowMinPrice", numInBags
else
-- lowest is below the min price, but there is a reset price
local priceResetBuyout = prices.resetPrice
local priceResetBid = priceResetBuyout * operation.bidPercent
activeAuctions = TSM.Scan:GetPlayerAuctionCount(itemString, priceResetBuyout, priceResetBid, perAuction, operation)
end
elseif isWhitelist and not isPlayer and not TSM.db.global.matchWhitelist then
-- lowest is somebody on the whitelist and we aren't price matching
return nil, "notPostingWhitelist", numInBags
elseif isPlayer or isWhitelist then
-- Either the player or a whitelist person is the lowest teir so use this tiers quantity of items
activeAuctions = TSM.Scan:GetPlayerAuctionCount(itemString, buyout or 0, bid or 0, perAuction, operation)
end
-- If we have a post cap of 20, and 10 active auctions, but we can only have 5 of the item then this will only let us create 5 auctions
-- however, if we have 20 of the item it will let us post another 10
local auctionsCreated = min(operation.postCap - activeAuctions, maxCanPost)
if auctionsCreated <= 0 then
return nil, "tooManyPosted", numInBags
end
if (auctionsCreated + activeAuctions) < operation.postCap then
-- can post at least one more
local extra = numInBags % perAuction
if perAuctionIsCap and extra > 0 then
extraStack = extra
end
end
if Post:FindItemSlot(itemString) then
local posts = { { perAuction, auctionsCreated } }
numInBags = numInBags - perAuction * auctionsCreated
if extraStack then
numInBags = numInBags - extraStack
tinsert(posts, { extraStack, 1 })
end
-- third return value specifies whether there's extra left over in the player's bags after this operation
return posts, nil, numInBags
end
end
function Post:GetPostPrice(itemString, operation)
local lowestBuyout, lowestBid, lowestOwner, isWhitelist, isPlayer = TSM.Scan:GetLowestAuction(itemString, operation)
local bid, buyout, info
local prices = TSM.Util:GetItemPrices(operation, itemString)
if not lowestOwner then
-- No other auctions up, default to normalPrice
info = "postingNormal"
buyout = prices.normalPrice
elseif prices.resetPrice and lowestBuyout <= prices.minPrice then
-- item is below min price and a priceReset is set
if operation.priceReset == "minPrice" then
info = "postingResetMin"
elseif operation.priceReset == "maxPrice" then
info = "postingResetMax"
elseif operation.priceReset == "normalPrice" then
info = "postingResetNormal"
else
-- should never happen, but better to throw an error here than cause issues later on
error("Unknown 'below minimum' price setting.")
end
buyout = prices.resetPrice
elseif isPlayer or (isWhitelist and lowestBuyout - prices.undercut <= prices.maxPrice) then
-- Either we already have one up or someone on the whitelist does
bid, buyout = min(lowestBid, lowestBuyout), lowestBuyout
info = isPlayer and "postingPlayer" or "postingWhitelist"
else
-- we've been undercut and we are going to undercut back
buyout = lowestBuyout - prices.undercut
-- if the cheapest is above our max price, follow the aboveMax setting
if buyout > prices.maxPrice then
if operation.aboveMax == "minPrice" then
info = "aboveMaxMin"
elseif operation.aboveMax == "maxPrice" then
info = "aboveMaxMax"
elseif operation.aboveMax == "normalPrice" then
info = "aboveMaxNormal"
else
-- should never happen, but better to throw an error here than cause issues later on
error("Unknown 'above maximum' price setting.")
end
buyout = prices.aboveMax
end
-- make sure the buyout and bid aren't below the minPrice
buyout = max(buyout, prices.minPrice)
-- Check if the bid is too low
bid = max(buyout * operation.bidPercent, prices.minPrice)
info = info or "postingUndercut"
end
-- set the bid if it hasn't been set
bid = bid or (buyout * operation.bidPercent)
return bid, buyout, info
end
function Post:QueueItemToPost(itemString, numStacks, stackSize, bid, buyout, postTime, operation)
itemLocations[itemString] = itemLocations[itemString] or Post:FindItemSlot(itemString, true)
for i = 1, numStacks do
local oBag, oSlot
for j = 1, #itemLocations[itemString] do
if itemLocations[itemString][j].quantity >= stackSize then
oBag, oSlot = itemLocations[itemString][j].bag, itemLocations[itemString][j].slot
itemLocations[itemString][j].quantity = itemLocations[itemString][j].quantity - stackSize
break
end
end
if not oBag or not oSlot then
oBag, oSlot = Post:FindItemSlot(itemString)
if not (oBag and oSlot) then break end
end
tinsert(postQueue, { bag = oBag, slot = oSlot, bid = bid, buyout = buyout, postTime = postTime, stackSize = stackSize, numStacks = (numStacks - i + 1), itemString = itemString, operation = operation })
totalToPost = totalToPost + 1
end
TSM.Manage:UpdateStatus("manage", totalPosted, totalToPost)
end
function Post:FindItemSlot(findItemString, allLocations)
local locations = {}
Post:UpdateBagState()
for _, data in ipairs(bagInfo) do
local bag, slot, itemString, quantity = unpack(data)
if findItemString == itemString then
if not allLocations then
return bag, slot
end
tinsert(locations, { bag = bag, slot = slot, quantity = quantity })
end
end
return allLocations and locations
end
function Post:GetNumInBags(itemString)
local num = 0
Post:UpdateBagState()
return bagState[itemString] or 0
end
local timeout = CreateFrame("Frame")
timeout:Hide()
timeout:SetScript("OnUpdate", function(self, elapsed)
self.timeLeft = self.timeLeft - elapsed
if self.timeLeft <= 0 or (postQueue[1] and postQueue[1].bag and postQueue[1].slot and not select(3, GetContainerItemInfo(postQueue[1].bag, postQueue[1].slot)) and not AuctionsCreateAuctionButton:IsEnabled()) then
tremove(postQueue, 1)
Post:UpdateItem()
end
end)
function Post:SetupForAction()
Post:RegisterEvent("CHAT_MSG_SYSTEM")
timeout:Hide()
ClearCursor()
TSM.Manage:UpdateStatus("manage", totalPosted, totalToPost)
wipe(currentItem)
currentItem = postQueue[1]
TSM.Manage:SetCurrentItem(currentItem)
GUI.buttons:Enable()
end
-- Check if an auction was posted and move on if so
function Post:CHAT_MSG_SYSTEM(_, msg)
if msg == ERR_AUCTION_STARTED then
count = count + 1
TSM.Manage:UpdateStatus("confirm", count, totalToPost)
end
end
local countFrame = CreateFrame("Frame")
countFrame:Hide()
countFrame.count = -1
countFrame.timeLeft = 10
countFrame:SetScript("OnUpdate", function(self, elapsed)
self.timeLeft = self.timeLeft - elapsed
if count >= totalToPost or self.timeLeft <= 0 then
self:Hide()
Post:Stop()
elseif count ~= self.count then
self.count = count
self.timeLeft = (totalToPost - count) * 2
end
end)
local function DelayFrame()
if #postQueue > 0 then
Post:UpdateItem()
TSMAPI:CancelFrame("postDelayFrame")
elseif not isScanning then
TSM.Manage:UpdateStatus("manage", totalPosted, totalToPost)
Post:Stop()
TSMAPI:CancelFrame("postDelayFrame")
end
end
function Post:UpdateItem()
ClearCursor()
timeout:Hide()
if #postQueue == 0 then
GUI.buttons:Disable()
if isScanning then
TSMAPI:CreateFunctionRepeat("postDelayFrame", DelayFrame)
else
TSM.Manage:UpdateStatus("manage", totalPosted + 1, totalToPost)
countFrame:Show()
end
return
end
totalPosted = totalPosted + 1
TSM.Manage:UpdateStatus("manage", totalPosted, totalToPost)
wipe(currentItem)
currentItem = postQueue[1]
TSM.Manage:SetCurrentItem(currentItem)
GUI.buttons:Enable()
end
function Post:DoAction()
timeout.timeLeft = 0.1
timeout:Show()
if not AuctionFrameAuctions.duration then
-- Fix in case Blizzard_AuctionUI hasn't set this value yet (which could cause an error)
AuctionFrameAuctions.duration = 2
end
if not currentItem.itemString then
timeout:Hide()
Post:SkipItem()
return
end
if type(currentItem.bag) ~= "number" or type(currentItem.slot) ~= "number" then
local bag, slot = Post:FindItemSlot(currentItem.itemString)
if not bag or not slot then
local link = select(2, TSMAPI:GetSafeItemInfo(currentItem.itemString)) or currentItem.itemString
TSM:Printf(L["Auctioning could not find %s in your bags so has skipped posting it. Running the scan again should resolve this issue."], link)
timeout:Hide()
Post:SkipItem()
return
end
currentItem.bag = bag
currentItem.slot = slot
end
local itemString = TSMAPI:GetBaseItemString(GetContainerItemLink(currentItem.bag, currentItem.slot), true)
if itemString ~= currentItem.itemString then
TSM:Print(L["Please don't move items around in your bags while a post scan is running! The item was skipped to avoid an incorrect item being posted."])
timeout:Hide()
Post:SkipItem()
return
end
PickupContainerItem(currentItem.bag, currentItem.slot)
ClickAuctionSellItemButton(AuctionsItemButton, "LeftButton")
StartAuction(currentItem.bid, currentItem.buyout, currentItem.postTime, currentItem.stackSize, 1)
GUI.buttons:Disable()
end
function Post:SkipItem()
local toSkip = {}
local skipped = tremove(postQueue, 1)
count = count + 1
for i, info in ipairs(postQueue) do
if info.itemString == skipped.itemString and info.bid == skipped.bid and info.buyout == skipped.buyout then
tinsert(toSkip, i)
end
end
sort(toSkip, function(a, b) return a > b end)
for _, index in ipairs(toSkip) do
tremove(postQueue, index)
count = count + 1
totalPosted = totalPosted + 1
end
TSM.Manage:UpdateStatus("manage", totalPosted, totalToPost)
TSM.Manage:UpdateStatus("confirm", count, totalToPost)
Post:UpdateItem()
end
function Post:Stop()
GUI:Stopped()
TSMAPI:CancelFrame("postDelayFrame")
Post:UnregisterAllEvents()
TSMAPI:FireEvent("AUCTIONING:POST:STOPPED")
wipe(currentItem)
totalToPost, totalPosted = 0, 0
isScanning = false
end
function Post:GetAHGoldTotal()
local total = 0
local incomingTotal = 0
for i = 1, GetNumAuctionItems("owner") do
local count, _, _, _, _, _, buyoutAmount = select(3, GetAuctionItemInfo("owner", i))
total = total + buyoutAmount
if count == 0 then
incomingTotal = incomingTotal + buyoutAmount
end
end
return TSMAPI:FormatTextMoneyIcon(total), TSMAPI:FormatTextMoneyIcon(incomingTotal)
end
function Post:GetCurrentItem()
return currentItem
end
function Post:EditPostPrice(itemString, buyout, operation)
local bid = buyout * operation.bidPercent
if currentItem.itemString == itemString then
currentItem.buyout = buyout
currentItem.bid = bid
end
for _, data in ipairs(postQueue) do
if data.itemString == itemString then
data.buyout = buyout
data.bid = bid
end
end
TSM.Manage:SetCurrentItem(currentItem)
end
function Post:DoneScanning()
isScanning = false
if scanItems and next(TSM.Scan.auctionData) then
local currentHash = TSM.Scan.auctionDataHash
local currentCount = TSM.Scan.auctionDataCount
if currentHash and currentCount and (currentHash ~= lastProcessedAuctionDataHash or currentCount ~= lastProcessedAuctionDataCount) then
lastProcessedAuctionDataHash = currentHash
lastProcessedAuctionDataCount = currentCount
TSMAPI:ModuleAPI("AuctionDB", "processScanData", TSM.Scan.auctionData, scanItems, time())
end
end
return totalToPost
end