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