TradeSkillMaster/LibTSM/Service/AuctionHouseWrapper.lua

735 lines
25 KiB
Lua

-- ------------------------------------------------------------------------------ --
-- 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("{<ItemLocation:(%d,%d)>}", 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