--[[ Auctioneer Version: 5.9.4961 (WhackyWallaby) Revision: $Id: CorePost.lua 4960 2010-10-19 21:00:30Z Nechckn $ URL: http://auctioneeraddon.com/ This is an addon for World of Warcraft that adds statistical history to the auction data that is collected when the auction is scanned, so that you can easily determine what price you will be able to sell an item for at auction or at a vendor whenever you mouse-over an item in the game License: This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program(see GPL.txt); if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Note: This AddOn's source code is specifically designed to work with World of Warcraft's interpreted AddOn system. You have an implicit license to use this AddOn with these facilities since that is its designated purpose as per: http://www.fsf.org/licensing/licenses/gpl-faq.html#InterpreterIncompat --]] --[[ Auctioneer Posting Engine. This code helps modules that need to post things to do so in an extremely easy and queueable fashion. This code takes "sigs" as input to most of it's functions. A "sig" is a string containing a colon seperated concatenation of itemId:suffixId:suffixFactor:enchantId A sig must be shortened by truncating trailing 0's - this is required for equality testing Any missing values at the end will be set to zero when the sig is decoded The function AucAdvanced.API.GetSigFromLink(link) may be used to construct a valid sig ]] if not AucAdvanced then return end local coremodule, internal = AucAdvanced.GetCoreModule("CorePost") -- internal is a shared space only accessible to code that can call GetCoreModule, -- which is only the .lua files in Auc-Advanced. Basically, we have an internal use only area. if not coremodule then return end -- Someone has explicitely broken us if (not AucAdvanced.Post) then AucAdvanced.Post = {} end local lib = AucAdvanced.Post local private = {} lib.Private = private local aucPrint = AucAdvanced.Print local Const = AucAdvanced.Const local debugPrint = AucAdvanced.Debug.DebugPrint local _TRANS = AucAdvanced.localizations local DecodeSig -- to be filled with AucAdvanced.API.DecodeSig when it has loaded -- local versions of API globals local floor = floor local type = type local GetItemInfo = GetItemInfo -- Local tooltip for getting soulbound line from tooltip contents local ScanTip = CreateFrame("GameTooltip", "AppraiserTip", UIParent, "GameTooltipTemplate") local ScanTip2 = _G["AppraiserTipTextLeft2"] local ScanTip3 = _G["AppraiserTipTextLeft3"] -- control constants used in the posting mechanism local LAG_ADJUST = (4 / 1000) local SIGLOCK_TIMEOUT = 8 -- seconds timeout waiting for bags to update after auction created local POST_TIMEOUT = 8 -- seconds general timeout after starting an auction, before deciding the auction has failed local POST_ERROR_PAUSE = 5 -- seconds pause after an error before trying next request local POST_THROTTLE = 0.1 -- time before starting to post the next item in the queue local POST_TIMER_INTERVAL = 0.5 -- default interval of updates from the timer local MINIMUM_DEPOSIT = 100 -- 1 silver minimum deposit local PROMPT_HEIGHT = 120 local PROMPT_MIN_WIDTH = 400 local BindTypes = { [ITEM_SOULBOUND] = "Bound", [ITEM_BIND_QUEST] = "Quest", [ITEM_BIND_ON_PICKUP] = "Bound", [ITEM_CONJURED] = "Conjured", [ITEM_ACCOUNTBOUND] = "Accountbound", [ITEM_BIND_TO_ACCOUNT] = "Accountbound", } local UIAuctionErrors = { [ERR_NOT_ENOUGH_MONEY] = "ERR_NOT_ENOUGH_MONEY", [ERR_AUCTION_BAG] = "ERR_AUCTION_BAG", [ERR_AUCTION_BOUND_ITEM] = "ERR_AUCTION_BOUND_ITEM", [ERR_AUCTION_CONJURED_ITEM] = "ERR_AUCTION_CONJURED_ITEM", [ERR_AUCTION_DATABASE_ERROR] = "ERR_AUCTION_DATABASE_ERROR", [ERR_AUCTION_ENOUGH_ITEMS] = "ERR_AUCTION_ENOUGH_ITEMS", [ERR_AUCTION_HOUSE_DISABLED] = "ERR_AUCTION_HOUSE_DISABLED", [ERR_AUCTION_LIMITED_DURATION_ITEM] = "ERR_AUCTION_LIMITED_DURATION_ITEM", [ERR_AUCTION_LOOT_ITEM] = "ERR_AUCTION_LOOT_ITEM", [ERR_AUCTION_QUEST_ITEM] = "ERR_AUCTION_QUEST_ITEM", [ERR_AUCTION_REPAIR_ITEM] = "ERR_AUCTION_REPAIR_ITEM", [ERR_AUCTION_USED_CHARGES] = "ERR_AUCTION_USED_CHARGES", [ERR_AUCTION_WRAPPED_ITEM] = "ERR_AUCTION_WRAPPED_ITEM", } -- todo: in OnLoad auto-replace values with translations based on table key: "ADV_Help_PostError"..key - e.g. "ADV_Help_PostErrorBound" -- Some of these errors are only of use when debugging, so should probably not be translated. i.e. the "InvalidX" codes local ErrorText = { Bound = "Cannot auction a Soulbound item", Accountbound = "Cannot auction an Account Bound item", Conjured = "Cannot auction a Conjured item", Quest = "Cannot auction a Quest item", Lootable = "Cannot auction a Lootable item", Damaged = "Cannot auction a Damaged item", InvalidBid = "Bid value is invalid", InvalidBuyout = "Buyout value is invalid", InvalidDuration = "Duration value is invalid", InvalidSig = "Function requires a valid item sig", InvalidSize = "Size value is invalid", InvalidMultiple = "Multiple stack value is invalid", UnknownItem = "Item is unknown", MaxSize = "Item cannot be stacked that high", NotFound = "Item was not found in inventory", NotEnough = "Not enough of item available", PayDeposit = "Not enough money to pay the deposit", FailTimeout = "Timed out while waiting for confirmation of posting", FailSlot = "Unable to place item in the Auction slot", FailStart = "Failed to start auction", FailMultisell = "Multisell failed to post all requested stacks", } lib.ErrorText = ErrorText -- local constants to index the posting request tables (deprecated) local REQ_SIG = 1 local REQ_COUNT = 2 local REQ_BID = 3 local REQ_BUYOUT = 4 local REQ_DURATION = 5 local REQ_NUMSTACKS = 6 -- function to create a new posting request table; keep sync'd with the above constants -- todo: when we stop using these deprecated constants, remove private.NewRequestTable and streamline into queueing function private.lastPostId = 0 function private.NewRequestTable(sig, count, bid, buyout, duration, numStacks) local postId = private.lastPostId + 1 private.lastPostId = postId local request = { -- backward compatibility indexed values (deprecated) sig, --REQ_SIG count, --REQ_COUNT bid, --REQ_BID buyout, --REQ_BUYOUT duration, --REQ_DURATION numStacks, --REQ_NUMSTACKS -- new style values sig = sig, count = count, bid = bid, buy = buyout, duration = duration, stacks = numStacks, id = postId, posted = 0, } return request end do --[[ Functions to safely handle the Post Request queue ]] local postRequests = {} local lastReported = 0 local reportLock = 0 local lastCountSig, lastCountRequests, lastCountItems, lastCountAuctions function private.QueueReport() lastCountSig = nil if reportLock ~= 0 then return end local queuelength = #postRequests if lastReported ~= queuelength then lastReported = queuelength AucAdvanced.SendProcessorMessage("postqueue", queuelength) end end --[[ not used in current version function private.SetQueueReports(activate) if activate then if reportLock > 0 then reportLock = reportLock - 1 end private.QueueReport() else reportLock = reportLock + 1 end end --]] function private.QueueInsert(request) tinsert(postRequests, request) private.QueueReport() end function private.QueueRemove(index) index = index or 1 if postRequests[index] then local request = tremove(postRequests, index) private.QueueReport() return request end end function private.QueueReorder(indexfrom, indexto) -- removes the request at position indexfrom and reinserts it at position indexto -- when indexto > indexfrom, be aware that the remove operation reindexes the table positions after indexfrom, before the reinsert occurs local queuelen = #postRequests if queuelen < 2 then return end indexfrom = indexfrom or 1 if not indexto or indexto > queuelen then indexto = queuelen end if indexfrom == indexto then return end local request = tremove(postRequests, indexfrom) if not request then return end tinsert(postRequests, indexto, request) private.QueueReport() return true end function private.GetQueueIndex(index) return postRequests[index] end function private.GetQueueIterator() return ipairs(postRequests) end --[[ GetQueueLen() Return number of requests remaining in the Post Queue --]] function lib.GetQueueLen() return #postRequests end --[[ GetQueueItemCount(sig) Return number of requests, total number of items and total number of auctions matching the sig --]] function lib.GetQueueItemCount(sig) if sig and sig == lastCountSig then -- "last item" cache: this function tends to get called multiple times for the same sig return lastCountRequests, lastCountItems, lastCountAuctions end local requestCount, itemCount, auctionCount = 0, 0, 0 for _, request in ipairs(postRequests) do if request.sig == sig then local numstacks = request.stacks requestCount = requestCount + 1 itemCount = itemCount + request.count * numstacks auctionCount = auctionCount + numstacks end end lastCountSig = sig lastCountRequests = requestCount lastCountItems = itemCount lastCountAuctions = auctionCount return requestCount, itemCount, auctionCount end --[[ CancelPostQueue() Safely removes all possible Post requests from the Post queue If we are in the process of posting an auction, that request cannot be removed --]] function lib.CancelPostQueue() if #postRequests > 0 then local request = postRequests[1] -- save the first request wipe(postRequests) if request.posting then if request.prompt then private.HidePrompt() elseif request.time then -- request is currently being posted, put it back in the queue until the posting resolves tinsert(postRequests, request) CancelSell() -- abort current Multisell operation end end private.QueueReport() end end end --of Post Request Queue section local AuctionDurationCode = { 1, --[1] 2, --[2] 3, --[3] [12] = 1, -- hours [24] = 2, [48] = 3, [720] = 1, -- minutes [1440] = 2, [2880] = 3, } function lib.ValidateAuctionDuration(duration) return AuctionDurationCode[duration] end function private.GetRequest(sig, size, bid, buyout, duration, multiple) local id = DecodeSig(sig) if not id then return nil, "InvalidSig" elseif type(size) ~= "number" or size < 1 then return nil, "InvalidSize" elseif type(bid) ~= "number" or bid < 1 then return nil, "InvalidBid" elseif type(buyout) ~= "number" or (buyout < bid and buyout ~= 0) then return nil, "InvalidBuyout" end duration = AuctionDurationCode[duration] if not duration then return nil, "InvalidDuration" end local name, link,_,_,_,_,_, maxSize,_, texture = GetItemInfo(id) if not name then return nil, "UnknownItem" elseif size > maxSize then return nil, "MaxSize" end multiple = tonumber(multiple) or 1 if multiple < 1 or multiple ~= floor(multiple) then return nil, "InvalidMultiple" end local available, total, _, _, _, reason = lib.CountAvailableItems(sig) if total == 0 then return nil, "NotFound" elseif available == 0 and reason then return nil, reason elseif available < size * multiple then return nil, "NotEnough" end local request = private.NewRequestTable(sig, size, bid, buyout, duration, multiple) return request end --[[ PostAuction(sig, size, bid, buyout, duration, [multiple]) Places the request to post a stack of the "sig" item, "size" high into the auction house for "bid" minimum bid, and "buy" buyout and posted for "duration" minutes. The request will be posted "multiple" number of times. This is the main entry point to the Post library for other AddOns, so has the strictest parameter checking "multiple" is optional, defaulting to 1. All other parameters are required. If successful it returns a request id; the id will be included in the "postresult" Processor message for each request If a problem is detected it returns nil, reason reason is an internal short text code; it can be converted to a displayable text message using lib.ErrorCodes[reason] ]] function lib.PostAuction(sig, size, bid, buyout, duration, multiple) local request, reason = private.GetRequest(sig, size, bid, buyout, duration, multiple) if not request then return nil, reason end private.QueueInsert(request) private.Wait(0) -- delay until next OnUpdate return request.id end --[[ PostAuctionClick(sig, size, bid, buyout, duration, multiple) As PostAuction, except that this function will attempt to post the auction immediately if possible May only be called from an OnClick handler --]] function lib.PostAuctionClick(sig, size, bid, buyout, duration, multiple) local request, reason = private.GetRequest(sig, size, bid, buyout, duration, multiple) if not request then return nil, reason end local noqueue = false -- placeholder for a Setting to block queueing when CorePost is busy local isEmpty = lib.GetQueueLen() == 0 if not isEmpty and noqueue then return nil, "Busy" end private.QueueInsert(request) local id = request.id if isEmpty then -- Attempt to post the auction immediately private.Wait(0) private.SigLockUpdate() if not private.IsSigLocked(request.sig) then if private.ClearAuctionSlot() then local bag, slot = private.SelectStack(request) if bag then if private.LoadAuctionSlot(request, bag, slot) then private.StartAuction(request) return id end end end end end if noqueue then -- posting failed, noqueue is flagged, so remove the request we just queued -- todo: consider using/modifying SetQueueReports for this eventuality lib.QueueRemove(lib.GetQueueLen()) return nil, "Busy" else return id end end --[[ DecodeSig(sig) DecodeSig(itemid, suffix, factor, enchant) Returns: itemid, suffix, factor, enchant Deprecated. Retained for library compatibility Real function moved to AucAdvanced.API, with the other sig functions ]] function lib.DecodeSig(matchId, matchSuffix, matchFactor, matchEnchant) if (type(matchId) == "string") then return DecodeSig(matchId) end matchId = tonumber(matchId) if not matchId or matchId == 0 then return end matchSuffix = tonumber(matchSuffix) or 0 matchFactor = tonumber(matchFactor) or 0 matchEnchant = tonumber(matchEnchant) or 0 return matchId, matchSuffix, matchFactor, matchEnchant end --[[ IsAuctionable(bag, slot) Returns: true : if the item is (probably) auctionable. false, reason : if the item is not auctionable reason is an internal (non-localized) string code, use lib.ErrorText[errorcode] for a printable text string This function does not check everything, but if it says no, then the item is definately not auctionable. ]] function lib.IsAuctionable(bag, slot) local _,_,_,_,_,lootable = GetContainerItemInfo(bag, slot) if lootable then return false, "Lootable" end ScanTip:SetOwner(UIParent, "ANCHOR_NONE") ScanTip:ClearLines() ScanTip:SetBagItem(bag, slot) local test = BindTypes[ScanTip2:GetText()] or BindTypes[ScanTip3:GetText()] ScanTip:Hide() if test then return false, test end -- Check for 'fixable' conditions only after checking all 'unfixable' conditions local damage, maxdur = GetContainerItemDurability(bag, slot) if damage and damage ~= maxdur then return false, "Damaged" end return true end --[[ CountAvailableItems(sig) Returns: availableCount, totalCount, unpostableCount, queuedCount, postedCount, unpostableError The Posting modules need to know how many items are available to be posted; this is not the same as the number of items currently in the bags --]] function lib.CountAvailableItems(sig) local matchId, matchSuffix, matchFactor, matchEnchant = DecodeSig(sig) if not matchId then return end local totalCount, unpostableCount = 0, 0 local unpostableError for bag = 0, NUM_BAG_FRAMES do for slot = 1, GetContainerNumSlots(bag) do local link = GetContainerItemLink(bag, slot) if link then local _, itemId, itemSuffix, itemFactor, itemEnchant = AucAdvanced.DecodeLink(link) if itemId == matchId and itemSuffix == matchSuffix and itemFactor == matchFactor and itemEnchant == matchEnchant then local _, count = GetContainerItemInfo(bag, slot) if not count or count < 1 then count = 1 end totalCount = totalCount + count local test, code = lib.IsAuctionable(bag, slot) if not test then unpostableCount = unpostableCount + count if unpostableError ~= "Damaged" then -- if there are both "Damaged" and "Soulbound" items, we want to report the "Damaged" code here unpostableError = code end end end end end end -- any items queued to be posted are unavailable local _, queuedCount = lib.GetQueueItemCount(sig) -- any items under SigLock are unavailable, still appear in bags, but are not included in queue count local siglockCount = private.GetSigLockCount(sig) return (totalCount - unpostableCount - queuedCount - siglockCount), totalCount, unpostableCount, queuedCount, siglockCount, unpostableError end --[[ FindMatchesInBags(sig) FindMatchesInBags(itemId, [suffix, [factor, [enchant, [seed] ] ] ]) Returns: { {bag, slot, count}, ... }, itemCount, blankBagId, blankSlotNumber, foundLink, foundLocked Library wrapper for the internal version, to check parameters (and to support anticipated future changes) ]] function lib.FindMatchesInBags(...) return private.FindMatchesInBags(lib.DecodeSig(...)) end -- Internal implementation of FindMatchesInBags -- This is no longer used by ProcessPosts, and is likely to become deprecated function private.FindMatchesInBags(matchId, matchSuffix, matchFactor, matchEnchant) if not matchId then return end local matches = {} local total = 0 local blankBag, blankSlot, foundLink, foundLocked local itemtype = GetItemFamily(matchId) or 0 if itemtype > 0 then -- check to see if item is itself a bag local _,_,_,_,_,_,_,_,equiploc = GetItemInfo(matchId) if equiploc == "INVTYPE_BAG" then itemtype = 0 -- can only be placed in general-purpose bags end end for bag = 0, 4 do local slots = GetContainerNumSlots(bag) if slots > 0 then local _, bagtype = GetContainerNumFreeSlots(bag) -- can this bag contain the item we're looking for? if bagtype == 0 or bit.band(bagtype, itemtype) ~= 0 then for slot = 1, slots do local link = GetContainerItemLink(bag,slot) if link then local texture, itemCount, locked, quality, readable = GetContainerItemInfo(bag,slot) local itype, itemId, suffix, factor, enchant = AucAdvanced.DecodeLink(link) if itype == "item" and itemId == matchId and suffix == matchSuffix and factor == matchFactor and enchant == matchEnchant and lib.IsAuctionable(bag, slot) then if not itemCount or itemCount < 1 then itemCount = 1 end tinsert(matches, {bag, slot, itemCount}) total = total + itemCount foundLink = link if locked then foundLocked = true end end else -- blank slot if not blankBag then blankBag = bag blankSlot = slot end end end end end end return matches, total, blankBag, blankSlot, foundLink, foundLocked end -- lookup table used by GetDepositCost to avoid a large if/elseif block local depositDurationMultiplier = { 3, --[1] 6, --[2] 12, --[3] [12] = 3, [24] = 6, [48] = 12, [720] = 3, [1440] = 6, [2880] = 12, } --[[ GetDepositCost(item, duration, faction, count) item: itemID or "itemString" or "itemName" or "itemLink" [Required] duration: 1, 2, 3 (Blizzard auction duration codes), 12, 24, 48 (hours), 720, 1440, 2880 (minutes) [defaults to 2] faction: "home" or "neutral" or "Neutral" [defaults to home] count: [defaults to 1] ]] function GetDepositCost(item, duration, faction, count) if not item then return end --[[ Deposit Cost = RoundDown(VendorPrice * FactionMultiplier * StackSize, 3) * DurationMultiplier FactionMultiplier = (0.15 for Home, 0.75 for Neutral) DurationMultiplier = (1 for 12hrs, 2 for 24hrs, 4 for 48hrs) However as there is no lua function for "round down to the nearest multiple of 3", we shall implement this by dividing the FactionMultiplier by 3 (0.05 and 0.25) using 'floor' to round down to the nearest integer and then multiplying the DurationMultiplier by 3 (3, 6 and 12) - [see lookup table above] --]] -- Set up function defaults if not specifically provided duration = depositDurationMultiplier[duration] or 6 if faction == "neutral" or faction == "Neutral" then faction = .25 else faction = .05 end count = count or 1 local _,_,_,_,_,_,_,_,_,_,gsv = GetItemInfo(item) if not gsv and GetSellValue then -- if item is not in local cache, fallback to GetSellValue -- some people may still be using a GetSellValue provider with a saved price database gsv = GetSellValue(item) end if gsv then local deposit = floor(faction * gsv * count) * duration if deposit < MINIMUM_DEPOSIT then deposit = MINIMUM_DEPOSIT end return deposit end end do --[[ SigLock 'Locks' a sig to prevent ProcessPosts from posting it until certain conditions apply This attempts to avoid Internal Auction Errors, which can sometimes be caused by trying to post multiple requests of the same item before the bags are updated --]] local lockedsigs local lastlocktime function private.IsSigLocked(sig) if lockedsigs and lockedsigs[sig] then return true end end function private.GetSigLockCount(sig) -- return count of items posted for locked sig if lockedsigs and lockedsigs[sig] then return lockedsigs[sig].postedcount end return 0 end function private.LockSig(request) local sig = request.sig local postedcount = request.count * request.posted local expectedcount = request.totalcount - postedcount lastlocktime = GetTime() if not lockedsigs then lockedsigs = {} end lockedsigs[sig] = { expectedcount = expectedcount, postedcount = postedcount, } end function private.SigLockClear() -- called when the posting timer is deactivated lockedsigs = nil end function private.SigLockBagUpdate() if not lockedsigs then return end -- BAG_UPDATE events often occur in batches -- so throttle our checks by delaying until the next OnUpdate private.Wait(0) end function private.SigLockUpdate() if not lockedsigs then return end -- use longer timeout delays if connectivity is bad, but always at least 1 second local _,_, lag = GetNetStats() lag = max(lag * LAG_ADJUST, 1) if GetTime() > lastlocktime + lag * SIGLOCK_TIMEOUT then -- global timeout for all SigLocks based on when the last item was locked lockedsigs = nil debugPrint("All SigLocks cleared due to timeout", "CorePost", "SigLock Timeout", "Info") return end -- count items in bags (only for locked sigs) local sigcounts = {} for bag = 0, NUM_BAG_FRAMES do for slot = 1, GetContainerNumSlots(bag) do local link = GetContainerItemLink(bag, slot) if link then local sig = AucAdvanced.API.GetSigFromLink(link) if lockedsigs[sig] then local _, count = GetContainerItemInfo(bag, slot) if not count or count < 1 then count = 1 end sigcounts[sig] = (sigcounts[sig] or 0) + count end end end end -- test each sig to see if it can be unlocked for sig, data in pairs(lockedsigs) do if not sigcounts[sig] or sigcounts[sig] <= data.expectedcount then -- number of items in bags now matches (or less than) expected count lockedsigs[sig] = nil end end if not next(lockedsigs) then lockedsigs = nil end -- delete table if empty end end -- SigLock --[[ PRIVATE: RequestDisplayString(request [, link]) Generates a display string for use in printout --]] function private.RequestDisplayString(request, link) local msg = link or request.link or AucAdvanced.API.GetLinkFromSig(request.sig) or "|cffff0000[Unknown]|r" local count = request.count local numstacks = request.stacks if count > 1 then msg = msg.."x"..count end if numstacks > 1 then msg = numstacks.." * "..msg end return msg end function private.TrackPostingSuccess() local request = private.GetQueueIndex(1) if not (request and request.posting) then return end local posted = request.posted + 1 request.posted = posted if posted >= request.stacks then -- all stacks posted ClearCursor() ClickAuctionSellItemButton() -- Clear Auction slot ClearCursor() private.LockSig(request) private.QueueRemove() private.Wait(POST_THROTTLE) else request.time = GetTime() -- renew timeout for the next stack end AucAdvanced.SendProcessorMessage("postresult", true, request.id, request) end function private.TrackPostingMultisellFail() ClearCursor() ClickAuctionSellItemButton() -- Clear Auction slot ClearCursor() local request = private.GetQueueIndex(1) if not (request and request.posting) then return end local link = request.link if not link then return end private.LockSig(request) private.QueueRemove() private.Wait(POST_ERROR_PAUSE) AucAdvanced.SendProcessorMessage("postresult", false, request.id, request, "FailMultisell") if request.cancelled and not private.lastUIError then -- cancelled by user: display a chat message instead of throwing an error aucPrint(("Multisell batch of %s was cancelled. %d were posted."):format(private.RequestDisplayString(request, link), request.posted)) else local msg = ("Failed to post all requested auctions of %s (posted %d)"):format(private.RequestDisplayString(request, link), request.posted) if private.lastUIError then msg = msg.."\nAdditional info: "..private.lastUIError end debugPrint(msg, "CorePost", "Posting Failure", "Warning") geterrorhandler()(msg) end end function private.TrackCancelSell() local request = private.GetQueueIndex(1) if not request or not request.posting then return end -- flag to suppress error messages when the cancelled Multisell 'fails' request.cancelled = true end --[[ PRIVATE: ClearAuctionSlot() Clears the cursor and the AuctionSellItem slot, and confirms that the clearing was successful Called before starting posting process In most other places we use a shorter inline code sequence, without the confirmations --]] function private.ClearAuctionSlot() -- cursor needs to be clear before we can attempt posting ClearCursor() if GetCursorInfo() or SpellIsTargeting() then return end -- auction slot needs to be clear if GetAuctionSellItemInfo() then ClickAuctionSellItemButton() ClearCursor() if GetAuctionSellItemInfo() then -- it's locked, wait for it to clear private.waitBagUpdate = true -- watch for bag changes too return end end return true end --[[ PRIVATE: SelectStack(request) Decide which stack to put into the Auction Slot for posting --]] function private.SelectStack(request) local sig, count = request.sig, request.count local matchId, matchSuffix, matchFactor, matchEnchant = DecodeSig(sig) local foundBag, foundSlot, foundStop, foundSize --[[ Selection algorithm version 4 Only useful when posting a single stack, as Multisell mode will ignore our selection Ignore un-auctionable stacks Return nil, nil if any matching stacks are locked Find the first stack of the exact size Otherwise find the smallest stack larger than the requested size Otherwise use the first stack found --]] for bag = 0, NUM_BAG_FRAMES do for slot = 1, GetContainerNumSlots(bag) do local link = GetContainerItemLink(bag, slot) if link then local _, itemId, itemSuffix, itemFactor, itemEnchant = AucAdvanced.DecodeLink(link) if itemId == matchId and itemSuffix == matchSuffix and itemFactor == matchFactor and itemEnchant == matchEnchant and lib.IsAuctionable(bag, slot) then local _, thiscount, locked = GetContainerItemInfo(bag,slot) if locked then return -- return immediately if we find a locked stack elseif not foundStop then if thiscount == count then -- found a stack the correct size foundBag = bag foundSlot = slot foundStop = true elseif thiscount > count and (not foundSize or thiscount < foundSize) then -- find the smallest stack larger than the requested size foundSize = thiscount foundBag = bag foundSlot = slot elseif not foundBag then -- record the first bag/slot, if none of the above cases apply foundBag = bag foundSlot = slot end end end end end end if foundBag then return foundBag, foundSlot end return nil, "NotFound" end --[[ PRIVATE: LoadAuctionSlot(request, bag, slot) Loads the item in bag/slot into AuctionSellItem slot AuctionSellItem slot and cursor must be clear Performs numerous checks to verify the item is loaded correctly and is postable --]] function private.LoadAuctionSlot(request, bag, slot) local link = GetContainerItemLink(bag, slot) local checkname = GetItemInfo(link) assert(link and checkname) PickupContainerItem(bag, slot) if not CursorHasItem() then -- failed to pick up from bags, probably due to some unfinished operation; wait for another cycle private.waitBagUpdate = true -- watch for bag changes too return end private.waitBagUpdate = nil private.lastUIError = nil if not AuctionFrameAuctions.duration then -- Fix in case Blizzard_AuctionUI hasn't set this value yet (causes an error otherwise) -- todo: periodically check if this is still needed AuctionFrameAuctions.duration = 2 end ClickAuctionSellItemButton() -- verify that the contents of the Auction slot are what we expect local name, texture, count, quality, canUse, price, pricePerUnit, stackCount, totalCount = GetAuctionSellItemInfo() if name ~= checkname then -- failed to drop item in auction slot, probably because item is not auctionable (but was missed by our checks) local msg = ("Unable to create auction for %s: %s"):format(private.RequestDisplayString(request, link), ErrorText["FailSlot"]) if private.lastUIError then msg = msg.."\nAdditional info: "..private.lastUIError end debugPrint(msg, "CorePost", "Posting Failure", "Warning") private.QueueRemove() private.Wait(POST_ERROR_PAUSE) AucAdvanced.SendProcessorMessage("postresult", false, request.id, request, "FailSlot") if private.lastUIError then geterrorhandler()(msg) else message(msg) end return end if totalCount < request.count * request.stacks then -- not enough items to complete this request; abort whole request ClickAuctionSellItemButton() ClearCursor() -- Put it back in the bags local msg = ("Unable to create auction for %s: %s"):format(private.RequestDisplayString(request, link), ErrorText["NotEnough"]) debugPrint(msg, "CorePost", "Posting Failure", "Warning") private.QueueRemove() private.Wait(POST_ERROR_PAUSE) AucAdvanced.SendProcessorMessage("postresult", false, request.id, request, "NotEnough") message(msg) return end if GetMoney() < CalculateAuctionDeposit(request.duration, request.count) * request.stacks then -- not enough money to pay the deposit ClickAuctionSellItemButton() ClearCursor() -- Put it back in the bags local msg = ("Unable to create auction for %s: %s"):format(private.RequestDisplayString(request, link), ErrorText["PayDeposit"]) debugPrint(msg, "CorePost", "Posting Failure", "Warning") private.QueueRemove() private.Wait(POST_ERROR_PAUSE) AucAdvanced.SendProcessorMessage("postresult", false, request.id, request, "PayDeposit") message(msg) return end -- todo: should we check private.lastUIError at this point for unknown errors? request.link = link -- store specific link (uniqueID) request.totalcount = totalCount -- this will be used by the SigLock mechanism request.selectedcount = count -- used by the Prompt sanity checks request.name = name -- used by the Prompt sanity checks request.texture = texture return true end --[[ PRIVATE: VerifyAuctionSlot(request) Checks that the item in the AuctionSellItem slot still matches the item in 'request' and is still sellable Used while the Prompt is open to see if something has changed --]] function private.VerifyAuctionSlot(request) local name, texture, count, quality, canUse, price, pricePerUnit, stackCount, totalCount = GetAuctionSellItemInfo() if name ~= request.name or count ~= request.selectedcount then -- Either slot has been cleared, or has been replaced with something else return end if totalCount < request.count * request.stacks then return end if GetMoney() < CalculateAuctionDeposit(request.duration, request.count) * request.stacks then return end return true end --[[ PRIVATE: PerformPosting() Called from the Prompt Yes/Continue button Note: silently does nothing when prompt is hidden, to handle macro spam --]] function private.PerformPosting() local request = private.promptRequest private.HidePrompt() if not request then return end request.posting = nil -- temporarily unflag until we complete the checks -- Sanity checks if request ~= private.GetQueueIndex(1) then return end if not private.VerifyAuctionSlot(request) then return end private.StartAuction(request) end --[[ PRIVATE: CancelPosting() Called from the Prompt No/Cancel button --]] function private.CancelPosting() local request = private.promptRequest private.HidePrompt() if request and request == private.GetQueueIndex(1) then ClearCursor() ClickAuctionSellItemButton() -- Clear Auction slot ClearCursor() private.QueueRemove() end end --[[ PRIVATE: ShowPrompt(request) Display the confirmation prompt Item must already be loaded in AuctionSellItem slot --]] function private.ShowPrompt(request) if private.promptRequest then error("CorePost:ShowPrompt - private.promptRequest is not nil") end if private.Prompt:IsShown() then error("CorePost:ShowPrompt - Prompt is already shown") end private.promptRequest = request request.prompt = true request.posting = true private.Prompt:Show() private.Prompt.Text1:SetText("Ready to post "..private.RequestDisplayString(request)) private.Prompt.Text2:SetText("Min Bid "..AucAdvanced.Coins(request.bid, true)..", Buyout "..AucAdvanced.Coins(request.buy, true)) private.Prompt.Item.tex:SetTexture(request.texture) local headwidth = (private.Prompt.Heading:GetStringWidth() or 0) + 70 local width1 = (private.Prompt.Text1:GetStringWidth() or 0) + 70 local width2 = (private.Prompt.Text2:GetStringWidth() or 0) + 70 private.Prompt.Frame:SetWidth(max(headwidth, width1, width2, PROMPT_MIN_WIDTH)) end --[[ PRIVATE: HidePrompt() Close the prompt and tidy up flags May be safely called at any time --]] function private.HidePrompt() local request = private.promptRequest private.Prompt:Hide() private.promptRequest = nil if request then request.prompt = nil end end --[[ PRIVATE: StartAuction(request) Starts the auction Item must already be loaded in AuctionSellItem slot In WoW4.0 or later must only be called from within an OnClick handler (hardware event required) --]] function private.StartAuction(request) debugPrint("Starting auction "..private.RequestDisplayString(request), "CorePost", "Starting Auction", "Info") private.lastUIError = nil StartAuction(request.bid, request.buy, request.duration, request.count, request.stacks) if UIAuctionErrors[private.lastUIError] then -- UI Error is one of the known Auction errors that prevent posting ClearCursor() ClickAuctionSellItemButton() ClearCursor() -- Put it back in the bags local msg = ("Unable to create auction for %s: %s"):format(private.RequestDisplayString(request), ErrorText["FailStart"]) msg = msg.."\nAdditional info: "..private.lastUIError debugPrint(msg, "CorePost", "Posting Failure", "Warning") private.QueueRemove() private.Wait(POST_ERROR_PAUSE) AucAdvanced.SendProcessorMessage("postresult", false, request.id, request, "FailStart") --message(msg) geterrorhandler()(msg) return end request.time = GetTime() -- record time of posting for calculating timeout request.posting = true return true end --[[ ProcessPosts() This function is responsible for maintaining and processing the post queue. Only called from OnUpdate. Use private.Wait(0) to trigger ProcessPosts on next update. ]] local function ProcessPosts() if lib.GetQueueLen() <= 0 or not (AuctionFrame and AuctionFrame:IsVisible()) then private.Wait() -- put timer to sleep return end private.Wait(POST_TIMER_INTERVAL) -- set default wait time (overwritten later in certain cases) local request = private.GetQueueIndex(1) if request.posting then -- This request is being posted. Check for timeout -- (Other success/fail situations are handled by the TrackPostingX functions) -- use longer timeout delays if connectivity is bad, but always at least 1 second if request.prompt or not request.time then return end local _,_, lag = GetNetStats() lag = max(lag * LAG_ADJUST, 1) if GetTime() > request.time + lag * POST_TIMEOUT then local msg = ("Unable to confirm auction for %s: %s"):format(private.RequestDisplayString(request), ErrorText["FailTimeout"]) if private.lastUIError then msg = msg.."\nAdditional info: "..private.lastUIError end debugPrint(msg, "CorePost", "Posting timeout", "Warning") private.QueueRemove() private.Wait(POST_ERROR_PAUSE) AucAdvanced.SendProcessorMessage("postresult", false, request.id, request, "FailTimeout") if private.lastUIError then geterrorhandler()(msg) else message(msg) end end return end private.SigLockUpdate() -- check the status of any SigLocks if private.IsSigLocked(request.sig) then -- see if we can find a request in the queue that is not SigLocked for index, req in private.GetQueueIterator() do if not private.IsSigLocked(req.sig) then private.QueueReorder(index, 1) -- move to the front of the queue private.Wait(0) -- wait for next OnUpdate return end end -- otherwise wait for the SigLock(s) to clear return end if not private.ClearAuctionSlot() then return end local bag, slot = private.SelectStack(request) if bag then if not private.LoadAuctionSlot(request, bag, slot) then return end private.ShowPrompt(request) elseif slot then -- bag == nil -- 'slot' contains the error code private.Wait(POST_ERROR_PAUSE) local msg = ("Aborting post request for %s: %s"):format(private.RequestDisplayString(request), ErrorText[slot]) debugPrint(msg, "CorePost", "Post request aborted", "Warning") private.QueueRemove() AucAdvanced.SendProcessorMessage("postresult", false, request.id, request, slot) message(msg) return else -- both bag and slot are nil: wait for another cycle -- no errors but for some reason we cannot post this request at this time -- (in current implementation, only occurs if we have found a locked stack) private.waitBagUpdate = true -- flag to trigger for bag changes too end end --[[ Frame for OnUpdate and OnEvent handlers ]] local EventFrame = CreateFrame("Frame") local EventFrameTimer -- Countdown timer EventFrame:Hide() -- Timer is initially stopped EventFrame:SetScript("OnUpdate", function(self, elapsed) EventFrameTimer = EventFrameTimer - elapsed if EventFrameTimer <= 0 then ProcessPosts() -- EventFrameTimer will be updated by ProcessPosts via a call to private.Wait() end end) --[[ PRIVATE: Wait(delay) Used to control the timer and event handler Use delay = nil to stop the timer Use delay >= 0 to start the timer and set the delay length --]] function private.Wait(delay) if delay then if not EventFrameTimer then EventFrame:Show() end EventFrameTimer = delay else if EventFrameTimer then EventFrame:Hide() EventFrameTimer = nil end private.SigLockClear() end end local function EventHandler(self, event, arg1, arg2) if not EventFrameTimer then if event == "AUCTION_HOUSE_SHOW" then if lib.GetQueueLen() > 0 then private.Wait(0) -- wake up timer end end return end if event == "CHAT_MSG_SYSTEM" then if arg1 == ERR_AUCTION_STARTED then private.TrackPostingSuccess() end elseif event == "UI_ERROR_MESSAGE" then private.lastUIError = arg1 elseif event == "AUCTION_MULTISELL_START" then if AuctionProgressFrame.fadeOut then -- stop the fade and set alpha back to full -- AuctionProgressFrame is a global defined in Blizzard_AuctionUI AuctionProgressFrame.fadeOut = nil AuctionProgressFrame:SetAlpha(1) end --elseif event == "AUCTION_MULTISELL_UPDATE" then elseif event == "AUCTION_MULTISELL_FAILURE" then private.TrackPostingMultisellFail() elseif event == "ITEM_LOCK_CHANGED" then if private.waitBagUpdate then private.waitBagUpdate = nil private.Wait(0) end elseif event == "BAG_UPDATE" then private.SigLockBagUpdate() if private.waitBagUpdate then private.waitBagUpdate = nil private.Wait(0) end elseif event == "AUCTION_HOUSE_CLOSED" then if lib.GetQueueLen() > 0 then private.HidePrompt() if AucAdvanced.Settings.GetSetting("post.clearonclose") then if AucAdvanced.Settings.GetSetting("post.confirmonclose") then StaticPopup_Show("POST_CANCEL_QUEUE_AH_CLOSED") else lib.CancelPostQueue() end end -- if currently multiselling, it will fail - treat as deliberate cancel to suppress error private.TrackCancelSell() end end end EventFrame:SetScript("OnEvent", EventHandler) EventFrame:RegisterEvent("CHAT_MSG_SYSTEM") EventFrame:RegisterEvent("UI_ERROR_MESSAGE") EventFrame:RegisterEvent("AUCTION_MULTISELL_START") --EventFrame:RegisterEvent("AUCTION_MULTISELL_UPDATE") EventFrame:RegisterEvent("AUCTION_MULTISELL_FAILURE") EventFrame:RegisterEvent("ITEM_LOCK_CHANGED") EventFrame:RegisterEvent("BAG_UPDATE") EventFrame:RegisterEvent("AUCTION_HOUSE_SHOW") EventFrame:RegisterEvent("AUCTION_HOUSE_CLOSED") function coremodule.OnLoad(addon) if addon == "auc-advanced" then -- Install values into locals/tables, that are not available until Auctioneer is fully loaded DecodeSig = AucAdvanced.API.DecodeSig for code, text in pairs(ErrorText) do local transkey = "ADV_Help_PostError"..code local transtext = _TRANS(transkey) if transtext ~= transkey then -- _TRANS returns transkey if there is no available translation ErrorText[code] = transtext end end end end -- Other hooks private.hook_CancelSell = CancelSell CancelSell = function(...) private.TrackCancelSell() -- needs to be pre-hooked return private.hook_CancelSell(...) end StaticPopupDialogs["POST_CANCEL_QUEUE_AH_CLOSED"] = { text = "The Auctionhouse has closed. Do you want to clear the Posting queue?", button1 = YES, button2 = NO, OnAccept = lib.CancelPostQueue, timeout = 20, whileDead = true, hideOnEscape = true, } --[[ Prompt Frame ]]-- -- (Cloned and modified from CoreBuy) --this is a anchor frame that never changes size private.Prompt = CreateFrame("Frame", "AuctioneerPostPrompt", UIParent) private.Prompt:Hide() private.Prompt:SetPoint("CENTER", UIParent, "CENTER", 0, -50) private.Prompt:SetFrameStrata("DIALOG") private.Prompt:SetHeight(PROMPT_HEIGHT) private.Prompt:SetWidth(PROMPT_MIN_WIDTH) private.Prompt:SetMovable(true) private.Prompt:SetClampedToScreen(true) --The "graphic" frame and backdrop that we resize private.Prompt.Frame = CreateFrame("Frame", nil, private.Prompt) private.Prompt.Frame:SetPoint("CENTER", private.Prompt, "CENTER" ) private.Prompt.Frame:SetFrameLevel(private.Prompt:GetFrameLevel() - 1) -- lower level than parent (backdrop) private.Prompt.Frame:SetHeight(PROMPT_HEIGHT) private.Prompt.Frame:SetWidth(PROMPT_MIN_WIDTH) private.Prompt.Frame:SetClampedToScreen(true) private.Prompt.Frame:SetBackdrop({ bgFile = "Interface/Tooltips/UI-Tooltip-Background", edgeFile = "Interface/Tooltips/UI-Tooltip-Border", tile = true, tileSize = 32, edgeSize = 32, insets = { left = 9, right = 9, top = 9, bottom = 9 } }) private.Prompt.Frame:SetBackdropColor(0,0,0,0.8) -- Helper functions local function ShowTooltip() local link = private.promptRequest and private.promptRequest.link if link then GameTooltip:SetOwner(private.Prompt.Item, "ANCHOR_TOPRIGHT") GameTooltip:SetHyperlink(link) end end local function HideTooltip() GameTooltip:Hide() end local function DragStart() private.Prompt:StartMoving() end local function DragStop() private.Prompt:StopMovingOrSizing() end private.Prompt.Item = CreateFrame("Button", nil, private.Prompt) -- todo: does this really need to be a button? private.Prompt.Item:SetNormalTexture("Interface\\Buttons\\UI-Slot-Background") private.Prompt.Item:GetNormalTexture():SetTexCoord(0,0.640625, 0, 0.640625) private.Prompt.Item:SetPoint("TOPLEFT", private.Prompt.Frame, "TOPLEFT", 15, -15) private.Prompt.Item:SetHeight(37) private.Prompt.Item:SetWidth(37) private.Prompt.Item:SetScript("OnEnter", ShowTooltip) private.Prompt.Item:SetScript("OnLeave", HideTooltip) private.Prompt.Item.tex = private.Prompt.Item:CreateTexture(nil, "OVERLAY") private.Prompt.Item.tex:SetPoint("TOPLEFT", private.Prompt.Item, "TOPLEFT", 0, 0) private.Prompt.Item.tex:SetPoint("BOTTOMRIGHT", private.Prompt.Item, "BOTTOMRIGHT", 0, 0) private.Prompt.Heading = private.Prompt:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") private.Prompt.Heading:SetPoint("CENTER", private.Prompt.Frame, "CENTER", 20, 30) --private.Prompt.Heading:SetPoint("TOPRIGHT", private.Prompt, "TOPRIGHT", -15, -20) --private.Prompt.Heading:SetJustifyH("CENTER") private.Prompt.Heading:SetText("Auctioneer needs a confirmation to continue posting") private.Prompt.Text1 = private.Prompt:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") private.Prompt.Text1:SetPoint("CENTER", private.Prompt.Frame, "CENTER", 20, 10) private.Prompt.Text2 = private.Prompt:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") private.Prompt.Text2:SetPoint("CENTER", private.Prompt.Frame, "CENTER", 20, -10) -- Yes and No buttons are named to allow macros to /click them private.Prompt.Yes = CreateFrame("Button", "AuctioneerPostPromptYes", private.Prompt, "OptionsButtonTemplate") private.Prompt.Yes:SetText(CONTINUE) --private.Prompt.Yes:SetPoint("BOTTOMRIGHT", private.Prompt, "BOTTOMRIGHT", -100, 10) private.Prompt.Yes:SetPoint("BOTTOMLEFT", private.Prompt, "BOTTOMLEFT", 100, 10) private.Prompt.Yes:SetScript("OnClick", private.PerformPosting) private.Prompt.No = CreateFrame("Button", "AuctioneerPostPromptNo", private.Prompt, "OptionsButtonTemplate") private.Prompt.No:SetText(CANCEL) --private.Prompt.No:SetPoint("BOTTOMRIGHT", private.Prompt.Yes, "BOTTOMLEFT", -60, 0) private.Prompt.No:SetPoint("BOTTOMLEFT", private.Prompt.Yes, "BOTTOMRIGHT", 60, 0) private.Prompt.No:SetScript("OnClick", private.CancelPosting) private.Prompt.DragTop = CreateFrame("Button", nil, private.Prompt) private.Prompt.DragTop:SetPoint("TOPLEFT", private.Prompt, "TOPLEFT", 10, -5) private.Prompt.DragTop:SetPoint("TOPRIGHT", private.Prompt, "TOPRIGHT", -10, -5) private.Prompt.DragTop:SetHeight(6) private.Prompt.DragTop:SetHighlightTexture("Interface\\FriendsFrame\\UI-FriendsFrame-HighlightBar") private.Prompt.DragTop:SetScript("OnMouseDown", DragStart) private.Prompt.DragTop:SetScript("OnMouseUp", DragStop) private.Prompt.DragBottom = CreateFrame("Button", nil, private.Prompt) private.Prompt.DragBottom:SetPoint("BOTTOMLEFT", private.Prompt, "BOTTOMLEFT", 10, 5) private.Prompt.DragBottom:SetPoint("BOTTOMRIGHT", private.Prompt, "BOTTOMRIGHT", -10, 5) private.Prompt.DragBottom:SetHeight(6) private.Prompt.DragBottom:SetHighlightTexture("Interface\\FriendsFrame\\UI-FriendsFrame-HighlightBar") private.Prompt.DragBottom:SetScript("OnMouseDown", DragStart) private.Prompt.DragBottom:SetScript("OnMouseUp", DragStop) AucAdvanced.RegisterRevision("$URL: http://svn.norganna.org/auctioneer/branches/5.9/Auc-Advanced/CorePost.lua $", "$Rev: 4960 $")