-- ------------------------------------------------------------------------------ -- -- TradeSkillMaster -- -- https://tradeskillmaster.com -- -- All Rights Reserved - Detailed license information included with addon. -- -- ------------------------------------------------------------------------------ -- local _, TSM = ... local AuctionHouseWrapper = TSM.Init("Service.AuctionHouseWrapper") local LibTSMClass = TSM.Include("LibTSMClass") local Log = TSM.Include("Util.Log") local Delay = TSM.Include("Util.Delay") local Event = TSM.Include("Util.Event") local Table = TSM.Include("Util.Table") local Future = TSM.Include("Util.Future") local Vararg = TSM.Include("Util.Vararg") local Analytics = TSM.Include("Util.Analytics") local Math = TSM.Include("Util.Math") local Debug = TSM.Include("Util.Debug") local APIWrapper = LibTSMClass.DefineClass("APIWrapper") local private = { wrappers = {}, events = {}, argsTemp = {}, sortsPartsTemp = {}, itemKeyPartsTemp = {}, searchQueryAPITimes = {}, isAHOpen = false, lastResponseReceived = 0, hookedTime = {}, lastAuctionCanceledAuctionId = nil, lastAuctionCanceledTime = 0, auctionIdUpdateCallbacks = {}, } local API_TIMEOUT = 5 local GET_ALL_TIMEOUT = 30 local SEARCH_QUERY_THROTTLE_INTERVAL = 60 local SEARCH_QUERY_THROTTLE_MAX = 100 local EMPTY_SORTS_TABLE = {} local ITEM_KEY_KEYS = { "itemID", "itemLevel", "itemSuffix", "battlePetSpeciesID", } local SILENT_EVENTS = { AUCTION_ITEM_LIST_UPDATE = true, REPLICATE_ITEM_LIST_UPDATE = true, } local GENERIC_EVENTS = { CHAT_MSG_SYSTEM = 1, UI_ERROR_MESSAGE = 2, } local GENERIC_EVENT_SEP = "/" local API_EVENT_INFO = TSM.IsWowClassic() and { -- Classic QueryAuctionItems = { AUCTION_ITEM_LIST_UPDATE = { result = true }, }, } or { -- Retail SendBrowseQuery = { AUCTION_HOUSE_BROWSE_RESULTS_UPDATED = { result = true }, }, SearchForFavorites = { AUCTION_HOUSE_BROWSE_RESULTS_UPDATED = { result = true }, }, SearchForItemKeys = { AUCTION_HOUSE_BROWSE_RESULTS_UPDATED = { result = true }, }, ReplicateItems = { REPLICATE_ITEM_LIST_UPDATE = { result = true }, }, RequestMoreBrowseResults = { AUCTION_HOUSE_BROWSE_RESULTS_ADDED = { result = 1 }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_DATABASE_ERROR] = { timeoutChange = 1 }, }, SendSearchQuery = { COMMODITY_SEARCH_RESULTS_UPDATED = { result = true, eventArgIndex = 1, apiArgIndex = 1, apiArgKey = "itemID" }, ITEM_SEARCH_RESULTS_UPDATED = { result = true, eventArgIndex = 1, apiArgIndex = 1 }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_DATABASE_ERROR] = { timeoutChange = 1 }, }, SendSellSearchQuery = { COMMODITY_SEARCH_RESULTS_UPDATED = { result = true, eventArgIndex = 1, apiArgIndex = 1, apiArgKey = "itemID" }, ITEM_SEARCH_RESULTS_UPDATED = { result = true, eventArgIndex = 1, apiArgIndex = 1 }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_DATABASE_ERROR] = { timeoutChange = 1 }, }, RequestMoreCommoditySearchResults = { COMMODITY_SEARCH_RESULTS_ADDED = { result = true }, }, RequestMoreItemSearchResults = { ITEM_SEARCH_RESULTS_ADDED = { result = true }, }, RefreshCommoditySearchResults = { COMMODITY_SEARCH_RESULTS_UPDATED = { result = true }, }, RefreshItemSearchResults = { ITEM_SEARCH_RESULTS_UPDATED = { result = true }, }, QueryOwnedAuctions = { OWNED_AUCTIONS_UPDATED = { result = true }, }, QueryBids = { BIDS_UPDATED = { result = true }, }, CancelAuction = { AUCTION_CANCELED = { result = true, eventArgIndex = 1, apiArgIndex = 1, compareFunc = function(eventArg, apiArg) return eventArg == 0 or apiArg == eventArg end }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_ITEM_NOT_FOUND] = { result = false }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_NOT_ENOUGH_MONEY] = { result = false }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_DATABASE_ERROR] = { result = false }, }, StartCommoditiesPurchase = { COMMODITY_PRICE_UPDATED = { result = 2, rawFilterFunc = function(apiArgs, unitPrice, totalPrice) return Math.Ceil((totalPrice / apiArgs[2]), COPPER_PER_SILVER) == apiArgs[3] and true end }, COMMODITY_PRICE_UNAVAILABLE = { result = false }, }, ConfirmCommoditiesPurchase = { COMMODITY_PURCHASE_SUCCEEDED = { result = true }, COMMODITY_PURCHASE_FAILED = { result = false }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_DATABASE_ERROR] = { result = false }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_HIGHER_BID] = { result = false }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_ITEM_NOT_FOUND] = { result = false }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_BID_OWN] = { result = false }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_NOT_ENOUGH_MONEY] = { result = false }, }, PlaceBid = { AUCTION_CANCELED = { result = true, eventArgIndex = 1, apiArgIndex = 1 }, ["CHAT_MSG_SYSTEM"..GENERIC_EVENT_SEP..ERR_AUCTION_BID_PLACED] = { result = true }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_DATABASE_ERROR] = { result = false }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_HIGHER_BID] = { result = false }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_ITEM_NOT_FOUND] = { result = false }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_BID_OWN] = { result = false }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_NOT_ENOUGH_MONEY] = { result = false }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_ITEM_MAX_COUNT] = { result = false }, }, PostItem = { AUCTION_HOUSE_AUCTION_CREATED = { result = true, rawFilterFunc = function(apiArgs) return apiArgs[3] <= 1 end }, AUCTION_MULTISELL_UPDATE = { result = true, rawFilterFunc = function(apiArgs, createdCount, totalToCreate) return createdCount == totalToCreate end }, AUCTION_MULTISELL_FAILURE = { result = false }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_ITEM_NOT_FOUND] = { result = false }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_DATABASE_ERROR] = { result = false }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_REPAIR_ITEM] = { result = nil }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_LIMITED_DURATION_ITEM] = { result = nil }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_USED_CHARGES] = { result = nil }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_WRAPPED_ITEM] = { result = nil }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_BAG] = { result = nil }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_NOT_ENOUGH_MONEY] = { result = nil }, }, PostCommodity = { AUCTION_HOUSE_AUCTION_CREATED = { result = true }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_ITEM_NOT_FOUND] = { result = false }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_DATABASE_ERROR] = { result = false }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_REPAIR_ITEM] = { result = nil }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_LIMITED_DURATION_ITEM] = { result = nil }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_USED_CHARGES] = { result = nil }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_WRAPPED_ITEM] = { result = nil }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_AUCTION_BAG] = { result = nil }, ["UI_ERROR_MESSAGE"..GENERIC_EVENT_SEP..ERR_NOT_ENOUGH_MONEY] = { result = nil }, }, } -- ============================================================================ -- Module Loading -- ============================================================================ AuctionHouseWrapper:OnModuleLoad(function() Event.Register("AUCTION_HOUSE_SHOW", private.AuctionHouseShowHandler) Event.Register("AUCTION_HOUSE_CLOSED", private.AuctionHouseClosedHandler) -- setup wrappers for apiName in pairs(API_EVENT_INFO) do private.wrappers[apiName] = APIWrapper(apiName) end if not TSM.IsWowClassic() then -- extra hooks to track search query calls since they are limited hooksecurefunc(C_AuctionHouse, "SendSearchQuery", function() tinsert(private.searchQueryAPITimes, GetTime()) end) hooksecurefunc(C_AuctionHouse, "SendSellSearchQuery", function() tinsert(private.searchQueryAPITimes, GetTime()) end) -- events to track auction purchases Event.Register("AUCTION_CANCELED", private.AuctionCanceledHandler) Event.Register("ITEM_SEARCH_RESULTS_UPDATED", private.ItemSearchResultsUpdated) -- general events Event.Register("AUCTION_HOUSE_THROTTLED_MESSAGE_RESPONSE_RECEIVED", private.ResponseReceivedHandler) -- extra events that are interesting to log Event.Register("AUCTION_HOUSE_NEW_RESULTS_RECEIVED", private.UnusedEventHandler) Event.Register("AUCTION_HOUSE_THROTTLED_MESSAGE_DROPPED", private.UnusedEventHandler) Event.Register("AUCTION_HOUSE_THROTTLED_MESSAGE_QUEUED", private.UnusedEventHandler) Event.Register("AUCTION_HOUSE_THROTTLED_MESSAGE_SENT", private.UnusedEventHandler) if not TSM.IsShadowlands() then Event.Register("AUCTION_HOUSE_THROTTLED_SPECIFIC_SEARCH_READY", private.UnusedEventHandler) end Event.Register("AUCTION_HOUSE_THROTTLED_SYSTEM_READY", private.UnusedEventHandler) end end) -- ============================================================================ -- Module Functions -- ============================================================================ function AuctionHouseWrapper.RegisterAuctionIdUpdateCallback(callback) tinsert(private.auctionIdUpdateCallbacks, callback) end function AuctionHouseWrapper.IsOpen() return private.isAHOpen end function AuctionHouseWrapper.GetAndResetTotalHookedTime() local total, topTime, topAddon = 0, nil, nil for addon, hookedTime in pairs(private.hookedTime) do total = total + hookedTime if hookedTime > (topTime or 0) then topTime = hookedTime topAddon = addon end end wipe(private.hookedTime) return total, topAddon, topTime end function AuctionHouseWrapper.SendBrowseQuery(query) assert(not TSM.IsWowClassic()) if not private.CheckAllIdle() then return end return private.wrappers.SendBrowseQuery:Start(query) end function AuctionHouseWrapper.RequestMoreBrowseResults() assert(not TSM.IsWowClassic()) if not private.CheckAllIdle() then return end return private.wrappers.RequestMoreBrowseResults:Start() end function AuctionHouseWrapper.SendSearchQuery(itemKey, isSell) assert(not TSM.IsWowClassic()) if not private.CheckAllIdle() then return end -- remove times which are beyond the throttle interval for i = #private.searchQueryAPITimes, 1, -1 do if GetTime() - private.searchQueryAPITimes[i] >= SEARCH_QUERY_THROTTLE_INTERVAL then tremove(private.searchQueryAPITimes, i) end end if #private.searchQueryAPITimes >= SEARCH_QUERY_THROTTLE_MAX then local delayTime = private.searchQueryAPITimes[1] + SEARCH_QUERY_THROTTLE_INTERVAL - GetTime() assert(delayTime > 0, "Invalid delay time: "..tostring(delayTime)) Log.Err("Search query can't be run for another %.3f seconds", delayTime) return nil, delayTime end assert(type(isSell) == "boolean") if isSell then return private.wrappers.SendSellSearchQuery:Start(itemKey, EMPTY_SORTS_TABLE, true) else return private.wrappers.SendSearchQuery:Start(itemKey, EMPTY_SORTS_TABLE, true) end end function AuctionHouseWrapper.RequestMoreCommoditySearchResults(itemId) assert(not TSM.IsWowClassic()) if not private.CheckAllIdle() then return end return private.wrappers.RequestMoreCommoditySearchResults:Start(itemId) end function AuctionHouseWrapper.RequestMoreItemSearchResults(itemKey) assert(not TSM.IsWowClassic()) if not private.CheckAllIdle() then return end return private.wrappers.RequestMoreItemSearchResults:Start(itemKey) end function AuctionHouseWrapper.QueryOwnedAuctions(sorts) assert(not TSM.IsWowClassic()) if not private.CheckAllIdle() then return end return private.wrappers.QueryOwnedAuctions:Start(sorts) end function AuctionHouseWrapper.CancelAuction(auctionId) assert(not TSM.IsWowClassic()) -- if QueryOwnedAuctions is pending, just cancel it private.wrappers.QueryOwnedAuctions:CancelIfPending() if not private.CheckAllIdle() then return end return private.wrappers.CancelAuction:Start(auctionId) end function AuctionHouseWrapper.StartCommoditiesPurchase(itemId, quantity, itemBuyout) assert(not TSM.IsWowClassic()) if not private.CheckAllIdle() then return end return private.wrappers.StartCommoditiesPurchase:Start(itemId, quantity, itemBuyout) end function AuctionHouseWrapper.ConfirmCommoditiesPurchase(itemId, quantity, totalBuyout) assert(not TSM.IsWowClassic()) if not private.CheckAllIdle() then return end return private.wrappers.ConfirmCommoditiesPurchase:Start(itemId, quantity, totalBuyout) end function AuctionHouseWrapper.PlaceBid(auctionId, bidBuyout) assert(not TSM.IsWowClassic()) if not private.CheckAllIdle() then return end return private.wrappers.PlaceBid:Start(auctionId, bidBuyout) end function AuctionHouseWrapper.PostItem(itemLocation, postTime, stackSize, bid, buyout) assert(not TSM.IsWowClassic()) if not private.CheckAllIdle() then return end return private.wrappers.PostItem:Start(itemLocation, postTime, stackSize, bid, buyout) end function AuctionHouseWrapper.PostCommodity(itemLocation, postTime, stackSize, itemBuyout) assert(not TSM.IsWowClassic()) if not private.CheckAllIdle() then return end return private.wrappers.PostCommodity:Start(itemLocation, postTime, stackSize, itemBuyout) end function AuctionHouseWrapper.QueryAuctionItems(name, minLevel, maxLevel, page, usable, quality, getAll, exact, filterData) assert(TSM.IsWowClassic()) local canSendQuery, canSendGetAll = CanSendAuctionQuery() if not canSendQuery or (getAll and not canSendGetAll) or not private.CheckAllIdle() then return end return private.wrappers.QueryAuctionItems:Start(name, minLevel, maxLevel, page, usable, quality, getAll, exact, filterData) end -- ============================================================================ -- APIWrapper Class -- ============================================================================ function APIWrapper.__init(self, name) self._name = name self._args = {} self._state = "IDLE" self._callTime = nil self._future = Future.New(self._name.."_FUTURE") self._future:SetScript("OnCleanup", function() if self._state == "PENDING_REQUESTED" then -- switch the current call to a hooked call self._state = "PENDING_HOOKED" elseif self._state == "DONE" then self._state = "IDLE" end end) self._timeoutWrapper = function() Log.Err("API timed out: %s(%s)", self._name, private.ArgsToStr(unpack(self._args))) return self:_Done(false) end -- hook the API hooksecurefunc(TSM.IsWowClassic() and _G or C_AuctionHouse, self._name, function(...) Log.Info("%s(%s)", self._name, private.ArgsToStr(...)) if self:_IsPending() and select("#", ...) == 0 then return end self:CancelIfPending() if self:_HandleAPICall(...) then for _, wrapper in pairs(private.wrappers) do if wrapper ~= self and GetTime() ~= private.lastResponseReceived then wrapper:CancelIfPending() end end end end) -- register related events for eventName in pairs(API_EVENT_INFO[self._name]) do private.RegisterForEvent(eventName, self) end end function APIWrapper.IsIdle(self) return self._state == "IDLE" end function APIWrapper.CancelIfPending(self) if not self:_IsPending() then return end Log.Warn("Canceling pending (%s, %s)", self._name, self._state) self:_Done(false) end function APIWrapper.Start(self, ...) if self._state ~= "IDLE" then Log.Err("API already in progress (%s)", self._name) return end self._state = "STARTING" self:_CallAPI(...) return self._future end function APIWrapper._IsPending(self) return self._state == "PENDING_REQUESTED" or self._state == "PENDING_HOOKED" end function APIWrapper._CallAPI(self, ...) return (TSM.IsWowClassic() and _G or C_AuctionHouse)[self._name](...) end function APIWrapper._HandleAPICall(self, ...) self._callTime = GetTime() if self._state == "IDLE" then self._state = "PENDING_HOOKED" self._hookAddon = strmatch(Debug.GetStackLevelLocation(3), "AddOns\\([^\\]+)\\") elseif self._state == "STARTING" then self._future:Start() self._state = "PENDING_REQUESTED" else error("Unexpected state: "..self._state) end Vararg.IntoTable(self._args, ...) local timeout = nil if not private.isAHOpen then timeout = 0 elseif self._name == "QueryAuctionItems" and select(7, ...) then timeout = GET_ALL_TIMEOUT else timeout = API_TIMEOUT end Delay.AfterTime(self._name.."_TIMEOUT", timeout, self._timeoutWrapper) return true end function APIWrapper._HandleEvent(self, eventName, ...) if self._state ~= "PENDING_REQUESTED" and self._state ~= "PENDING_HOOKED" then return end local eventIsValid, result = self:_ValidateEvent(eventName, ...) if not eventIsValid then Log.Info("Ignoring invalidated event") return end self:_Done(result) end function APIWrapper._ValidateEvent(self, eventName, ...) local info = nil if GENERIC_EVENTS[eventName] then local arg = ... info = API_EVENT_INFO[self._name][eventName..GENERIC_EVENT_SEP..arg] else info = API_EVENT_INFO[self._name][eventName] end assert(info) if info.timeoutChange then Delay.Cancel(self._name.."_TIMEOUT") Delay.AfterTime(self._name.."_TIMEOUT", info.timeoutChange, self._timeoutWrapper) return false end local eventIsValid, result = true, nil if type(info.result) == "number" then result = select(info.result, ...) else result = info.result end if info.rawFilterFunc then if not info.rawFilterFunc(self._args, ...) then eventIsValid = false end elseif info.eventArgIndex then local eventValue = select(info.eventArgIndex, ...) local apiValue = self._args[info.apiArgIndex] if info.apiArgKey then apiValue = apiValue[info.apiArgKey] end local argMatches = nil assert(type(eventValue) == type(apiValue)) if info.compareFunc then argMatches = info.compareFunc(eventValue, apiValue) elseif private.IsItemKey(eventValue) then argMatches = true for _, key in ipairs(ITEM_KEY_KEYS) do if eventValue[key] ~= apiValue[key] then argMatches = false break end end elseif type(eventValue) == "table" then argMatches = Table.Equal(eventValue, apiValue) else argMatches = eventValue == apiValue end if not argMatches then eventIsValid = false end end return eventIsValid, result end function APIWrapper._Done(self, result) wipe(self._args) local hookAddon = self._hookAddon self._hookAddon = nil local totalTime = Math.Round((GetTime() - (self._callTime or GetTime())) * 1000) self._callTime = nil Delay.Cancel(self._name.."_TIMEOUT") if self._state == "PENDING_REQUESTED" then if totalTime > 0 then Analytics.Action("AH_API_TIME", private.GetAnalyticsRegionRealm(), self._name, result and totalTime or -1) end self._state = "DONE" -- need to do this last as it might trigger another API call or OnCleanup on the future self._future:Done(result) elseif self._state == "PENDING_HOOKED" then self._state = "IDLE" if hookAddon then private.hookedTime[hookAddon] = (private.hookedTime[hookAddon] or 0) + totalTime / 1000 end else error("Unexpected state: "..self._state) end end -- ============================================================================ -- Private Helper Functions -- ============================================================================ function private.AuctionHouseShowHandler() private.isAHOpen = true end function private.AuctionHouseClosedHandler() private.isAHOpen = false for _, wrapper in pairs(private.wrappers) do wrapper:CancelIfPending() end end function private.IsItemKey(value) if type(value) ~= "table" then return false end for _, key in ipairs(ITEM_KEY_KEYS) do if not value[key] then return false end end return true end function private.ItemKeyToStr(itemKey) assert(#private.itemKeyPartsTemp == 0) if itemKey.itemID ~= 0 then tinsert(private.itemKeyPartsTemp, "itemID="..itemKey.itemID) end if itemKey.itemLevel ~= 0 then tinsert(private.itemKeyPartsTemp, "itemLevel="..itemKey.itemLevel) end if itemKey.itemSuffix ~= 0 then tinsert(private.itemKeyPartsTemp, "itemSuffix="..itemKey.itemSuffix) end if itemKey.battlePetSpeciesID ~= 0 then tinsert(private.itemKeyPartsTemp, "battlePetSpeciesID="..itemKey.battlePetSpeciesID) end local result = format("{%s}", table.concat(private.itemKeyPartsTemp, ",")) wipe(private.itemKeyPartsTemp) return result end function private.SortsToStr(sorts) assert(#private.sortsPartsTemp == 0) for _, sort in ipairs(sorts) do local name = Table.KeyByValue(Enum.AuctionHouseSortOrder, sort.sortOrder) or "?" tinsert(private.sortsPartsTemp, format("%s%s", sort.reverseSort and "-" or "", name)) end local result = format("{%s}", table.concat(private.sortsPartsTemp, ",")) wipe(private.sortsPartsTemp) return result end function private.ArgToStr(arg) if type(arg) == "table" then local count = Table.Count(arg) if private.IsItemKey(arg) then return private.ItemKeyToStr(arg) elseif arg.searchString then return format("{searchString=\"%s\", sorts=%s, minLevel=%s, maxLevel=%s, filters=%s, itemClassFilters=%s}", arg.searchString, private.SortsToStr(arg.sorts), private.ArgToStr(arg.minLevel), private.ArgToStr(arg.maxLevel), private.ArgToStr(arg.filters), private.ArgToStr(arg.itemClassFilters)) elseif arg.IsBagAndSlot then return format("{}", arg:GetBagAndSlot()) elseif count == 0 then return "{}" elseif count == #arg then if type(arg[1]) == "table" and arg[1].sortOrder then return format("{sorts=%s}", private.SortsToStr(arg)) end return format("{<%d items>}", count) else return "{...}" end else return tostring(arg) end end function private.ArgsToStr(...) assert(#private.argsTemp == 0) for _, arg in Vararg.Iterator(...) do tinsert(private.argsTemp, private.ArgToStr(arg)) end local result = table.concat(private.argsTemp, ",") wipe(private.argsTemp) return result end function private.RegisterForEvent(eventName, wrapper) local genericEventArg = nil eventName, genericEventArg = strsplit(GENERIC_EVENT_SEP, eventName) if not private.events[eventName] then private.events[eventName] = {} Event.Register(eventName, private.EventHandler) end if genericEventArg then private.events[eventName][genericEventArg] = private.events[eventName][genericEventArg] or {} tinsert(private.events[eventName][genericEventArg], wrapper) else tinsert(private.events[eventName], wrapper) end end function private.EventHandler(eventName, ...) -- reduce the log spam of generic events by combining the message with the name and discarding arguments local genericEventArg = nil if eventName == "UI_ERROR_MESSAGE" and select(1, ...) == ERR_AUCTION_DATABASE_ERROR then -- log an analytics event for "Internal Auction Error" messages for apiName, wrapper in pairs(private.wrappers) do if not wrapper:IsIdle() then Analytics.Action("AH_INTERNAL_ERROR", private.GetAnalyticsRegionRealm(), apiName) break end end end if GENERIC_EVENTS[eventName] then genericEventArg = select(GENERIC_EVENTS[eventName], ...) assert(genericEventArg) if not private.events[eventName][genericEventArg] then return end private.EventHandlerHelper(private.events[eventName][genericEventArg], eventName, genericEventArg) else private.EventHandlerHelper(private.events[eventName], eventName, ...) end end function private.ResponseReceivedHandler(eventName, ...) Log.Info("%s (%s)", eventName, private.ArgsToStr(...)) private.lastResponseReceived = GetTime() end function private.UnusedEventHandler(eventName, ...) Log.Info("%s (%s)", eventName, private.ArgsToStr(...)) end function private.EventHandlerHelper(wrappers, eventName, ...) if not SILENT_EVENTS[eventName] then Log.Info("%s (%s)", eventName, private.ArgsToStr(...)) end for _, wrapper in ipairs(wrappers) do wrapper:_HandleEvent(eventName, ...) end end function private.CheckAllIdle() for apiName, wrapper in pairs(private.wrappers) do if not wrapper:IsIdle() then Log.Err("Another wrapper is pending (%s)", apiName) return false end end return true end function private.AuctionCanceledHandler(_, auctionId) private.lastAuctionCanceledAuctionId = auctionId private.lastAuctionCanceledTime = GetTime() end function private.ItemSearchResultsUpdated(_, itemKey, auctionId) if private.lastAuctionCanceledTime == GetTime() and auctionId then Log.Info("Auction ID changed from %s to %s", tostring(private.lastAuctionCanceledAuctionId), tostring(auctionId)) local newResultInfo = nil for i = 1, C_AuctionHouse.GetNumItemSearchResults(itemKey) do local info = C_AuctionHouse.GetItemSearchResultInfo(itemKey, i) if info.auctionID == auctionId then newResultInfo = info break end end if not newResultInfo then Log.Warn("Failed to find new result info") end for _, callback in ipairs(private.auctionIdUpdateCallbacks) do callback(private.lastAuctionCanceledAuctionId, auctionId, newResultInfo) end private.lastAuctionCanceledAuctionId = nil private.lastAuctionCanceledTime = 0 end end function private.GetAnalyticsRegionRealm() return TSM.GetRegion().."-"..gsub(GetRealmName(), "\226", "'") end