AuctionLite-classic/QueryAuctions.lua

459 lignes
13 KiB
Lua

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