------------------------------------------------------------------------------- -- Query.lua -- -- Queries the auction house. ------------------------------------------------------------------------------- local _ local L = LibStub("AceLocale-3.0"):GetLocale("AuctionLite", false) -- Maximum number of bytes in the first argument of QueryAuctionItems(). local MAX_QUERY_BYTES = 63; -- Maximum number of retries if we get duplicate pages. local MAX_RETRIES = 3; -- State of our current auction query. local QUERY_STATE_SEND = 1; -- ready to request a new page local QUERY_STATE_WAIT = 2; -- waiting for results of previous request local QUERY_STATE_APPROVE = 3; -- waiting for approval of a purchase -- Time to wait (in seconds) after incomplete results are returned. local QUERY_DELAY = 5; -- Info about current AH query. local Query = nil; -- Is the current call to QueryAuctionItems ours? local OurQuery = false; -- Start an auction query. function AuctionLite:StartQuery(newQuery) if Query ~= nil and Query.state == QUERY_STATE_APPROVE then self:CancelQuery(); end if Query == nil then Query = newQuery; Query.state = QUERY_STATE_SEND; Query.page = 0; Query.retries = 0; Query.data = {}; return true; else return false; end end -- Cancel an auction query. function AuctionLite:CancelQuery() if Query ~= nil then if Query.state == QUERY_STATE_APPROVE then assert(Query.found ~= nil); Query.found = nil; if Query.finish ~= nil then Query.finish(true); end end self:QueryEnd(); end end -- Cancel our queries if we see somebody else interfere. function AuctionLite:QueryAuctionItems_Hook() if not OurQuery then self:CancelQuery(); end end -- Called periodically to check whether a new query should be sent. function AuctionLite:QueryUpdate() -- Find out whether we can send queries. local canSend, canGetAll = CanSendAuctionQuery("list"); if canSend and Query ~= nil and Query.state == QUERY_STATE_SEND then -- Determine the query string. local name = nil; if Query.name ~= nil then name = Query.name; elseif Query.link ~= nil then name = self:SplitLink(Query.link); end -- Did we get a reasonable query? We need a name, and if it's a getAll -- query, it should be on the first page with no shopping list. if name ~= nil and (not Query.getAll or (Query.page == 0 and Query.listing == nil)) then -- Truncate to avoid disconnects. name = self:Truncate(name, MAX_QUERY_BYTES); -- Was getAll requested, and can we actually use it? local getAll = false; if Query.getAll then if canGetAll then getAll = true; else Query.getAll = false; self:Print(L["|cffffd000[Note]|r Fast auction scans can only be used once every 15 minutes. Using a slow scan for now."]); end end -- Submit the query. OurQuery = true; QueryAuctionItems(name, 0, 0, Query.page, false, -1, getAll, Query.exact); OurQuery = false; -- Wait for our result. self:QueryWait(); -- If this is the first query, notify the caller (mostly so that -- they know whether we're doing getAll or not). if Query.update ~= nil and Query.page == 0 then Query.update(0, getAll); end else self:CancelQuery(); end end -- Are we waiting for a more detailed update? If so, check to see -- whether we've timed out. if Query ~= nil and Query.state == QUERY_STATE_WAIT and Query.time ~= nil and Query.time + QUERY_DELAY < time() then Query.time = nil; self:QueryNewData(); end end -- Wait for purchase approval. function AuctionLite:QueryRequestApproval() assert(Query ~= nil and Query.state == QUERY_STATE_WAIT and Query.found ~= nil); Query.state = QUERY_STATE_APPROVE; end -- Wait for incoming data. function AuctionLite:QueryWait() assert(Query ~= nil and (Query.state == QUERY_STATE_SEND or Query.state == QUERY_STATE_APPROVE)); Query.state = QUERY_STATE_WAIT; end -- Get the next page. function AuctionLite:QueryNext() assert(Query ~= nil and (Query.state == QUERY_STATE_WAIT or Query.state == QUERY_STATE_APPROVE)); Query.state = QUERY_STATE_SEND; Query.page = Query.page + 1; Query.retries = 0; end -- Get the current page again. function AuctionLite:QueryCurrent() assert(Query ~= nil and Query.state == QUERY_STATE_WAIT); Query.state = QUERY_STATE_SEND; end -- End the current query. function AuctionLite:QueryEnd() assert(Query ~= nil); Query = nil; end -- Is there currently a query pending? function AuctionLite:QueryInProgress() return (Query ~= nil and Query.state ~= QUERY_STATE_APPROVE); end -- Compute the average and standard deviation of the points in data. function AuctionLite:ComputeStats(data) local count = 0; local sum = 0; local sumSquared = 0; for _, listing in ipairs(data) do if listing.keep then count = count + listing.count; sum = sum + listing.price * listing.count; sumSquared = sumSquared + (listing.price ^ 2) * listing.count; end end local avg = 0; local stddev = 0; if count ~= 0 then avg = sum / count; stddev = math.max(0, sumSquared / count - (sum ^ 2 / count ^ 2)) ^ 0.5; end return avg, stddev; end -- Analyze an AH query result. function AuctionLite:AnalyzeData(rawData) local results = {}; local itemData = {}; local i; -- Split up our data into tables for each item. for _, entry in ipairs(rawData) do local link = entry.link; local count = entry.count; local bid = entry.bid; local buyout = entry.buyout local owner = entry.owner; local bidder = entry.highBidder; if link ~= nil then local price = buyout / count; if price <= 0 then price = bid / count; end local keep = owner ~= UnitName("player") and buyout > 0; local listing = { bid = bid, buyout = buyout, price = price, count = count, owner = owner, bidder = bidder, keep = keep }; if itemData[link] == nil then itemData[link] = {}; end table.insert(itemData[link], listing); end end -- Process each data set. local link, data; for link, data in pairs(itemData) do local done = false; -- Discard any points that are more than 2 SDs away from the mean. -- Repeat until no such points exist. while not done do done = true; local avg, stddev = self:ComputeStats(data); for _, listing in ipairs(data) do if listing.keep and math.abs(listing.price - avg) > 2.5 * stddev then listing.keep = false; done = false; end end end -- We've converged. Compute our min price and other stats. local result = { price = 1000000000, items = 0, listings = 0, itemsMine = 0, listingsMine = 0, itemsAll = 0, listingsAll = 0 }; local setPrice = false; for _, listing in ipairs(data) do if listing.keep then result.items = result.items + listing.count; result.listings = result.listings + 1; if listing.price < result.price then result.price = listing.price; setPrice = true; end end if listing.owner == UnitName("player") then result.itemsMine = result.itemsMine + listing.count; result.listingsMine = result.listingsMine + 1; end result.itemsAll = result.itemsAll + listing.count; result.listingsAll = result.listingsAll + 1; end -- If we kept no data (e.g., all auctions are ours), pick the first -- price. By construction of itemData, there is at least one entry. if not setPrice then result.price = data[1].price; result.priceIsMine = true; end result.data = data; results[link] = result; end return results; end -- Approve purchase of a pending item. function AuctionLite:QueryApprove() assert(Query ~= nil); assert(Query.state == QUERY_STATE_APPROVE); assert(Query.found ~= nil); -- Place the request bid or buyout. local price; if Query.isBuyout then price = Query.found.buyout; else price = Query.found.bid; end if price <= GetMoney() then PlaceAuctionBid("list", Query.found.index, price); self:IgnoreMessage(ERR_AUCTION_BID_PLACED); if Query.isBuyout then self:IgnoreMessage(ERR_AUCTION_WON_S:format(Query.name)); end Query.listing.purchased = true; end -- Clean up. Query.found = nil; -- End the query. local oldQuery = Query; self:QueryEnd(); if oldQuery.finish ~= nil then oldQuery.finish(); end end -- Cancel the current purchase and restart the approval process. function AuctionLite:RestartApproval() assert(Query ~= nil); assert(Query.state == QUERY_STATE_APPROVE); assert(Query.found ~= nil); -- Forget the found item. Query.found = nil; -- Tell the "Buy" frame. self:CancelRequestApproval(); -- Go back to the waiting state. self:QueryWait(); end -- We've got new data. function AuctionLite:QueryNewData() assert(Query ~= nil); assert(Query.state == QUERY_STATE_WAIT); -- We've completed one of our own queries. local seen = Query.page * NUM_AUCTION_ITEMS_PER_PAGE + Query.batch; -- If we're running a getAll query, we'd better have seen everything. assert(not Query.getAll or seen == Query.total); -- Update status. local pct = 0; if Query.total > 0 then pct = math.floor(seen * 100 / Query.total); end if Query.update ~= nil then Query.update(pct, Query.getAll); end -- Handle the new data based on the kind of query. if Query.listing == nil then -- This is a search query, not a purchase. if seen < Query.total then -- Request the next page. self:QueryNext(); else local oldQuery = Query; -- We're done. End the query and return the results. self:QueryEnd(); -- Update our price info. local results = self:AnalyzeData(oldQuery.data); for link, result in pairs(results) do self:UpdateHistoricalPrice(link, result); end -- Notify our caller. if oldQuery.finish ~= nil then oldQuery.finish(results, oldQuery.link); end end else assert(not Query.getAll); assert(Query.found == nil); -- See if we've found the auction we're looking for. local i; for i = 1, Query.batch do local listing = Query.data[Query.page * NUM_AUCTION_ITEMS_PER_PAGE + i]; if self:MatchListing(Query.name, Query.listing, listing) then Query.found = listing; Query.found.index = i; break; end end -- If we found something, request approval. -- Otherwise, get the next page or end the query. if Query.found ~= nil then self:RequestApproval(); self:QueryRequestApproval(); elseif seen < Query.total then self:QueryNext(); else local oldQuery = Query; self:QueryEnd(); if oldQuery.finish ~= nil then oldQuery.finish(); end end end end -- Handle a completed auction query. function AuctionLite:AUCTION_ITEM_LIST_UPDATE() -- If we were waiting for approval of a purchase, and the list of items -- changed from underneath us, we need to find the item again. if Query ~= nil and Query.state == QUERY_STATE_APPROVE then self:RestartApproval(); end -- Now handle the data for real. if Query ~= nil and Query.state == QUERY_STATE_WAIT then Query.batch, Query.total = GetNumAuctionItems("list"); -- Workaround for Blizzard bug in getAll queries that can cause it -- to return bogus values for Query.total. if Query.getAll and Query.total ~= Query.batch then Query.total = Query.batch; end local incomplete = 0; local i; -- Doing this speeds up performance by approx. 9500%. I didn't actually measure it, but it is absolutely staggering what a boost we get. AuctionFrameBrowse:UnregisterEvent("AUCTION_ITEM_LIST_UPDATE") C_Timer.After(2, function() AuctionFrameBrowse:RegisterEvent("AUCTION_ITEM_LIST_UPDATE") end) for i = 1, Query.batch do local listing = self:GetListing("list", i); -- Sometimes we get incomplete records. Is this one of them? if listing.owner == nil then incomplete = incomplete + 1; end -- Record the data. Query.data[Query.page * NUM_AUCTION_ITEMS_PER_PAGE + i] = listing; end local duplicate = Query.page > 0 and self:MatchPages(Query.data, Query.page - 1, Query.page); -- If we got a duplicate record, request the current one again. -- If it's an incomplete record, wait. -- Otherwise, process the data. if duplicate and Query.retries < MAX_RETRIES then Query.retries = Query.retries + 1; self:QueryCurrent(); elseif Query.wait and incomplete > 0 then Query.time = time(); else Query.time = nil; self:QueryNewData(); end end end