initial commit
This commit is contained in:
74
LibTSM/Service/AltTracking.lua
Normal file
74
LibTSM/Service/AltTracking.lua
Normal file
@@ -0,0 +1,74 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local AltTracking = TSM.Init("Service.AltTracking")
|
||||
local Database = TSM.Include("Util.Database")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Vararg = TSM.Include("Util.Vararg")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local private = {
|
||||
quantityDB = nil,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
AltTracking:OnSettingsLoad(function()
|
||||
private.quantityDB = Database.NewSchema("INVENTORY_ALT_QUANTITY")
|
||||
:AddUniqueStringField("itemString")
|
||||
:AddNumberField("quantity")
|
||||
:Commit()
|
||||
|
||||
local altItemQuantity = TempTable.Acquire()
|
||||
for _, factionrealm, character, syncScopeKey in Settings.ConnectedFactionrealmAltCharacterIterator() do
|
||||
for _, key in Vararg.Iterator("bagQuantity", "bankQuantity", "reagentBankQuantity", "auctionQuantity", "mailQuantity") do
|
||||
for itemString, quantity in pairs(Settings.Get("sync", syncScopeKey, "internalData", key)) do
|
||||
altItemQuantity[itemString] = (altItemQuantity[itemString] or 0) + quantity
|
||||
end
|
||||
end
|
||||
local pendingMailLookup = Settings.Get("factionrealm", factionrealm, "internalData", "pendingMail")
|
||||
local pendingMail = pendingMailLookup[character]
|
||||
local isValid = true
|
||||
if pendingMail then
|
||||
for itemString, quantity in pairs(pendingMail) do
|
||||
if type(quantity) ~= "number" then
|
||||
isValid = false
|
||||
break
|
||||
end
|
||||
altItemQuantity[itemString] = (altItemQuantity[itemString] or 0) + quantity
|
||||
end
|
||||
end
|
||||
if not isValid then
|
||||
pendingMailLookup[character] = nil
|
||||
end
|
||||
end
|
||||
private.quantityDB:BulkInsertStart()
|
||||
for itemString, quantity in pairs(altItemQuantity) do
|
||||
private.quantityDB:BulkInsertNewRow(itemString, quantity)
|
||||
end
|
||||
private.quantityDB:BulkInsertEnd()
|
||||
TempTable.Release(altItemQuantity)
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function AltTracking.BaseItemIterator()
|
||||
return private.quantityDB:NewQuery()
|
||||
:Select("itemString")
|
||||
:IteratorAndRelease()
|
||||
end
|
||||
|
||||
function AltTracking.GetQuantityByBaseItemString(baseItemString)
|
||||
return private.quantityDB:GetUniqueRowField("itemString", baseItemString, "quantity") or 0
|
||||
end
|
||||
734
LibTSM/Service/AuctionHouseWrapper.lua
Normal file
734
LibTSM/Service/AuctionHouseWrapper.lua
Normal file
@@ -0,0 +1,734 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- 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
|
||||
65
LibTSM/Service/AuctionScan.lua
Normal file
65
LibTSM/Service/AuctionScan.lua
Normal file
@@ -0,0 +1,65 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Auction scanning functions.
|
||||
-- @module AuctionScan
|
||||
|
||||
local _, TSM = ...
|
||||
local AuctionScan = TSM.Init("Service.AuctionScan")
|
||||
local ScanManager = TSM.Include("Service.AuctionScanClasses.ScanManager")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function AuctionScan.GetManager()
|
||||
return ScanManager.Get()
|
||||
end
|
||||
|
||||
function AuctionScan.CanBid(subRow)
|
||||
if not subRow:IsSubRow() then
|
||||
return false
|
||||
end
|
||||
local _, _, _, isHighBidder = subRow:GetBidInfo()
|
||||
local displayedBid = subRow:GetDisplayedBids()
|
||||
local buyout = subRow:GetBuyouts()
|
||||
if isHighBidder then
|
||||
return false
|
||||
elseif displayedBid == buyout then
|
||||
return false
|
||||
elseif not TSM.IsWowClassic() and subRow:IsCommodity() then
|
||||
return false
|
||||
elseif GetMoney() < subRow:GetRequiredBid() then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function AuctionScan.CanBuyout(subRow, auctionScan)
|
||||
if not subRow:IsSubRow() then
|
||||
return false
|
||||
end
|
||||
local buyout, itemBuyout = subRow:GetBuyouts()
|
||||
if not TSM.IsWowClassic() then
|
||||
buyout = itemBuyout
|
||||
end
|
||||
local itemString = subRow:GetItemString()
|
||||
if buyout == 0 or GetMoney() < buyout then
|
||||
return false
|
||||
elseif not TSM.IsWowClassic() and subRow:IsCommodity() then
|
||||
-- make sure it's the cheapest
|
||||
local isCheapest = false
|
||||
for _, query in auctionScan:QueryIterator() do
|
||||
isCheapest = isCheapest or subRow == query:GetCheapestSubRow(itemString)
|
||||
end
|
||||
if not isCheapest then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
675
LibTSM/Service/AuctionScanClasses/Query.lua
Normal file
675
LibTSM/Service/AuctionScanClasses/Query.lua
Normal file
@@ -0,0 +1,675 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- AuctionQuery Class.
|
||||
-- A class which is used to build a query to scan the auciton house.
|
||||
-- @classmod AuctionQuery
|
||||
|
||||
local _, TSM = ...
|
||||
local Query = TSM.Init("Service.AuctionScanClasses.Query")
|
||||
local String = TSM.Include("Util.String")
|
||||
local ObjectPool = TSM.Include("Util.ObjectPool")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Table = TSM.Include("Util.Table")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local AuctionHouseWrapper = TSM.Include("Service.AuctionHouseWrapper")
|
||||
local Scanner = TSM.Include("Service.AuctionScanClasses.Scanner")
|
||||
local LibTSMClass = TSM.Include("LibTSMClass")
|
||||
local AuctionQuery = LibTSMClass.DefineClass("AuctionQuery")
|
||||
local private = {
|
||||
objectPool = ObjectPool.New("AUCTION_SCAN_QUERY", AuctionQuery),
|
||||
}
|
||||
local ITEM_SPECIFIC = newproxy()
|
||||
local ITEM_BASE = newproxy()
|
||||
local DEFAULT_SORTS = TSM.IsWowClassic() and
|
||||
{ -- classic
|
||||
"seller",
|
||||
"quantity",
|
||||
"unitprice",
|
||||
} or
|
||||
{ -- retail
|
||||
{ sortOrder = Enum.AuctionHouseSortOrder.Price, reverseSort = false },
|
||||
{ sortOrder = Enum.AuctionHouseSortOrder.Name, reverseSort = false },
|
||||
}
|
||||
local EMPTY_SORTS = {}
|
||||
local INV_TYPES = {
|
||||
CHEST = TSM.IsShadowlands() and Enum.InventoryType.IndexChestType or LE_INVENTORY_TYPE_CHEST_TYPE,
|
||||
ROBE = TSM.IsShadowlands() and Enum.InventoryType.IndexRobeType or LE_INVENTORY_TYPE_ROBE_TYPE,
|
||||
NECK = TSM.IsShadowlands() and Enum.InventoryType.IndexNeckType or LE_INVENTORY_TYPE_NECK_TYPE,
|
||||
FINGER = TSM.IsShadowlands() and Enum.InventoryType.IndexFingerType or LE_INVENTORY_TYPE_FINGER_TYPE,
|
||||
TRINKET = TSM.IsShadowlands() and Enum.InventoryType.IndexTrinketType or LE_INVENTORY_TYPE_TRINKET_TYPE,
|
||||
HOLDABLE = TSM.IsShadowlands() and Enum.InventoryType.IndexHoldableType or LE_INVENTORY_TYPE_HOLDABLE_TYPE,
|
||||
BODY = TSM.IsShadowlands() and Enum.InventoryType.IndexBodyType or LE_INVENTORY_TYPE_BODY_TYPE,
|
||||
CLOAK = TSM.IsShadowlands() and Enum.InventoryType.IndexCloakType or LE_INVENTORY_TYPE_CLOAK_TYPE,
|
||||
}
|
||||
assert(Table.Count(INV_TYPES) == 8)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Query.Get()
|
||||
return private.objectPool:Get()
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Class Meta Methods
|
||||
-- ============================================================================
|
||||
|
||||
function AuctionQuery.__init(self)
|
||||
self._str = ""
|
||||
self._strLower = ""
|
||||
self._strMatch = ""
|
||||
self._exact = false
|
||||
self._minQuality = -math.huge
|
||||
self._maxQuality = math.huge
|
||||
self._minLevel = -math.huge
|
||||
self._maxLevel = math.huge
|
||||
self._minItemLevel = -math.huge
|
||||
self._maxItemLevel = math.huge
|
||||
self._class = nil
|
||||
self._subClass = nil
|
||||
self._invType = nil
|
||||
self._classFilter1 = {}
|
||||
self._classFilter2 = {}
|
||||
self._usable = false
|
||||
self._uncollected = false
|
||||
self._upgrades = false
|
||||
self._unlearned = false
|
||||
self._canLearn = false
|
||||
self._minPrice = 0
|
||||
self._maxPrice = math.huge
|
||||
self._items = {}
|
||||
self._customFilters = {}
|
||||
self._isBrowseDoneFunc = nil
|
||||
self._specifiedPage = nil
|
||||
self._getAll = nil
|
||||
self._resolveSellers = false
|
||||
self._callback = nil
|
||||
self._queryTemp = {}
|
||||
self._filtersTemp = {}
|
||||
self._classFiltersTemp = {}
|
||||
self._browseResults = {}
|
||||
self._page = 0
|
||||
end
|
||||
|
||||
function AuctionQuery.Release(self)
|
||||
self._str = ""
|
||||
self._strLower = ""
|
||||
self._strMatch = ""
|
||||
self._exact = false
|
||||
self._minQuality = -math.huge
|
||||
self._maxQuality = math.huge
|
||||
self._minLevel = -math.huge
|
||||
self._maxLevel = math.huge
|
||||
self._minItemLevel = -math.huge
|
||||
self._maxItemLevel = math.huge
|
||||
self._class = nil
|
||||
self._subClass = nil
|
||||
self._invType = nil
|
||||
wipe(self._classFilter1)
|
||||
wipe(self._classFilter2)
|
||||
self._usable = false
|
||||
self._uncollected = false
|
||||
self._upgrades = false
|
||||
self._unlearned = false
|
||||
self._canLearn = false
|
||||
self._minPrice = 0
|
||||
self._maxPrice = math.huge
|
||||
wipe(self._items)
|
||||
wipe(self._customFilters)
|
||||
self._isBrowseDoneFunc = nil
|
||||
self._specifiedPage = nil
|
||||
self._getAll = nil
|
||||
self._resolveSellers = false
|
||||
self._callback = nil
|
||||
wipe(self._queryTemp)
|
||||
wipe(self._filtersTemp)
|
||||
wipe(self._classFiltersTemp)
|
||||
for _, row in pairs(self._browseResults) do
|
||||
row:Release()
|
||||
end
|
||||
wipe(self._browseResults)
|
||||
self._page = 0
|
||||
private.objectPool:Recycle(self)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Public Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
function AuctionQuery.SetStr(self, str, exact)
|
||||
self._str = str or ""
|
||||
self._strLower = strlower(self._str)
|
||||
self._strMatch = String.Escape(self._strLower)
|
||||
self._exact = exact or false
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetQualityRange(self, minQuality, maxQuality)
|
||||
self._minQuality = minQuality or -math.huge
|
||||
self._maxQuality = maxQuality or math.huge
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetLevelRange(self, minLevel, maxLevel)
|
||||
self._minLevel = minLevel or -math.huge
|
||||
self._maxLevel = maxLevel or math.huge
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetItemLevelRange(self, minItemLevel, maxItemLevel)
|
||||
self._minItemLevel = minItemLevel or -math.huge
|
||||
self._maxItemLevel = maxItemLevel or math.huge
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetClass(self, class, subClass, invType)
|
||||
self._class = class or nil
|
||||
self._subClass = subClass or nil
|
||||
self._invType = invType or nil
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetUsable(self, usable)
|
||||
self._usable = usable or false
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetUncollected(self, uncollected)
|
||||
self._uncollected = uncollected or false
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetUpgrades(self, upgrades)
|
||||
self._upgrades = upgrades or false
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetUnlearned(self, unlearned)
|
||||
self._unlearned = unlearned or false
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetCanLearn(self, canLearn)
|
||||
self._canLearn = canLearn or false
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetPriceRange(self, minPrice, maxPrice)
|
||||
self._minPrice = minPrice or 0
|
||||
self._maxPrice = maxPrice or math.huge
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetItems(self, items)
|
||||
wipe(self._items)
|
||||
if type(items) == "table" then
|
||||
for _, itemString in ipairs(items) do
|
||||
local baseItemString = ItemString.GetBaseFast(itemString)
|
||||
self._items[itemString] = ITEM_SPECIFIC
|
||||
if baseItemString ~= itemString then
|
||||
self._items[baseItemString] = self._items[baseItemString] or ITEM_BASE
|
||||
end
|
||||
end
|
||||
elseif type(items) == "string" then
|
||||
local itemString = items
|
||||
local baseItemString = ItemString.GetBaseFast(itemString)
|
||||
self._items[itemString] = ITEM_SPECIFIC
|
||||
if baseItemString ~= itemString then
|
||||
self._items[baseItemString] = self._items[baseItemString] or ITEM_BASE
|
||||
end
|
||||
elseif items ~= nil then
|
||||
error("Invalid items type: "..tostring(items))
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.AddCustomFilter(self, func)
|
||||
self._customFilters[func] = true
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetIsBrowseDoneFunction(self, func)
|
||||
self._isBrowseDoneFunc = func
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetPage(self, page)
|
||||
if page == nil then
|
||||
self._specifiedPage = nil
|
||||
elseif type(page) == "number" or page == "FIRST" or page == "LAST" then
|
||||
assert(TSM.IsWowClassic())
|
||||
self._specifiedPage = page
|
||||
else
|
||||
error("Invalid page: "..tostring(page))
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetGetAll(self, getAll)
|
||||
-- only currently support GetAll on classic
|
||||
assert(not getAll or TSM.IsWowClassic())
|
||||
self._getAll = getAll
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetResolveSellers(self, resolveSellers)
|
||||
self._resolveSellers = resolveSellers
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.SetCallback(self, callback)
|
||||
self._callback = callback
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionQuery.Browse(self, forceNoScan)
|
||||
assert(not TSM.IsWowClassic() or not forceNoScan)
|
||||
|
||||
local noScan = forceNoScan or false
|
||||
if not TSM.IsWowClassic() then
|
||||
local numItems = 0
|
||||
for _, itemType in pairs(self._items) do
|
||||
if itemType == ITEM_SPECIFIC then
|
||||
numItems = numItems + 1
|
||||
end
|
||||
end
|
||||
if numItems > 0 and numItems < 500 then
|
||||
-- it's faster to just issue individual item searches instead of a browse query
|
||||
noScan = true
|
||||
end
|
||||
end
|
||||
|
||||
if noScan then
|
||||
assert(not TSM.IsWowClassic())
|
||||
local itemKeys = TempTable.Acquire()
|
||||
for itemString in pairs(self._items) do
|
||||
if itemString == ItemString.GetBaseFast(itemString) then
|
||||
local itemId, battlePetSpeciesId = nil, nil
|
||||
if ItemString.IsPet(itemString) then
|
||||
itemId = ItemString.ToId(ItemString.GetPetCage())
|
||||
battlePetSpeciesId = ItemString.ToId(itemString)
|
||||
else
|
||||
itemId = ItemString.ToId(itemString)
|
||||
battlePetSpeciesId = 0
|
||||
end
|
||||
local itemKey = C_AuctionHouse.MakeItemKey(itemId, 0, 0, battlePetSpeciesId)
|
||||
-- FIX for 9.0.1 bug where MakeItemKey randomly adds an itemLevel which breaks scanning
|
||||
itemKey.itemLevel = 0
|
||||
tinsert(itemKeys, itemKey)
|
||||
end
|
||||
end
|
||||
local future = Scanner.BrowseNoScan(self, itemKeys, self._browseResults, self._callback)
|
||||
TempTable.Release(itemKeys)
|
||||
return future
|
||||
else
|
||||
self._page = 0
|
||||
return Scanner.Browse(self, self._resolveSellers, self._browseResults, self._callback)
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionQuery.GetSearchProgress(self)
|
||||
if TSM.IsWowClassic() then
|
||||
return 1
|
||||
end
|
||||
local progress, totalNum = 0, 0
|
||||
for _, row in pairs(self._browseResults) do
|
||||
progress = progress + row:_GetSearchProgress()
|
||||
totalNum = totalNum + 1
|
||||
end
|
||||
if totalNum == 0 then
|
||||
return 0
|
||||
end
|
||||
return progress / totalNum
|
||||
end
|
||||
|
||||
function AuctionQuery.GetBrowseResults(self, baseItemString)
|
||||
return self._browseResults[baseItemString]
|
||||
end
|
||||
|
||||
function AuctionQuery.ItemSubRowIterator(self, itemString)
|
||||
local result = TempTable.Acquire()
|
||||
local baseItemString = ItemString.GetBaseFast(itemString)
|
||||
local isBaseItemString = itemString == baseItemString
|
||||
local row = self._browseResults[baseItemString]
|
||||
if row then
|
||||
for _, subRow in row:SubRowIterator() do
|
||||
local subRowBaseItemString = subRow:GetBaseItemString()
|
||||
local subRowItemString = subRow:GetItemString()
|
||||
if (isBaseItemString and subRowBaseItemString == itemString) or (not isBaseItemString and subRowItemString == itemString) then
|
||||
tinsert(result, subRow)
|
||||
end
|
||||
end
|
||||
end
|
||||
return TempTable.Iterator(result)
|
||||
end
|
||||
|
||||
function AuctionQuery.GetCheapestSubRow(self, itemString)
|
||||
assert(not TSM.IsWowClassic())
|
||||
local cheapest, cheapestItemBuyout = nil, nil
|
||||
for _, subRow in self:ItemSubRowIterator(itemString) do
|
||||
local quantity = subRow:GetQuantities()
|
||||
local _, numOwnerItems = subRow:GetOwnerInfo()
|
||||
local _, itemBuyout = subRow:GetBuyouts()
|
||||
if numOwnerItems ~= quantity and itemBuyout < (cheapestItemBuyout or math.huge) then
|
||||
cheapest = subRow
|
||||
cheapestItemBuyout = itemBuyout
|
||||
end
|
||||
end
|
||||
return cheapest
|
||||
end
|
||||
|
||||
function AuctionQuery.BrowseResultsIterator(self)
|
||||
return pairs(self._browseResults)
|
||||
end
|
||||
|
||||
function AuctionQuery.RemoveResultRow(self, row)
|
||||
local baseItemString = row:GetBaseItemString()
|
||||
assert(baseItemString and self._browseResults[baseItemString])
|
||||
self._browseResults[baseItemString] = nil
|
||||
row:Release()
|
||||
if self._callback then
|
||||
self._callback(self)
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionQuery.Search(self, row, useCachedData)
|
||||
assert(not TSM.IsWowClassic())
|
||||
assert(self._browseResults)
|
||||
return Scanner.Search(self, self._resolveSellers, useCachedData, row, self._callback)
|
||||
end
|
||||
|
||||
function AuctionQuery.CancelBrowseOrSearch(self)
|
||||
Scanner.Cancel()
|
||||
end
|
||||
|
||||
function AuctionQuery.ItemIterator(self)
|
||||
return private.ItemIteratorHelper, self._items, nil
|
||||
end
|
||||
|
||||
function AuctionQuery.WipeBrowseResults(self)
|
||||
for _, row in pairs(self._browseResults) do
|
||||
row:Release()
|
||||
end
|
||||
wipe(self._browseResults)
|
||||
if self._callback then
|
||||
self._callback(self)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
function AuctionQuery._SetSort(self)
|
||||
if not TSM.IsWowClassic() then
|
||||
return true
|
||||
end
|
||||
|
||||
local sorts = (type(self._specifiedPage) == "string" or self._getAll) and EMPTY_SORTS or DEFAULT_SORTS
|
||||
|
||||
if GetAuctionSort("list", #sorts + 1) == nil then
|
||||
local properlySorted = true
|
||||
for i, col in ipairs(sorts) do
|
||||
local sortCol, sortReversed = GetAuctionSort("list", #sorts - i + 1)
|
||||
-- we never care to reverse a sort so if it's reversed then it's not properly sorted
|
||||
if sortCol ~= col or sortReversed then
|
||||
properlySorted = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if properlySorted then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
SortAuctionClearSort("list")
|
||||
for _, col in ipairs(sorts) do
|
||||
SortAuctionSetSort("list", col, false)
|
||||
end
|
||||
SortAuctionApplySort("list")
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function AuctionQuery._SendWowQuery(self)
|
||||
-- build the class filters
|
||||
wipe(self._classFiltersTemp)
|
||||
wipe(self._classFilter1)
|
||||
wipe(self._classFilter2)
|
||||
if self._invType == INV_TYPES.CHEST or self._invType == INV_TYPES.ROBE then
|
||||
-- default AH only sends in queries for robe chest type, we need to mimic this when using a chest filter
|
||||
self._classFilter1.classID = LE_ITEM_CLASS_ARMOR
|
||||
self._classFilter1.subClassID = self._subClass
|
||||
self._classFilter1.inventoryType = INV_TYPES.CHEST
|
||||
tinsert(self._classFiltersTemp, self._classFilter1)
|
||||
self._classFilter2.classID = LE_ITEM_CLASS_ARMOR
|
||||
self._classFilter2.subClassID = self._subClass
|
||||
self._classFilter2.inventoryType = INV_TYPES.ROBE
|
||||
tinsert(self._classFiltersTemp, self._classFilter2)
|
||||
elseif self._invType == INV_TYPES.NECK or self._invType == INV_TYPES.FINGER or self._invType == INV_TYPES.TRINKET or self._invType == INV_TYPES.HOLDABLE or self._invType == INV_TYPES.BODY then
|
||||
self._classFilter1.classID = LE_ITEM_CLASS_ARMOR
|
||||
self._classFilter1.subClassID = LE_ITEM_ARMOR_GENERIC
|
||||
self._classFilter1.inventoryType = self._invType
|
||||
tinsert(self._classFiltersTemp, self._classFilter1)
|
||||
elseif self._invType == INV_TYPES.CLOAK then
|
||||
self._classFilter1.classID = LE_ITEM_CLASS_ARMOR
|
||||
self._classFilter1.subClassID = LE_ITEM_ARMOR_CLOTH
|
||||
self._classFilter1.inventoryType = self._invType
|
||||
tinsert(self._classFiltersTemp, self._classFilter1)
|
||||
elseif self._class then
|
||||
self._classFilter1.classID = self._class
|
||||
self._classFilter1.subClassID = self._subClass
|
||||
self._classFilter1.inventoryType = self._invType
|
||||
tinsert(self._classFiltersTemp, self._classFilter1)
|
||||
end
|
||||
|
||||
-- build the query
|
||||
local minLevel = self._minLevel ~= -math.huge and self._minLevel or nil
|
||||
local maxLevel = self._maxLevel ~= math.huge and self._maxLevel or nil
|
||||
if TSM.IsWowClassic() then
|
||||
if self._specifiedPage == "LAST" then
|
||||
self._page = max(ceil(select(2, GetNumAuctionItems("list")) / NUM_AUCTION_ITEMS_PER_PAGE) - 1, 0)
|
||||
elseif self._specifiedPage == "FIRST" then
|
||||
self._page = 0
|
||||
elseif self._specifiedPage then
|
||||
self._page = self._specifiedPage
|
||||
end
|
||||
local minQuality = self._minQuality == -math.huge and 0 or self._minQuality
|
||||
return AuctionHouseWrapper.QueryAuctionItems(self._str, minLevel, maxLevel, self._page, self._usable, minQuality, self._getAll, self._exact, self._classFiltersTemp)
|
||||
else
|
||||
wipe(self._filtersTemp)
|
||||
if self._uncollected then
|
||||
tinsert(self._filtersTemp, Enum.AuctionHouseFilter.UncollectedOnly)
|
||||
end
|
||||
if self._usable then
|
||||
tinsert(self._filtersTemp, Enum.AuctionHouseFilter.UsableOnly)
|
||||
end
|
||||
if self._upgrades then
|
||||
tinsert(self._filtersTemp, Enum.AuctionHouseFilter.UpgradesOnly)
|
||||
end
|
||||
if self._exact then
|
||||
tinsert(self._filtersTemp, Enum.AuctionHouseFilter.ExactMatch)
|
||||
end
|
||||
local minQuality = self._minQuality == -math.huge and 0 or self._minQuality
|
||||
for i = minQuality + Enum.AuctionHouseFilter.PoorQuality, min(self._maxQuality + Enum.AuctionHouseFilter.PoorQuality, Enum.AuctionHouseFilter.ArtifactQuality) do
|
||||
tinsert(self._filtersTemp, i)
|
||||
end
|
||||
wipe(self._queryTemp)
|
||||
self._queryTemp.searchString = self._str
|
||||
self._queryTemp.minLevel = minLevel
|
||||
self._queryTemp.maxLevel = maxLevel
|
||||
self._queryTemp.sorts = DEFAULT_SORTS
|
||||
self._queryTemp.filters = self._filtersTemp
|
||||
self._queryTemp.itemClassFilters = self._classFiltersTemp
|
||||
return AuctionHouseWrapper.SendBrowseQuery(self._queryTemp)
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionQuery._IsFiltered(self, row, isSubRow, itemKey)
|
||||
local baseItemString = row:GetBaseItemString()
|
||||
local itemString = row:GetItemString()
|
||||
assert(baseItemString)
|
||||
local name, quality, itemLevel, maxItemLevel = row:GetItemInfo(itemKey)
|
||||
local _, itemBuyout, minItemBuyout = row:GetBuyouts(itemKey)
|
||||
if row:IsSubRow() and itemBuyout == 0 then
|
||||
_, itemBuyout = row:GetBidInfo()
|
||||
end
|
||||
|
||||
if next(self._items) then
|
||||
if not self._items[baseItemString] then
|
||||
return true
|
||||
end
|
||||
if isSubRow and itemString and self._items[itemString] ~= ITEM_SPECIFIC and self._items[baseItemString] ~= ITEM_SPECIFIC then
|
||||
return true
|
||||
elseif not isSubRow and itemString and not self._items[itemString] then
|
||||
return true
|
||||
end
|
||||
end
|
||||
if self._str ~= "" and name then
|
||||
name = strlower(name)
|
||||
if not strmatch(name, self._strMatch) or (self._exact and name ~= self._strLower) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
if self._minLevel ~= -math.huge or self._maxLevel ~= math.huge then
|
||||
local minLevel = TSM.IsShadowlands() and ItemString.IsPet(baseItemString) and (itemLevel or maxItemLevel) or ItemInfo.GetMinLevel(baseItemString)
|
||||
if minLevel < self._minLevel or minLevel > self._maxLevel then
|
||||
return true
|
||||
end
|
||||
end
|
||||
if itemLevel and (itemLevel < self._minItemLevel or itemLevel > self._maxItemLevel) then
|
||||
return true
|
||||
end
|
||||
if maxItemLevel and maxItemLevel < self._minItemLevel then
|
||||
return true
|
||||
end
|
||||
if quality and (quality < self._minQuality or quality > self._maxQuality) then
|
||||
return true
|
||||
end
|
||||
if self._class and ItemInfo.GetClassId(baseItemString) ~= self._class then
|
||||
return true
|
||||
end
|
||||
if self._subClass and ItemInfo.GetSubClassId(baseItemString) ~= self._subClass then
|
||||
return true
|
||||
end
|
||||
if self._invType and ItemInfo.GetInvSlotId(baseItemString) ~= self._invType then
|
||||
return true
|
||||
end
|
||||
if self._unlearned and CanIMogIt:PlayerKnowsTransmog(ItemInfo.GetLink(baseItemString)) then
|
||||
return true
|
||||
end
|
||||
if self._canLearn and not CanIMogIt:CharacterCanLearnTransmog(ItemInfo.GetLink(baseItemString)) then
|
||||
return true
|
||||
end
|
||||
if itemBuyout and (itemBuyout < self._minPrice or itemBuyout > self._maxPrice) then
|
||||
return true
|
||||
end
|
||||
if minItemBuyout and minItemBuyout > self._maxPrice then
|
||||
return true
|
||||
end
|
||||
for func in pairs(self._customFilters) do
|
||||
if func(self, row, isSubRow, itemKey) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function AuctionQuery._BrowseIsDone(self, isRetry)
|
||||
if TSM.IsWowClassic() then
|
||||
local numAuctions, totalAuctions = GetNumAuctionItems("list")
|
||||
if totalAuctions <= NUM_AUCTION_ITEMS_PER_PAGE and numAuctions ~= totalAuctions then
|
||||
-- there are cases where we get (0, 1) from the API - no idea why so just assume we're not done
|
||||
return false
|
||||
end
|
||||
local numPages = ceil(totalAuctions / NUM_AUCTION_ITEMS_PER_PAGE)
|
||||
if self._getAll then
|
||||
return true
|
||||
end
|
||||
if self._specifiedPage then
|
||||
if isRetry then
|
||||
return false
|
||||
end
|
||||
-- check if we're on the right page
|
||||
local specifiedPage = (self._specifiedPage == "FIRST" and 0) or (self._specifiedPage == "LAST" and numPages - 1) or self._specifiedPage
|
||||
return self._page == specifiedPage
|
||||
elseif self._isBrowseDoneFunc and self._isBrowseDoneFunc(self) then
|
||||
return true
|
||||
else
|
||||
return self._page >= numPages
|
||||
end
|
||||
else
|
||||
if self._isBrowseDoneFunc and self._isBrowseDoneFunc(self) then
|
||||
return true
|
||||
end
|
||||
return C_AuctionHouse.HasFullBrowseResults()
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionQuery._BrowseIsPageValid(self)
|
||||
if TSM.IsWowClassic() then
|
||||
if self._specifiedPage then
|
||||
return self:_BrowseIsDone()
|
||||
else
|
||||
return true
|
||||
end
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionQuery._BrowseRequestMore(self, isRetry)
|
||||
if TSM.IsWowClassic() then
|
||||
assert(not self._getAll)
|
||||
if self._specifiedPage then
|
||||
return self:_SendWowQuery()
|
||||
end
|
||||
if not isRetry then
|
||||
self._page = self._page + 1
|
||||
end
|
||||
return self:_SendWowQuery()
|
||||
else
|
||||
return AuctionHouseWrapper.RequestMoreBrowseResults()
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionQuery._OnSubRowRemoved(self, row)
|
||||
local baseItemString = row:GetBaseItemString()
|
||||
assert(row == self._browseResults[baseItemString])
|
||||
if row:GetNumSubRows() == 0 then
|
||||
self._browseResults[baseItemString] = nil
|
||||
row:Release()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.ItemIteratorHelper(items, index)
|
||||
while true do
|
||||
local itemString, itemType = next(items, index)
|
||||
if not itemString then
|
||||
return
|
||||
elseif itemType == ITEM_SPECIFIC then
|
||||
return itemString
|
||||
end
|
||||
index = itemString
|
||||
end
|
||||
end
|
||||
134
LibTSM/Service/AuctionScanClasses/QueryUtil.lua
Normal file
134
LibTSM/Service/AuctionScanClasses/QueryUtil.lua
Normal file
@@ -0,0 +1,134 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local QueryUtil = TSM.Init("Service.AuctionScanClasses.QueryUtil")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local Threading = TSM.Include("Service.Threading")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local Query = TSM.Include("Service.AuctionScanClasses.Query")
|
||||
local private = {
|
||||
itemListSortValue = {},
|
||||
}
|
||||
local MAX_ITEM_INFO_RETRIES = 30
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function QueryUtil.GenerateThreaded(itemList, callback, context)
|
||||
-- get all the item info into the game's cache
|
||||
for _ = 1, MAX_ITEM_INFO_RETRIES do
|
||||
local isMissingItemInfo = false
|
||||
for _, itemString in ipairs(itemList) do
|
||||
if not private.HasInfo(itemString) then
|
||||
isMissingItemInfo = true
|
||||
end
|
||||
Threading.Yield()
|
||||
end
|
||||
if not isMissingItemInfo then
|
||||
break
|
||||
end
|
||||
Threading.Sleep(0.1)
|
||||
end
|
||||
|
||||
-- remove items we're missing info for
|
||||
for i = #itemList, 1, -1 do
|
||||
if not private.HasInfo(itemList[i]) then
|
||||
Log.Err("Missing item info for %s", itemList[i])
|
||||
tremove(itemList, i)
|
||||
end
|
||||
Threading.Yield()
|
||||
end
|
||||
if #itemList == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
-- add all the items
|
||||
if TSM.IsWowClassic() then
|
||||
for _, itemString in ipairs(itemList) do
|
||||
private.GenerateQuery(callback, context, itemString, private.GetItemQueryInfo(itemString))
|
||||
end
|
||||
else
|
||||
-- sort the item list so all base items are grouped together but keep relative ordering between base items the same
|
||||
wipe(private.itemListSortValue)
|
||||
for i, itemString in ipairs(itemList) do
|
||||
local baseItemString = ItemString.GetBaseFast(itemString)
|
||||
private.itemListSortValue[baseItemString] = private.itemListSortValue[baseItemString] or i
|
||||
private.itemListSortValue[itemString] = private.itemListSortValue[baseItemString]
|
||||
end
|
||||
sort(itemList, private.ItemListSortHelper)
|
||||
local currentBaseItemString = nil
|
||||
local currentItems = TempTable.Acquire()
|
||||
for _, itemString in ipairs(itemList) do
|
||||
local baseItemString = ItemString.GetBaseFast(itemString)
|
||||
assert(baseItemString)
|
||||
if baseItemString == currentBaseItemString then
|
||||
-- same base item
|
||||
tinsert(currentItems, itemString)
|
||||
else
|
||||
-- new base item
|
||||
if currentBaseItemString then
|
||||
private.GenerateQuery(callback, context, currentItems, ItemInfo.GetName(currentBaseItemString))
|
||||
wipe(currentItems)
|
||||
end
|
||||
currentBaseItemString = baseItemString
|
||||
tinsert(currentItems, itemString)
|
||||
end
|
||||
end
|
||||
if currentBaseItemString then
|
||||
private.GenerateQuery(callback, context, currentItems, ItemInfo.GetName(currentBaseItemString))
|
||||
wipe(currentItems)
|
||||
end
|
||||
TempTable.Release(currentItems)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.GetItemQueryInfo(itemString)
|
||||
local name = ItemInfo.GetName(itemString)
|
||||
local level = ItemInfo.GetMinLevel(itemString) or 0
|
||||
local quality = ItemInfo.GetQuality(itemString)
|
||||
local classId = ItemInfo.GetClassId(itemString) or 0
|
||||
local subClassId = ItemInfo.GetSubClassId(itemString) or 0
|
||||
-- Ignoring level because level can now vary
|
||||
if itemString == ItemString.GetBase(itemString) and (classId == LE_ITEM_CLASS_WEAPON or classId == LE_ITEM_CLASS_ARMOR or (classId == LE_ITEM_CLASS_GEM and subClassId == LE_ITEM_GEM_ARTIFACTRELIC)) then
|
||||
level = nil
|
||||
end
|
||||
return name, level, level, quality, classId, subClassId
|
||||
end
|
||||
|
||||
function private.HasInfo(itemString)
|
||||
return ItemInfo.GetName(itemString) and ItemInfo.GetQuality(itemString) and ItemInfo.GetMinLevel(itemString)
|
||||
end
|
||||
|
||||
function private.GenerateQuery(callback, context, items, name, minLevel, maxLevel, quality, class, subClass)
|
||||
local query = Query.Get()
|
||||
:SetStr(name, false)
|
||||
:SetQualityRange(quality, quality)
|
||||
:SetLevelRange(minLevel, maxLevel)
|
||||
:SetClass(class, subClass)
|
||||
:SetItems(items)
|
||||
callback(query, context)
|
||||
end
|
||||
|
||||
function private.ItemListSortHelper(a, b)
|
||||
local aSortValue = private.itemListSortValue[a]
|
||||
local bSortValue = private.itemListSortValue[b]
|
||||
if aSortValue ~= bSortValue then
|
||||
return aSortValue < bSortValue
|
||||
end
|
||||
return a < b
|
||||
end
|
||||
671
LibTSM/Service/AuctionScanClasses/ResultRow.lua
Normal file
671
LibTSM/Service/AuctionScanClasses/ResultRow.lua
Normal file
@@ -0,0 +1,671 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local ResultRow = TSM.Init("Service.AuctionScanClasses.ResultRow")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local ObjectPool = TSM.Include("Util.ObjectPool")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Table = TSM.Include("Util.Table")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local LibTSMClass = TSM.Include("LibTSMClass")
|
||||
local Util = TSM.Include("Service.AuctionScanClasses.Util")
|
||||
local AuctionHouseWrapper = TSM.Include("Service.AuctionHouseWrapper")
|
||||
local ResultSubRow = TSM.Include("Service.AuctionScanClasses.ResultSubRow")
|
||||
local ResultRowWrapper = LibTSMClass.DefineClass("ResultRowWrapper")
|
||||
local private = {
|
||||
objectPool = ObjectPool.New("AUCTION_SCAN_RESULT_ROW", ResultRowWrapper),
|
||||
}
|
||||
local SUB_ROW_SEARCH_INDEX_MULTIPLIER = 1000000
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function ResultRow.Get(query, itemKey, minPrice, totalQuantity)
|
||||
local row = private.objectPool:Get()
|
||||
row:_Acquire(query, itemKey, minPrice, totalQuantity)
|
||||
return row
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- ResultRowWrapper - Meta Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
function ResultRowWrapper.__init(self)
|
||||
self._query = nil
|
||||
self._items = {}
|
||||
self._baseItemString = nil
|
||||
self._canHaveNonBaseItemString = nil
|
||||
self._minPrice = nil
|
||||
self._hasItemInfo = nil
|
||||
self._isCommodity = nil
|
||||
self._notFiltered = false
|
||||
self._searchIndex = nil
|
||||
self._subRows = {}
|
||||
self._minBrowseId = nil
|
||||
end
|
||||
|
||||
function ResultRowWrapper._Acquire(self, query, item, minPrice, totalQuantity)
|
||||
self._query = query
|
||||
if TSM.IsWowClassic() then
|
||||
assert(not minPrice and not totalQuantity)
|
||||
tinsert(self._items, item)
|
||||
self._baseItemString = ItemString.GetBase(item)
|
||||
else
|
||||
item._minPrice = minPrice
|
||||
item._totalQuantity = totalQuantity
|
||||
tinsert(self._items, item)
|
||||
self._baseItemString = ItemString.GetBaseFromItemKey(item)
|
||||
end
|
||||
self._canHaveNonBaseItemString = nil
|
||||
self._minPrice = nil
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- ResultRowWrapper - Public Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
function ResultRowWrapper.Merge(self, item, minPrice, totalQuantity)
|
||||
-- check if we already have this item
|
||||
for i = 1, #self._items do
|
||||
if item == self._items[i] then
|
||||
return
|
||||
end
|
||||
if type(item) == "table" then
|
||||
local isEqual = true
|
||||
for k in pairs(item) do
|
||||
if item[k] ~= self._items[i][k] then
|
||||
isEqual = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if isEqual then
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
self._hasItemInfo = nil
|
||||
if TSM.IsWowClassic() then
|
||||
assert(not minPrice and not totalQuantity)
|
||||
assert(self._baseItemString == ItemString.GetBase(item))
|
||||
tinsert(self._items, item)
|
||||
self._notFiltered = false
|
||||
else
|
||||
assert(self._baseItemString == ItemString.GetBaseFromItemKey(item))
|
||||
item._minPrice = minPrice
|
||||
item._totalQuantity = totalQuantity
|
||||
tinsert(self._items, item)
|
||||
self._notFiltered = false
|
||||
end
|
||||
self._canHaveNonBaseItemString = nil
|
||||
end
|
||||
|
||||
function ResultRowWrapper.Release(self)
|
||||
wipe(self._items)
|
||||
self._baseItemString = nil
|
||||
self._canHaveNonBaseItemString = nil
|
||||
self._minPrice = nil
|
||||
self._hasItemInfo = nil
|
||||
self._isCommodity = nil
|
||||
self._notFiltered = false
|
||||
self._searchIndex = nil
|
||||
self._minBrowseId = nil
|
||||
for _, subRow in pairs(self._subRows) do
|
||||
subRow:Release()
|
||||
end
|
||||
wipe(self._subRows)
|
||||
private.objectPool:Recycle(self)
|
||||
end
|
||||
|
||||
function ResultRowWrapper.IsSubRow(self)
|
||||
return false
|
||||
end
|
||||
|
||||
function ResultRowWrapper.PopulateBrowseData(self)
|
||||
assert(self._baseItemString)
|
||||
if self._hasItemInfo then
|
||||
-- already have our item info
|
||||
return true
|
||||
elseif not Util.HasItemInfo(self._baseItemString) then
|
||||
-- don't have item info yet
|
||||
return false
|
||||
end
|
||||
|
||||
if not TSM.IsWowClassic() then
|
||||
-- cache the commodity status since it's referenced a ton
|
||||
if self._isCommodity == nil then
|
||||
self._isCommodity = ItemInfo.IsCommodity(self._baseItemString)
|
||||
assert(self._isCommodity ~= nil)
|
||||
end
|
||||
end
|
||||
|
||||
-- check if we have info for all the items and try to fetch it if not
|
||||
local missingInfo = false
|
||||
for _, item in ipairs(self._items) do
|
||||
if TSM.IsWowClassic() then
|
||||
if not Util.HasItemInfo(ItemString.Get(item)) then
|
||||
missingInfo = true
|
||||
end
|
||||
else
|
||||
if not item._itemKeyInfo then
|
||||
item._itemKeyInfo = C_AuctionHouse.GetItemKeyInfo(item, true)
|
||||
if not item._itemKeyInfo then
|
||||
missingInfo = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if missingInfo then
|
||||
return false
|
||||
end
|
||||
|
||||
self._hasItemInfo = true
|
||||
return true
|
||||
end
|
||||
|
||||
function ResultRowWrapper.IsFiltered(self, query)
|
||||
assert(#self._items > 0)
|
||||
if self._notFiltered then
|
||||
return false
|
||||
end
|
||||
|
||||
-- check if the whole row is filtered
|
||||
if query:_IsFiltered(self, false) then
|
||||
return true
|
||||
end
|
||||
|
||||
-- filter our items
|
||||
for i = #self._items, 1, -1 do
|
||||
if query:_IsFiltered(self, false, self._items[i]) then
|
||||
tremove(self._items, i)
|
||||
end
|
||||
end
|
||||
self._canHaveNonBaseItemString = nil
|
||||
self._minPrice = nil
|
||||
if #self._items == 0 then
|
||||
-- no more items, so the entire row is filtered
|
||||
return true
|
||||
end
|
||||
|
||||
-- not filtered (cache this result)
|
||||
self._notFiltered = true
|
||||
return false
|
||||
end
|
||||
|
||||
function ResultRowWrapper.SearchReset(self)
|
||||
assert(not TSM.IsWowClassic())
|
||||
assert(#self._items > 0)
|
||||
self._searchIndex = 1
|
||||
end
|
||||
|
||||
function ResultRowWrapper.SearchNext(self)
|
||||
assert(not TSM.IsWowClassic())
|
||||
assert(self._searchIndex)
|
||||
if self._searchIndex == #self._items then
|
||||
self._searchIndex = nil
|
||||
return false
|
||||
end
|
||||
self._searchIndex = self._searchIndex + 1
|
||||
return true
|
||||
end
|
||||
|
||||
function ResultRowWrapper.SearchIsReady(self)
|
||||
assert(not TSM.IsWowClassic())
|
||||
assert(self._searchIndex)
|
||||
-- the client needs to have the item key info cached before we can run the search
|
||||
return C_AuctionHouse.GetItemKeyInfo(self._items[self._searchIndex], true) and true or false
|
||||
end
|
||||
|
||||
function ResultRowWrapper.SearchSend(self)
|
||||
assert(not TSM.IsWowClassic())
|
||||
assert(self._searchIndex)
|
||||
local itemKey = self._items[self._searchIndex]
|
||||
-- send a sell query if we don't have browse results for the itemKey
|
||||
-- for some reason sell queries don't work for commodities or pets
|
||||
local isSellQuery = not self._isCommodity and not ItemString.IsPet(self._baseItemString) and not itemKey._totalQuantity
|
||||
return AuctionHouseWrapper.SendSearchQuery(itemKey, isSellQuery)
|
||||
end
|
||||
|
||||
function ResultRowWrapper.HasCachedSearchData(self)
|
||||
local itemKey = self._items[self._searchIndex]
|
||||
if self._isCommodity then
|
||||
return C_AuctionHouse.HasFullCommoditySearchResults(itemKey.itemID)
|
||||
else
|
||||
return C_AuctionHouse.HasFullItemSearchResults(itemKey)
|
||||
end
|
||||
end
|
||||
|
||||
function ResultRowWrapper.SearchCheckStatus(self)
|
||||
assert(not TSM.IsWowClassic())
|
||||
assert(self._searchIndex)
|
||||
local itemKey = self._items[self._searchIndex]
|
||||
|
||||
-- check if we have the full results
|
||||
local hasFullResults = nil
|
||||
if self._isCommodity then
|
||||
hasFullResults = C_AuctionHouse.HasFullCommoditySearchResults(itemKey.itemID)
|
||||
else
|
||||
hasFullResults = C_AuctionHouse.HasFullItemSearchResults(itemKey)
|
||||
end
|
||||
if hasFullResults then
|
||||
return true
|
||||
end
|
||||
|
||||
-- request more results
|
||||
if self._isCommodity then
|
||||
return false, AuctionHouseWrapper.RequestMoreCommoditySearchResults(itemKey.itemID)
|
||||
else
|
||||
return false, AuctionHouseWrapper.RequestMoreItemSearchResults(itemKey)
|
||||
end
|
||||
end
|
||||
|
||||
function ResultRowWrapper.PopulateSubRows(self, browseId, index, itemLink)
|
||||
if TSM.IsWowClassic() then
|
||||
-- remove any prior results with a different browseId
|
||||
assert(index and not self._searchIndex)
|
||||
local subRow = ResultSubRow.Get(self)
|
||||
subRow:_SetRawData(index, browseId, itemLink)
|
||||
local _, hashNoSeller = subRow:GetHashes()
|
||||
if self._minBrowseId and self._minBrowseId ~= browseId then
|
||||
-- check if this subRow already exists with a prior browseId
|
||||
for i, existingSubRow in ipairs(self._subRows) do
|
||||
local _, existingHashNoSeller = existingSubRow:GetHashes()
|
||||
local _, _, existingBrowseId = existingSubRow:GetListingInfo()
|
||||
if hashNoSeller == existingHashNoSeller and browseId ~= existingBrowseId then
|
||||
-- replace the existing subRow
|
||||
existingSubRow:Release()
|
||||
self._subRows[i] = subRow
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
tinsert(self._subRows, subRow)
|
||||
else
|
||||
assert(self._searchIndex and not index)
|
||||
local subRowOffset = self._searchIndex * SUB_ROW_SEARCH_INDEX_MULTIPLIER
|
||||
local itemKey = self._items[self._searchIndex]
|
||||
local numAuctions = nil
|
||||
if self:IsCommodity() then
|
||||
numAuctions = C_AuctionHouse.GetNumCommoditySearchResults(itemKey.itemID)
|
||||
else
|
||||
numAuctions = C_AuctionHouse.GetNumItemSearchResults(itemKey)
|
||||
end
|
||||
if itemKey._numAuctions and numAuctions ~= itemKey._numAuctions then
|
||||
-- the results changed so clear out our existing data
|
||||
for i = itemKey._numAuctions, 1, -1 do
|
||||
if i > numAuctions then
|
||||
self._subRows[subRowOffset + i]:Release()
|
||||
self._subRows[subRowOffset + i] = nil
|
||||
else
|
||||
self._subRows[subRowOffset + i]:_SetRawData(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
itemKey._numAuctions = numAuctions
|
||||
for i = 1, numAuctions do
|
||||
self._subRows[subRowOffset + i] = self._subRows[subRowOffset + i] or ResultSubRow.Get(self)
|
||||
local subRow = self._subRows[subRowOffset + i]
|
||||
if not subRow:HasRawData() or not subRow:HasOwners() then
|
||||
local result = nil
|
||||
if self:IsCommodity() then
|
||||
result = C_AuctionHouse.GetCommoditySearchResultInfo(itemKey.itemID, i)
|
||||
else
|
||||
result = C_AuctionHouse.GetItemSearchResultInfo(itemKey, i)
|
||||
end
|
||||
subRow:_SetRawData(result, browseId)
|
||||
end
|
||||
end
|
||||
end
|
||||
self._minBrowseId = min(self._minBrowseId or math.huge, browseId)
|
||||
end
|
||||
|
||||
function ResultRowWrapper.FilterSubRows(self, query)
|
||||
local subRowOffset = TSM.IsWowClassic() and 0 or (self._searchIndex * SUB_ROW_SEARCH_INDEX_MULTIPLIER)
|
||||
if TSM.IsWowClassic() then
|
||||
for i = #self._subRows, 1, -1 do
|
||||
if query:_IsFiltered(self._subRows[i], true) then
|
||||
self:_RemoveSubRowByIndex(i)
|
||||
end
|
||||
end
|
||||
else
|
||||
local itemKey = self._items[self._searchIndex]
|
||||
for j = itemKey._numAuctions, 1, -1 do
|
||||
local subRow = self._subRows[subRowOffset + j]
|
||||
if query:_IsFiltered(subRow, true) then
|
||||
self:_RemoveSubRowByIndex(j)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- merge subRows with identical hashes
|
||||
local numSubRows = nil
|
||||
local hashIndexLookup = TempTable.Acquire()
|
||||
local index = 1
|
||||
while true do
|
||||
numSubRows = TSM.IsWowClassic() and #self._subRows or self._items[self._searchIndex]._numAuctions
|
||||
if index > numSubRows then
|
||||
break
|
||||
end
|
||||
local subRow = self._subRows[subRowOffset + index]
|
||||
local hash = subRow:GetHashes()
|
||||
local prevIndex = hashIndexLookup[hash]
|
||||
if prevIndex then
|
||||
-- there was a previous subRow with the same hash
|
||||
self._subRows[subRowOffset + prevIndex]:Merge(subRow)
|
||||
-- remove this subRow
|
||||
self:_RemoveSubRowByIndex(index)
|
||||
else
|
||||
hashIndexLookup[hash] = index
|
||||
index = index + 1
|
||||
end
|
||||
end
|
||||
TempTable.Release(hashIndexLookup)
|
||||
return numSubRows == 0
|
||||
end
|
||||
|
||||
function ResultRowWrapper.GetNumSubRows(self)
|
||||
if TSM.IsWowClassic() then
|
||||
return #self._subRows
|
||||
else
|
||||
local result = 0
|
||||
for _, itemKey in ipairs(self._items) do
|
||||
result = result + (itemKey._numAuctions or 0)
|
||||
end
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
function ResultRowWrapper.SubRowIterator(self, searchOnly)
|
||||
if TSM.IsWowClassic() then
|
||||
return ipairs(self._subRows)
|
||||
else
|
||||
if searchOnly then
|
||||
local result = TempTable.Acquire()
|
||||
assert(self._searchIndex)
|
||||
for i = 1, self._items[self._searchIndex]._numAuctions do
|
||||
local subRow = self._subRows[self._searchIndex * SUB_ROW_SEARCH_INDEX_MULTIPLIER + i]
|
||||
assert(subRow)
|
||||
tinsert(result, subRow)
|
||||
end
|
||||
return TempTable.Iterator(result)
|
||||
else
|
||||
return private.SubRowIteratorHelper, self, SUB_ROW_SEARCH_INDEX_MULTIPLIER
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ResultRowWrapper.IsCommodity(self)
|
||||
assert(self._isCommodity ~= nil)
|
||||
return self._isCommodity
|
||||
end
|
||||
|
||||
function ResultRowWrapper.HasItemInfo(self)
|
||||
return self._hasItemInfo
|
||||
end
|
||||
|
||||
function ResultRowWrapper.GetBaseItemString(self)
|
||||
return self._baseItemString
|
||||
end
|
||||
|
||||
function ResultRowWrapper.GetItemString(self)
|
||||
if TSM.IsWowClassic() or not self._hasItemInfo or self._canHaveNonBaseItemString then
|
||||
return nil
|
||||
end
|
||||
if self._canHaveNonBaseItemString == nil then
|
||||
for _, itemKey in ipairs(self._items) do
|
||||
if ItemInfo.CanHaveVariations(self._baseItemString) or itemKey.battlePetSpeciesID ~= 0 or itemKey.itemSuffix ~= 0 or itemKey.itemLevel ~= 0 then
|
||||
-- this item can have variations, so we don't know its itemString
|
||||
self._canHaveNonBaseItemString = true
|
||||
return nil
|
||||
end
|
||||
end
|
||||
self._canHaveNonBaseItemString = false
|
||||
end
|
||||
return self._baseItemString
|
||||
end
|
||||
|
||||
function ResultRowWrapper.GetItemInfo(self, itemKey)
|
||||
if TSM.IsWowClassic() or not self._hasItemInfo then
|
||||
return nil, nil, nil, nil
|
||||
end
|
||||
itemKey = itemKey or (#self._items == 1 and self._items[1] or nil)
|
||||
assert(not itemKey or itemKey._itemKeyInfo)
|
||||
local baseItemString = self:GetBaseItemString()
|
||||
local itemString = self:GetItemString()
|
||||
local itemName, quality, itemLevel, maxItemLevel = nil, nil, nil, nil
|
||||
if itemString then
|
||||
-- this item can't have variations, so we can know the name / level / quality
|
||||
itemName = ItemInfo.GetName(baseItemString)
|
||||
itemLevel = ItemInfo.GetItemLevel(baseItemString)
|
||||
quality = ItemInfo.GetQuality(baseItemString)
|
||||
assert(itemName and itemLevel and quality)
|
||||
else
|
||||
if itemKey and not itemKey._totalQuantity then
|
||||
-- if we didn't do a browse, then don't use this itemKey
|
||||
itemKey = nil
|
||||
end
|
||||
if itemKey then
|
||||
-- grab the name from the itemKeyInfo
|
||||
itemName = itemKey._itemKeyInfo.itemName
|
||||
assert(itemName)
|
||||
end
|
||||
local hasSingleAuction = itemKey and itemKey._totalQuantity == 1
|
||||
if hasSingleAuction then
|
||||
-- grab the quality from the itemKeyInfo since there's only one listing
|
||||
quality = itemKey._itemKeyInfo.quality
|
||||
assert(quality)
|
||||
end
|
||||
if not ItemString.IsPet(self._baseItemString) then
|
||||
-- for non-pets, we can maybe grab the itemLevel from the itemKey
|
||||
if itemKey then
|
||||
itemLevel = itemKey.itemLevel ~= 0 and itemKey.itemLevel or nil
|
||||
else
|
||||
-- only use the itemLevel from the itemKeys if they are all the same
|
||||
local itemKeyItemLevel = self._items[1].itemLevel
|
||||
for i = 2, #self._items do
|
||||
if self._items[i].itemLevel ~= itemKeyItemLevel then
|
||||
itemKeyItemLevel = nil
|
||||
break
|
||||
end
|
||||
end
|
||||
itemLevel = (itemKeyItemLevel or 0) ~= 0 and itemKeyItemLevel or nil
|
||||
end
|
||||
elseif itemKey and itemKey._itemKeyInfo.battlePetLink then
|
||||
if hasSingleAuction then
|
||||
-- grab the itemLevel from the link since there's only one listing
|
||||
itemLevel = ItemInfo.GetItemLevel(itemKey._itemKeyInfo.battlePetLink)
|
||||
assert(itemLevel)
|
||||
else
|
||||
-- grab the maxItemLevel from the link
|
||||
maxItemLevel = ItemInfo.GetItemLevel(itemKey._itemKeyInfo.battlePetLink)
|
||||
assert(maxItemLevel)
|
||||
end
|
||||
end
|
||||
end
|
||||
return itemName, quality, itemLevel, maxItemLevel
|
||||
end
|
||||
|
||||
function ResultRowWrapper.GetBuyouts(self, resultItemKey)
|
||||
if TSM.IsWowClassic() then
|
||||
return nil, nil, nil
|
||||
end
|
||||
assert(#self._items > 0)
|
||||
if resultItemKey then
|
||||
return nil, nil, resultItemKey._minPrice
|
||||
else
|
||||
if self._minPrice == nil then
|
||||
for _, itemKey in ipairs(self._items) do
|
||||
if not itemKey._minPrice then
|
||||
self._minPrice = -1
|
||||
return nil, nil, nil
|
||||
end
|
||||
self._minPrice = min(self._minPrice or math.huge, itemKey._minPrice)
|
||||
end
|
||||
elseif self._minPrice == -1 then
|
||||
return nil, nil, nil
|
||||
end
|
||||
return nil, nil, self._minPrice
|
||||
end
|
||||
end
|
||||
|
||||
function ResultRowWrapper.GetQuantities(self)
|
||||
local totalQuantity = 0
|
||||
if TSM.IsWowClassic() then
|
||||
for _, subRow in ipairs(self._subRows) do
|
||||
local quantity, numAuctions = subRow:GetQuantities()
|
||||
totalQuantity = totalQuantity + quantity * numAuctions
|
||||
end
|
||||
else
|
||||
for _, itemKey in ipairs(self._items) do
|
||||
if not itemKey._totalQuantity then
|
||||
return
|
||||
end
|
||||
totalQuantity = totalQuantity + itemKey._totalQuantity
|
||||
end
|
||||
end
|
||||
return totalQuantity, 1
|
||||
end
|
||||
|
||||
function ResultRowWrapper.GetMaxQuantities(self)
|
||||
assert(self:IsCommodity())
|
||||
local totalQuantity = 0
|
||||
for _, subRow in self:SubRowIterator() do
|
||||
local _, numOwnerItems = subRow:GetOwnerInfo()
|
||||
local quantityAvailable = subRow:GetQuantities() - numOwnerItems
|
||||
totalQuantity = totalQuantity + quantityAvailable
|
||||
end
|
||||
return totalQuantity
|
||||
end
|
||||
|
||||
function ResultRowWrapper.RemoveSubRow(self, subRow)
|
||||
local index = Table.KeyByValue(self._subRows, subRow)
|
||||
if TSM.IsWowClassic() then
|
||||
self:_RemoveSubRowByIndex(index)
|
||||
else
|
||||
local searchIndex = floor(index / SUB_ROW_SEARCH_INDEX_MULTIPLIER)
|
||||
index = index % SUB_ROW_SEARCH_INDEX_MULTIPLIER
|
||||
assert(self._subRows[searchIndex * SUB_ROW_SEARCH_INDEX_MULTIPLIER + index] == subRow)
|
||||
local prevSearchIndex = self._searchIndex
|
||||
self._searchIndex = searchIndex
|
||||
self:_RemoveSubRowByIndex(index)
|
||||
self._searchIndex = prevSearchIndex
|
||||
end
|
||||
self._query:_OnSubRowRemoved(self)
|
||||
end
|
||||
|
||||
function ResultRowWrapper.WipeSearchResults(self)
|
||||
wipe(self._subRows)
|
||||
if not TSM.IsWowClassic() then
|
||||
for _, itemKey in ipairs(self._items) do
|
||||
itemKey._numAuctions = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ResultRowWrapper.GetQuery(self)
|
||||
return self._query
|
||||
end
|
||||
|
||||
function ResultRowWrapper.DecrementQuantity(self, amount)
|
||||
assert(self:IsCommodity() and not TSM.IsWowClassic() and #self._items == 1)
|
||||
local index = 1
|
||||
while amount > 0 do
|
||||
local subRow = self._subRows[index + SUB_ROW_SEARCH_INDEX_MULTIPLIER]
|
||||
assert(subRow)
|
||||
local _, numOwnerItems = subRow:GetOwnerInfo()
|
||||
local quantityAvailable = subRow:GetQuantities() - numOwnerItems
|
||||
if quantityAvailable > 0 then
|
||||
local usedQuantity = min(quantityAvailable, amount)
|
||||
local prevItemBuyout = floor(subRow._buyout / subRow._quantity)
|
||||
amount = amount - usedQuantity
|
||||
subRow._quantity = subRow._quantity - usedQuantity
|
||||
subRow._buyout = prevItemBuyout * subRow._quantity
|
||||
subRow._minBid = subRow._buyout
|
||||
if numOwnerItems == 0 and subRow._quantity == 0 then
|
||||
self:RemoveSubRow(subRow)
|
||||
else
|
||||
index = index + 1
|
||||
end
|
||||
else
|
||||
index = index + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ResultRowWrapper.GetMinBrowseId(self)
|
||||
return self._minBrowseId
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- ResultRowWrapper - Private Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
function ResultRowWrapper._RemoveSubRowByIndex(self, index)
|
||||
if TSM.IsWowClassic() then
|
||||
self._subRows[index]:Release()
|
||||
tremove(self._subRows, index)
|
||||
else
|
||||
local subRowOffset = self._searchIndex * SUB_ROW_SEARCH_INDEX_MULTIPLIER
|
||||
local itemKey = self._items[self._searchIndex]
|
||||
self._subRows[subRowOffset + index]:Release()
|
||||
self._subRows[subRowOffset + index] = nil
|
||||
-- shift the other subRows for this item down
|
||||
for i = index, itemKey._numAuctions - 1 do
|
||||
self._subRows[subRowOffset + i] = self._subRows[subRowOffset + i + 1]
|
||||
end
|
||||
self._subRows[subRowOffset + itemKey._numAuctions] = nil
|
||||
itemKey._numAuctions = itemKey._numAuctions - 1
|
||||
end
|
||||
end
|
||||
|
||||
function ResultRowWrapper._GetSearchProgress(self)
|
||||
assert(not TSM.IsWowClassic())
|
||||
if #self._items == 0 then
|
||||
return 0
|
||||
end
|
||||
local numSearched = 0
|
||||
for _, itemKey in ipairs(self._items) do
|
||||
if itemKey._numAuctions then
|
||||
numSearched = numSearched + 1
|
||||
end
|
||||
end
|
||||
return numSearched / #self._items
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.SubRowIteratorHelper(row, index)
|
||||
local searchIndex = floor(index / SUB_ROW_SEARCH_INDEX_MULTIPLIER)
|
||||
local subRowIndex = index % SUB_ROW_SEARCH_INDEX_MULTIPLIER
|
||||
while true do
|
||||
local itemKey = row._items[searchIndex]
|
||||
if not itemKey then
|
||||
return
|
||||
end
|
||||
|
||||
if subRowIndex >= (itemKey._numAuctions or 0) then
|
||||
searchIndex = searchIndex + 1
|
||||
subRowIndex = 0
|
||||
else
|
||||
subRowIndex = subRowIndex + 1
|
||||
index = searchIndex * SUB_ROW_SEARCH_INDEX_MULTIPLIER + subRowIndex
|
||||
return index, row._subRows[index]
|
||||
end
|
||||
end
|
||||
end
|
||||
364
LibTSM/Service/AuctionScanClasses/ResultSubRow.lua
Normal file
364
LibTSM/Service/AuctionScanClasses/ResultSubRow.lua
Normal file
@@ -0,0 +1,364 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local ResultSubRow = TSM.Init("Service.AuctionScanClasses.ResultSubRow")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local ObjectPool = TSM.Include("Util.ObjectPool")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local LibTSMClass = TSM.Include("LibTSMClass")
|
||||
local Util = TSM.Include("Service.AuctionScanClasses.Util")
|
||||
local ResultSubRowWrapper = LibTSMClass.DefineClass("ResultSubRowWrapper")
|
||||
local private = {
|
||||
objectPool = ObjectPool.New("AUCTION_SCAN_RESULT_SUB_ROW", ResultSubRowWrapper),
|
||||
ownersTemp = {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function ResultSubRow.Get(resultRow)
|
||||
local subRow = private.objectPool:Get()
|
||||
subRow:_Acquire(resultRow)
|
||||
return subRow
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- ResultSubRowWrapper - Meta Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
function ResultSubRowWrapper.__init(self)
|
||||
self._resultRow = nil
|
||||
self._itemLink = nil
|
||||
self._buyout = nil
|
||||
self._minBid = nil
|
||||
self._currentBid = nil
|
||||
self._minIncrement = nil
|
||||
self._isHighBidder = nil
|
||||
self._quantity = nil
|
||||
self._timeLeft = nil
|
||||
self._ownerStr = nil
|
||||
self._hasOwners = false
|
||||
self._numOwnerItems = nil
|
||||
self._auctionId = nil
|
||||
self._hash = nil
|
||||
self._hashNoSeller = nil
|
||||
self._browseId = nil
|
||||
self._numAuctions = 1
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper._Acquire(self, resultRow)
|
||||
self._resultRow = resultRow
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- ResultSubRowWrapper - Public Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
function ResultSubRowWrapper.Merge(self, other)
|
||||
if TSM.IsWowClassic() then
|
||||
self._numAuctions = self._numAuctions + other._numAuctions
|
||||
else
|
||||
if self:IsCommodity() then
|
||||
self._quantity = self._quantity + other._quantity
|
||||
self._numOwnerItems = self._numOwnerItems + other._numOwnerItems
|
||||
else
|
||||
self._numAuctions = self._numAuctions + other._numAuctions
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.Release(self)
|
||||
self._resultRow = nil
|
||||
self._numAuctions = 1
|
||||
self:_SetRawData(nil)
|
||||
private.objectPool:Recycle(self)
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.IsSubRow(self)
|
||||
return true
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.HasRawData(self)
|
||||
return self._timeLeft and true or false
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.HasOwners(self)
|
||||
return self._hasOwners
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.HasItemString(self)
|
||||
assert(self:HasRawData())
|
||||
local itemString = ItemString.Get(self._itemLink)
|
||||
if not Util.HasItemInfo(itemString) then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.IsCommodity(self)
|
||||
return self._resultRow:IsCommodity()
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetResultRow(self)
|
||||
return self._resultRow
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetBaseItemString(self)
|
||||
return self._resultRow:GetBaseItemString()
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetItemString(self)
|
||||
assert(self:HasRawData())
|
||||
local itemString = ItemString.Get(self._itemLink)
|
||||
return itemString or self._resultRow:GetItemString()
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetItemInfo(self)
|
||||
assert(self:HasItemString())
|
||||
local itemString = ItemString.Get(self._itemLink)
|
||||
local itemName = ItemInfo.GetName(itemString)
|
||||
local quality = ItemInfo.GetQuality(itemString)
|
||||
local itemLevel = ItemInfo.GetItemLevel(itemString)
|
||||
assert(itemName and quality and itemLevel)
|
||||
return itemName, quality, itemLevel, nil
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetBuyouts(self)
|
||||
assert(self:HasRawData())
|
||||
return self._buyout, floor(self._buyout / self._quantity), nil
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetBidInfo(self)
|
||||
assert(self:HasRawData())
|
||||
local itemMinBid = Math.Floor(self._minBid / self._quantity, TSM.IsWowClassic() and 1 or COPPER_PER_SILVER)
|
||||
return self._minBid, itemMinBid, self._currentBid, self._isHighBidder, self._minIncrement
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetRequiredBid(self)
|
||||
local requiredBid = nil
|
||||
if TSM.IsWowClassic() then
|
||||
requiredBid = self._currentBid == 0 and self._minBid or (self._currentBid + self._minIncrement)
|
||||
else
|
||||
requiredBid = self._minBid
|
||||
end
|
||||
return requiredBid
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetDisplayedBids(self)
|
||||
local displayedBid = self._currentBid == 0 and self._minBid or self._currentBid
|
||||
local itemDisplayedBid = Math.Floor(displayedBid / self._quantity, TSM.IsWowClassic() and 1 or COPPER_PER_SILVER)
|
||||
return displayedBid, itemDisplayedBid
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetLinks(self)
|
||||
assert(self:HasRawData())
|
||||
local rawLink = self._itemLink
|
||||
local itemLink = ItemInfo.GeneralizeLink(rawLink)
|
||||
return itemLink, rawLink
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetListingInfo(self)
|
||||
assert(self:HasRawData())
|
||||
return self._timeLeft, self._auctionId, self._browseId
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetQuantities(self)
|
||||
assert(self:HasRawData())
|
||||
return self._quantity, self._numAuctions
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetOwnerInfo(self)
|
||||
assert(self:HasRawData())
|
||||
return self._ownerStr, self._numOwnerItems
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.GetHashes(self)
|
||||
if not self._hash then
|
||||
assert(self:HasRawData())
|
||||
if TSM.IsWowClassic() then
|
||||
self._hash = strjoin("~", tostringall(self._itemLink, self._minBid, self._minIncrement, self._buyout, self._currentBid, self._ownerStr, self._timeLeft, self._quantity, self._isHighBidder))
|
||||
self._hashNoSeller = strjoin("~", tostringall(self._itemLink, self._minBid, self._minIncrement, self._buyout, self._currentBid, self._timeLeft, self._quantity, self._isHighBidder))
|
||||
else
|
||||
local baseItemString = self:GetBaseItemString()
|
||||
local itemMinBid = Math.Floor(self._minBid / self._quantity, COPPER_PER_SILVER)
|
||||
local itemBuyout = floor(self._buyout / self._quantity)
|
||||
local itemKeyId, itemKeySpeciesId = nil, nil
|
||||
if ItemString.IsPet(baseItemString) then
|
||||
itemKeyId = ItemString.ToId(ItemString.GetPetCage())
|
||||
itemKeySpeciesId = ItemString.ToId(baseItemString)
|
||||
elseif ItemString.IsItem(baseItemString) then
|
||||
itemKeyId = ItemString.ToId(baseItemString)
|
||||
itemKeySpeciesId = 0
|
||||
else
|
||||
error("Invalid baseItemString: "..tostring(baseItemString))
|
||||
end
|
||||
if self:IsCommodity() then
|
||||
self._hash = strjoin("~", tostringall(itemKeyId, itemBuyout, self._auctionId, self._ownerStr))
|
||||
self._hashNoSeller = strjoin("~", tostringall(itemKeyId, itemBuyout, self._auctionId))
|
||||
else
|
||||
self._hash = strjoin("~", tostringall(itemKeyId, itemKeySpeciesId, self._itemLink, itemMinBid, itemBuyout, self._currentBid, self._quantity, self._isHighBidder, self._ownerStr, self._auctionId))
|
||||
self._hashNoSeller = strjoin("~", tostringall(itemKeyId, itemKeySpeciesId, self._itemLink, itemMinBid, itemBuyout, self._currentBid, self._quantity, self._isHighBidder, self._auctionId))
|
||||
end
|
||||
end
|
||||
end
|
||||
return self._hash, self._hashNoSeller
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.EqualsIndex(self, index, noSeller)
|
||||
assert(TSM.IsWowClassic())
|
||||
local _, _, stackSize, _, _, _, _, minBid, minIncrement, buyout, bid, isHighBidder, _, seller, sellerFull = GetAuctionItemInfo("list", index)
|
||||
seller = Util.FixSellerName(seller, sellerFull) or "?"
|
||||
-- this is to get around a bug in Blizzard's code where the minIncrement value will be inconsistent for auctions where the player is the highest bidder
|
||||
minIncrement = isHighBidder and 0 or minIncrement
|
||||
if minBid ~= self._minBid or minIncrement ~= self._minIncrement or buyout ~= self._buyout or bid ~= self._currentBid or stackSize == self._quantity and isHighBidder ~= self._isHighBidder then
|
||||
return false
|
||||
elseif not noSeller and seller ~= self._ownerStr then
|
||||
return false
|
||||
elseif GetAuctionItemLink("list", index) ~= self._itemLink then
|
||||
return false
|
||||
elseif GetAuctionItemTimeLeft("list", index) ~= self._timeLeft then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.DecrementQuantity(self, amount)
|
||||
if TSM.IsWowClassic() then
|
||||
assert(amount == self._quantity)
|
||||
self._numAuctions = self._numAuctions - 1
|
||||
if self._numAuctions == 0 then
|
||||
self._resultRow:RemoveSubRow(self)
|
||||
end
|
||||
else
|
||||
if self:IsCommodity() then
|
||||
self._resultRow:DecrementQuantity(amount)
|
||||
else
|
||||
assert(amount == 1 and amount == self._quantity)
|
||||
self._numAuctions = self._numAuctions - 1
|
||||
assert(self._numOwnerItems <= self._numAuctions)
|
||||
if self._numAuctions == 0 then
|
||||
self._resultRow:RemoveSubRow(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ResultSubRowWrapper.UpdateResultInfo(self, newAuctionId, newResultInfo)
|
||||
if newResultInfo then
|
||||
self:_SetRawData(newResultInfo, self._browseId)
|
||||
else
|
||||
self._auctionId = newAuctionId
|
||||
self._hash = nil
|
||||
self._hashNoSeller = nil
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- ResultRowWrapper - Private Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
function ResultSubRowWrapper._SetRawData(self, data, browseId, itemLink)
|
||||
self._hash = nil
|
||||
self._hashNoSeller = nil
|
||||
self._browseId = browseId
|
||||
if data then
|
||||
if TSM.IsWowClassic() then
|
||||
local _, _, stackSize, _, _, _, _, minBid, minIncrement, buyout, bid, isHighBidder, _, seller, sellerFull = GetAuctionItemInfo("list", data)
|
||||
seller = Util.FixSellerName(seller, sellerFull)
|
||||
-- this is to get around a bug in Blizzard's code where the minIncrement value will be inconsistent for auctions where the player is the highest bidder
|
||||
minIncrement = isHighBidder and 0 or minIncrement
|
||||
self._itemLink = itemLink
|
||||
self._buyout = buyout
|
||||
self._minBid = minBid
|
||||
self._currentBid = bid
|
||||
self._minIncrement = minIncrement
|
||||
self._isHighBidder = isHighBidder
|
||||
self._quantity = stackSize
|
||||
self._timeLeft = GetAuctionItemTimeLeft("list", data)
|
||||
self._ownerStr = seller or "?"
|
||||
self._hasOwners = seller and true or false
|
||||
self._numOwnerItems = 0
|
||||
self._auctionId = 0
|
||||
else
|
||||
if self._resultRow:IsCommodity() then
|
||||
local baseItemString = self._resultRow:GetBaseItemString()
|
||||
self._itemLink = ItemInfo.GetLink(baseItemString)
|
||||
else
|
||||
self._itemLink = data.itemLink
|
||||
end
|
||||
|
||||
if self:IsCommodity() then
|
||||
self._quantity = data.quantity
|
||||
self._buyout = data.unitPrice * data.quantity
|
||||
self._minBid = self._buyout
|
||||
self._currentBid = 0
|
||||
self._minIncrement = 0
|
||||
self._isHighBidder = data.bidder and data.bidder == UnitGUID("player") or false
|
||||
self._numOwnerItems = data.numOwnerItems or 0
|
||||
-- convert the timeLeftSeconds to regular timeLeft
|
||||
if data.timeLeftSeconds < 60 * 60 then
|
||||
self._timeLeft = 1
|
||||
elseif data.timeLeftSeconds < 2 * 60 * 60 then
|
||||
self._timeLeft = 2
|
||||
elseif data.timeLeftSeconds < 12 * 60 * 60 then
|
||||
self._timeLeft = 3
|
||||
else
|
||||
self._timeLeft = 4
|
||||
end
|
||||
else
|
||||
self._quantity = 1
|
||||
self._numAuctions = data.quantity
|
||||
self._buyout = data.buyoutAmount or 0
|
||||
self._minBid = data.minBid or data.buyoutAmount
|
||||
self._currentBid = data.bidAmount or 0
|
||||
self._minIncrement = 0
|
||||
self._isHighBidder = false
|
||||
self._numOwnerItems = data.containsAccountItem and data.quantity or 0
|
||||
self._timeLeft = data.timeLeft + 1
|
||||
end
|
||||
|
||||
self._hasOwners = #data.owners > 0
|
||||
assert(#private.ownersTemp == 0)
|
||||
for _, owner in ipairs(data.owners) do
|
||||
if owner == "player" then
|
||||
owner = UnitName("player")
|
||||
elseif owner == "" then
|
||||
owner = "?"
|
||||
self._hasOwners = false
|
||||
end
|
||||
tinsert(private.ownersTemp, owner)
|
||||
end
|
||||
self._ownerStr = table.concat(private.ownersTemp, ",")
|
||||
wipe(private.ownersTemp)
|
||||
self._auctionId = data.auctionID
|
||||
end
|
||||
assert(self._itemLink and self._quantity and self._buyout and self._minBid and self._currentBid and self._numOwnerItems and self._timeLeft and self._ownerStr and self._auctionId)
|
||||
else
|
||||
self._itemLink = nil
|
||||
self._buyout = nil
|
||||
self._minBid = nil
|
||||
self._currentBid = nil
|
||||
self._minIncrement = nil
|
||||
self._isHighBidder = nil
|
||||
self._quantity = nil
|
||||
self._timeLeft = nil
|
||||
self._ownerStr = nil
|
||||
self._hasOwners = false
|
||||
self._numOwnerItems = nil
|
||||
self._auctionId = nil
|
||||
end
|
||||
end
|
||||
620
LibTSM/Service/AuctionScanClasses/ScanManager.lua
Normal file
620
LibTSM/Service/AuctionScanClasses/ScanManager.lua
Normal file
@@ -0,0 +1,620 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
-- This file contains code for scanning the auction house
|
||||
local _, TSM = ...
|
||||
local ScanManager = TSM.Init("Service.AuctionScanClasses.ScanManager")
|
||||
local L = TSM.Include("Locale").GetTable()
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
local ObjectPool = TSM.Include("Util.ObjectPool")
|
||||
local AuctionHouseWrapper = TSM.Include("Service.AuctionHouseWrapper")
|
||||
local Threading = TSM.Include("Service.Threading")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local Query = TSM.Include("Service.AuctionScanClasses.Query")
|
||||
local QueryUtil = TSM.Include("Service.AuctionScanClasses.QueryUtil")
|
||||
local AuctionScanManager = TSM.Include("LibTSMClass").DefineClass("AuctionScanManager")
|
||||
local private = {
|
||||
objectPool = ObjectPool.New("AUCTION_SCAN_MANAGER", AuctionScanManager),
|
||||
}
|
||||
-- arbitrary estimate that finishing the browse request is worth 10% of the query's progress
|
||||
local BROWSE_PROGRESS = 0.1
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function ScanManager.Get()
|
||||
return private.objectPool:Get()
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Class Meta Methods
|
||||
-- ============================================================================
|
||||
|
||||
function AuctionScanManager.__init(self)
|
||||
self._resolveSellers = nil
|
||||
self._ignoreItemLevel = nil
|
||||
self._queries = {}
|
||||
self._queriesScanned = 0
|
||||
self._queryDidBrowse = false
|
||||
self._onProgressUpdateHandler = nil
|
||||
self._onQueryDoneHandler = nil
|
||||
self._resultsUpdateCallbacks = {}
|
||||
self._nextSearchItemFunction = nil
|
||||
self._currentSearchChangedCallback = nil
|
||||
self._findResult = {}
|
||||
self._cancelled = false
|
||||
self._shouldPause = false
|
||||
self._paused = false
|
||||
self._scanQuery = nil
|
||||
self._findQuery = nil
|
||||
self._numItems = nil
|
||||
self._queryCallback = function(query, searchRow)
|
||||
for func in pairs(self._resultsUpdateCallbacks) do
|
||||
func(self, query, searchRow)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionScanManager._Release(self)
|
||||
self._resolveSellers = nil
|
||||
self._ignoreItemLevel = nil
|
||||
for _, query in ipairs(self._queries) do
|
||||
query:Release()
|
||||
end
|
||||
wipe(self._queries)
|
||||
self._queriesScanned = 0
|
||||
self._queryDidBrowse = false
|
||||
self._onProgressUpdateHandler = nil
|
||||
self._onQueryDoneHandler = nil
|
||||
wipe(self._resultsUpdateCallbacks)
|
||||
self._nextSearchItemFunction = nil
|
||||
self._currentSearchChangedCallback = nil
|
||||
self._cancelled = false
|
||||
self._shouldPause = false
|
||||
self._paused = false
|
||||
wipe(self._findResult)
|
||||
self._scanQuery = nil
|
||||
if self._findQuery then
|
||||
self._findQuery:Release()
|
||||
self._findQuery = nil
|
||||
end
|
||||
self._numItems = nil
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Public Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
function AuctionScanManager.Release(self)
|
||||
self:_Release()
|
||||
private.objectPool:Recycle(self)
|
||||
end
|
||||
|
||||
function AuctionScanManager.SetResolveSellers(self, resolveSellers)
|
||||
self._resolveSellers = resolveSellers
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionScanManager.SetIgnoreItemLevel(self, ignoreItemLevel)
|
||||
self._ignoreItemLevel = ignoreItemLevel
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionScanManager.SetScript(self, script, handler)
|
||||
if script == "OnProgressUpdate" then
|
||||
self._onProgressUpdateHandler = handler
|
||||
elseif script == "OnQueryDone" then
|
||||
self._onQueryDoneHandler = handler
|
||||
elseif script == "OnCurrentSearchChanged" then
|
||||
self._currentSearchChangedCallback = handler
|
||||
else
|
||||
error("Unknown AuctionScanManager script: "..tostring(script))
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
function AuctionScanManager.AddResultsUpdateCallback(self, func)
|
||||
self._resultsUpdateCallbacks[func] = true
|
||||
end
|
||||
|
||||
function AuctionScanManager.RemoveResultsUpdateCallback(self, func)
|
||||
self._resultsUpdateCallbacks[func] = nil
|
||||
end
|
||||
|
||||
function AuctionScanManager.SetNextSearchItemFunction(self, func)
|
||||
self._nextSearchItemFunction = func
|
||||
end
|
||||
|
||||
function AuctionScanManager.GetNumQueries(self)
|
||||
return #self._queries
|
||||
end
|
||||
|
||||
function AuctionScanManager.QueryIterator(self, offset)
|
||||
return private.QueryIteratorHelper, self._queries, offset or 0
|
||||
end
|
||||
|
||||
function AuctionScanManager.NewQuery(self)
|
||||
local query = Query.Get()
|
||||
self:_AddQuery(query)
|
||||
return query
|
||||
end
|
||||
|
||||
function AuctionScanManager.AddItemListQueriesThreaded(self, itemList)
|
||||
assert(Threading.IsThreadContext())
|
||||
-- remove duplicates
|
||||
local usedItems = TempTable.Acquire()
|
||||
for i = #itemList, 1, -1 do
|
||||
local itemString = itemList[i]
|
||||
if usedItems[itemString] then
|
||||
tremove(itemList, i)
|
||||
end
|
||||
usedItems[itemString] = true
|
||||
end
|
||||
TempTable.Release(usedItems)
|
||||
self._numItems = #itemList
|
||||
QueryUtil.GenerateThreaded(itemList, private.NewQueryCallback, self)
|
||||
end
|
||||
|
||||
function AuctionScanManager.ScanQueriesThreaded(self)
|
||||
assert(Threading.IsThreadContext())
|
||||
self._queriesScanned = 0
|
||||
self._cancelled = false
|
||||
AuctionHouseWrapper.GetAndResetTotalHookedTime()
|
||||
self:_NotifyProgressUpdate()
|
||||
|
||||
-- loop through each filter to perform
|
||||
local allSuccess = true
|
||||
while self._queriesScanned < #self._queries do
|
||||
local query = self._queries[self._queriesScanned + 1]
|
||||
-- run the browse query
|
||||
local querySuccess, numNewResults = self:_ProcessQuery(query)
|
||||
if not querySuccess then
|
||||
allSuccess = false
|
||||
break
|
||||
end
|
||||
self._queriesScanned = self._queriesScanned + 1
|
||||
self:_NotifyProgressUpdate()
|
||||
if self._onQueryDoneHandler then
|
||||
self:_onQueryDoneHandler(query, numNewResults)
|
||||
end
|
||||
self:_Pause()
|
||||
end
|
||||
|
||||
if allSuccess then
|
||||
local hookedTime, topAddon, topTime = AuctionHouseWrapper.GetAndResetTotalHookedTime()
|
||||
if hookedTime > 1 and topAddon ~= "Blizzard_AuctionHouseUI" then
|
||||
Log.PrintfUser(L["Scan was slowed down by %s seconds by other AH addons (%s seconds by %s)."], Math.Round(hookedTime, 0.1), Math.Round(topTime, 0.1), topAddon)
|
||||
end
|
||||
end
|
||||
return allSuccess
|
||||
end
|
||||
|
||||
function AuctionScanManager.FindAuctionThreaded(self, findSubRow, noSeller)
|
||||
assert(Threading.IsThreadContext())
|
||||
wipe(self._findResult)
|
||||
if TSM.IsWowClassic() then
|
||||
return self:_FindAuctionThreaded(findSubRow, noSeller)
|
||||
else
|
||||
return self:_FindAuctionThreaded83(findSubRow, noSeller)
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionScanManager.PrepareForBidOrBuyout(self, index, subRow, noSeller, quantity, itemBuyout)
|
||||
if TSM.IsWowClassic() then
|
||||
return subRow:EqualsIndex(index, noSeller)
|
||||
else
|
||||
local itemString = subRow:GetItemString()
|
||||
if ItemInfo.IsCommodity(itemString) then
|
||||
local future = AuctionHouseWrapper.StartCommoditiesPurchase(ItemString.ToId(itemString), quantity, itemBuyout)
|
||||
if not future then
|
||||
return false
|
||||
end
|
||||
return true, future
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionScanManager.PlaceBidOrBuyout(self, index, bidBuyout, subRow, quantity)
|
||||
if TSM.IsWowClassic() then
|
||||
PlaceAuctionBid("list", index, bidBuyout)
|
||||
return true
|
||||
else
|
||||
local itemString = subRow:GetItemString()
|
||||
local future = nil
|
||||
if ItemInfo.IsCommodity(itemString) then
|
||||
local itemId = ItemString.ToId(itemString)
|
||||
future = AuctionHouseWrapper.ConfirmCommoditiesPurchase(itemId, quantity)
|
||||
else
|
||||
local _, auctionId = subRow:GetListingInfo()
|
||||
future = AuctionHouseWrapper.PlaceBid(auctionId, bidBuyout)
|
||||
quantity = 1
|
||||
end
|
||||
if not future then
|
||||
return false
|
||||
end
|
||||
-- TODO: return this future and record the buyout once the future is done
|
||||
future:Cancel()
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionScanManager.GetProgress(self)
|
||||
local numQueries = self:GetNumQueries()
|
||||
if self._queriesScanned == numQueries then
|
||||
return 1
|
||||
end
|
||||
local currentQuery = self._queries[self._queriesScanned + 1]
|
||||
local searchProgress = nil
|
||||
if not self._queryDidBrowse or TSM.IsWowClassic() then
|
||||
searchProgress = 0
|
||||
else
|
||||
searchProgress = currentQuery:GetSearchProgress() * (1 - BROWSE_PROGRESS) + BROWSE_PROGRESS
|
||||
end
|
||||
local queryStep = 1 / numQueries
|
||||
local progress = min((self._queriesScanned + searchProgress) * queryStep, 1)
|
||||
return progress, self._paused
|
||||
end
|
||||
|
||||
function AuctionScanManager.Cancel(self)
|
||||
self._cancelled = true
|
||||
if self._scanQuery then
|
||||
self._scanQuery:CancelBrowseOrSearch()
|
||||
self._scanQuery = nil
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionScanManager.SetPaused(self, paused)
|
||||
self._shouldPause = paused
|
||||
if self._scanQuery then
|
||||
self._scanQuery:CancelBrowseOrSearch()
|
||||
self._scanQuery = nil
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionScanManager.GetNumItems(self)
|
||||
return self._numItems
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Class Methods
|
||||
-- ============================================================================
|
||||
|
||||
function AuctionScanManager._AddQuery(self, query)
|
||||
query:SetResolveSellers(self._resolveSellers)
|
||||
query:SetCallback(self._queryCallback)
|
||||
tinsert(self._queries, query)
|
||||
end
|
||||
|
||||
function AuctionScanManager._IsCancelled(self)
|
||||
return self._cancelled
|
||||
end
|
||||
|
||||
function AuctionScanManager._Pause(self)
|
||||
if not self._shouldPause then
|
||||
return
|
||||
end
|
||||
self._paused = true
|
||||
self:_NotifyProgressUpdate()
|
||||
if self._currentSearchChangedCallback then
|
||||
self:_currentSearchChangedCallback()
|
||||
end
|
||||
while self._shouldPause do
|
||||
Threading.Yield(true)
|
||||
end
|
||||
self._paused = false
|
||||
self:_NotifyProgressUpdate()
|
||||
if self._currentSearchChangedCallback then
|
||||
self:_currentSearchChangedCallback()
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionScanManager._NotifyProgressUpdate(self)
|
||||
if self._onProgressUpdateHandler then
|
||||
self:_onProgressUpdateHandler()
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionScanManager._ProcessQuery(self, query)
|
||||
local prevMaxBrowseId = 0
|
||||
for _, row in query:BrowseResultsIterator() do
|
||||
prevMaxBrowseId = max(prevMaxBrowseId, row:GetMinBrowseId())
|
||||
end
|
||||
|
||||
-- run the browse query
|
||||
self._queryDidBrowse = false
|
||||
while not self:_DoBrowse(query) do
|
||||
if self._shouldPause then
|
||||
-- this browse failed due to a pause request, so try again after we're resumed
|
||||
self:_Pause()
|
||||
-- wipe the browse results since we're going to do another search
|
||||
query:WipeBrowseResults()
|
||||
else
|
||||
return false, 0
|
||||
end
|
||||
end
|
||||
self._queryDidBrowse = true
|
||||
self:_NotifyProgressUpdate()
|
||||
|
||||
local numNewResults = 0
|
||||
if TSM.IsWowClassic() then
|
||||
for _, row in query:BrowseResultsIterator() do
|
||||
if row:GetMinBrowseId() > prevMaxBrowseId then
|
||||
numNewResults = numNewResults + row:GetNumSubRows()
|
||||
end
|
||||
end
|
||||
return true, numNewResults
|
||||
end
|
||||
|
||||
local rows = Threading.AcquireSafeTempTable()
|
||||
for baseItemString, row in query:BrowseResultsIterator() do
|
||||
rows[baseItemString] = row
|
||||
end
|
||||
while true do
|
||||
local baseItemString, row = nil, nil
|
||||
if self._nextSearchItemFunction then
|
||||
baseItemString = self._nextSearchItemFunction()
|
||||
row = baseItemString and rows[baseItemString]
|
||||
end
|
||||
if not row then
|
||||
baseItemString, row = next(rows)
|
||||
end
|
||||
if not row then
|
||||
break
|
||||
end
|
||||
rows[baseItemString] = nil
|
||||
if self._currentSearchChangedCallback then
|
||||
self:_currentSearchChangedCallback(baseItemString)
|
||||
end
|
||||
-- store all the existing auctionIds so we can see what changed
|
||||
local prevAuctionIds = Threading.AcquireSafeTempTable()
|
||||
for _, subRow in row:SubRowIterator() do
|
||||
local _, auctionId = subRow:GetListingInfo()
|
||||
assert(not prevAuctionIds[auctionId])
|
||||
prevAuctionIds[auctionId] = true
|
||||
end
|
||||
-- send the query for this item
|
||||
while not self:_DoSearch(query, row) do
|
||||
if self._shouldPause then
|
||||
-- this search failed due to a pause request, so try again after we're resumed
|
||||
self:_Pause()
|
||||
-- wipe the search results since we're going to do another search
|
||||
row:WipeSearchResults()
|
||||
else
|
||||
Threading.ReleaseSafeTempTable(prevAuctionIds)
|
||||
Threading.ReleaseSafeTempTable(rows)
|
||||
return false, numNewResults
|
||||
end
|
||||
end
|
||||
|
||||
local numSubRows = row:GetNumSubRows()
|
||||
for _, subRow in row:SubRowIterator() do
|
||||
local _, auctionId = subRow:GetListingInfo()
|
||||
if not prevAuctionIds[auctionId] then
|
||||
numNewResults = numNewResults + 1
|
||||
end
|
||||
end
|
||||
Threading.ReleaseSafeTempTable(prevAuctionIds)
|
||||
if numSubRows == 0 then
|
||||
-- remove this row since there are no search results
|
||||
query:RemoveResultRow(row)
|
||||
end
|
||||
|
||||
self:_NotifyProgressUpdate()
|
||||
self:_Pause()
|
||||
Threading.Yield()
|
||||
end
|
||||
Threading.ReleaseSafeTempTable(rows)
|
||||
return true, numNewResults
|
||||
end
|
||||
|
||||
function AuctionScanManager._DoBrowse(self, query, ...)
|
||||
return self:_DoBrowseSearchHelper(query, query:Browse(...))
|
||||
end
|
||||
|
||||
function AuctionScanManager._DoSearch(self, query, ...)
|
||||
return self:_DoBrowseSearchHelper(query, query:Search(...))
|
||||
end
|
||||
|
||||
function AuctionScanManager._DoBrowseSearchHelper(self, query, future)
|
||||
if not future then
|
||||
return false
|
||||
end
|
||||
self._scanQuery = query
|
||||
local result = Threading.WaitForFuture(future)
|
||||
self._scanQuery = nil
|
||||
Threading.Yield()
|
||||
return result
|
||||
end
|
||||
|
||||
function AuctionScanManager._FindAuctionThreaded(self, row, noSeller)
|
||||
self._cancelled = false
|
||||
-- make sure we're not in the middle of a query where the results are going to change on us
|
||||
Threading.WaitForFunction(CanSendAuctionQuery)
|
||||
|
||||
-- search the current page for the auction
|
||||
if self:_FindAuctionOnCurrentPage(row, noSeller) then
|
||||
Log.Info("Found on current page")
|
||||
return self._findResult
|
||||
end
|
||||
|
||||
-- search for the item
|
||||
local page, maxPage = 0, nil
|
||||
while true do
|
||||
-- query the AH
|
||||
if self._findQuery then
|
||||
self._findQuery:Release()
|
||||
end
|
||||
local itemString = row:GetItemString()
|
||||
local level = ItemInfo.GetMinLevel(itemString)
|
||||
local quality = ItemInfo.GetQuality(itemString)
|
||||
assert(level and quality)
|
||||
self._findQuery = Query.Get()
|
||||
:SetStr(ItemInfo.GetName(itemString), true)
|
||||
:SetQualityRange(quality, quality)
|
||||
:SetLevelRange(level, level)
|
||||
:SetClass(ItemInfo.GetClassId(itemString), ItemInfo.GetSubClassId(itemString))
|
||||
:SetItems(itemString)
|
||||
:SetResolveSellers(not noSeller)
|
||||
:SetPage(page)
|
||||
local filterSuccess = self:_DoBrowse(self._findQuery)
|
||||
if self._findQuery then
|
||||
self._findQuery:Release()
|
||||
self._findQuery = nil
|
||||
end
|
||||
if not filterSuccess then
|
||||
break
|
||||
end
|
||||
-- search this page for the row
|
||||
if self:_FindAuctionOnCurrentPage(row, noSeller) then
|
||||
Log.Info("Found auction (%d)", page)
|
||||
return self._findResult
|
||||
elseif self:_IsCancelled() then
|
||||
break
|
||||
end
|
||||
|
||||
local numPages = ceil(select(2, GetNumAuctionItems("list")) / NUM_AUCTION_ITEMS_PER_PAGE)
|
||||
local canBeLater = private.FindAuctionCanBeOnLaterPage(row)
|
||||
maxPage = maxPage or numPages - 1
|
||||
if not canBeLater and page < maxPage then
|
||||
maxPage = page
|
||||
end
|
||||
if canBeLater and page < maxPage then
|
||||
Log.Info("Trying next page (%d)", page + 1)
|
||||
page = page + 1
|
||||
else
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionScanManager._FindAuctionOnCurrentPage(self, subRow, noSeller)
|
||||
local found = false
|
||||
for i = 1, GetNumAuctionItems("list") do
|
||||
if subRow:EqualsIndex(i, noSeller) then
|
||||
tinsert(self._findResult, i)
|
||||
found = true
|
||||
end
|
||||
end
|
||||
return found
|
||||
end
|
||||
|
||||
function AuctionScanManager._FindAuctionThreaded83(self, findSubRow, noSeller)
|
||||
assert(findSubRow:IsSubRow())
|
||||
self._cancelled = false
|
||||
noSeller = noSeller or findSubRow:IsCommodity()
|
||||
|
||||
local row = findSubRow:GetResultRow()
|
||||
local findHash, findHashNoSeller = findSubRow:GetHashes()
|
||||
|
||||
if not self:_DoSearch(row:GetQuery(), row, false) then
|
||||
return nil
|
||||
end
|
||||
local result = nil
|
||||
-- first try to find a subRow with a full matching hash
|
||||
for _, subRow in row:SubRowIterator() do
|
||||
local quantity, numAuctions = subRow:GetQuantities()
|
||||
local hash = subRow:GetHashes()
|
||||
if hash == findHash then
|
||||
result = (result or 0) + quantity * numAuctions
|
||||
end
|
||||
end
|
||||
if result then
|
||||
return result
|
||||
end
|
||||
-- next try to find the first subRow with a matching no-seller hash
|
||||
local firstHash = nil
|
||||
for _, subRow in row:SubRowIterator() do
|
||||
local quantity, numAuctions = subRow:GetQuantities()
|
||||
local hash, hashNoSeller = subRow:GetHashes()
|
||||
if (not firstHash or hash == firstHash) and hashNoSeller == findHashNoSeller then
|
||||
firstHash = hash
|
||||
result = (result or 0) + quantity * numAuctions
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.NewQueryCallback(query, self)
|
||||
self:_AddQuery(query)
|
||||
end
|
||||
|
||||
function private.FindAuctionCanBeOnLaterPage(row)
|
||||
local pageAuctions = GetNumAuctionItems("list")
|
||||
if pageAuctions == 0 then
|
||||
-- there are no auctions on this page, so it cannot be on a later one
|
||||
return false
|
||||
end
|
||||
local _, _, stackSize, _, _, _, _, _, _, buyout, _, _, _, seller, sellerFull = GetAuctionItemInfo("list", pageAuctions)
|
||||
|
||||
local itemBuyout = (buyout > 0) and floor(buyout / stackSize) or 0
|
||||
local _, rowItemBuyout = row:GetBuyouts()
|
||||
if rowItemBuyout > itemBuyout then
|
||||
-- item must be on a later page since it would be sorted after the last auction on this page
|
||||
return true
|
||||
elseif rowItemBuyout < itemBuyout then
|
||||
-- item cannot be on a later page since it would be sorted before the last auction on this page
|
||||
return false
|
||||
end
|
||||
|
||||
local rowStackSize = row:GetQuantities()
|
||||
if rowStackSize > stackSize then
|
||||
-- item must be on a later page since it would be sorted after the last auction on this page
|
||||
return true
|
||||
elseif rowStackSize < stackSize then
|
||||
-- item cannot be on a later page since it would be sorted before the last auction on this page
|
||||
return false
|
||||
end
|
||||
|
||||
seller = private.FixSellerName(seller, sellerFull) or "?"
|
||||
local rowSeller = row:GetOwnerInfo()
|
||||
if rowSeller > seller then
|
||||
-- item must be on a later page since it would be sorted after the last auction on this page
|
||||
return true
|
||||
elseif rowSeller < seller then
|
||||
-- item cannot be on a later page since it would be sorted before the last auction on this page
|
||||
return false
|
||||
end
|
||||
|
||||
-- all the things we are sorting on are the same, so the auction could be on a later page
|
||||
return true
|
||||
end
|
||||
|
||||
function private.FixSellerName(seller, sellerFull)
|
||||
local realm = GetRealmName()
|
||||
if sellerFull and strjoin("-", seller, realm) ~= sellerFull then
|
||||
return sellerFull
|
||||
else
|
||||
return seller
|
||||
end
|
||||
end
|
||||
|
||||
function private.QueryIteratorHelper(tbl, index)
|
||||
index = index + 1
|
||||
if index > #tbl then
|
||||
return
|
||||
end
|
||||
return index, tbl[index]
|
||||
end
|
||||
511
LibTSM/Service/AuctionScanClasses/Scanner.lua
Normal file
511
LibTSM/Service/AuctionScanClasses/Scanner.lua
Normal file
@@ -0,0 +1,511 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Scanner = TSM.Init("Service.AuctionScanClasses.Scanner")
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local FSM = TSM.Include("Util.FSM")
|
||||
local Future = TSM.Include("Util.Future")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local AuctionHouseWrapper = TSM.Include("Service.AuctionHouseWrapper")
|
||||
local Util = TSM.Include("Service.AuctionScanClasses.Util")
|
||||
local ResultRow = TSM.Include("Service.AuctionScanClasses.ResultRow")
|
||||
local private = {
|
||||
resolveSellers = nil,
|
||||
pendingFuture = nil,
|
||||
query = nil,
|
||||
browseResults = nil,
|
||||
callback = nil,
|
||||
browseId = 1,
|
||||
browseIsNoScan = false,
|
||||
browseIndex = 1,
|
||||
browsePendingIndexes = {},
|
||||
searchRow = nil,
|
||||
useCachedData = nil,
|
||||
retryCount = 0,
|
||||
requestFuture = Future.New("AUCTION_SCANNER_FUTURE"),
|
||||
requestResult = nil,
|
||||
fsm = nil,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Scanner:OnModuleLoad(function()
|
||||
private.requestFuture:SetScript("OnCleanup", function()
|
||||
Delay.Cancel("AUCTION_SCANNER_DONE")
|
||||
private.fsm:ProcessEvent("EV_CANCEL")
|
||||
end)
|
||||
|
||||
if TSM.IsWowClassic() then
|
||||
Event.Register("AUCTION_ITEM_LIST_UPDATE", function()
|
||||
private.fsm:SetLoggingEnabled(false)
|
||||
private.fsm:ProcessEvent("EV_BROWSE_RESULTS_UPDATED")
|
||||
private.fsm:SetLoggingEnabled(true)
|
||||
end)
|
||||
else
|
||||
Event.Register("COMMODITY_SEARCH_RESULTS_UPDATED", function()
|
||||
private.fsm:ProcessEvent("EV_SEARCH_RESULTS_UPDATED")
|
||||
end)
|
||||
Event.Register("ITEM_SEARCH_RESULTS_UPDATED", function()
|
||||
private.fsm:ProcessEvent("EV_SEARCH_RESULTS_UPDATED")
|
||||
end)
|
||||
end
|
||||
|
||||
private.fsm = FSM.New("AUCTION_SCANNER_FSM")
|
||||
:AddState(FSM.NewState("ST_INIT")
|
||||
:SetOnEnter(function()
|
||||
private.query = nil
|
||||
private.resolveSellers = nil
|
||||
private.useCachedData = nil
|
||||
private.searchRow = nil
|
||||
private.callback = nil
|
||||
private.retryCount = 0
|
||||
Delay.Cancel("AUCTION_SCANNER_RETRY")
|
||||
if private.pendingFuture then
|
||||
private.pendingFuture:Cancel()
|
||||
private.pendingFuture = nil
|
||||
end
|
||||
end)
|
||||
:AddTransition("ST_BROWSE_SORT")
|
||||
:AddTransition("ST_BROWSE_CHECKING")
|
||||
:AddTransition("ST_SEARCH_GET_KEY")
|
||||
:AddEvent("EV_START_BROWSE", function(_, query, resolveSellers, browseResults, callback)
|
||||
assert(not private.query)
|
||||
private.query = query
|
||||
private.resolveSellers = resolveSellers
|
||||
private.browseResults = browseResults
|
||||
private.browseId = private.browseId + 1
|
||||
private.browseIsNoScan = false
|
||||
private.callback = callback
|
||||
return "ST_BROWSE_SORT"
|
||||
end)
|
||||
:AddEvent("EV_START_BROWSE_NO_SCAN", function(_, query, itemKeys, browseResults, callback)
|
||||
assert(not TSM.IsWowClassic())
|
||||
assert(not private.query)
|
||||
private.query = query
|
||||
private.browseResults = browseResults
|
||||
private.browseId = private.browseId + 1
|
||||
private.browseIsNoScan = true
|
||||
private.callback = callback
|
||||
for _, itemKey in ipairs(itemKeys) do
|
||||
local baseItemString = ItemString.GetBaseFromItemKey(itemKey)
|
||||
private.ProcessBrowseResult(baseItemString, itemKey)
|
||||
end
|
||||
return "ST_BROWSE_CHECKING"
|
||||
end)
|
||||
:AddEvent("EV_START_SEARCH", function(_, query, resolveSellers, useCachedData, searchRow, callback)
|
||||
assert(not TSM.IsWowClassic())
|
||||
assert(not private.query)
|
||||
private.query = query
|
||||
private.resolveSellers = resolveSellers
|
||||
private.useCachedData = useCachedData
|
||||
private.searchRow = searchRow
|
||||
private.callback = callback
|
||||
private.searchRow:SearchReset()
|
||||
return "ST_SEARCH_GET_KEY"
|
||||
end)
|
||||
)
|
||||
:AddState(FSM.NewState("ST_BROWSE_SORT")
|
||||
:SetOnEnter(function()
|
||||
if not private.query:_SetSort() then
|
||||
Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.5, private.RetryHandler)
|
||||
return
|
||||
end
|
||||
return "ST_BROWSE_SEND"
|
||||
end)
|
||||
:AddTransition("ST_BROWSE_SORT")
|
||||
:AddTransition("ST_BROWSE_SEND")
|
||||
:AddTransition("ST_CANCELING")
|
||||
:AddEventTransition("EV_RETRY", "ST_BROWSE_SORT")
|
||||
:AddEventTransition("EV_CANCEL", "ST_CANCELING")
|
||||
)
|
||||
:AddState(FSM.NewState("ST_BROWSE_SEND")
|
||||
:SetOnEnter(function()
|
||||
private.HandleAuctionHouseWrapperResult(private.query:_SendWowQuery())
|
||||
end)
|
||||
:AddTransition("ST_BROWSE_SEND")
|
||||
:AddTransition("ST_BROWSE_CHECKING")
|
||||
:AddTransition("ST_CANCELING")
|
||||
:AddEvent("EV_FUTURE_SUCCESS", function()
|
||||
if TSM.IsWowClassic() then
|
||||
private.browseIndex = 1
|
||||
wipe(private.browsePendingIndexes)
|
||||
else
|
||||
for _, result in ipairs(C_AuctionHouse.GetBrowseResults()) do
|
||||
local baseItemString = ItemString.GetBaseFromItemKey(result.itemKey)
|
||||
private.ProcessBrowseResult(baseItemString, result.itemKey, result.minPrice, result.totalQuantity)
|
||||
end
|
||||
end
|
||||
return "ST_BROWSE_CHECKING"
|
||||
end)
|
||||
:AddEventTransition("EV_RETRY", "ST_BROWSE_SEND")
|
||||
:AddEventTransition("EV_CANCEL", "ST_CANCELING")
|
||||
)
|
||||
:AddState(FSM.NewState("ST_BROWSE_CHECKING")
|
||||
:SetOnEnter(function()
|
||||
if not private.query:_BrowseIsPageValid() then
|
||||
-- this page isn't valid, so go to the next page
|
||||
return "ST_BROWSE_REQUEST_MORE"
|
||||
elseif not private.CheckBrowseResults() then
|
||||
-- result's aren't valid yet, so check again
|
||||
Delay.AfterFrame("AUCTION_SCANNER_RETRY", 1, private.RetryHandler)
|
||||
return
|
||||
end
|
||||
-- we're done with this set of browse results
|
||||
if private.callback then
|
||||
private.callback(private.query)
|
||||
end
|
||||
if private.browseIsNoScan or private.query:_BrowseIsDone() then
|
||||
-- we're done
|
||||
return "ST_BROWSE_DONE"
|
||||
else
|
||||
-- move on to the next page
|
||||
return "ST_BROWSE_REQUEST_MORE"
|
||||
end
|
||||
end)
|
||||
:AddTransition("ST_BROWSE_CHECKING")
|
||||
:AddTransition("ST_BROWSE_DONE")
|
||||
:AddTransition("ST_BROWSE_REQUEST_MORE")
|
||||
:AddTransition("ST_CANCELING")
|
||||
:AddEventTransition("EV_RETRY", "ST_BROWSE_CHECKING")
|
||||
:AddEventTransition("EV_BROWSE_RESULTS_UPDATED", "ST_BROWSE_CHECKING")
|
||||
:AddEventTransition("EV_CANCEL", "ST_CANCELING")
|
||||
)
|
||||
:AddState(FSM.NewState("ST_BROWSE_REQUEST_MORE")
|
||||
:SetOnEnter(function(_, isRetry)
|
||||
if private.query:_BrowseIsDone(isRetry) then
|
||||
return "ST_BROWSE_CHECKING"
|
||||
else
|
||||
private.HandleAuctionHouseWrapperResult(private.query:_BrowseRequestMore(isRetry))
|
||||
end
|
||||
end)
|
||||
:AddTransition("ST_BROWSE_REQUEST_MORE")
|
||||
:AddTransition("ST_BROWSE_CHECKING")
|
||||
:AddTransition("ST_CANCELING")
|
||||
:AddEvent("EV_FUTURE_SUCCESS", function(_, ...)
|
||||
if TSM.IsWowClassic() then
|
||||
private.browseIndex = 1
|
||||
wipe(private.browsePendingIndexes)
|
||||
else
|
||||
local newResults = ...
|
||||
for _, result in ipairs(newResults) do
|
||||
local baseItemString = ItemString.GetBaseFromItemKey(result.itemKey)
|
||||
private.ProcessBrowseResult(baseItemString, result.itemKey, result.minPrice, result.totalQuantity)
|
||||
end
|
||||
end
|
||||
return "ST_BROWSE_CHECKING"
|
||||
end)
|
||||
:AddEvent("EV_RETRY", function()
|
||||
return "ST_BROWSE_REQUEST_MORE", true
|
||||
end)
|
||||
:AddEventTransition("EV_CANCEL", "ST_CANCELING")
|
||||
)
|
||||
:AddState(FSM.NewState("ST_BROWSE_DONE")
|
||||
:SetOnEnter(function()
|
||||
private.HandleRequestDone(true)
|
||||
return "ST_INIT"
|
||||
end)
|
||||
:AddTransition("ST_INIT")
|
||||
)
|
||||
:AddState(FSM.NewState("ST_SEARCH_GET_KEY")
|
||||
:SetOnEnter(function()
|
||||
assert(not TSM.IsWowClassic())
|
||||
if not private.searchRow:SearchIsReady() then
|
||||
Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.1, private.RetryHandler)
|
||||
return
|
||||
end
|
||||
return "ST_SEARCH_SEND"
|
||||
end)
|
||||
:AddTransition("ST_SEARCH_GET_KEY")
|
||||
:AddTransition("ST_SEARCH_SEND")
|
||||
:AddTransition("ST_CANCELING")
|
||||
:AddEventTransition("EV_FUTURE_SUCCESS", "ST_SEARCH_SEND")
|
||||
:AddEventTransition("EV_RETRY", "ST_SEARCH_GET_KEY")
|
||||
:AddEventTransition("EV_CANCEL", "ST_CANCELING")
|
||||
)
|
||||
:AddState(FSM.NewState("ST_SEARCH_SEND")
|
||||
:SetOnEnter(function()
|
||||
assert(not TSM.IsWowClassic())
|
||||
if not AuctionHouseWrapper.IsOpen() then
|
||||
return "ST_CANCELING"
|
||||
end
|
||||
if private.useCachedData and private.searchRow:HasCachedSearchData() then
|
||||
return "ST_SEARCH_REQUEST_MORE"
|
||||
end
|
||||
local future, delayTime = private.searchRow:SearchSend()
|
||||
if future then
|
||||
private.HandleAuctionHouseWrapperResult(future)
|
||||
else
|
||||
if not delayTime then
|
||||
Log.Err("Failed to send search query - retrying")
|
||||
delayTime = 0.5
|
||||
end
|
||||
-- try again after a delay
|
||||
Delay.AfterTime("AUCTION_SCANNER_RETRY", delayTime, private.RetryHandler)
|
||||
end
|
||||
end)
|
||||
:AddTransition("ST_SEARCH_SEND")
|
||||
:AddTransition("ST_SEARCH_REQUEST_MORE")
|
||||
:AddTransition("ST_CANCELING")
|
||||
:AddEventTransition("EV_FUTURE_SUCCESS", "ST_SEARCH_REQUEST_MORE")
|
||||
:AddEventTransition("EV_RETRY", "ST_SEARCH_SEND")
|
||||
:AddEventTransition("EV_CANCEL", "ST_CANCELING")
|
||||
)
|
||||
:AddState(FSM.NewState("ST_SEARCH_REQUEST_MORE")
|
||||
:SetOnEnter(function()
|
||||
assert(not TSM.IsWowClassic())
|
||||
local baseItemString = private.searchRow:GetBaseItemString()
|
||||
-- get if the item is a commodity or not
|
||||
local isCommodity = ItemInfo.IsCommodity(baseItemString)
|
||||
if isCommodity == nil then
|
||||
Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.1, private.RetryHandler)
|
||||
return
|
||||
end
|
||||
|
||||
local isDone, future = private.searchRow:SearchCheckStatus()
|
||||
if isDone then
|
||||
return "ST_SEARCH_CHECKING"
|
||||
elseif future then
|
||||
private.HandleAuctionHouseWrapperResult(future)
|
||||
else
|
||||
Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.5, private.RetryHandler)
|
||||
end
|
||||
end)
|
||||
:AddTransition("ST_SEARCH_SEND")
|
||||
:AddTransition("ST_SEARCH_CHECKING")
|
||||
:AddTransition("ST_CANCELING")
|
||||
:AddEventTransition("EV_FUTURE_SUCCESS", "ST_SEARCH_CHECKING")
|
||||
:AddEventTransition("EV_RETRY", "ST_SEARCH_SEND")
|
||||
:AddEventTransition("EV_CANCEL", "ST_CANCELING")
|
||||
)
|
||||
:AddState(FSM.NewState("ST_SEARCH_CHECKING")
|
||||
:SetOnEnter(function()
|
||||
assert(not TSM.IsWowClassic())
|
||||
Delay.Cancel("AUCTION_SCANNER_RETRY")
|
||||
private.searchRow:PopulateSubRows(private.browseId)
|
||||
|
||||
-- check if all the sub rows have their data
|
||||
local isDone = true
|
||||
for _, subRow in private.searchRow:SubRowIterator(true) do
|
||||
if not subRow:HasRawData() or not subRow:HasItemString() then
|
||||
isDone = false
|
||||
elseif private.resolveSellers and not subRow:HasOwners() and not private.query:_IsFiltered(subRow, true) then
|
||||
-- waiting for owner info
|
||||
isDone = false
|
||||
end
|
||||
end
|
||||
|
||||
if not isDone and private.retryCount >= 100 then
|
||||
-- out of retries, so give up
|
||||
return "ST_SEARCH_DONE", false
|
||||
elseif not isDone then
|
||||
-- we'll try again
|
||||
private.retryCount = private.retryCount + 1
|
||||
Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.5, private.RetryHandler)
|
||||
return
|
||||
end
|
||||
|
||||
-- filter the sub rows we don't care about
|
||||
private.searchRow:FilterSubRows(private.query)
|
||||
|
||||
if private.callback then
|
||||
private.callback(private.query, private.searchRow)
|
||||
end
|
||||
if private.searchRow:SearchNext() then
|
||||
-- there is more to search
|
||||
return "ST_SEARCH_GET_KEY"
|
||||
else
|
||||
-- scanned everything
|
||||
return "ST_SEARCH_DONE", true
|
||||
end
|
||||
end)
|
||||
:AddTransition("ST_SEARCH_GET_KEY")
|
||||
:AddTransition("ST_SEARCH_CHECKING")
|
||||
:AddTransition("ST_SEARCH_DONE")
|
||||
:AddTransition("ST_CANCELING")
|
||||
:AddEventTransition("EV_RETRY", "ST_SEARCH_CHECKING")
|
||||
:AddEventTransition("EV_SEARCH_RESULTS_UPDATED", "ST_SEARCH_CHECKING")
|
||||
:AddEventTransition("EV_CANCEL", "ST_CANCELING")
|
||||
)
|
||||
:AddState(FSM.NewState("ST_SEARCH_DONE")
|
||||
:SetOnEnter(function(_, result)
|
||||
assert(not TSM.IsWowClassic())
|
||||
private.HandleRequestDone(result)
|
||||
return "ST_INIT"
|
||||
end)
|
||||
:AddTransition("ST_INIT")
|
||||
)
|
||||
:AddState(FSM.NewState("ST_CANCELING")
|
||||
:SetOnEnter(function()
|
||||
Delay.Cancel("AUCTION_SCANNER_DONE")
|
||||
return "ST_INIT"
|
||||
end)
|
||||
:AddTransition("ST_INIT")
|
||||
)
|
||||
:Init("ST_INIT", nil)
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Scanner.Browse(query, resolveSellers, results, callback)
|
||||
private.requestFuture:Start()
|
||||
private.fsm:ProcessEvent("EV_START_BROWSE", query, resolveSellers, results, callback)
|
||||
return private.requestFuture
|
||||
end
|
||||
|
||||
function Scanner.BrowseNoScan(query, itemKeys, results, callback)
|
||||
assert(not TSM.IsWowClassic())
|
||||
private.requestFuture:Start()
|
||||
private.fsm:ProcessEvent("EV_START_BROWSE_NO_SCAN", query, itemKeys, results, callback)
|
||||
return private.requestFuture
|
||||
end
|
||||
|
||||
function Scanner.Search(query, resolveSellers, useCachedData, browseRow, callback)
|
||||
assert(not TSM.IsWowClassic())
|
||||
private.requestFuture:Start()
|
||||
private.fsm:ProcessEvent("EV_START_SEARCH", query, resolveSellers, useCachedData, browseRow, callback)
|
||||
return private.requestFuture
|
||||
end
|
||||
|
||||
function Scanner.Cancel()
|
||||
private.requestFuture:Done(false)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.ProcessBrowseResult(baseItemString, ...)
|
||||
if private.browseResults[baseItemString] then
|
||||
private.browseResults[baseItemString]:Merge(...)
|
||||
else
|
||||
private.browseResults[baseItemString] = ResultRow.Get(private.query, ...)
|
||||
end
|
||||
return private.browseResults[baseItemString]
|
||||
end
|
||||
|
||||
function private.PendingFutureDoneHandler()
|
||||
local result = private.pendingFuture:GetValue()
|
||||
private.pendingFuture = nil
|
||||
if result then
|
||||
private.fsm:ProcessEvent("EV_FUTURE_SUCCESS", result)
|
||||
else
|
||||
Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.1, private.RetryHandler)
|
||||
end
|
||||
end
|
||||
|
||||
function private.RetryHandler()
|
||||
private.fsm:SetLoggingEnabled(false)
|
||||
private.fsm:ProcessEvent("EV_RETRY")
|
||||
private.fsm:SetLoggingEnabled(true)
|
||||
end
|
||||
|
||||
function private.RequestDoneHandler()
|
||||
local result = private.requestResult
|
||||
private.requestResult = nil
|
||||
private.requestFuture:Done(result)
|
||||
end
|
||||
|
||||
function private.HandleAuctionHouseWrapperResult(future)
|
||||
if future then
|
||||
private.pendingFuture = future
|
||||
private.pendingFuture:SetScript("OnDone", private.PendingFutureDoneHandler)
|
||||
else
|
||||
Delay.AfterTime("AUCTION_SCANNER_RETRY", 0.1, private.RetryHandler)
|
||||
end
|
||||
end
|
||||
|
||||
function private.HandleRequestDone(result)
|
||||
private.requestResult = result
|
||||
-- delay a bit so that we complete our current FSM transition
|
||||
Delay.AfterTime("AUCTION_SCANNER_DONE", 0, private.RequestDoneHandler)
|
||||
end
|
||||
|
||||
function private.CheckBrowseResults()
|
||||
if TSM.IsWowClassic() then
|
||||
-- process as many auctions as we can
|
||||
local numAuctions = GetNumAuctionItems("list")
|
||||
for i = #private.browsePendingIndexes, 1, -1 do
|
||||
local index = private.browsePendingIndexes[i]
|
||||
if private.ProcessBrowseResultClassic(index) then
|
||||
tremove(private.browsePendingIndexes, i)
|
||||
end
|
||||
end
|
||||
local index = private.browseIndex
|
||||
while index <= numAuctions and #private.browsePendingIndexes < 50 do
|
||||
if not private.ProcessBrowseResultClassic(index) then
|
||||
tinsert(private.browsePendingIndexes, index)
|
||||
end
|
||||
index = index + 1
|
||||
end
|
||||
private.browseIndex = index
|
||||
if private.browseIndex <= numAuctions or #private.browsePendingIndexes > 0 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- check if there's data still pending
|
||||
local hasPendingData = false
|
||||
for _, row in pairs(private.browseResults) do
|
||||
if not row:PopulateBrowseData() then
|
||||
hasPendingData = true
|
||||
-- keep going so we issue requests for all pending rows
|
||||
end
|
||||
end
|
||||
if hasPendingData then
|
||||
return false
|
||||
end
|
||||
|
||||
-- filter the results
|
||||
local numRemoved = 0
|
||||
for baseItemString, row in pairs(private.browseResults) do
|
||||
-- filter the itemKeys we don't care about and rows which don't match the query
|
||||
if row:IsFiltered(private.query) then
|
||||
private.browseResults[baseItemString] = nil
|
||||
numRemoved = numRemoved + 1
|
||||
end
|
||||
if TSM.IsWowClassic() then
|
||||
if row:FilterSubRows(private.query) then
|
||||
-- no more subRows, so filter the entire row
|
||||
private.browseResults[baseItemString] = nil
|
||||
numRemoved = numRemoved + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
if numRemoved > 0 then
|
||||
Log.Info("Removed %d results", numRemoved)
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function private.ProcessBrowseResultClassic(index)
|
||||
local rawName, _, stackSize, _, _, _, _, _, _, buyout, _, _, _, seller, sellerFull = GetAuctionItemInfo("list", index)
|
||||
local itemLink = GetAuctionItemLink("list", index)
|
||||
local baseItemString = ItemString.GetBase(itemLink)
|
||||
local timeLeft = GetAuctionItemTimeLeft("list", index)
|
||||
seller = Util.FixSellerName(seller, sellerFull)
|
||||
if not rawName or rawName == "" or not baseItemString or not buyout or not stackSize or not timeLeft or (not seller and private.resolveSellers) then
|
||||
return false
|
||||
end
|
||||
local row = private.ProcessBrowseResult(baseItemString, itemLink)
|
||||
-- amazingly, GetAuctionItemLink could return nil the next time it's called (within the same frame), so pass through our itemLink
|
||||
row:PopulateSubRows(private.browseId, index, itemLink)
|
||||
return true
|
||||
end
|
||||
38
LibTSM/Service/AuctionScanClasses/Util.lua
Normal file
38
LibTSM/Service/AuctionScanClasses/Util.lua
Normal file
@@ -0,0 +1,38 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Util = TSM.Init("Service.AuctionScanClasses.Util")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Util.HasItemInfo(itemString)
|
||||
local itemName = ItemInfo.GetName(itemString)
|
||||
local itemLevel = ItemInfo.GetItemLevel(itemString)
|
||||
local quality = ItemInfo.GetQuality(itemString)
|
||||
local minLevel = ItemInfo.GetMinLevel(itemString)
|
||||
local hasIsCommodity = TSM.IsWowClassic() or ItemInfo.IsCommodity(itemString) ~= nil
|
||||
local hasCanHaveVariations = ItemInfo.CanHaveVariations(itemString) ~= nil
|
||||
local result = itemName and itemLevel and quality and minLevel and hasIsCommodity and hasCanHaveVariations
|
||||
if not result then
|
||||
ItemInfo.FetchInfo(itemString)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function Util.FixSellerName(seller, sellerFull)
|
||||
local realm = GetRealmName()
|
||||
if sellerFull and strjoin("-", seller, realm) ~= sellerFull then
|
||||
return sellerFull
|
||||
else
|
||||
return seller
|
||||
end
|
||||
end
|
||||
626
LibTSM/Service/AuctionTracking.lua
Normal file
626
LibTSM/Service/AuctionTracking.lua
Normal file
@@ -0,0 +1,626 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local AuctionTracking = TSM.Init("Service.AuctionTracking")
|
||||
local L = TSM.Include("Locale").GetTable()
|
||||
local Database = TSM.Include("Util.Database")
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Sound = TSM.Include("Util.Sound")
|
||||
local Money = TSM.Include("Util.Money")
|
||||
local Analytics = TSM.Include("Util.Analytics")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local AuctionHouseWrapper = TSM.Include("Service.AuctionHouseWrapper")
|
||||
local private = {
|
||||
settings = nil,
|
||||
indexDB = nil,
|
||||
quantityDB = nil,
|
||||
updateQuery = nil, -- luacheck: ignore 1004 - just stored for GC reasons
|
||||
isAHOpen = false,
|
||||
callbacks = {},
|
||||
expiresCallbacks = {},
|
||||
indexUpdates = {
|
||||
list = {},
|
||||
pending = {},
|
||||
},
|
||||
cancelAuctionId = nil,
|
||||
lastScanNum = nil,
|
||||
ignoreUpdateEvent = nil,
|
||||
lastPurchase = {},
|
||||
prevLineId = nil,
|
||||
prevLineResult = nil,
|
||||
origChatFrameOnEvent = nil,
|
||||
pendingFuture = nil,
|
||||
auctionIdToLink = {},
|
||||
auctionIdToItemBuyout = {},
|
||||
prevLogTime = 0,
|
||||
prevLogNum = math.huge,
|
||||
}
|
||||
local PLAYER_NAME = UnitName("player")
|
||||
local SALE_HINT_SEP = "\001"
|
||||
local SALE_HINT_EXPIRE_TIME = 33 * 24 * 60 * 60
|
||||
local SORT_ORDER = not TSM.IsWowClassic() and {
|
||||
{ sortOrder = Enum.AuctionHouseSortOrder.Name, reverseSort = false },
|
||||
{ sortOrder = Enum.AuctionHouseSortOrder.Price, reverseSort = false },
|
||||
}
|
||||
local AUCTIONABLE_WOW_TOKEN_ITEM_ID = 122270
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
AuctionTracking:OnSettingsLoad(function()
|
||||
private.settings = Settings.NewView()
|
||||
:AddKey("char", "internalData", "auctionSaleHints")
|
||||
:AddKey("char", "internalData", "auctionPrices")
|
||||
:AddKey("char", "internalData", "auctionMessages")
|
||||
:AddKey("factionrealm", "internalData", "expiringAuction")
|
||||
:AddKey("sync", "internalData", "auctionQuantity")
|
||||
:AddKey("global", "coreOptions", "auctionSaleSound")
|
||||
Event.Register("AUCTION_HOUSE_SHOW", private.AuctionHouseShowHandler)
|
||||
Event.Register("AUCTION_HOUSE_CLOSED", private.AuctionHouseClosedHandler)
|
||||
if TSM.IsWowClassic() then
|
||||
Event.Register("AUCTION_OWNED_LIST_UPDATE", private.AuctionOwnedListUpdateHandler)
|
||||
else
|
||||
Event.Register("OWNED_AUCTIONS_UPDATED", private.AuctionOwnedListUpdateHandler)
|
||||
Event.Register("AUCTION_CANCELED", private.AuctionCanceledHandler)
|
||||
end
|
||||
private.indexDB = Database.NewSchema("AUCTION_TRACKING_INDEXES")
|
||||
:AddUniqueNumberField("index")
|
||||
:AddStringField("itemString")
|
||||
:AddSmartMapField("baseItemString", ItemString.GetBaseMap(), "itemString")
|
||||
:AddStringField("itemLink")
|
||||
:AddNumberField("itemTexture")
|
||||
:AddStringField("itemName")
|
||||
:AddNumberField("itemQuality")
|
||||
:AddNumberField("duration")
|
||||
:AddStringField("highBidder")
|
||||
:AddNumberField("currentBid")
|
||||
:AddNumberField("buyout")
|
||||
:AddNumberField("stackSize")
|
||||
:AddNumberField("saleStatus")
|
||||
:AddNumberField("auctionId")
|
||||
:AddIndex("index")
|
||||
:AddIndex("saleStatus")
|
||||
:AddIndex("auctionId")
|
||||
:Commit()
|
||||
private.quantityDB = Database.NewSchema("AUCTION_TRACKING_QUANTITY")
|
||||
:AddUniqueStringField("itemString")
|
||||
:AddNumberField("quantity")
|
||||
:Commit()
|
||||
private.updateQuery = private.indexDB:NewQuery()
|
||||
:SetUpdateCallback(private.OnCallbackQueryUpdated)
|
||||
|
||||
private.RebuildQuantityDB()
|
||||
for info, timestamp in pairs(private.settings.auctionSaleHints) do
|
||||
if time() > timestamp + SALE_HINT_EXPIRE_TIME then
|
||||
private.settings.auctionSaleHints[info] = nil
|
||||
end
|
||||
end
|
||||
|
||||
if TSM.IsWowClassic() then
|
||||
hooksecurefunc("PostAuction", function(_, _, duration)
|
||||
private.PostAuctionHookHandler(duration)
|
||||
end)
|
||||
else
|
||||
hooksecurefunc(C_AuctionHouse, "PostCommodity", function(_, duration)
|
||||
private.PostAuctionHookHandler(duration)
|
||||
end)
|
||||
hooksecurefunc(C_AuctionHouse, "PostItem", function(_, duration)
|
||||
private.PostAuctionHookHandler(duration)
|
||||
end)
|
||||
hooksecurefunc(C_AuctionHouse, "CancelAuction", function(auctionId)
|
||||
private.cancelAuctionId = auctionId
|
||||
end)
|
||||
end
|
||||
|
||||
-- setup enhanced sale / buy messages
|
||||
ChatFrame_AddMessageEventFilter("CHAT_MSG_SYSTEM", private.FilterSystemMsg)
|
||||
if TSM.IsWowClassic() then
|
||||
hooksecurefunc("PlaceAuctionBid", function(_, index, amountPaid)
|
||||
local link = GetAuctionItemLink("list", index)
|
||||
local name, _, stackSize, _, _, _, _, _, _, buyout = GetAuctionItemInfo("list", index)
|
||||
if amountPaid == buyout then
|
||||
wipe(private.lastPurchase)
|
||||
private.lastPurchase.name = name
|
||||
private.lastPurchase.link = link
|
||||
private.lastPurchase.stackSize = stackSize
|
||||
private.lastPurchase.buyout = buyout
|
||||
end
|
||||
end)
|
||||
else
|
||||
Event.Register("ITEM_SEARCH_RESULTS_UPDATED", function(_, itemKey)
|
||||
wipe(private.auctionIdToLink)
|
||||
wipe(private.auctionIdToItemBuyout)
|
||||
for i = 1, C_AuctionHouse.GetNumItemSearchResults(itemKey) do
|
||||
local info = C_AuctionHouse.GetItemSearchResultInfo(itemKey, i)
|
||||
if info.buyoutAmount then
|
||||
private.auctionIdToLink[info.auctionID] = info.itemLink
|
||||
private.auctionIdToItemBuyout[info.auctionID] = info.buyoutAmount
|
||||
end
|
||||
end
|
||||
end)
|
||||
hooksecurefunc(C_AuctionHouse, "PlaceBid", function(auctionId, bidPlaced)
|
||||
local link = private.auctionIdToLink[auctionId]
|
||||
local buyout = private.auctionIdToItemBuyout[auctionId]
|
||||
if not link or buyout ~= bidPlaced then
|
||||
return
|
||||
end
|
||||
wipe(private.lastPurchase)
|
||||
private.lastPurchase.name = ItemInfo.GetName(link)
|
||||
private.lastPurchase.link = link
|
||||
private.lastPurchase.stackSize = 1
|
||||
private.lastPurchase.buyout = bidPlaced
|
||||
end)
|
||||
hooksecurefunc(C_AuctionHouse, "ConfirmCommoditiesPurchase", function(itemId, quantity)
|
||||
local link = ItemInfo.GetLink("i:"..itemId)
|
||||
if not link then
|
||||
return
|
||||
end
|
||||
local origQuantity = quantity
|
||||
local buyout = 0
|
||||
for i = 1, C_AuctionHouse.GetNumCommoditySearchResults(itemId) do
|
||||
local info = C_AuctionHouse.GetCommoditySearchResultInfo(itemId, i)
|
||||
local resultQuantity = min(quantity, info.quantity - info.numOwnerItems)
|
||||
buyout = buyout + resultQuantity * info.unitPrice
|
||||
quantity = quantity - resultQuantity
|
||||
if quantity == 0 then
|
||||
break
|
||||
end
|
||||
end
|
||||
if quantity > 0 then
|
||||
return
|
||||
end
|
||||
private.lastPurchase.name = ItemInfo.GetName(link)
|
||||
private.lastPurchase.link = link
|
||||
private.lastPurchase.stackSize = origQuantity
|
||||
private.lastPurchase.buyout = buyout
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|
||||
AuctionTracking:OnGameDataLoad(function()
|
||||
-- setup auction created / cancelled filtering
|
||||
-- NOTE: this is delayed until the game is loaded to avoid taint issues
|
||||
local ElvUIChat, ElvUIChatIsEnabled = nil, nil
|
||||
if IsAddOnLoaded("ElvUI") and ElvUI then
|
||||
ElvUIChat = ElvUI[1]:GetModule("Chat")
|
||||
if ElvUI[3].chat.enable then
|
||||
ElvUIChatIsEnabled = true
|
||||
end
|
||||
end
|
||||
if ElvUIChatIsEnabled then
|
||||
private.origChatFrameOnEvent = ElvUIChat.ChatFrame_OnEvent
|
||||
ElvUIChat.ChatFrame_OnEvent = private.ChatFrameOnEvent
|
||||
else
|
||||
private.origChatFrameOnEvent = ChatFrame_OnEvent
|
||||
ChatFrame_OnEvent = private.ChatFrameOnEvent
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function AuctionTracking.RegisterCallback(callback)
|
||||
tinsert(private.callbacks, callback)
|
||||
end
|
||||
|
||||
function AuctionTracking.RegisterExpiresCallback(callback)
|
||||
tinsert(private.expiresCallbacks, callback)
|
||||
end
|
||||
|
||||
function AuctionTracking.DatabaseFieldIterator()
|
||||
return private.indexDB:FieldIterator()
|
||||
end
|
||||
|
||||
function AuctionTracking.BaseItemIterator()
|
||||
return private.quantityDB:NewQuery()
|
||||
:Select("itemString")
|
||||
:IteratorAndRelease()
|
||||
end
|
||||
|
||||
function AuctionTracking.CreateQuery()
|
||||
return private.indexDB:NewQuery()
|
||||
end
|
||||
|
||||
function AuctionTracking.CreateQueryUnsold()
|
||||
return AuctionTracking.CreateQuery()
|
||||
:Equal("saleStatus", 0)
|
||||
end
|
||||
|
||||
function AuctionTracking.CreateQueryUnsoldItem(itemString)
|
||||
return AuctionTracking.CreateQueryUnsold()
|
||||
:Equal(itemString == ItemString.GetBaseFast(itemString) and "baseItemString" or "itemString", itemString)
|
||||
end
|
||||
|
||||
function AuctionTracking.GetSaleHintItemString(name, stackSize, buyout)
|
||||
for info in pairs(private.settings.auctionSaleHints) do
|
||||
local infoName, itemString, infoStackSize, infoBuyout = strsplit(SALE_HINT_SEP, info)
|
||||
if infoName == name and tonumber(infoStackSize) == stackSize and tonumber(infoBuyout) == buyout then
|
||||
return itemString
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function AuctionTracking.GetQuantityByBaseItemString(baseItemString)
|
||||
return private.quantityDB:GetUniqueRowField("itemString", baseItemString, "quantity") or 0
|
||||
end
|
||||
|
||||
function AuctionTracking.QueryOwnedAuctions()
|
||||
if not private.isAHOpen then
|
||||
return
|
||||
end
|
||||
if TSM.IsWowClassic() then
|
||||
GetOwnerAuctionItems()
|
||||
else
|
||||
if private.pendingFuture then
|
||||
return
|
||||
end
|
||||
private.pendingFuture = AuctionHouseWrapper.QueryOwnedAuctions(SORT_ORDER)
|
||||
if not private.pendingFuture then
|
||||
Delay.AfterTime(0.5, AuctionTracking.QueryOwnedAuctions)
|
||||
return
|
||||
end
|
||||
private.pendingFuture:SetScript("OnDone", private.PendingFutureOnDone)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Event Handlers
|
||||
-- ============================================================================
|
||||
|
||||
function private.AuctionHouseShowHandler()
|
||||
private.isAHOpen = true
|
||||
if TSM.IsWowClassic() then
|
||||
AuctionTracking.QueryOwnedAuctions()
|
||||
-- We don't always get AUCTION_OWNED_LIST_UPDATE events, so do our own scanning if needed
|
||||
Delay.AfterTime("AUCTION_BACKGROUND_SCAN", 1, private.DoBackgroundScan, 1)
|
||||
else
|
||||
Delay.AfterTime(0.1, AuctionTracking.QueryOwnedAuctions)
|
||||
end
|
||||
end
|
||||
|
||||
function private.AuctionHouseClosedHandler()
|
||||
private.isAHOpen = false
|
||||
Delay.Cancel("AUCTION_BACKGROUND_SCAN")
|
||||
end
|
||||
|
||||
function private.DoBackgroundScan()
|
||||
if private.GetNumOwnedAuctions() ~= private.lastScanNum then
|
||||
private.AuctionOwnedListUpdateHandler()
|
||||
end
|
||||
end
|
||||
|
||||
function private.AuctionOwnedListUpdateHandler()
|
||||
if private.ignoreUpdateEvent then
|
||||
return
|
||||
end
|
||||
wipe(private.indexUpdates.pending)
|
||||
wipe(private.indexUpdates.list)
|
||||
local numOwned = private.GetNumOwnedAuctions()
|
||||
for i = 1, numOwned do
|
||||
if not private.indexUpdates.pending[i] then
|
||||
private.indexUpdates.pending[i] = true
|
||||
tinsert(private.indexUpdates.list, i)
|
||||
end
|
||||
end
|
||||
if numOwned == 0 and private.settings.expiringAuction[PLAYER_NAME] then
|
||||
private.settings.expiringAuction[PLAYER_NAME] = nil
|
||||
for _, callback in ipairs(private.expiresCallbacks) do
|
||||
callback()
|
||||
end
|
||||
end
|
||||
Delay.AfterFrame("AUCTION_OWNED_LIST_SCAN", 2, private.AuctionOwnedListUpdateDelayed)
|
||||
end
|
||||
|
||||
function private.AuctionCanceledHandler(_, auctionId)
|
||||
if not private.cancelAuctionId or auctionId ~= 0 then
|
||||
-- an auction was bought, so rescan the owned auctions
|
||||
AuctionTracking.QueryOwnedAuctions()
|
||||
return
|
||||
end
|
||||
local row = private.indexDB:NewQuery()
|
||||
:Equal("auctionId", private.cancelAuctionId)
|
||||
:GetFirstResultAndRelease()
|
||||
private.cancelAuctionId = nil
|
||||
if not row then
|
||||
return
|
||||
end
|
||||
|
||||
local baseItemString = row:GetField("baseItemString")
|
||||
local stackSize = row:GetField("stackSize")
|
||||
assert(stackSize <= private.settings.auctionQuantity[baseItemString])
|
||||
private.settings.auctionQuantity[baseItemString] = private.settings.auctionQuantity[baseItemString] - stackSize
|
||||
private.RebuildQuantityDB()
|
||||
private.indexDB:DeleteRow(row)
|
||||
row:Release()
|
||||
end
|
||||
|
||||
function private.AuctionOwnedListUpdateDelayed()
|
||||
if not private.isAHOpen then
|
||||
return
|
||||
elseif AuctionFrame and AuctionFrame:IsVisible() and AuctionFrame.selectedTab == 3 then
|
||||
-- default UI auctions tab is visible, so scan later
|
||||
Delay.AfterFrame("AUCTION_OWNED_LIST_SCAN", 2, private.AuctionOwnedListUpdateDelayed)
|
||||
return
|
||||
elseif not TSM.IsWowClassic() and not C_AuctionHouse.HasFullOwnedAuctionResults() then
|
||||
-- don't have all the results yet, so try again in a moment
|
||||
Delay.AfterFrame("AUCTION_OWNED_LIST_SCAN", 0.1, private.AuctionOwnedListUpdateDelayed)
|
||||
return
|
||||
end
|
||||
if TSM.IsWowClassic() then
|
||||
-- check if we need to change the sort
|
||||
local needsSort = false
|
||||
local numColumns = #AuctionSort.owner_duration
|
||||
for i, info in ipairs(AuctionSort.owner_duration) do
|
||||
local col, reversed = GetAuctionSort("owner", numColumns - i + 1)
|
||||
-- we want to do the opposite order
|
||||
reversed = not reversed
|
||||
if col ~= info.column or info.reverse ~= reversed then
|
||||
needsSort = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if needsSort then
|
||||
Log.Info("Sorting owner auctions")
|
||||
-- ignore events while changing the sort
|
||||
private.ignoreUpdateEvent = true
|
||||
AuctionFrame_SetSort("owner", "duration", true)
|
||||
SortAuctionApplySort("owner")
|
||||
private.ignoreUpdateEvent = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- scan the auctions
|
||||
local shouldLog = GetTime() - private.prevLogTime > 5
|
||||
if shouldLog then
|
||||
private.prevLogTime = GetTime()
|
||||
end
|
||||
wipe(private.settings.auctionQuantity)
|
||||
private.indexDB:TruncateAndBulkInsertStart()
|
||||
local expire = math.huge
|
||||
for i = #private.indexUpdates.list, 1, -1 do
|
||||
local index = private.indexUpdates.list[i]
|
||||
local auctionId, link, name, texture, stackSize, quality, minBid, buyout, bid, highBidder, saleStatus, duration, shouldIgnore = private.GetOwnedAuctionInfo(index)
|
||||
if shouldIgnore then
|
||||
private.indexUpdates.pending[index] = nil
|
||||
tremove(private.indexUpdates.list, i)
|
||||
else
|
||||
name = name or ItemInfo.GetName(link)
|
||||
texture = texture or ItemInfo.GetTexture(link)
|
||||
quality = quality or ItemInfo.GetQuality(link)
|
||||
if link and name and texture and quality then
|
||||
assert(saleStatus == 0 or saleStatus == 1)
|
||||
highBidder = highBidder or ""
|
||||
local itemString = ItemString.Get(link)
|
||||
local currentBid = highBidder ~= "" and bid or minBid
|
||||
if not currentBid and saleStatus == 1 and not TSM.IsWowClassic() then
|
||||
-- sometimes wow doesn't tell us the current bid on sold auctions on retail
|
||||
currentBid = 0
|
||||
end
|
||||
if saleStatus == 0 then
|
||||
if TSM.IsWowClassic() then
|
||||
if duration == 1 then -- 30 min
|
||||
expire = min(expire, time() + 0.5 * 60 * 60)
|
||||
elseif duration == 2 then -- 2 hours
|
||||
expire = min(expire, time() + 2 * 60 * 60)
|
||||
elseif duration == 3 then -- 12 hours
|
||||
expire = min(expire, time() + 12 * 60 * 60)
|
||||
end
|
||||
else
|
||||
duration = time() + duration
|
||||
expire = min(expire, duration)
|
||||
end
|
||||
local baseItemString = ItemString.GetBaseFast(itemString)
|
||||
private.settings.auctionQuantity[baseItemString] = (private.settings.auctionQuantity[baseItemString] or 0) + stackSize
|
||||
local hintInfo = strjoin(SALE_HINT_SEP, ItemInfo.GetName(link), itemString, stackSize, buyout)
|
||||
private.settings.auctionSaleHints[hintInfo] = time()
|
||||
else
|
||||
duration = time() + duration
|
||||
end
|
||||
private.indexUpdates.pending[index] = nil
|
||||
tremove(private.indexUpdates.list, i)
|
||||
private.indexDB:BulkInsertNewRow(index, itemString, link, texture, name, quality, duration, highBidder, currentBid, buyout, stackSize, saleStatus, auctionId)
|
||||
elseif shouldLog then
|
||||
Log.Warn("Missing info (%s, %s, %s, %s)", gsub(tostring(link), "\124", "\\124"), tostring(name), tostring(texture), tostring(quality))
|
||||
if link and strmatch(link, "item:") and not TSM.IsWowClassic() then
|
||||
Analytics.Action("AUCTION_TRACKING_MISSING_INFO", link)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
private.RebuildQuantityDB()
|
||||
private.indexDB:BulkInsertEnd()
|
||||
|
||||
if expire ~= math.huge and (private.settings.expiringAuction[PLAYER_NAME] or math.huge) > expire then
|
||||
private.settings.expiringAuction[PLAYER_NAME] = expire
|
||||
for _, callback in ipairs(private.expiresCallbacks) do
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
if shouldLog or #private.indexUpdates.list ~= private.prevLogNum then
|
||||
Log.Info("Scanned auctions (left=%d)", #private.indexUpdates.list)
|
||||
private.prevLogNum = #private.indexUpdates.list
|
||||
end
|
||||
if #private.indexUpdates.list > 0 then
|
||||
-- some failed to scan so try again
|
||||
Delay.AfterFrame("AUCTION_OWNED_LIST_SCAN", 2, private.AuctionOwnedListUpdateDelayed)
|
||||
else
|
||||
private.lastScanNum = private.GetNumOwnedAuctions()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.RebuildQuantityDB()
|
||||
private.quantityDB:TruncateAndBulkInsertStart()
|
||||
for itemString, quantity in pairs(private.settings.auctionQuantity) do
|
||||
if quantity > 0 then
|
||||
private.quantityDB:BulkInsertNewRow(itemString, quantity)
|
||||
else
|
||||
private.settings.auctionQuantity[itemString] = nil
|
||||
end
|
||||
end
|
||||
private.quantityDB:BulkInsertEnd()
|
||||
end
|
||||
|
||||
function private.GetNumOwnedAuctions()
|
||||
if TSM.IsWowClassic() then
|
||||
return GetNumAuctionItems("owner")
|
||||
else
|
||||
return C_AuctionHouse.GetNumOwnedAuctions()
|
||||
end
|
||||
end
|
||||
|
||||
function private.GetOwnedAuctionInfo(index)
|
||||
if TSM.IsWowClassic() then
|
||||
local name, texture, stackSize, quality, _, _, _, minBid, _, buyout, bid, highBidder, _, _, _, saleStatus = GetAuctionItemInfo("owner", index)
|
||||
local link = name and name ~= "" and GetAuctionItemLink("owner", index)
|
||||
if not link then
|
||||
return
|
||||
end
|
||||
local duration = GetAuctionItemTimeLeft("owner", index)
|
||||
return index, link, name, texture, stackSize, quality, minBid, buyout, bid, highBidder, saleStatus, duration
|
||||
else
|
||||
local info = C_AuctionHouse.GetOwnedAuctionInfo(index)
|
||||
if info.itemKey.itemID == AUCTIONABLE_WOW_TOKEN_ITEM_ID then
|
||||
-- this is a token, so just ignore it
|
||||
return nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, true
|
||||
end
|
||||
local link = info and info.itemLink
|
||||
if not link then
|
||||
return
|
||||
end
|
||||
local bid = info.bidAmount or info.buyoutAmount
|
||||
local minBid = bid
|
||||
return info.auctionID, link, nil, nil, info.quantity, nil, minBid, info.buyoutAmount or 0, bid, info.bidder or "", info.status, info.timeLeftSeconds
|
||||
end
|
||||
end
|
||||
|
||||
function private.OnCallbackQueryUpdated()
|
||||
for _, callback in ipairs(private.callbacks) do
|
||||
callback()
|
||||
end
|
||||
-- updating the auction prices / messages is very low-priority, so throttle it to at most every 0.5 seconds
|
||||
Delay.AfterTime("UPDATE_AUCTION_PRICES_MESSAGES_THROTTLE", 0.5, private.UpdateAuctionPricesMessages)
|
||||
end
|
||||
|
||||
function private.PostAuctionHookHandler(duration)
|
||||
local days = nil
|
||||
if duration == 1 then
|
||||
days = 0.5
|
||||
elseif duration == 2 then
|
||||
days = 1
|
||||
elseif duration == 3 then
|
||||
days = 2
|
||||
end
|
||||
|
||||
local expiration = time() + (days * 24 * 60 * 60)
|
||||
if (private.settings.expiringAuction[PLAYER_NAME] or math.huge) < expiration then
|
||||
return
|
||||
end
|
||||
private.settings.expiringAuction[PLAYER_NAME] = expiration
|
||||
for _, callback in ipairs(private.expiresCallbacks) do
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
function private.UpdateAuctionPricesMessages()
|
||||
local INVALID_STACK_SIZE = -1
|
||||
-- recycle tables from private.settings.auctionPrices if we can so we're not creating a ton of garbage
|
||||
local freeTables = TempTable.Acquire()
|
||||
for _, tbl in pairs(private.settings.auctionPrices) do
|
||||
wipe(tbl)
|
||||
tinsert(freeTables, tbl)
|
||||
end
|
||||
wipe(private.settings.auctionPrices)
|
||||
wipe(private.settings.auctionMessages)
|
||||
local auctionPrices = TempTable.Acquire()
|
||||
local auctionStackSizes = TempTable.Acquire()
|
||||
local query = AuctionTracking.CreateQueryUnsold()
|
||||
:Select("itemLink", "stackSize", "buyout")
|
||||
:GreaterThan("buyout", 0)
|
||||
:OrderBy("index", true)
|
||||
for _, link, stackSize, buyout in query:IteratorAndRelease() do
|
||||
auctionPrices[link] = auctionPrices[link] or tremove(freeTables) or {}
|
||||
if stackSize ~= auctionStackSizes[link] then
|
||||
auctionStackSizes[link] = stackSize
|
||||
end
|
||||
tinsert(auctionPrices[link], buyout)
|
||||
end
|
||||
for link, prices in pairs(auctionPrices) do
|
||||
local name = ItemInfo.GetName(link)
|
||||
if auctionStackSizes[link] ~= INVALID_STACK_SIZE then
|
||||
sort(prices)
|
||||
private.settings.auctionPrices[link] = prices
|
||||
private.settings.auctionMessages[format(ERR_AUCTION_SOLD_S, name)] = link
|
||||
end
|
||||
end
|
||||
TempTable.Release(freeTables)
|
||||
TempTable.Release(auctionPrices)
|
||||
TempTable.Release(auctionStackSizes)
|
||||
end
|
||||
|
||||
function private.ChatFrameOnEvent(self, event, msg, ...)
|
||||
-- surpress auction created / cancelled spam
|
||||
if event == "CHAT_MSG_SYSTEM" and (msg == ERR_AUCTION_STARTED or msg == ERR_AUCTION_REMOVED) then
|
||||
return
|
||||
end
|
||||
return private.origChatFrameOnEvent(self, event, msg, ...)
|
||||
end
|
||||
|
||||
function private.FilterSystemMsg(_, _, msg, ...)
|
||||
local lineID = select(10, ...)
|
||||
if lineID ~= private.prevLineId then
|
||||
private.prevLineId = lineID
|
||||
private.prevLineResult = nil
|
||||
local link = private.settings.auctionMessages and private.settings.auctionMessages[msg]
|
||||
if private.lastPurchase.name and msg == format(ERR_AUCTION_WON_S, private.lastPurchase.name) then
|
||||
-- we just bought an auction
|
||||
private.prevLineResult = format(L["You won an auction for %sx%d for %s"], private.lastPurchase.link, private.lastPurchase.stackSize, Money.ToString(private.lastPurchase.buyout, "|cffffffff"))
|
||||
return nil, private.prevLineResult, ...
|
||||
elseif link then
|
||||
-- we may have just sold an auction
|
||||
local price = tremove(private.settings.auctionPrices[link], 1)
|
||||
local numAuctions = #private.settings.auctionPrices[link]
|
||||
if not price then
|
||||
-- couldn't determine the price, so just replace the link
|
||||
private.prevLineResult = format(ERR_AUCTION_SOLD_S, link)
|
||||
Sound.PlaySound(private.settings.auctionSaleSound)
|
||||
return nil, private.prevLineResult, ...
|
||||
end
|
||||
if numAuctions == 0 then -- this was the last auction
|
||||
private.settings.auctionMessages[msg] = nil
|
||||
end
|
||||
private.prevLineResult = format(L["Your auction of %s has sold for %s!"], link, Money.ToString(price, "|cffffffff"))
|
||||
Sound.PlaySound(private.settings.auctionSaleSound)
|
||||
return nil, private.prevLineResult, ...
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function private.PendingFutureOnDone()
|
||||
-- we also hook the event, so don't care what the result is
|
||||
private.pendingFuture:GetValue()
|
||||
private.pendingFuture = nil
|
||||
end
|
||||
615
LibTSM/Service/BagTracking.lua
Normal file
615
LibTSM/Service/BagTracking.lua
Normal file
@@ -0,0 +1,615 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local BagTracking = TSM.Init("Service.BagTracking")
|
||||
local Database = TSM.Include("Util.Database")
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local SlotId = TSM.Include("Util.SlotId")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local InventoryInfo = TSM.Include("Service.InventoryInfo")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local private = {
|
||||
slotDB = nil,
|
||||
quantityDB = nil,
|
||||
settings = nil,
|
||||
bagUpdates = {
|
||||
pending = {},
|
||||
bagList = {},
|
||||
bankList = {},
|
||||
},
|
||||
bankSlotUpdates = {
|
||||
pending = {},
|
||||
list = {},
|
||||
},
|
||||
reagentBankSlotUpdates = {
|
||||
pending = {},
|
||||
list = {},
|
||||
},
|
||||
bankOpen = false,
|
||||
isFirstBankOpen = true,
|
||||
callbackQuery = nil, -- luacheck: ignore 1004 - just stored for GC reasons
|
||||
callbacks = {},
|
||||
}
|
||||
local BANK_BAG_SLOTS = {}
|
||||
local BANK_NON_REAGENT_BAG_SLOTS = {}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Population of the Static Data
|
||||
-- ============================================================================
|
||||
|
||||
do
|
||||
BANK_BAG_SLOTS[BANK_CONTAINER] = true
|
||||
BANK_NON_REAGENT_BAG_SLOTS[BANK_CONTAINER] = true
|
||||
for i = NUM_BAG_SLOTS + 1, NUM_BAG_SLOTS + NUM_BANKBAGSLOTS do
|
||||
BANK_BAG_SLOTS[i] = true
|
||||
BANK_NON_REAGENT_BAG_SLOTS[i] = true
|
||||
end
|
||||
if not TSM.IsWowClassic() then
|
||||
BANK_BAG_SLOTS[REAGENTBANK_CONTAINER] = true
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
BagTracking:OnSettingsLoad(function()
|
||||
Event.Register("BAG_UPDATE", private.BagUpdateHandler)
|
||||
Event.Register("BAG_UPDATE_DELAYED", private.BagUpdateDelayedHandler)
|
||||
Event.Register("BANKFRAME_OPENED", private.BankOpenedHandler)
|
||||
Event.Register("BANKFRAME_CLOSED", private.BankClosedHandler)
|
||||
Event.Register("PLAYERBANKSLOTS_CHANGED", private.BankSlotChangedHandler)
|
||||
if not TSM.IsWowClassic() then
|
||||
Event.Register("PLAYERREAGENTBANKSLOTS_CHANGED", private.ReagentBankSlotChangedHandler)
|
||||
end
|
||||
private.slotDB = Database.NewSchema("BAG_TRACKING_SLOTS")
|
||||
:AddUniqueNumberField("slotId")
|
||||
:AddNumberField("bag")
|
||||
:AddNumberField("slot")
|
||||
:AddStringField("itemLink")
|
||||
:AddStringField("itemString")
|
||||
:AddSmartMapField("baseItemString", ItemString.GetBaseMap(), "itemString")
|
||||
:AddNumberField("itemTexture")
|
||||
:AddNumberField("quantity")
|
||||
:AddBooleanField("isBoP")
|
||||
:AddBooleanField("isBoA")
|
||||
:AddIndex("slotId")
|
||||
:AddIndex("bag")
|
||||
:AddIndex("itemString")
|
||||
:AddIndex("baseItemString")
|
||||
:Commit()
|
||||
private.quantityDB = Database.NewSchema("BAG_TRACKING_QUANTITY")
|
||||
:AddUniqueStringField("itemString")
|
||||
:AddNumberField("bagQuantity")
|
||||
:AddNumberField("bankQuantity")
|
||||
:AddNumberField("reagentBankQuantity")
|
||||
:Commit()
|
||||
private.callbackQuery = private.slotDB:NewQuery()
|
||||
:SetUpdateCallback(private.OnCallbackQueryUpdated)
|
||||
private.settings = Settings.NewView()
|
||||
:AddKey("sync", "internalData", "bagQuantity")
|
||||
:AddKey("sync", "internalData", "bankQuantity")
|
||||
:AddKey("sync", "internalData", "reagentBankQuantity")
|
||||
|
||||
local items = TempTable.Acquire()
|
||||
local bagQuantity = TempTable.Acquire()
|
||||
local bankQuantity = TempTable.Acquire()
|
||||
local reagentBankQuantity = TempTable.Acquire()
|
||||
for itemString, quantity in pairs(private.settings.bagQuantity) do
|
||||
if itemString == ItemString.GetBase(itemString) then
|
||||
items[itemString] = true
|
||||
bagQuantity[itemString] = quantity
|
||||
else
|
||||
private.settings.bagQuantity[itemString] = nil
|
||||
end
|
||||
end
|
||||
for itemString, quantity in pairs(private.settings.bankQuantity) do
|
||||
if itemString == ItemString.GetBase(itemString) then
|
||||
items[itemString] = true
|
||||
bankQuantity[itemString] = quantity
|
||||
else
|
||||
private.settings.bankQuantity[itemString] = nil
|
||||
end
|
||||
end
|
||||
for itemString, quantity in pairs(private.settings.reagentBankQuantity) do
|
||||
if itemString == ItemString.GetBase(itemString) then
|
||||
items[itemString] = true
|
||||
reagentBankQuantity[itemString] = quantity
|
||||
else
|
||||
private.settings.reagentBankQuantity[itemString] = nil
|
||||
end
|
||||
end
|
||||
private.quantityDB:BulkInsertStart()
|
||||
for itemString in pairs(items) do
|
||||
local total = (bagQuantity[itemString] or 0) + (bankQuantity[itemString] or 0) + (reagentBankQuantity[itemString] or 0)
|
||||
if total > 0 then
|
||||
private.quantityDB:BulkInsertNewRow(itemString, bagQuantity[itemString] or 0, bankQuantity[itemString] or 0, reagentBankQuantity[itemString] or 0)
|
||||
end
|
||||
end
|
||||
private.quantityDB:BulkInsertEnd()
|
||||
TempTable.Release(items)
|
||||
TempTable.Release(bagQuantity)
|
||||
TempTable.Release(bankQuantity)
|
||||
TempTable.Release(reagentBankQuantity)
|
||||
end)
|
||||
|
||||
BagTracking:OnGameDataLoad(function()
|
||||
-- we'll scan all the bags and reagent bank right away, so wipe the existing quantities
|
||||
wipe(private.settings.bagQuantity)
|
||||
wipe(private.settings.reagentBankQuantity)
|
||||
private.quantityDB:SetQueryUpdatesPaused(true)
|
||||
local query = private.quantityDB:NewQuery()
|
||||
for _, row in query:Iterator() do
|
||||
local oldValue = row:GetField("bagQuantity") + row:GetField("reagentBankQuantity")
|
||||
if row:GetField("bankQuantity") == 0 then
|
||||
-- remove this row
|
||||
assert(oldValue > 0)
|
||||
private.quantityDB:DeleteRow(row)
|
||||
elseif oldValue ~= 0 then
|
||||
-- update this row
|
||||
row:SetField("bagQuantity", 0)
|
||||
:SetField("reagentBankQuantity", 0)
|
||||
:Update()
|
||||
end
|
||||
end
|
||||
query:Release()
|
||||
private.quantityDB:SetQueryUpdatesPaused(false)
|
||||
|
||||
-- WoW does not fire an update event for the backpack when you log in, so trigger one
|
||||
private.BagUpdateHandler(nil, 0)
|
||||
private.BagUpdateDelayedHandler()
|
||||
-- trigger an update event for all bank (initial container) and reagent bank slots since we won't get one otherwise on login
|
||||
assert(GetContainerNumSlots(BANK_CONTAINER) == NUM_BANKGENERIC_SLOTS)
|
||||
for slot = 1, GetContainerNumSlots(BANK_CONTAINER) do
|
||||
private.BankSlotChangedHandler(nil, slot)
|
||||
end
|
||||
if not TSM.IsWowClassic() and IsReagentBankUnlocked() then
|
||||
for slot = 1, GetContainerNumSlots(REAGENTBANK_CONTAINER) do
|
||||
private.ReagentBankSlotChangedHandler(nil, slot)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function BagTracking.RegisterCallback(callback)
|
||||
tinsert(private.callbacks, callback)
|
||||
end
|
||||
|
||||
function BagTracking.BaseItemIterator()
|
||||
return private.quantityDB:NewQuery()
|
||||
:Select("itemString")
|
||||
:IteratorAndRelease()
|
||||
end
|
||||
|
||||
function BagTracking.FilterQueryBags(query)
|
||||
return query
|
||||
:GreaterThanOrEqual("slotId", SlotId.Join(0, 1))
|
||||
:LessThanOrEqual("slotId", SlotId.Join(NUM_BAG_SLOTS + 1, 0))
|
||||
end
|
||||
|
||||
function BagTracking.CreateQueryBags()
|
||||
return BagTracking.FilterQueryBags(private.slotDB:NewQuery())
|
||||
end
|
||||
|
||||
function BagTracking.CreateQueryBagsAuctionable()
|
||||
return BagTracking.CreateQueryBags()
|
||||
:Equal("isBoP", false)
|
||||
:Equal("isBoA", false)
|
||||
:Custom(private.NoUsedChargesQueryFilter)
|
||||
end
|
||||
|
||||
function BagTracking.CreateQueryBagsItem(itemString)
|
||||
local query = BagTracking.CreateQueryBags()
|
||||
if itemString == ItemString.GetBaseFast(itemString) then
|
||||
query:Equal("baseItemString", itemString)
|
||||
else
|
||||
query:Equal("itemString", itemString)
|
||||
end
|
||||
return query
|
||||
end
|
||||
|
||||
function BagTracking.CreateQueryBagsItemAuctionable(itemString)
|
||||
return BagTracking.CreateQueryBagsItem(itemString)
|
||||
:Equal("isBoP", false)
|
||||
:Equal("isBoA", false)
|
||||
:Custom(private.NoUsedChargesQueryFilter)
|
||||
end
|
||||
|
||||
function BagTracking.GetNumMailable(itemString)
|
||||
return BagTracking.CreateQueryBagsItem(itemString)
|
||||
:Equal("isBoP", false)
|
||||
:SumAndRelease("quantity") or 0
|
||||
end
|
||||
|
||||
function BagTracking.CreateQueryBank()
|
||||
return private.slotDB:NewQuery()
|
||||
:InTable("bag", BANK_BAG_SLOTS)
|
||||
end
|
||||
|
||||
function BagTracking.CreateQueryBankItem(itemString)
|
||||
local query = BagTracking.CreateQueryBank()
|
||||
if itemString == ItemString.GetBaseFast(itemString) then
|
||||
query:Equal("baseItemString", itemString)
|
||||
else
|
||||
query:Equal("itemString", itemString)
|
||||
end
|
||||
return query
|
||||
end
|
||||
|
||||
function BagTracking.ForceBankQuantityDeduction(itemString, quantity)
|
||||
if private.bankOpen then
|
||||
return
|
||||
end
|
||||
private.slotDB:SetQueryUpdatesPaused(true)
|
||||
local query = private.slotDB:NewQuery()
|
||||
:Equal("itemString", itemString)
|
||||
:InTable("bag", BANK_NON_REAGENT_BAG_SLOTS)
|
||||
local baseItemString = ItemString.GetBaseFast(itemString)
|
||||
for _, row in query:Iterator() do
|
||||
if quantity > 0 then
|
||||
local rowQuantity, rowBag = row:GetFields("quantity", "bag")
|
||||
if rowQuantity <= quantity then
|
||||
private.ChangeBagItemTotal(rowBag, baseItemString, -rowQuantity)
|
||||
private.slotDB:DeleteRow(row)
|
||||
quantity = quantity - rowQuantity
|
||||
else
|
||||
row:SetField("quantity", rowQuantity - quantity)
|
||||
:Update()
|
||||
private.ChangeBagItemTotal(rowBag, baseItemString, -quantity)
|
||||
quantity = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
query:Release()
|
||||
private.slotDB:SetQueryUpdatesPaused(false)
|
||||
end
|
||||
|
||||
function BagTracking.GetQuantityBySlotId(slotId)
|
||||
return private.slotDB:GetUniqueRowField("slotId", slotId, "quantity")
|
||||
end
|
||||
|
||||
function BagTracking.GetBagsQuantityByBaseItemString(baseItemString)
|
||||
return private.quantityDB:GetUniqueRowField("itemString", baseItemString, "bagQuantity") or 0
|
||||
end
|
||||
|
||||
function BagTracking.GetBankQuantityByBaseItemString(baseItemString)
|
||||
return private.quantityDB:GetUniqueRowField("itemString", baseItemString, "bankQuantity") or 0
|
||||
end
|
||||
|
||||
function BagTracking.GetReagentBankQuantityByBaseItemString(baseItemString)
|
||||
return private.quantityDB:GetUniqueRowField("itemString", baseItemString, "reagentBankQuantity") or 0
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Event Handlers
|
||||
-- ============================================================================
|
||||
|
||||
function private.BankOpenedHandler()
|
||||
if private.isFirstBankOpen then
|
||||
private.isFirstBankOpen = false
|
||||
-- this is the first time opening the bank so we'll scan all the items so wipe our existing quantities
|
||||
wipe(private.settings.bankQuantity)
|
||||
private.quantityDB:SetQueryUpdatesPaused(true)
|
||||
local query = private.quantityDB:NewQuery()
|
||||
for _, row in query:Iterator() do
|
||||
local oldValue = row:GetField("bankQuantity")
|
||||
if row:GetField("bagQuantity") + row:GetField("reagentBankQuantity") == 0 then
|
||||
-- remove this row
|
||||
assert(oldValue > 0)
|
||||
private.quantityDB:DeleteRow(row)
|
||||
elseif oldValue ~= 0 then
|
||||
-- update this row
|
||||
row:SetField("bankQuantity", 0)
|
||||
:Update()
|
||||
end
|
||||
end
|
||||
query:Release()
|
||||
private.quantityDB:SetQueryUpdatesPaused(false)
|
||||
end
|
||||
private.bankOpen = true
|
||||
private.BagUpdateDelayedHandler()
|
||||
private.BankSlotUpdateDelayed()
|
||||
end
|
||||
|
||||
function private.BankClosedHandler()
|
||||
private.bankOpen = false
|
||||
end
|
||||
|
||||
function private.BagUpdateHandler(_, bag)
|
||||
if private.bagUpdates.pending[bag] then
|
||||
return
|
||||
end
|
||||
private.bagUpdates.pending[bag] = true
|
||||
if bag >= BACKPACK_CONTAINER and bag <= NUM_BAG_SLOTS then
|
||||
tinsert(private.bagUpdates.bagList, bag)
|
||||
elseif bag == BANK_CONTAINER or (bag > NUM_BAG_SLOTS and bag <= NUM_BAG_SLOTS + NUM_BANKBAGSLOTS) then
|
||||
tinsert(private.bagUpdates.bankList, bag)
|
||||
elseif bag ~= KEYRING_CONTAINER then
|
||||
error("Unexpected bag: "..tostring(bag))
|
||||
end
|
||||
end
|
||||
|
||||
function private.BagUpdateDelayedHandler()
|
||||
private.slotDB:SetQueryUpdatesPaused(true)
|
||||
|
||||
-- scan any pending bags
|
||||
for i = #private.bagUpdates.bagList, 1, -1 do
|
||||
local bag = private.bagUpdates.bagList[i]
|
||||
if private.ScanBagOrBank(bag) then
|
||||
private.bagUpdates.pending[bag] = nil
|
||||
tremove(private.bagUpdates.bagList, i)
|
||||
end
|
||||
end
|
||||
if #private.bagUpdates.bagList > 0 then
|
||||
-- some failed to scan so try again
|
||||
Delay.AfterFrame("bagBankScan", 2, private.BagUpdateDelayedHandler)
|
||||
end
|
||||
|
||||
if private.bankOpen then
|
||||
-- scan any pending bank bags
|
||||
for i = #private.bagUpdates.bankList, 1, -1 do
|
||||
local bag = private.bagUpdates.bankList[i]
|
||||
if private.ScanBagOrBank(bag) then
|
||||
private.bagUpdates.pending[bag] = nil
|
||||
tremove(private.bagUpdates.bankList, i)
|
||||
end
|
||||
end
|
||||
if #private.bagUpdates.bankList > 0 then
|
||||
-- some failed to scan so try again
|
||||
Delay.AfterFrame("bagBankScan", 2, private.BagUpdateDelayedHandler)
|
||||
end
|
||||
end
|
||||
|
||||
private.slotDB:SetQueryUpdatesPaused(false)
|
||||
end
|
||||
|
||||
function private.BankSlotChangedHandler(_, slot)
|
||||
if slot > NUM_BANKGENERIC_SLOTS then
|
||||
private.BagUpdateHandler(nil, slot - NUM_BANKGENERIC_SLOTS)
|
||||
return
|
||||
end
|
||||
if private.bankSlotUpdates.pending[slot] then
|
||||
return
|
||||
end
|
||||
private.bankSlotUpdates.pending[slot] = true
|
||||
tinsert(private.bankSlotUpdates.list, slot)
|
||||
Delay.AfterFrame("bankSlotScan", 2, private.BankSlotUpdateDelayed)
|
||||
end
|
||||
|
||||
-- this is not a WoW event, but we fake it based on a delay from private.BankSlotChangedHandler
|
||||
function private.BankSlotUpdateDelayed()
|
||||
if not private.bankOpen then
|
||||
return
|
||||
end
|
||||
private.slotDB:SetQueryUpdatesPaused(true)
|
||||
|
||||
-- scan any pending slots
|
||||
for i = #private.bankSlotUpdates.list, 1, -1 do
|
||||
local slot = private.bankSlotUpdates.list[i]
|
||||
if private.ScanBankSlot(slot) then
|
||||
private.bankSlotUpdates.pending[slot] = nil
|
||||
tremove(private.bankSlotUpdates.list, i)
|
||||
end
|
||||
end
|
||||
if #private.bankSlotUpdates.list > 0 then
|
||||
-- some failed to scan so try again
|
||||
Delay.AfterFrame("bankSlotScan", 2, private.BankSlotUpdateDelayed)
|
||||
end
|
||||
|
||||
private.slotDB:SetQueryUpdatesPaused(false)
|
||||
end
|
||||
|
||||
function private.ReagentBankSlotChangedHandler(_, slot)
|
||||
if private.reagentBankSlotUpdates.pending[slot] then
|
||||
return
|
||||
end
|
||||
private.reagentBankSlotUpdates.pending[slot] = true
|
||||
tinsert(private.reagentBankSlotUpdates.list, slot)
|
||||
Delay.AfterFrame("reagentBankSlotScan", 2, private.ReagentBankSlotUpdateDelayed)
|
||||
end
|
||||
|
||||
-- this is not a WoW event, but we fake it based on a delay from private.ReagentBankSlotChangedHandler
|
||||
function private.ReagentBankSlotUpdateDelayed()
|
||||
private.slotDB:SetQueryUpdatesPaused(true)
|
||||
|
||||
-- scan any pending slots
|
||||
for i = #private.reagentBankSlotUpdates.list, 1, -1 do
|
||||
local slot = private.reagentBankSlotUpdates.list[i]
|
||||
if private.ScanReagentBankSlot(slot) then
|
||||
private.reagentBankSlotUpdates.pending[slot] = nil
|
||||
tremove(private.reagentBankSlotUpdates.list, i)
|
||||
end
|
||||
end
|
||||
if #private.reagentBankSlotUpdates.list > 0 then
|
||||
-- some failed to scan so try again
|
||||
Delay.AfterFrame("reagentBankSlotScan", 2, private.ReagentBankSlotUpdateDelayed)
|
||||
end
|
||||
|
||||
private.slotDB:SetQueryUpdatesPaused(false)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Scanning Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.ScanBagOrBank(bag)
|
||||
local numSlots = GetContainerNumSlots(bag)
|
||||
private.RemoveExtraSlots(bag, numSlots)
|
||||
local result = true
|
||||
for slot = 1, numSlots do
|
||||
if not private.ScanBagSlot(bag, slot) then
|
||||
result = false
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function private.ScanBankSlot(slot)
|
||||
return private.ScanBagSlot(BANK_CONTAINER, slot)
|
||||
end
|
||||
|
||||
function private.ScanReagentBankSlot(slot)
|
||||
return private.ScanBagSlot(REAGENTBANK_CONTAINER, slot)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.NoUsedChargesQueryFilter(row)
|
||||
return not InventoryInfo.HasUsedCharges(row:GetFields("bag", "slot"))
|
||||
end
|
||||
|
||||
function private.RemoveExtraSlots(bag, numSlots)
|
||||
-- the number of slots of this bag may have changed, in which case we should remove any higher ones from our DB
|
||||
local query = private.slotDB:NewQuery()
|
||||
:Equal("bag", bag)
|
||||
:GreaterThan("slot", numSlots)
|
||||
for _, row in query:Iterator() do
|
||||
local baseItemString, quantity = row:GetFields("baseItemString", "quantity")
|
||||
private.ChangeBagItemTotal(bag, baseItemString, -quantity)
|
||||
private.slotDB:DeleteRow(row)
|
||||
end
|
||||
query:Release()
|
||||
end
|
||||
|
||||
function private.ScanBagSlot(bag, slot)
|
||||
local texture, quantity, _, _, _, _, link, _, _, itemId = GetContainerItemInfo(bag, slot)
|
||||
if quantity and not itemId then
|
||||
-- we are pending item info for this slot so try again later to scan it
|
||||
return false
|
||||
elseif quantity == 0 then
|
||||
-- this item is going away, so try again later to scan it
|
||||
return false
|
||||
end
|
||||
local baseItemString = link and ItemString.GetBase(link)
|
||||
local slotId = SlotId.Join(bag, slot)
|
||||
local row = private.slotDB:GetUniqueRow("slotId", slotId)
|
||||
if baseItemString then
|
||||
local isBoP, isBoA = nil, nil
|
||||
if row then
|
||||
if row:GetField("itemLink") == link then
|
||||
-- the item didn't change, so use the previous values
|
||||
isBoP, isBoA = row:GetFields("isBoP", "isBoA")
|
||||
else
|
||||
isBoP, isBoA = InventoryInfo.IsSoulbound(bag, slot)
|
||||
if isBoP == nil then
|
||||
Log.Err("Failed to get soulbound info for %d,%d (%s)", bag, slot, link or "?")
|
||||
return false
|
||||
end
|
||||
end
|
||||
-- remove the old row from the item totals
|
||||
local oldBaseItemString, oldQuantity = row:GetFields("baseItemString", "quantity")
|
||||
private.ChangeBagItemTotal(bag, oldBaseItemString, -oldQuantity)
|
||||
else
|
||||
isBoP, isBoA = InventoryInfo.IsSoulbound(bag, slot)
|
||||
if isBoP == nil then
|
||||
Log.Err("Failed to get soulbound info for %d,%d (%s)", bag, slot, link or "?")
|
||||
return false
|
||||
end
|
||||
-- there was nothing here previously so create a new row
|
||||
row = private.slotDB:NewRow()
|
||||
:SetField("slotId", slotId)
|
||||
:SetField("bag", bag)
|
||||
:SetField("slot", slot)
|
||||
end
|
||||
-- update the row
|
||||
row:SetField("itemLink", link)
|
||||
:SetField("itemString", ItemString.Get(link))
|
||||
:SetField("itemTexture", texture or ItemInfo.GetTexture(link))
|
||||
:SetField("quantity", quantity)
|
||||
:SetField("isBoP", isBoP)
|
||||
:SetField("isBoA", isBoA)
|
||||
:CreateOrUpdateAndRelease()
|
||||
-- add to the item totals
|
||||
private.ChangeBagItemTotal(bag, baseItemString, quantity)
|
||||
elseif row then
|
||||
-- nothing here now so delete the row and remove from the item totals
|
||||
local oldBaseItemString, oldQuantity = row:GetFields("baseItemString", "quantity")
|
||||
private.ChangeBagItemTotal(bag, oldBaseItemString, -oldQuantity)
|
||||
private.slotDB:DeleteRow(row)
|
||||
row:Release()
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function private.OnCallbackQueryUpdated()
|
||||
for _, callback in ipairs(private.callbacks) do
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
function private.ChangeBagItemTotal(bag, itemString, changeQuantity)
|
||||
local totalsTable = nil
|
||||
local field = nil
|
||||
if bag >= BACKPACK_CONTAINER and bag <= NUM_BAG_SLOTS then
|
||||
totalsTable = private.settings.bagQuantity
|
||||
field = "bagQuantity"
|
||||
elseif bag == BANK_CONTAINER or (bag > NUM_BAG_SLOTS and bag <= NUM_BAG_SLOTS + NUM_BANKBAGSLOTS) then
|
||||
totalsTable = private.settings.bankQuantity
|
||||
field = "bankQuantity"
|
||||
elseif bag == REAGENTBANK_CONTAINER then
|
||||
totalsTable = private.settings.reagentBankQuantity
|
||||
field = "reagentBankQuantity"
|
||||
else
|
||||
error("Unexpected bag: "..tostring(bag))
|
||||
end
|
||||
totalsTable[itemString] = (totalsTable[itemString] or 0) + changeQuantity
|
||||
private.UpdateQuantity(itemString, field, changeQuantity)
|
||||
assert(totalsTable[itemString] >= 0)
|
||||
if totalsTable[itemString] == 0 then
|
||||
totalsTable[itemString] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function private.UpdateQuantity(itemString, field, quantity)
|
||||
assert(itemString and field and quantity)
|
||||
assert(quantity ~= 0)
|
||||
if not private.quantityDB:HasUniqueRow("itemString", itemString) then
|
||||
-- create a new row
|
||||
private.quantityDB:NewRow()
|
||||
:SetField("itemString", itemString)
|
||||
:SetField("bagQuantity", 0)
|
||||
:SetField("bankQuantity", 0)
|
||||
:SetField("reagentBankQuantity", 0)
|
||||
:Create()
|
||||
end
|
||||
|
||||
local row = private.quantityDB:GetUniqueRow("itemString", itemString)
|
||||
local totalQuantity = row:GetField("bagQuantity") + row:GetField("bankQuantity") + row:GetField("reagentBankQuantity")
|
||||
local oldValue = row:GetField(field)
|
||||
local newValue = oldValue + quantity
|
||||
assert(newValue >= 0)
|
||||
if newValue == 0 and totalQuantity == oldValue then
|
||||
-- remove this row
|
||||
private.quantityDB:DeleteRow(row)
|
||||
else
|
||||
-- update this row
|
||||
row:SetField(field, oldValue + quantity)
|
||||
:Update()
|
||||
end
|
||||
row:Release()
|
||||
end
|
||||
67
LibTSM/Service/BlackMarket.lua
Normal file
67
LibTSM/Service/BlackMarket.lua
Normal file
@@ -0,0 +1,67 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
-- This file contains all the code for TSM's standalone features
|
||||
|
||||
local _, TSM = ...
|
||||
local BlackMarket = TSM.Init("Service.BlackMarket")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local private = {
|
||||
data = nil,
|
||||
time = nil,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
BlackMarket:OnModuleLoad(function()
|
||||
-- setup BMAH scanning
|
||||
if not TSM.IsWowClassic() then
|
||||
Event.Register("BLACK_MARKET_ITEM_UPDATE", private.ScanBMAH)
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function BlackMarket.GetScanData()
|
||||
return private.data, private.time
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Features
|
||||
-- ============================================================================
|
||||
|
||||
function private.ScanBMAH()
|
||||
local numItems = C_BlackMarket.GetNumItems()
|
||||
if not numItems then
|
||||
return
|
||||
end
|
||||
local items = TempTable.Acquire()
|
||||
for i = 1, numItems do
|
||||
local _, _, quantity, _, _, _, _, _, minBid, minIncr, currBid, _, numBids, timeLeft, itemLink, bmId = C_BlackMarket.GetItemInfoByIndex(i)
|
||||
local itemID = ItemString.ToId(itemLink)
|
||||
if itemID then
|
||||
minBid = floor(minBid / COPPER_PER_GOLD)
|
||||
minIncr = floor(minIncr / COPPER_PER_GOLD)
|
||||
currBid = floor(currBid / COPPER_PER_GOLD)
|
||||
tinsert(items, "[" .. strjoin(",", bmId, itemID, quantity, timeLeft, minBid, minIncr, currBid, numBids, time()) .. "]")
|
||||
end
|
||||
end
|
||||
private.data = "[" .. table.concat(items, ",") .. "]"
|
||||
private.time = time()
|
||||
TempTable.Release(items)
|
||||
end
|
||||
182
LibTSM/Service/Conversions.lua
Normal file
182
LibTSM/Service/Conversions.lua
Normal file
@@ -0,0 +1,182 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Conversions = TSM.Init("Service.Conversions")
|
||||
local DisenchantInfo = TSM.Include("Data.DisenchantInfo")
|
||||
local Mill = TSM.Include("Data.Mill")
|
||||
local Prospect = TSM.Include("Data.Prospect")
|
||||
local Transform = TSM.Include("Data.Transform")
|
||||
local VendorTrade = TSM.Include("Data.VendorTrade")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local Table = TSM.Include("Util.Table")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
Conversions.METHOD = {
|
||||
DISENCHANT = newproxy(),
|
||||
MILL = newproxy(),
|
||||
PROSPECT = newproxy(),
|
||||
TRANSFORM = newproxy(),
|
||||
VENDOR_TRADE = newproxy(),
|
||||
CRAFT = newproxy(),
|
||||
}
|
||||
local private = {
|
||||
data = {},
|
||||
sourceItemCache = {},
|
||||
skippedConversions = {},
|
||||
}
|
||||
local MAX_CONVERSION_DEPTH = 3
|
||||
local EMPTY_CONVERSION = newproxy()
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Conversions:OnModuleLoad(function()
|
||||
Table.SetReadOnly(Conversions.METHOD)
|
||||
for itemString in DisenchantInfo.TargetItemIterator() do
|
||||
ItemInfo.FetchInfo(itemString)
|
||||
end
|
||||
for targetItemString in Mill.TargetItemIterator() do
|
||||
for sourceItemString in Mill.SourceItemIterator(targetItemString) do
|
||||
local rate = Mill.GetRate(targetItemString, sourceItemString)
|
||||
private.Add(targetItemString, sourceItemString, Conversions.METHOD.MILL, rate)
|
||||
end
|
||||
end
|
||||
for targetItemString in Prospect.TargetItemIterator() do
|
||||
for sourceItemString in Prospect.SourceItemIterator(targetItemString) do
|
||||
local rate, amount, minAmount, maxAmount = Prospect.GetRate(targetItemString, sourceItemString)
|
||||
private.Add(targetItemString, sourceItemString, Conversions.METHOD.PROSPECT, rate, amount, minAmount, maxAmount)
|
||||
end
|
||||
end
|
||||
for targetItemString in Transform.TargetItemIterator() do
|
||||
for sourceItemString in Transform.SourceItemIterator(targetItemString) do
|
||||
local rate = Transform.GetRate(targetItemString, sourceItemString)
|
||||
private.Add(targetItemString, sourceItemString, Conversions.METHOD.TRANSFORM, rate)
|
||||
end
|
||||
end
|
||||
for targetItemString in VendorTrade.TargetItemIterator() do
|
||||
for sourceItemString in VendorTrade.SourceItemIterator(targetItemString) do
|
||||
local rate = VendorTrade.GetRate(targetItemString, sourceItemString)
|
||||
private.Add(targetItemString, sourceItemString, Conversions.METHOD.VENDOR_TRADE, rate)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Conversions.AddCraft(targetItemString, sourceItemString, rate)
|
||||
private.Add(targetItemString, sourceItemString, Conversions.METHOD.CRAFT, rate)
|
||||
end
|
||||
|
||||
function Conversions.TargetItemsByMethodIterator(sourceItemString, method)
|
||||
local context = TempTable.Acquire()
|
||||
context.sourceItemString = sourceItemString
|
||||
context.method = method
|
||||
return private.TargetItemsByMethodIteratorHelper, context, nil
|
||||
end
|
||||
|
||||
function Conversions.GetTargetItemByName(targetItemName)
|
||||
targetItemName = strlower(targetItemName)
|
||||
for targetItemString in pairs(private.data) do
|
||||
local name = ItemInfo.GetName(targetItemString)
|
||||
if name and strlower(name) == targetItemName then
|
||||
return targetItemString
|
||||
end
|
||||
end
|
||||
for targetItemString in DisenchantInfo.TargetItemIterator() do
|
||||
local name = ItemInfo.GetName(targetItemString)
|
||||
if name and strlower(name) == targetItemName then
|
||||
return targetItemString
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Conversions.GetSourceItems(targetItemString)
|
||||
if not targetItemString or not private.data[targetItemString] or private.sourceItemCache[targetItemString] == EMPTY_CONVERSION then
|
||||
return
|
||||
end
|
||||
if not private.sourceItemCache[targetItemString] then
|
||||
local depthLookup = TempTable.Acquire()
|
||||
depthLookup[targetItemString] = -1 -- set this so we don't loop back through the target item
|
||||
private.sourceItemCache[targetItemString] = {}
|
||||
private.GetSourceItemsHelper(targetItemString, private.sourceItemCache[targetItemString], depthLookup, 0, 1)
|
||||
TempTable.Release(depthLookup)
|
||||
if not next(private.sourceItemCache[targetItemString]) then
|
||||
private.sourceItemCache[targetItemString] = EMPTY_CONVERSION
|
||||
return
|
||||
end
|
||||
end
|
||||
return private.sourceItemCache[targetItemString]
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.Add(targetItemString, sourceItemString, method, rate, amount, minAmount, maxAmount)
|
||||
targetItemString = ItemString.GetBase(targetItemString)
|
||||
sourceItemString = ItemString.GetBase(sourceItemString)
|
||||
assert(targetItemString and sourceItemString)
|
||||
|
||||
private.data[targetItemString] = private.data[targetItemString] or {}
|
||||
if private.data[targetItemString][sourceItemString] then
|
||||
-- if there is more than one way to go from source to target, then just skip all conversions between these items
|
||||
private.skippedConversions[targetItemString..sourceItemString] = true
|
||||
private.data[targetItemString][sourceItemString] = nil
|
||||
end
|
||||
if private.skippedConversions[targetItemString..sourceItemString] then
|
||||
return
|
||||
end
|
||||
|
||||
private.data[targetItemString][sourceItemString] = {
|
||||
method = method,
|
||||
rate = rate,
|
||||
amount = amount,
|
||||
minAmount = minAmount,
|
||||
maxAmount = maxAmount,
|
||||
}
|
||||
ItemInfo.FetchInfo(targetItemString)
|
||||
ItemInfo.FetchInfo(sourceItemString)
|
||||
wipe(private.sourceItemCache)
|
||||
end
|
||||
|
||||
function private.GetSourceItemsHelper(targetItemString, result, depthLookup, currentDepth, currentRate)
|
||||
if currentDepth >= MAX_CONVERSION_DEPTH or not private.data[targetItemString] then
|
||||
return
|
||||
end
|
||||
for sourceItemString, info in pairs(private.data[targetItemString]) do
|
||||
if not result[sourceItemString] or depthLookup[sourceItemString] > currentDepth then
|
||||
local rate = info.rate * currentRate
|
||||
result[sourceItemString] = rate
|
||||
depthLookup[sourceItemString] = currentDepth
|
||||
private.GetSourceItemsHelper(sourceItemString, result, depthLookup, currentDepth + 1, rate)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function private.TargetItemsByMethodIteratorHelper(context, index)
|
||||
while true do
|
||||
index = next(private.data, index)
|
||||
local items = private.data[index]
|
||||
if not items then
|
||||
TempTable.Release(context)
|
||||
return
|
||||
end
|
||||
local info = items[context.sourceItemString]
|
||||
if info and ((not context.method and info.method ~= Conversions.METHOD.CRAFT) or info.method == context.method) then
|
||||
return index, info.rate, info.amount, info.minAmount, info.maxAmount
|
||||
end
|
||||
end
|
||||
end
|
||||
1038
LibTSM/Service/CustomPrice.lua
Normal file
1038
LibTSM/Service/CustomPrice.lua
Normal file
File diff suppressed because it is too large
Load Diff
769
LibTSM/Service/ErrorHandler.lua
Normal file
769
LibTSM/Service/ErrorHandler.lua
Normal file
@@ -0,0 +1,769 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
-- TSM's error handler
|
||||
|
||||
local _, TSM = ...
|
||||
local ErrorHandler = TSM.Init("Service.ErrorHandler")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local String = TSM.Include("Util.String")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local JSON = TSM.Include("Util.JSON")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local L = TSM.Include("Locale").GetTable()
|
||||
local private = {
|
||||
origErrorHandler = nil,
|
||||
errorFrame = nil,
|
||||
isSilent = nil,
|
||||
errorSuppressed = nil,
|
||||
errorReports = {},
|
||||
num = 0,
|
||||
localLinesTemp = {},
|
||||
hitInternalError = false,
|
||||
isManual = nil,
|
||||
ignoreErrors = false,
|
||||
globalNameTranslation = {},
|
||||
}
|
||||
local MAX_ERROR_REPORT_AGE = 7 * 24 * 60 * 60 -- 1 week
|
||||
local MAX_STACK_DEPTH = 50
|
||||
local ADDON_SUITES = {
|
||||
"ArkInventory",
|
||||
"AtlasLoot",
|
||||
"Altoholic",
|
||||
"Auc-",
|
||||
"Bagnon",
|
||||
"BigWigs",
|
||||
"Broker",
|
||||
"ButtonFacade",
|
||||
"Carbonite",
|
||||
"DataStore",
|
||||
"DBM",
|
||||
"Dominos",
|
||||
"DXE",
|
||||
"EveryQuest",
|
||||
"Forte",
|
||||
"FuBar",
|
||||
"GatherMate2",
|
||||
"Grid",
|
||||
"LightHeaded",
|
||||
"LittleWigs",
|
||||
"Masque",
|
||||
"MogIt",
|
||||
"Odyssey",
|
||||
"Overachiever",
|
||||
"PitBull4",
|
||||
"Prat-3.0",
|
||||
"RaidAchievement",
|
||||
"Skada",
|
||||
"SpellFlash",
|
||||
"TidyPlates",
|
||||
"TipTac",
|
||||
"Titan",
|
||||
"UnderHood",
|
||||
"WowPro",
|
||||
"ZOMGBuffs",
|
||||
}
|
||||
local OLD_TSM_MODULES = {
|
||||
"TradeSkillMaster_Accounting",
|
||||
"TradeSkillMaster_AuctionDB",
|
||||
"TradeSkillMaster_Auctioning",
|
||||
"TradeSkillMaster_Crafting",
|
||||
"TradeSkillMaster_Destroying",
|
||||
"TradeSkillMaster_Mailing",
|
||||
"TradeSkillMaster_Shopping",
|
||||
"TradeSkillMaster_Vendoring",
|
||||
"TradeSkillMaster_Warehousing",
|
||||
}
|
||||
local PRINT_PREFIX = "|cffff0000TSM:|r "
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function ErrorHandler.ShowForThread(err, thread)
|
||||
local stackLine = debugstack(thread, 0, 1, 0)
|
||||
local oldModule = strmatch(stackLine, "(lMaster_[A-Za-z]+)")
|
||||
if oldModule and tContains(OLD_TSM_MODULES, "TradeSkil"..oldModule) then
|
||||
-- ignore errors from old modules
|
||||
return
|
||||
end
|
||||
-- show an error, but don't cause an exception to be thrown
|
||||
private.isSilent = true
|
||||
private.ErrorHandler(err, thread)
|
||||
end
|
||||
|
||||
function ErrorHandler.ShowManual()
|
||||
private.isManual = true
|
||||
-- show an error, but don't cause an exception to be thrown
|
||||
private.isSilent = true
|
||||
private.ErrorHandler("Manually triggered error")
|
||||
end
|
||||
|
||||
function ErrorHandler.SaveReports(appDB)
|
||||
if private.errorFrame then
|
||||
private.errorFrame:Hide()
|
||||
end
|
||||
appDB.errorReports = appDB.errorReports or { updateTime = 0, data = {} }
|
||||
if #private.errorReports > 0 then
|
||||
appDB.errorReports.updateTime = private.errorReports[#private.errorReports].timestamp
|
||||
end
|
||||
-- remove any events which are too old
|
||||
for i = #appDB.errorReports.data, 1, -1 do
|
||||
local timestamp = strmatch(appDB.errorReports.data[i], "([0-9]+)%]$") or ""
|
||||
if (tonumber(timestamp) or 0) < time() - MAX_ERROR_REPORT_AGE then
|
||||
tremove(appDB.errorReports.data, i)
|
||||
end
|
||||
end
|
||||
for _, report in ipairs(private.errorReports) do
|
||||
local line = format("[%s,\"%s\",%d]", JSON.Encode(report.errorInfo), report.details, report.timestamp)
|
||||
tinsert(appDB.errorReports.data, line)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Error Handler
|
||||
-- ============================================================================
|
||||
|
||||
function private.ErrorHandler(msg, thread)
|
||||
-- ignore errors while we are handling this error
|
||||
private.ignoreErrors = true
|
||||
local isSilent = private.isSilent
|
||||
private.isSilent = nil
|
||||
local isManual = private.isManual
|
||||
private.isManual = nil
|
||||
private.CreateErrorFrame()
|
||||
|
||||
if type(thread) ~= "thread" then
|
||||
thread = nil
|
||||
end
|
||||
|
||||
if private.errorFrame:IsVisible() and private.errorSuppressed then
|
||||
-- already showing an error and suppressed another one, so silently ignore this one
|
||||
private.ignoreErrors = false
|
||||
return true
|
||||
end
|
||||
|
||||
-- shorten the paths in the error message
|
||||
msg = gsub(msg, "%.%.%.T?r?a?d?e?S?k?i?l?l?M?a?ster([_A-Za-z]*)\\", "TradeSkillMaster%1\\")
|
||||
msg = strsub(msg, strfind(msg, "TradeSkillMaster") or 1)
|
||||
msg = gsub(msg, "TradeSkillMaster([^%.])", "TSM%1")
|
||||
|
||||
-- build our global name translation table
|
||||
wipe(private.globalNameTranslation)
|
||||
pcall(function()
|
||||
local UIElements = TSM.Include("UI.UIElements")
|
||||
local temp = {}
|
||||
UIElements.GetDebugNameTranslation(temp)
|
||||
for k, v in pairs(temp) do
|
||||
private.globalNameTranslation[String.Escape(k)] = v
|
||||
end
|
||||
end)
|
||||
|
||||
-- build stack trace with locals and get addon name
|
||||
local stackInfo = private.GetStackInfo(msg, thread)
|
||||
local addonName = isSilent and "TradeSkillMaster" or nil
|
||||
for _, info in ipairs(stackInfo) do
|
||||
if not addonName then
|
||||
addonName = strmatch(info.file, "[A-Za-z]+%.lua") and private.IsTSMAddon(info.file) or nil
|
||||
end
|
||||
end
|
||||
if not isManual and addonName ~= "TradeSkillMaster" then
|
||||
-- not a TSM error
|
||||
private.ignoreErrors = false
|
||||
return false
|
||||
end
|
||||
|
||||
if not TSM.IsDevVersion() and not isManual then
|
||||
-- log the error (use a format string in case there are '%' characters in the msg)
|
||||
Log.Err("%s", msg)
|
||||
end
|
||||
|
||||
if private.errorFrame:IsVisible() then
|
||||
-- already showing an error, so suppress this one and return
|
||||
private.errorSuppressed = true
|
||||
print(PRINT_PREFIX..L["Additional error suppressed"])
|
||||
return true
|
||||
end
|
||||
|
||||
private.num = private.num + 1
|
||||
local clientVersion, clientBuild = GetBuildInfo()
|
||||
local errorInfo = {
|
||||
msg = #stackInfo > 0 and gsub(msg, String.Escape(stackInfo[1].file)..":"..stackInfo[1].line..": ", "") or msg,
|
||||
stackInfo = stackInfo,
|
||||
time = time(),
|
||||
debugTime = floor(debugprofilestop()),
|
||||
client = format("%s (%s)", clientVersion, clientBuild),
|
||||
locale = GetLocale(),
|
||||
inCombat = tostring(InCombatLockdown() and true or false),
|
||||
version = TSM.GetVersion(),
|
||||
}
|
||||
|
||||
-- temp table info
|
||||
local tempTableLines = {}
|
||||
for _, info in ipairs(TempTable.GetDebugInfo()) do
|
||||
tinsert(tempTableLines, info)
|
||||
end
|
||||
errorInfo.tempTableStr = table.concat(tempTableLines, "\n")
|
||||
|
||||
-- object pool info
|
||||
local status, objectPoolInfo = pcall(function() return TSM.Include("Util.ObjectPool").GetDebugInfo() end)
|
||||
local objectPoolLines = {}
|
||||
if status then
|
||||
for name, objectInfo in pairs(objectPoolInfo) do
|
||||
tinsert(objectPoolLines, format("%s (%d created, %d in use)", name, objectInfo.numCreated, objectInfo.numInUse))
|
||||
for _, info in ipairs(objectInfo.info) do
|
||||
tinsert(objectPoolLines, " "..info)
|
||||
end
|
||||
end
|
||||
end
|
||||
errorInfo.objectPoolStr = status and table.concat(objectPoolLines, "\n") or tostring(objectPoolInfo)
|
||||
|
||||
-- TSM thread info
|
||||
local threadInfoStr = nil
|
||||
status, threadInfoStr = pcall(function() return TSM.Include("Service.Threading").GetDebugStr() end)
|
||||
errorInfo.threadInfoStr = tostring(threadInfoStr)
|
||||
|
||||
-- recent debug log entries
|
||||
local entries = {}
|
||||
for i = Log.Length(), 1, -1 do
|
||||
local severity, location, timeStr, logMsg = Log.Get(i)
|
||||
tinsert(entries, format("%s [%s] {%s} %s", timeStr, severity, location, logMsg))
|
||||
end
|
||||
errorInfo.debugLogStr = table.concat(entries, "\n")
|
||||
|
||||
-- addons
|
||||
local hasAddonSuite = {}
|
||||
local addonsLines = {}
|
||||
for i = 1, GetNumAddOns() do
|
||||
local name, _, _, loadable = GetAddOnInfo(i)
|
||||
if loadable then
|
||||
local version = strtrim(GetAddOnMetadata(name, "X-Curse-Packaged-Version") or GetAddOnMetadata(name, "Version") or "")
|
||||
local loaded = IsAddOnLoaded(i)
|
||||
local isSuite = nil
|
||||
for _, commonTerm in ipairs(ADDON_SUITES) do
|
||||
if strsub(name, 1, #commonTerm) == commonTerm then
|
||||
isSuite = commonTerm
|
||||
break
|
||||
end
|
||||
end
|
||||
local commonTerm = "TradeSkillMaster"
|
||||
if isSuite then
|
||||
if not hasAddonSuite[isSuite] then
|
||||
tinsert(addonsLines, name.." ("..version..")"..(loaded and "" or " [Not Loaded]"))
|
||||
hasAddonSuite[isSuite] = true
|
||||
end
|
||||
elseif strsub(name, 1, #commonTerm) == commonTerm then
|
||||
name = gsub(name, "TradeSkillMaster", "TSM")
|
||||
tinsert(addonsLines, name.." ("..version..")"..(loaded and "" or " [Not Loaded]"))
|
||||
else
|
||||
tinsert(addonsLines, name.." ("..version..")"..(loaded and "" or " [Not Loaded]"))
|
||||
end
|
||||
end
|
||||
end
|
||||
errorInfo.addonsStr = table.concat(addonsLines, "\n")
|
||||
|
||||
-- show this error
|
||||
local stackInfoLines = {}
|
||||
for _, info in ipairs(errorInfo.stackInfo) do
|
||||
local localsStr = info.locals ~= "" and ("\n |cffaaaaaa"..gsub(info.locals, "\n", "\n ").."|r") or ""
|
||||
local locationStr = info.line ~= 0 and strjoin(":", info.file, info.line) or info.file
|
||||
tinsert(stackInfoLines, locationStr.." <"..info.func..">"..localsStr)
|
||||
end
|
||||
private.errorFrame.errorStr = strjoin("\n",
|
||||
private.FormatErrorMessageSection("Message", msg),
|
||||
private.FormatErrorMessageSection("Time", date("%m/%d/%y %H:%M:%S", errorInfo.time).." ("..floor(errorInfo.debugTime)..")"),
|
||||
private.FormatErrorMessageSection("Client", errorInfo.client),
|
||||
private.FormatErrorMessageSection("Locale", errorInfo.locale),
|
||||
private.FormatErrorMessageSection("Combat", errorInfo.inCombat),
|
||||
private.FormatErrorMessageSection("Error Count", private.num),
|
||||
private.FormatErrorMessageSection("Stack Trace", table.concat(stackInfoLines, "\n"), true),
|
||||
private.FormatErrorMessageSection("Temp Tables", errorInfo.tempTableStr, true),
|
||||
private.FormatErrorMessageSection("Object Pools", errorInfo.objectPoolStr, true),
|
||||
private.FormatErrorMessageSection("Running Threads", errorInfo.threadInfoStr, true),
|
||||
private.FormatErrorMessageSection("Debug Log", errorInfo.debugLogStr, true),
|
||||
private.FormatErrorMessageSection("Addons", errorInfo.addonsStr, true)
|
||||
)
|
||||
-- remove unprintable characters
|
||||
private.errorFrame.errorStr = gsub(private.errorFrame.errorStr, "[%z\001-\008\011-\031]", "?")
|
||||
private.errorFrame.errorInfo = errorInfo
|
||||
private.errorFrame.isManual = isManual
|
||||
private.errorFrame:Show()
|
||||
print(PRINT_PREFIX..L["Looks like TradeSkillMaster has encountered an error. Please help the author fix this error by following the instructions shown."])
|
||||
if TSM.__IS_TEST_ENV then
|
||||
print(private.errorFrame.errorStr)
|
||||
end
|
||||
|
||||
private.ignoreErrors = false
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.GetStackInfo(msg, thread)
|
||||
local errLocation = strmatch(msg, "[A-Za-z]+%.lua:[0-9]+")
|
||||
local stackInfo = {}
|
||||
local stackStarted = false
|
||||
for i = 0, MAX_STACK_DEPTH do
|
||||
local prevStackFunc = #stackInfo > 0 and stackInfo[#stackInfo].func or nil
|
||||
local file, line, func, localsStr, newPrevStackFunc = private.GetStackLevelInfo(i, thread, prevStackFunc)
|
||||
if newPrevStackFunc then
|
||||
stackInfo[#stackInfo].func = newPrevStackFunc
|
||||
end
|
||||
if file then
|
||||
if not stackStarted then
|
||||
if errLocation then
|
||||
stackStarted = strmatch(file..":"..line, "[A-Za-z]+%.lua:[0-9]+") == errLocation
|
||||
else
|
||||
stackStarted = i > (thread and 1 or 4) and file ~= "[C]"
|
||||
end
|
||||
end
|
||||
if stackStarted then
|
||||
tinsert(stackInfo, {
|
||||
file = file,
|
||||
line = line,
|
||||
func = func,
|
||||
locals = localsStr,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
return stackInfo
|
||||
end
|
||||
|
||||
function private.GetStackLevelInfo(level, thread, prevStackFunc)
|
||||
local stackLine = nil
|
||||
if thread then
|
||||
stackLine = debugstack(thread, level, 1, 0)
|
||||
else
|
||||
level = level + 1
|
||||
stackLine = debugstack(level, 1, 0)
|
||||
end
|
||||
stackLine = gsub(stackLine, "^%[string \"@([^%.]+%.lua)\"%]", "%1")
|
||||
local locals = debuglocals(level)
|
||||
stackLine = gsub(stackLine, "%.%.%.T?r?a?d?e?S?k?i?l?l?M?a?ster([_A-Za-z]*)\\", "TradeSkillMaster%1\\")
|
||||
stackLine = gsub(stackLine, "%.%.%.", "")
|
||||
stackLine = gsub(stackLine, "`", "<", 1)
|
||||
stackLine = gsub(stackLine, "'", ">", 1)
|
||||
stackLine = strtrim(stackLine)
|
||||
if stackLine == "" then
|
||||
return
|
||||
end
|
||||
|
||||
-- Parse out the file, line, and function name
|
||||
local locationStr, functionStr = strmatch(stackLine, "^(.-): in function (<[^\n]*>)")
|
||||
if not locationStr then
|
||||
locationStr, functionStr = strmatch(stackLine, "^(.-): in (main chunk)")
|
||||
end
|
||||
if not locationStr then
|
||||
return
|
||||
end
|
||||
locationStr = strsub(locationStr, strfind(locationStr, "TradeSkillMaster") or 1)
|
||||
locationStr = gsub(locationStr, "TradeSkillMaster([^%.])", "TSM%1")
|
||||
functionStr = functionStr and gsub(gsub(functionStr, ".*\\", ""), "[<>]", "") or ""
|
||||
local file, line = strmatch(locationStr, "^(.+):(%d+)$")
|
||||
file = file or locationStr
|
||||
line = tonumber(line) or 0
|
||||
|
||||
local func = strsub(functionStr, strfind(functionStr, "`") and 2 or 1, -1) or "?"
|
||||
func = func ~= "" and func or "?"
|
||||
|
||||
if strfind(locationStr, "LibTSMClass%.lua:") then
|
||||
-- ignore stack frames from the class code's wrapper function
|
||||
if func ~= "?" and prevStackFunc and not strmatch(func, "^.+:[0-9]+$") and strmatch(prevStackFunc, "^.+:[0-9]+$") then
|
||||
-- this stack frame includes the class method we were accessing in the previous one, so go back and fix it up
|
||||
local className = locals and strmatch(locals, "\n +str = \"([A-Za-z_0-9]+):[0-9A-F]+\"\n") or "?"
|
||||
prevStackFunc = className.."."..func
|
||||
end
|
||||
return nil, nil, nil, nil, prevStackFunc
|
||||
end
|
||||
|
||||
-- add locals for addon functions (debuglocals() doesn't always work - or ever for threads)
|
||||
local localsStr = locals and private.ParseLocals(locals, file) or ""
|
||||
return file, line, func, localsStr, nil
|
||||
end
|
||||
|
||||
function private.ParseLocals(locals, file)
|
||||
if strmatch(file, "^%[") then
|
||||
return
|
||||
end
|
||||
|
||||
local fileName = strmatch(file, "([A-Za-z%-_0-9]+)%.lua")
|
||||
local isBlizzardFile = strmatch(file, "Interface\\FrameXML\\")
|
||||
local isPrivateTable, isLocaleTable, isPackageTable, isSelfTable = false, false, false, false
|
||||
wipe(private.localLinesTemp)
|
||||
locals = gsub(locals, "<([a-z]+)> {[\n\t ]+}", "<%1> {}")
|
||||
locals = gsub(locals, " = <function> defined @", "@")
|
||||
locals = gsub(locals, "<table> {", "{")
|
||||
|
||||
for localLine in gmatch(locals, "[^\n]+") do
|
||||
local shouldIgnoreLine = false
|
||||
if strmatch(localLine, "^ *%(") then
|
||||
shouldIgnoreLine = true
|
||||
elseif strmatch(localLine, "LibTSMClass%.lua:") then
|
||||
-- ignore class methods
|
||||
shouldIgnoreLine = true
|
||||
elseif strmatch(localLine, "<unnamed> {}$") then
|
||||
-- ignore internal WoW frame members
|
||||
shouldIgnoreLine = true
|
||||
end
|
||||
if not shouldIgnoreLine then
|
||||
local level = #strmatch(localLine, "^ *")
|
||||
localLine = strrep(" ", level)..strtrim(localLine)
|
||||
localLine = gsub(localLine, "Interface\\[Aa]dd[Oo]ns\\TradeSkillMaster", "TSM")
|
||||
localLine = gsub(localLine, "\124", "\\124")
|
||||
for matchStr, replaceStr in pairs(private.globalNameTranslation) do
|
||||
localLine = gsub(localLine, matchStr, replaceStr)
|
||||
end
|
||||
if level > 0 then
|
||||
if isBlizzardFile then
|
||||
-- for Blizzard stack frames, only include level 0 locals
|
||||
shouldIgnoreLine = true
|
||||
elseif strmatch(localLine, "^ *[_]*[A-Z].+@TSM") then
|
||||
-- ignore table methods (based on their name being UpperCamelCase - potentially with leading underscores)
|
||||
shouldIgnoreLine = true
|
||||
elseif isLocaleTable then
|
||||
-- ignore everything within the locale table
|
||||
shouldIgnoreLine = true
|
||||
elseif isPackageTable then
|
||||
-- ignore the package table completely
|
||||
shouldIgnoreLine = true
|
||||
elseif (isSelfTable or isPrivateTable) and strmatch(localLine, "^ *[_a-zA-Z0-9]+ = {}") then
|
||||
-- ignore empty tables within objects or the private table
|
||||
shouldIgnoreLine = true
|
||||
elseif strmatch(localLine, "^%s+0 = <userdata>$") then
|
||||
-- remove userdata table entries
|
||||
shouldIgnoreLine = true
|
||||
end
|
||||
end
|
||||
if not shouldIgnoreLine then
|
||||
tinsert(private.localLinesTemp, localLine)
|
||||
end
|
||||
if level == 0 then
|
||||
isPackageTable = strmatch(localLine, "%s*"..fileName.." = {") and true or false
|
||||
isPrivateTable = strmatch(localLine, "%s*private = {") and true or false
|
||||
isLocaleTable = strmatch(localLine, "%s*L = {") and true or false
|
||||
isSelfTable = strmatch(localLine, "%s*self = {") and true or false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- remove any top-level empty tables
|
||||
local i = #private.localLinesTemp
|
||||
while i > 0 do
|
||||
if i > 1 and private.localLinesTemp[i] == "}" and strmatch(private.localLinesTemp[i - 1], "^[A-Za-z_].* = {$") then
|
||||
tremove(private.localLinesTemp, i)
|
||||
tremove(private.localLinesTemp, i - 1)
|
||||
i = i - 2
|
||||
elseif strmatch(private.localLinesTemp[i], "^[A-Za-z_].* = {}$") then
|
||||
tremove(private.localLinesTemp, i)
|
||||
i = i - 1
|
||||
else
|
||||
i = i - 1
|
||||
end
|
||||
end
|
||||
return #private.localLinesTemp > 0 and table.concat(private.localLinesTemp, "\n") or nil
|
||||
end
|
||||
|
||||
function private.IsTSMAddon(str)
|
||||
if strfind(str, "Auc-Adcanced\\CoreScan.lua") then
|
||||
-- ignore auctioneer errors
|
||||
return nil
|
||||
elseif strfind(str, "Master\\External\\") then
|
||||
-- ignore errors from libraries
|
||||
return nil
|
||||
elseif strfind(str, "Master\\Core\\API.lua") then
|
||||
-- ignore errors from public APIs
|
||||
return nil
|
||||
elseif strfind(str, "Master_AppHelper\\") then
|
||||
return "TradeSkillMaster_AppHelper"
|
||||
elseif strfind(str, "lMaster\\") then
|
||||
return "TradeSkillMaster"
|
||||
elseif strfind(str, "ster\\Core\\UI\\") then
|
||||
return "TradeSkillMaster"
|
||||
elseif strfind(str, "r\\LibTSM\\") then
|
||||
return "TradeSkillMaster"
|
||||
elseif strfind(str, "^TSM\\") then
|
||||
return "TradeSkillMaster"
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function private.AddonBlockedHandler(event, addonName, addonFunc)
|
||||
if not strmatch(addonName, "TradeSkillMaster") then
|
||||
return
|
||||
end
|
||||
-- just log it - it might not be TSM that cause the taint
|
||||
Log.Err("[%s] AddOn '%s' tried to call the protected function '%s'.", event, addonName or "<name>", addonFunc or "<func>")
|
||||
end
|
||||
|
||||
function private.SanitizeString(str)
|
||||
str = gsub(str, "\124cff[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]([^\124]+)\124r", "%1")
|
||||
str = gsub(str, "[\\]+", "/")
|
||||
str = gsub(str, "\"", "'")
|
||||
return str
|
||||
end
|
||||
|
||||
function private.FormatErrorMessageSection(heading, info, isMultiLine)
|
||||
-- replace unprintable characters with "?"
|
||||
info = gsub(info, "[^\t\n -~]", "?")
|
||||
local prefix = nil
|
||||
if isMultiLine then
|
||||
prefix = info ~= "" and "\n " or ""
|
||||
info = gsub(info, "\n", "\n ")
|
||||
else
|
||||
prefix = info ~= "" and " " or ""
|
||||
end
|
||||
return "|cff99ffff"..heading..":|r"..prefix..info
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Error Frame
|
||||
-- ============================================================================
|
||||
|
||||
function private.CreateErrorFrame()
|
||||
if private.errorFrame then
|
||||
return
|
||||
end
|
||||
local STEPS_TEXT = "Steps leading up to the error:\n1) List\n2) Steps\n3) Here"
|
||||
local frame = CreateFrame("Frame", nil, UIParent, TSM.IsShadowlands() and "BackdropTemplate" or nil)
|
||||
private.errorFrame = frame
|
||||
frame:Hide()
|
||||
frame:SetWidth(500)
|
||||
frame:SetHeight(400)
|
||||
frame:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
frame:SetPoint("RIGHT", -100, 0)
|
||||
frame:SetBackdrop({
|
||||
bgFile = "Interface\\Buttons\\WHITE8X8",
|
||||
edgeFile = "Interface\\Buttons\\WHITE8X8",
|
||||
edgeSize = 2,
|
||||
})
|
||||
frame:SetBackdropColor(0, 0, 0, 1)
|
||||
frame:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)
|
||||
frame:SetScript("OnShow", function(self)
|
||||
self.showingError = self.isManual or TSM.IsDevVersion()
|
||||
self.details = STEPS_TEXT
|
||||
if self.showingError then
|
||||
-- this is a dev version so show the error (only)
|
||||
self.text:SetText("Looks like TradeSkillMaster has encountered an error.")
|
||||
self.switchBtn:SetText("Hide Error")
|
||||
self.editBox:SetText(self.errorStr)
|
||||
else
|
||||
self.text:SetText("Looks like TradeSkillMaster has encountered an error. Please provide the steps which lead to this error to help the TSM team fix it, then click either button at the bottom of the window to automatically report this error.")
|
||||
self.switchBtn:SetText("Show Error")
|
||||
self.editBox:SetText(self.details)
|
||||
end
|
||||
end)
|
||||
frame:SetScript("OnHide", function()
|
||||
local details = private.errorFrame.showingError and private.errorFrame.details or private.errorFrame.editBox:GetText()
|
||||
local changedDetails = details ~= STEPS_TEXT
|
||||
if (not TSM.IsDevVersion() and not private.errorFrame.isManual and (changedDetails or private.num == 1)) or IsShiftKeyDown() then
|
||||
tinsert(private.errorReports, {
|
||||
errorInfo = private.errorFrame.errorInfo,
|
||||
details = private.SanitizeString(details),
|
||||
timestamp = time(),
|
||||
})
|
||||
end
|
||||
private.errorSuppressed = nil
|
||||
end)
|
||||
|
||||
local title = frame:CreateFontString()
|
||||
title:SetHeight(20)
|
||||
title:SetPoint("TOPLEFT", 0, -10)
|
||||
title:SetPoint("TOPRIGHT", 0, -10)
|
||||
title:SetFontObject(GameFontNormalLarge)
|
||||
title:SetTextColor(1, 1, 1, 1)
|
||||
title:SetJustifyH("CENTER")
|
||||
title:SetJustifyV("MIDDLE")
|
||||
local status, versionText = pcall(TSM.GetVersion)
|
||||
versionText = status and versionText or "?"
|
||||
title:SetText("TSM Error Window ("..versionText..")")
|
||||
|
||||
local hLine = frame:CreateTexture(nil, "ARTWORK")
|
||||
hLine:SetHeight(2)
|
||||
hLine:SetColorTexture(0.3, 0.3, 0.3, 1)
|
||||
hLine:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -10)
|
||||
hLine:SetPoint("TOPRIGHT", title, "BOTTOMRIGHT", 0, -10)
|
||||
|
||||
local text = frame:CreateFontString()
|
||||
frame.text = text
|
||||
text:SetHeight(45)
|
||||
text:SetPoint("TOPLEFT", hLine, "BOTTOMLEFT", 8, -8)
|
||||
text:SetPoint("TOPRIGHT", hLine, "BOTTOMRIGHT", -8, -8)
|
||||
text:SetFontObject(GameFontNormal)
|
||||
text:SetTextColor(1, 1, 1, 1)
|
||||
text:SetJustifyH("LEFT")
|
||||
text:SetJustifyV("MIDDLE")
|
||||
|
||||
local switchBtn = CreateFrame("Button", nil, frame)
|
||||
frame.switchBtn = switchBtn
|
||||
switchBtn:SetPoint("TOPRIGHT", -4, -10)
|
||||
switchBtn:SetWidth(100)
|
||||
switchBtn:SetHeight(20)
|
||||
local fontString = switchBtn:CreateFontString()
|
||||
fontString:SetFontObject(GameFontNormalSmall)
|
||||
fontString:SetJustifyH("CENTER")
|
||||
fontString:SetJustifyV("MIDDLE")
|
||||
switchBtn:SetFontString(fontString)
|
||||
switchBtn:SetScript("OnClick", function(self)
|
||||
private.errorFrame.showingError = not private.errorFrame.showingError
|
||||
if private.errorFrame.showingError then
|
||||
private.errorFrame.details = private.errorFrame.editBox:GetText()
|
||||
self:SetText("Hide Error")
|
||||
private.errorFrame.editBox:SetText(private.errorFrame.errorStr)
|
||||
else
|
||||
self:SetText("Show Error")
|
||||
private.errorFrame.editBox:SetText(private.errorFrame.details)
|
||||
end
|
||||
end)
|
||||
|
||||
local hLine2 = frame:CreateTexture(nil, "ARTWORK")
|
||||
hLine2:SetHeight(2)
|
||||
hLine2:SetColorTexture(0.3, 0.3, 0.3, 1)
|
||||
hLine2:SetPoint("TOPLEFT", text, "BOTTOMLEFT", -8, -4)
|
||||
hLine2:SetPoint("TOPRIGHT", text, "BOTTOMRIGHT", 8, -4)
|
||||
|
||||
local scrollFrame = CreateFrame("ScrollFrame", nil, frame, "UIPanelScrollFrameTemplate")
|
||||
scrollFrame:SetPoint("TOPLEFT", hLine2, "BOTTOMLEFT", 8, -4)
|
||||
scrollFrame:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -26, 38)
|
||||
|
||||
local editBox = CreateFrame("EditBox", nil, scrollFrame)
|
||||
frame.editBox = editBox
|
||||
editBox:SetWidth(scrollFrame:GetWidth())
|
||||
editBox:SetFontObject(ChatFontNormal)
|
||||
editBox:SetMultiLine(true)
|
||||
editBox:SetAutoFocus(false)
|
||||
editBox:SetMaxLetters(0)
|
||||
editBox:SetTextColor(1, 1, 1, 1)
|
||||
editBox:SetScript("OnUpdate", function(self)
|
||||
local offset = scrollFrame:GetVerticalScroll()
|
||||
self:SetHitRectInsets(0, 0, offset, self:GetHeight() - offset - scrollFrame:GetHeight())
|
||||
end)
|
||||
editBox:SetScript("OnEditFocusGained", function(self)
|
||||
self:HighlightText()
|
||||
end)
|
||||
editBox:SetScript("OnCursorChanged", function(self)
|
||||
if private.errorFrame.showingError and self:HasFocus() then
|
||||
self:HighlightText()
|
||||
end
|
||||
end)
|
||||
editBox:SetScript("OnEscapePressed", function(self)
|
||||
if private.errorFrame.showingError then
|
||||
self:HighlightText(0, 0)
|
||||
end
|
||||
self:ClearFocus()
|
||||
end)
|
||||
scrollFrame:SetScrollChild(editBox)
|
||||
|
||||
local hLine3 = frame:CreateTexture(nil, "ARTWORK")
|
||||
hLine3:SetHeight(2)
|
||||
hLine3:SetColorTexture(0.3, 0.3, 0.3, 1)
|
||||
hLine3:SetPoint("BOTTOMLEFT", frame, 0, 35)
|
||||
hLine3:SetPoint("BOTTOMRIGHT", frame, 0, 35)
|
||||
|
||||
local reloadBtn = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
|
||||
frame.reloadBtn = reloadBtn
|
||||
reloadBtn:SetPoint("BOTTOMLEFT", 4, 4)
|
||||
reloadBtn:SetWidth(120)
|
||||
reloadBtn:SetHeight(30)
|
||||
reloadBtn:SetText(RELOADUI)
|
||||
reloadBtn:SetScript("OnClick", function()
|
||||
frame:Hide()
|
||||
ReloadUI()
|
||||
end)
|
||||
|
||||
local closeBtn = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
|
||||
frame.closeBtn = closeBtn
|
||||
closeBtn:SetPoint("BOTTOMRIGHT", -4, 4)
|
||||
closeBtn:SetWidth(120)
|
||||
closeBtn:SetHeight(30)
|
||||
closeBtn:SetText(DONE)
|
||||
closeBtn:SetScript("OnClick", function()
|
||||
frame:Hide()
|
||||
end)
|
||||
|
||||
local stepsText = frame:CreateFontString()
|
||||
frame.stepsText = stepsText
|
||||
stepsText:SetWidth(200)
|
||||
stepsText:SetHeight(30)
|
||||
stepsText:SetPoint("BOTTOM", 0, 4)
|
||||
stepsText:SetFontObject(GameFontNormal)
|
||||
stepsText:SetTextColor(1, 0, 0, 1)
|
||||
stepsText:SetJustifyH("CENTER")
|
||||
stepsText:SetJustifyV("MIDDLE")
|
||||
stepsText:SetText("Please enter steps before submitting")
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Register Error Handler
|
||||
-- ============================================================================
|
||||
|
||||
do
|
||||
private.origErrorHandler = geterrorhandler()
|
||||
local function ErrorHandlerFunc(errMsg, isBugGrabber)
|
||||
local tsmErrMsg = strtrim(tostring(errMsg))
|
||||
if private.ignoreErrors then
|
||||
-- we're ignoring errors
|
||||
tsmErrMsg = nil
|
||||
elseif strmatch(tsmErrMsg, "auc%-stat%-wowuction") or strmatch(tsmErrMsg, "TheUndermineJournal%.lua") or strmatch(tsmErrMsg, "\\SavedVariables\\TradeSkillMaster") or strmatch(tsmErrMsg, "AddOn TradeSkillMaster[_a-zA-Z]* attempted") or (strmatch(tsmErrMsg, "ItemTooltipClasses\\Wrapper%.lua:98") and strmatch(tsmErrMsg, "SetQuest")) then
|
||||
-- explicitly ignore these errors
|
||||
tsmErrMsg = nil
|
||||
end
|
||||
if tsmErrMsg then
|
||||
-- look at the stack trace to see if this is a TSM error
|
||||
for i = 2, MAX_STACK_DEPTH do
|
||||
local stackLine = debugstack(i, 1, 0)
|
||||
local oldModule = strmatch(stackLine, "(lMaster_[A-Za-z]+)")
|
||||
if oldModule and tContains(OLD_TSM_MODULES, "TradeSkil"..oldModule) then
|
||||
-- ignore errors from old modules
|
||||
return
|
||||
end
|
||||
if not strmatch(stackLine, "^%[C%]:") and not strmatch(stackLine, "%(tail call%):") and not strmatch(stackLine, "^%[string \"[^@]") and not strmatch(stackLine, "lMaster\\External\\[A-Za-z0-9%-_%.]+\\") and not strmatch(stackLine, "SharedXML") and not strmatch(stackLine, "CallbackHandler") and not strmatch(stackLine, "!BugGrabber") and not strmatch(stackLine, "ErrorHandler%.lua") then
|
||||
if not private.IsTSMAddon(stackLine) then
|
||||
tsmErrMsg = nil
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if tsmErrMsg then
|
||||
local status, ret = pcall(private.ErrorHandler, tsmErrMsg)
|
||||
if status and ret then
|
||||
return ret
|
||||
elseif not status and not private.hitInternalError then
|
||||
private.hitInternalError = true
|
||||
print("Internal TSM error: "..tostring(ret))
|
||||
end
|
||||
end
|
||||
local oldModule = strmatch(errMsg, "(lMaster_[A-Za-z]+)")
|
||||
if oldModule and tContains(OLD_TSM_MODULES, "TradeSkil"..oldModule) then
|
||||
-- ignore errors from old modules
|
||||
return
|
||||
end
|
||||
if not isBugGrabber then
|
||||
return private.origErrorHandler and private.origErrorHandler(errMsg) or nil
|
||||
end
|
||||
end
|
||||
seterrorhandler(ErrorHandlerFunc)
|
||||
if BugGrabber and BugGrabber.RegisterCallback then
|
||||
BugGrabber.RegisterCallback({}, "BugGrabber_BugGrabbed", function(_, errObj)
|
||||
ErrorHandlerFunc(errObj.message, true)
|
||||
end)
|
||||
end
|
||||
Event.Register("ADDON_ACTION_FORBIDDEN", private.AddonBlockedHandler)
|
||||
Event.Register("ADDON_ACTION_BLOCKED", private.AddonBlockedHandler)
|
||||
end
|
||||
272
LibTSM/Service/GuildTracking.lua
Normal file
272
LibTSM/Service/GuildTracking.lua
Normal file
@@ -0,0 +1,272 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local GuildTracking = TSM.Init("Service.GuildTracking")
|
||||
local Database = TSM.Include("Util.Database")
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local SlotId = TSM.Include("Util.SlotId")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local private = {
|
||||
settings = nil,
|
||||
slotDB = nil,
|
||||
quantityDB = nil,
|
||||
isOpen = nil,
|
||||
pendingPetSlotIds = {},
|
||||
}
|
||||
local PLAYER_NAME = UnitName("player")
|
||||
local PLAYER_GUILD = nil
|
||||
local MAX_PET_SCANS = 10
|
||||
-- don't use MAX_GUILDBANK_SLOTS_PER_TAB since it isn't available right away
|
||||
local GUILD_BANK_TAB_SLOTS = 98
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
GuildTracking:OnSettingsLoad(function()
|
||||
private.settings = Settings.NewView()
|
||||
:AddKey("factionrealm", "internalData", "characterGuilds")
|
||||
:AddKey("factionrealm", "internalData", "guildVaults")
|
||||
private.slotDB = Database.NewSchema("GUILD_TRACKING_SLOTS")
|
||||
:AddUniqueNumberField("slotId")
|
||||
:AddNumberField("tab")
|
||||
:AddNumberField("slot")
|
||||
:AddStringField("itemString")
|
||||
:AddSmartMapField("baseItemString", ItemString.GetBaseMap(), "itemString")
|
||||
:AddNumberField("quantity")
|
||||
:AddIndex("slotId")
|
||||
:AddIndex("itemString")
|
||||
:Commit()
|
||||
private.quantityDB = Database.NewSchema("GUILD_TRACKING_QUANTITY")
|
||||
:AddUniqueStringField("itemString")
|
||||
:AddNumberField("quantity")
|
||||
:Commit()
|
||||
if not TSM.IsWowClassic() then
|
||||
Event.Register("GUILDBANKFRAME_OPENED", private.GuildBankFrameOpenedHandler)
|
||||
Event.Register("GUILDBANKFRAME_CLOSED", private.GuildBankFrameClosedHandler)
|
||||
Event.Register("GUILDBANKBAGSLOTS_CHANGED", private.GuildBankBagSlotsChangedHandler)
|
||||
Delay.AfterFrame(1, private.GetGuildName)
|
||||
Event.Register("PLAYER_GUILD_UPDATE", private.GetGuildName)
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function GuildTracking.BaseItemIterator()
|
||||
return private.quantityDB:NewQuery()
|
||||
:Select("itemString")
|
||||
:IteratorAndRelease()
|
||||
end
|
||||
|
||||
function GuildTracking.CreateQuery()
|
||||
return private.slotDB:NewQuery()
|
||||
end
|
||||
|
||||
function GuildTracking.CreateQueryItem(itemString)
|
||||
local query = GuildTracking.CreateQuery()
|
||||
if itemString == ItemString.GetBaseFast(itemString) then
|
||||
query:Equal("baseItemString", itemString)
|
||||
else
|
||||
query:Equal("itemString", itemString)
|
||||
end
|
||||
return query
|
||||
end
|
||||
|
||||
function GuildTracking.GetQuantityByBaseItemString(baseItemString)
|
||||
return private.quantityDB:GetUniqueRowField("itemString", baseItemString, "quantity") or 0
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.GetGuildName()
|
||||
if not IsInGuild() then
|
||||
private.settings.characterGuilds[PLAYER_NAME] = nil
|
||||
return
|
||||
end
|
||||
PLAYER_GUILD = GetGuildInfo("player")
|
||||
if not PLAYER_GUILD then
|
||||
-- try again next frame
|
||||
Delay.AfterFrame(1, private.GetGuildName)
|
||||
return
|
||||
end
|
||||
|
||||
private.settings.characterGuilds[PLAYER_NAME] = PLAYER_GUILD
|
||||
|
||||
-- clean up any guilds with no players in them
|
||||
local validGuilds = TempTable.Acquire()
|
||||
for _, character in Settings.CharacterByAccountFactionrealmIterator() do
|
||||
local guild = private.settings.characterGuilds[character]
|
||||
if guild then
|
||||
validGuilds[guild] = true
|
||||
end
|
||||
end
|
||||
for character, guild in pairs(private.settings.characterGuilds) do
|
||||
if not validGuilds[guild] then
|
||||
private.settings.characterGuilds[character] = nil
|
||||
end
|
||||
end
|
||||
for guild in pairs(private.settings.guildVaults) do
|
||||
if not validGuilds[guild] then
|
||||
private.settings.guildVaults[guild] = nil
|
||||
end
|
||||
end
|
||||
TempTable.Release(validGuilds)
|
||||
|
||||
private.settings.guildVaults[PLAYER_GUILD] = private.settings.guildVaults[PLAYER_GUILD] or {}
|
||||
for itemString, quantity in pairs(private.settings.guildVaults[PLAYER_GUILD]) do
|
||||
if quantity <= 0 or itemString ~= ItemString.GetBase(itemString) then
|
||||
private.settings.guildVaults[PLAYER_GUILD][itemString] = nil
|
||||
end
|
||||
end
|
||||
private.RebuildQuantityDB()
|
||||
end
|
||||
|
||||
function private.RebuildQuantityDB()
|
||||
private.quantityDB:TruncateAndBulkInsertStart()
|
||||
for itemString, quantity in pairs(private.settings.guildVaults[PLAYER_GUILD]) do
|
||||
if quantity > 0 then
|
||||
private.quantityDB:BulkInsertNewRow(itemString, quantity)
|
||||
else
|
||||
private.settings.guildVaults[PLAYER_GUILD][itemString] = nil
|
||||
end
|
||||
end
|
||||
private.quantityDB:BulkInsertEnd()
|
||||
end
|
||||
|
||||
function private.GuildBankFrameOpenedHandler()
|
||||
local initialTab = GetCurrentGuildBankTab()
|
||||
for i = 1, GetNumGuildBankTabs() do
|
||||
QueryGuildBankTab(i)
|
||||
end
|
||||
QueryGuildBankTab(initialTab)
|
||||
private.isOpen = true
|
||||
end
|
||||
|
||||
function private.GuildBankFrameClosedHandler()
|
||||
private.isOpen = nil
|
||||
end
|
||||
|
||||
function private.GuildBankBagSlotsChangedHandler()
|
||||
Delay.AfterFrame("guildBankScan", 2, private.GuildBankChangedDelayed)
|
||||
end
|
||||
|
||||
function private.GuildBankChangedDelayed()
|
||||
if not private.isOpen then
|
||||
return
|
||||
end
|
||||
if not PLAYER_GUILD then
|
||||
-- we don't have the guild name yet, so try again after a short delay
|
||||
Delay.AfterFrame("guildBankScan", 2, private.GuildBankChangedDelayed)
|
||||
return
|
||||
end
|
||||
private.ScanGuildBank()
|
||||
end
|
||||
|
||||
function private.ScanGuildBank()
|
||||
wipe(private.settings.guildVaults[PLAYER_GUILD])
|
||||
wipe(private.pendingPetSlotIds)
|
||||
private.slotDB:TruncateAndBulkInsertStart()
|
||||
local didFail = false
|
||||
for tab = 1, GetNumGuildBankTabs() do
|
||||
-- only scan tabs which we have at least enough withdrawals to withdraw every slot
|
||||
local _, _, _, _, numWithdrawals = GetGuildBankTabInfo(tab)
|
||||
if numWithdrawals == -1 or numWithdrawals >= GUILD_BANK_TAB_SLOTS then
|
||||
for slot = 1, GUILD_BANK_TAB_SLOTS do
|
||||
local itemLink = GetGuildBankItemLink(tab, slot)
|
||||
if itemLink then
|
||||
local slotId = SlotId.Join(tab, slot)
|
||||
local baseItemString = ItemString.GetBase(itemLink)
|
||||
if baseItemString == ItemString.GetPetCage() then
|
||||
private.pendingPetSlotIds[slotId] = true
|
||||
baseItemString = nil
|
||||
end
|
||||
if baseItemString then
|
||||
local _, quantity = GetGuildBankItemInfo(tab, slot)
|
||||
if quantity == 0 then
|
||||
-- the info for this slot isn't fully loaded yet
|
||||
Log.Err("Failed to scan guild bank slot (%d)", slotId)
|
||||
didFail = true
|
||||
break
|
||||
end
|
||||
private.settings.guildVaults[PLAYER_GUILD][baseItemString] = (private.settings.guildVaults[PLAYER_GUILD][baseItemString] or 0) + quantity
|
||||
local itemString = ItemString.Get(itemLink)
|
||||
private.slotDB:BulkInsertNewRow(slotId, tab, slot, itemString, quantity)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if didFail then
|
||||
break
|
||||
end
|
||||
end
|
||||
private.RebuildQuantityDB()
|
||||
private.slotDB:BulkInsertEnd()
|
||||
if didFail then
|
||||
Delay.AfterFrame("guildBankScan", 2, private.GuildBankChangedDelayed)
|
||||
elseif next(private.pendingPetSlotIds) then
|
||||
Delay.AfterFrame("guildBankPetScan", 2, private.ScanPetsDeferred)
|
||||
else
|
||||
Delay.Cancel("guildBankPetScan")
|
||||
end
|
||||
end
|
||||
|
||||
function private.ScanPetsDeferred()
|
||||
if not TSMScanTooltip then
|
||||
CreateFrame("GameTooltip", "TSMScanTooltip", UIParent, "GameTooltipTemplate")
|
||||
end
|
||||
TSMScanTooltip:SetOwner(UIParent, "ANCHOR_NONE")
|
||||
TSMScanTooltip:ClearLines()
|
||||
|
||||
local numPetSlotIdsScanned = 0
|
||||
local toRemove = TempTable.Acquire()
|
||||
private.slotDB:BulkInsertStart()
|
||||
for slotId in pairs(private.pendingPetSlotIds) do
|
||||
local tab, slot = SlotId.Split(slotId)
|
||||
local speciesId, level, rarity = TSMScanTooltip:SetGuildBankItem(tab, slot)
|
||||
if speciesId and level and rarity then
|
||||
local itemString = "p:"..speciesId..":"..level..":"..rarity
|
||||
if itemString then
|
||||
tinsert(toRemove, slotId)
|
||||
local _, quantity = GetGuildBankItemInfo(tab, slot)
|
||||
local baseItemString = ItemString.GetBase(itemString)
|
||||
private.settings.guildVaults[PLAYER_GUILD][baseItemString] = (private.settings.guildVaults[PLAYER_GUILD][baseItemString] or 0) + quantity
|
||||
private.slotDB:BulkInsertNewRow(slotId, tab, slot, itemString, quantity)
|
||||
end
|
||||
end
|
||||
-- throttle how many pet slots we scan per call (regardless of whether or not it was successful)
|
||||
numPetSlotIdsScanned = numPetSlotIdsScanned + 1
|
||||
if numPetSlotIdsScanned == MAX_PET_SCANS then
|
||||
break
|
||||
end
|
||||
end
|
||||
private.RebuildQuantityDB()
|
||||
private.slotDB:BulkInsertEnd()
|
||||
Log.Info("Scanned %d pet slots", numPetSlotIdsScanned)
|
||||
for _, slotId in ipairs(toRemove) do
|
||||
private.pendingPetSlotIds[slotId] = nil
|
||||
end
|
||||
TempTable.Release(toRemove)
|
||||
|
||||
if next(private.pendingPetSlotIds) then
|
||||
-- there are more to scan
|
||||
Delay.AfterFrame("guildBankPetScan", 2, private.ScanPetsDeferred)
|
||||
end
|
||||
end
|
||||
149
LibTSM/Service/Inventory.lua
Normal file
149
LibTSM/Service/Inventory.lua
Normal file
@@ -0,0 +1,149 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Inventory = TSM.Init("Service.Inventory")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local CustomPrice = TSM.Include("Service.CustomPrice")
|
||||
local BagTracking = TSM.Include("Service.BagTracking")
|
||||
local AuctionTracking = TSM.Include("Service.AuctionTracking")
|
||||
local MailTracking = TSM.Include("Service.MailTracking")
|
||||
local Sync = TSM.Include("Service.Sync")
|
||||
local private = {
|
||||
settings = nil,
|
||||
callbacks = {},
|
||||
}
|
||||
local PLAYER_NAME = UnitName("player")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Inventory:OnSettingsLoad(function()
|
||||
private.settings = Settings.NewView()
|
||||
:AddKey("factionrealm", "internalData", "pendingMail")
|
||||
:AddKey("factionrealm", "coreOptions", "ignoreGuilds")
|
||||
:AddKey("factionrealm", "internalData", "guildVaults")
|
||||
BagTracking.RegisterCallback(private.QuantityChangedCallback)
|
||||
AuctionTracking.RegisterCallback(private.QuantityChangedCallback)
|
||||
MailTracking.RegisterCallback(private.QuantityChangedCallback)
|
||||
Sync.RegisterMirrorCallback(private.QuantityChangedCallback)
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Inventory.RegisterCallback(callback)
|
||||
tinsert(private.callbacks, callback)
|
||||
end
|
||||
|
||||
function Inventory.GetBagQuantity(itemString, character, factionrealm)
|
||||
return private.InventoryQuantityHelper(itemString, "bagQuantity", character, factionrealm)
|
||||
end
|
||||
|
||||
function Inventory.GetBankQuantity(itemString, character, factionrealm)
|
||||
return private.InventoryQuantityHelper(itemString, "bankQuantity", character, factionrealm)
|
||||
end
|
||||
|
||||
function Inventory.GetReagentBankQuantity(itemString, character, factionrealm)
|
||||
return private.InventoryQuantityHelper(itemString, "reagentBankQuantity", character, factionrealm)
|
||||
end
|
||||
|
||||
function Inventory.GetAuctionQuantity(itemString, character, factionrealm)
|
||||
return private.InventoryQuantityHelper(itemString, "auctionQuantity", character, factionrealm)
|
||||
end
|
||||
|
||||
function Inventory.GetMailQuantity(itemString, character, factionrealm)
|
||||
itemString = ItemString.GetBaseFast(itemString)
|
||||
character = character or PLAYER_NAME
|
||||
local pendingQuantity = itemString and private.settings.pendingMail[character] and private.settings.pendingMail[character][itemString] or 0
|
||||
return private.InventoryQuantityHelper(itemString, "mailQuantity", character, factionrealm) + pendingQuantity
|
||||
end
|
||||
|
||||
function Inventory.GetGuildQuantity(itemString, guild)
|
||||
itemString = ItemString.GetBase(itemString)
|
||||
if not itemString then
|
||||
return 0
|
||||
end
|
||||
guild = guild or (IsInGuild() and GetGuildInfo("player") or nil)
|
||||
if not guild or private.settings.ignoreGuilds[guild] then
|
||||
return 0
|
||||
end
|
||||
return private.settings.guildVaults[guild] and private.settings.guildVaults[guild][itemString] or 0
|
||||
end
|
||||
|
||||
function Inventory.GetPlayerTotals(itemString)
|
||||
itemString = ItemString.GetBaseFast(itemString)
|
||||
local numPlayer, numAlts, numAuctions, numAltAuctions = 0, 0, 0, 0
|
||||
numPlayer = numPlayer + Inventory.GetBagQuantity(itemString)
|
||||
numPlayer = numPlayer + Inventory.GetBankQuantity(itemString)
|
||||
numPlayer = numPlayer + Inventory.GetReagentBankQuantity(itemString)
|
||||
numPlayer = numPlayer + Inventory.GetMailQuantity(itemString)
|
||||
numAuctions = numAuctions + Inventory.GetAuctionQuantity(itemString)
|
||||
for _, factionrealm, character in Settings.ConnectedFactionrealmAltCharacterIterator() do
|
||||
numAlts = numAlts + Inventory.GetBagQuantity(itemString, character, factionrealm)
|
||||
numAlts = numAlts + Inventory.GetBankQuantity(itemString, character, factionrealm)
|
||||
numAlts = numAlts + Inventory.GetReagentBankQuantity(itemString, character, factionrealm)
|
||||
numAlts = numAlts + Inventory.GetMailQuantity(itemString, character, factionrealm)
|
||||
local auctionQuantity = Inventory.GetAuctionQuantity(itemString, character, factionrealm)
|
||||
numAltAuctions = numAltAuctions + auctionQuantity
|
||||
numAuctions = numAuctions + auctionQuantity
|
||||
end
|
||||
return numPlayer, numAlts, numAuctions, numAltAuctions
|
||||
end
|
||||
|
||||
function Inventory.GetGuildTotal(itemString)
|
||||
itemString = ItemString.GetBaseFast(itemString)
|
||||
if not itemString then
|
||||
return 0
|
||||
end
|
||||
local numGuild = 0
|
||||
for guild, data in pairs(private.settings.guildVaults) do
|
||||
if not private.settings.ignoreGuilds[guild] then
|
||||
numGuild = numGuild + (data[itemString] or 0)
|
||||
end
|
||||
end
|
||||
return numGuild
|
||||
end
|
||||
|
||||
function Inventory.GetTotalQuantity(itemString)
|
||||
local numPlayer, numAlts, numAuctions = Inventory.GetPlayerTotals(itemString)
|
||||
local numGuild = Inventory.GetGuildTotal(itemString)
|
||||
return numPlayer + numAlts + numAuctions + numGuild
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.GetCharacterInventoryData(settingKey, character, factionrealm)
|
||||
local scopeKey = character and Settings.GetSyncScopeKeyByCharacter(character, factionrealm) or nil
|
||||
return Settings.Get("sync", scopeKey, "internalData", settingKey)
|
||||
end
|
||||
|
||||
function private.InventoryQuantityHelper(itemString, settingKey, character, factionrealm)
|
||||
itemString = ItemString.GetBase(itemString)
|
||||
if not itemString then
|
||||
return 0
|
||||
end
|
||||
local tbl = private.GetCharacterInventoryData(settingKey, character, factionrealm)
|
||||
return tbl and tbl[itemString] or 0
|
||||
end
|
||||
|
||||
function private.QuantityChangedCallback()
|
||||
CustomPrice.OnSourceChange("NumInventory")
|
||||
for _, callback in ipairs(private.callbacks) do
|
||||
callback()
|
||||
end
|
||||
end
|
||||
232
LibTSM/Service/InventoryInfo.lua
Normal file
232
LibTSM/Service/InventoryInfo.lua
Normal file
@@ -0,0 +1,232 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local InventoryInfo = TSM.Init("Service.InventoryInfo")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local SlotId = TSM.Include("Util.SlotId")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local Table = TSM.Include("Util.Table")
|
||||
local private = {
|
||||
slotIdLocked = {},
|
||||
slotIdSoulboundCached = {},
|
||||
slotIdIsBoP = {},
|
||||
slotIdIsBoA = {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
InventoryInfo:OnModuleLoad(function()
|
||||
Event.Register("ITEM_LOCKED", private.ItemLockedHandler)
|
||||
Event.Register("ITEM_UNLOCKED", private.ItemUnlockedHandler)
|
||||
Event.Register("BAG_UPDATE", private.BagUpdateHandler)
|
||||
Event.Register("PLAYERBANKSLOTS_CHANGED", private.BankSlotChangedHandler)
|
||||
if not TSM.IsWowClassic() then
|
||||
Event.Register("PLAYERREAGENTBANKSLOTS_CHANGED", private.ReagentBankSlotChangedHandler)
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Check if an item will go in a bag.
|
||||
-- @tparam string link The item
|
||||
-- @tparam number bag The bag index
|
||||
-- @treturn boolean Whether or not the item will go in the bag
|
||||
function InventoryInfo.ItemWillGoInBag(link, bag)
|
||||
if not link or not bag then
|
||||
return
|
||||
end
|
||||
if bag == BACKPACK_CONTAINER or bag == BANK_CONTAINER then
|
||||
return true
|
||||
elseif bag == REAGENTBANK_CONTAINER then
|
||||
return IsReagentBankUnlocked() and ItemInfo.IsCraftingReagent(link)
|
||||
end
|
||||
local itemFamily = GetItemFamily(link) or 0
|
||||
if ItemInfo.GetClassId(link) == LE_ITEM_CLASS_CONTAINER then
|
||||
-- bags report their family as what can go inside them, not what they can go inside
|
||||
itemFamily = 0
|
||||
end
|
||||
local _, bagFamily = GetContainerNumFreeSlots(bag)
|
||||
if not bagFamily then
|
||||
return
|
||||
end
|
||||
return bagFamily == 0 or bit.band(itemFamily, bagFamily) > 0
|
||||
end
|
||||
|
||||
function InventoryInfo.IsBagSlotLocked(bag, slot)
|
||||
return private.slotIdLocked[SlotId.Join(bag, slot)]
|
||||
end
|
||||
|
||||
function InventoryInfo.IsSoulbound(bag, slot)
|
||||
local slotId = SlotId.Join(bag, slot)
|
||||
if private.slotIdSoulboundCached[slotId] then
|
||||
return private.slotIdIsBoP[slotId], private.slotIdIsBoA[slotId]
|
||||
end
|
||||
if not TSMScanTooltip then
|
||||
CreateFrame("GameTooltip", "TSMScanTooltip", UIParent, "GameTooltipTemplate")
|
||||
end
|
||||
|
||||
TSMScanTooltip:SetOwner(UIParent, "ANCHOR_NONE")
|
||||
TSMScanTooltip:ClearLines()
|
||||
|
||||
if GetContainerItemID(bag, slot) == ItemString.ToId(ItemString.GetPetCage()) then
|
||||
-- battle pets are never BoP or BoA
|
||||
private.slotIdSoulboundCached[slotId] = true
|
||||
private.slotIdIsBoP[slotId] = false
|
||||
private.slotIdIsBoA[slotId] = false
|
||||
return false, false
|
||||
end
|
||||
|
||||
-- set TSMScanTooltip to show the inventory item
|
||||
if bag == BANK_CONTAINER then
|
||||
TSMScanTooltip:SetInventoryItem("player", BankButtonIDToInvSlotID(slot))
|
||||
elseif bag == REAGENTBANK_CONTAINER then
|
||||
TSMScanTooltip:SetInventoryItem("player", ReagentBankButtonIDToInvSlotID(slot))
|
||||
else
|
||||
TSMScanTooltip:SetBagItem(bag, slot)
|
||||
end
|
||||
|
||||
-- scan the tooltip
|
||||
local numLines = TSMScanTooltip:NumLines()
|
||||
if numLines < 1 then
|
||||
-- the tooltip didn't fully load or there's nothing in this slot
|
||||
return nil, nil
|
||||
end
|
||||
local isBOP, isBOA = false, false
|
||||
for id = 2, numLines do
|
||||
local text = private.GetTooltipText(_G["TSMScanTooltipTextLeft"..id])
|
||||
if text then
|
||||
if (text == ITEM_BIND_ON_PICKUP and id < 4) or text == ITEM_SOULBOUND or text == ITEM_BIND_QUEST then
|
||||
isBOP = true
|
||||
break
|
||||
elseif (text == ITEM_ACCOUNTBOUND or text == ITEM_BIND_TO_ACCOUNT or text == ITEM_BIND_TO_BNETACCOUNT or text == ITEM_BNETACCOUNTBOUND) then
|
||||
isBOA = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
private.slotIdSoulboundCached[slotId] = true
|
||||
private.slotIdIsBoP[slotId] = isBOP
|
||||
private.slotIdIsBoA[slotId] = isBOA
|
||||
return isBOP, isBOA
|
||||
end
|
||||
|
||||
function InventoryInfo.HasUsedCharges(bag, slot)
|
||||
-- figure out if this item has a max number of charges
|
||||
local itemId = GetContainerItemID(bag, slot)
|
||||
if not itemId or itemId == ItemString.ToId(ItemString.GetPetCage()) then
|
||||
return false
|
||||
end
|
||||
if not TSMScanTooltip then
|
||||
CreateFrame("GameTooltip", "TSMScanTooltip", UIParent, "GameTooltipTemplate")
|
||||
end
|
||||
|
||||
TSMScanTooltip:SetOwner(UIParent, "ANCHOR_NONE")
|
||||
TSMScanTooltip:ClearLines()
|
||||
TSMScanTooltip:SetItemByID(itemId)
|
||||
|
||||
local maxCharges = private.GetScanTooltipCharges()
|
||||
if not maxCharges then
|
||||
return false
|
||||
end
|
||||
|
||||
-- set TSMScanTooltip to show the inventory item
|
||||
if bag == BANK_CONTAINER then
|
||||
TSMScanTooltip:SetInventoryItem("player", BankButtonIDToInvSlotID(slot))
|
||||
elseif bag == REAGENTBANK_CONTAINER then
|
||||
TSMScanTooltip:SetInventoryItem("player", ReagentBankButtonIDToInvSlotID(slot))
|
||||
else
|
||||
TSMScanTooltip:SetBagItem(bag, slot)
|
||||
end
|
||||
|
||||
-- check if there are used charges
|
||||
if maxCharges and private.GetScanTooltipCharges() ~= maxCharges then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.ItemLockedHandler(_, bag, slot)
|
||||
if not slot then
|
||||
return
|
||||
end
|
||||
private.slotIdLocked[SlotId.Join(bag, slot)] = true
|
||||
end
|
||||
|
||||
function private.ItemUnlockedHandler(_, bag, slot)
|
||||
if not slot then
|
||||
return
|
||||
end
|
||||
private.slotIdLocked[SlotId.Join(bag, slot)] = nil
|
||||
end
|
||||
|
||||
function private.BagUpdateHandler(_, bag)
|
||||
-- clear the soulbound cache for everything in this bag
|
||||
Table.Filter(private.slotIdSoulboundCached, private.SlotIdSoulboundCachedFilter, bag)
|
||||
end
|
||||
|
||||
function private.BankSlotChangedHandler(_, slot)
|
||||
if slot <= NUM_BANKGENERIC_SLOTS then
|
||||
-- one of the slots of the primary bank container changed, so just clear the cache for this slot
|
||||
private.slotIdSoulboundCached[SlotId.Join(BANK_CONTAINER, slot)] = nil
|
||||
else
|
||||
-- one of the extra bank bags changed, so clear the cache for the entire bag
|
||||
Table.Filter(private.slotIdSoulboundCached, private.SlotIdSoulboundCachedFilter, slot - NUM_BANKGENERIC_SLOTS)
|
||||
end
|
||||
end
|
||||
|
||||
function private.SlotIdSoulboundCachedFilter(slotId, _, bag)
|
||||
return SlotId.Split(slotId) == bag
|
||||
end
|
||||
|
||||
function private.ReagentBankSlotChangedHandler(_, slot)
|
||||
-- clear the soulbound cache for this slot
|
||||
private.slotIdSoulboundCached[SlotId.Join(REAGENTBANK_CONTAINER, slot)] = nil
|
||||
end
|
||||
|
||||
function private.GetTooltipText(text)
|
||||
local textStr = strtrim(text and text:GetText() or "")
|
||||
if textStr == "" then return end
|
||||
|
||||
local r, g, b = text:GetTextColor()
|
||||
return textStr, floor(r * 256), floor(g * 256), floor(b * 256)
|
||||
end
|
||||
|
||||
function private.GetScanTooltipCharges()
|
||||
for id = 2, TSMScanTooltip:NumLines() do
|
||||
local text = private.GetTooltipText(_G["TSMScanTooltipTextLeft"..id])
|
||||
local num = text and strmatch(text, "%d+")
|
||||
local chargesStr = gsub(ITEM_SPELL_CHARGES, "%%d", "%%d+")
|
||||
if strfind(chargesStr, ":") then
|
||||
if num == 1 then
|
||||
chargesStr = gsub(chargesStr, "\1244(.+):.+;", "%1")
|
||||
else
|
||||
chargesStr = gsub(chargesStr, "\1244.+:(.+);", "%1")
|
||||
end
|
||||
end
|
||||
|
||||
local maxCharges = text and strmatch(text, "^"..chargesStr.."$")
|
||||
|
||||
if maxCharges then
|
||||
return maxCharges
|
||||
end
|
||||
end
|
||||
end
|
||||
436
LibTSM/Service/ItemFilter.lua
Normal file
436
LibTSM/Service/ItemFilter.lua
Normal file
@@ -0,0 +1,436 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local ItemFilter = TSM.Init("Service.ItemFilter")
|
||||
local LibTSMClass = TSM.Include("LibTSMClass")
|
||||
local L = TSM.Include("Locale").GetTable()
|
||||
local Filter = LibTSMClass.DefineClass("ItemFilter")
|
||||
local ItemClass = TSM.Include("Data.ItemClass")
|
||||
local Money = TSM.Include("Util.Money")
|
||||
local String = TSM.Include("Util.String")
|
||||
local Vararg = TSM.Include("Util.Vararg")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function ItemFilter.New()
|
||||
return Filter()
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Filter Class
|
||||
-- ============================================================================
|
||||
|
||||
function Filter.__init(self)
|
||||
self._isValid = nil
|
||||
self._str = nil
|
||||
self._escapedStr = nil
|
||||
self._class = nil
|
||||
self._subClass = nil
|
||||
self._invSlotId = nil
|
||||
self._minQuality = nil
|
||||
self._maxQuality = nil
|
||||
self._minLevel = nil
|
||||
self._maxLevel = nil
|
||||
self._minItemLevel = nil
|
||||
self._maxItemLevel = nil
|
||||
self._minPrice = nil
|
||||
self._maxPrice = nil
|
||||
self._maxQuantity = nil
|
||||
self._uncollected = nil
|
||||
self._usable = nil
|
||||
self._upgrades = nil
|
||||
self._unlearned = nil
|
||||
self._canlearn = nil
|
||||
self._exactOnly = nil
|
||||
self._crafting = nil
|
||||
self._disenchant = nil
|
||||
self._item = nil
|
||||
|
||||
self:_Reset()
|
||||
end
|
||||
|
||||
function Filter._Reset(self)
|
||||
self._isValid = nil
|
||||
self._str = ""
|
||||
self._escapedStr = ""
|
||||
self._class = nil
|
||||
self._subClass = nil
|
||||
self._invSlotId = nil
|
||||
self._minQuality = -math.huge
|
||||
self._maxQuality = math.huge
|
||||
self._minLevel = 0
|
||||
self._maxLevel = math.huge
|
||||
self._minItemLevel = 0
|
||||
self._maxItemLevel = math.huge
|
||||
self._minPrice = 0
|
||||
self._maxPrice = math.huge
|
||||
self._maxQuantity = math.huge
|
||||
self._uncollected = nil
|
||||
self._usable = nil
|
||||
self._upgrades = nil
|
||||
self._unlearned = nil
|
||||
self._canlearn = nil
|
||||
self._exactOnly = nil
|
||||
self._crafting = nil
|
||||
self._disenchant = nil
|
||||
self._item = nil
|
||||
end
|
||||
|
||||
function Filter._ItemQualityToIndex(self, str)
|
||||
for i = 0, 7 do
|
||||
local text = _G["ITEM_QUALITY"..i.."_DESC"]
|
||||
if strlower(str) == strlower(text) then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Filter.ParseStr(self, str)
|
||||
self:_Reset()
|
||||
local errMsg = nil
|
||||
local numLevelParts, numItemLevelParts, numPriceParts, numQualityParts = 0, 0, 0, 0
|
||||
self._isValid = nil
|
||||
local hasNonCraftingPart = false
|
||||
for i, part in Vararg.Iterator(strsplit("/", strtrim(str))) do
|
||||
part = strtrim(part)
|
||||
if self._isValid ~= nil then
|
||||
-- already done iterating, but can't break / return out of a VarargIterator
|
||||
if strmatch(part, "^[ip]:[0-9]+") then
|
||||
-- request item info in case we failed due to not having it (for next time)
|
||||
ItemInfo.FetchInfo(part)
|
||||
end
|
||||
elseif i == 1 then
|
||||
-- first part must be a filter string or an item
|
||||
if strmatch(part, "^[ip]:[0-9]+") then
|
||||
local name = ItemInfo.GetName(part)
|
||||
local level = ItemInfo.GetMinLevel(part)
|
||||
local quality = ItemInfo.GetQuality(part)
|
||||
if not name or not level or not quality then
|
||||
errMsg = L["The specified item was not found."]
|
||||
self._isValid = false
|
||||
else
|
||||
self._exactOnly = true
|
||||
self._item = part
|
||||
self._str = strlower(name)
|
||||
self._escapedStr = String.Escape(self._str)
|
||||
self._minQuality = quality
|
||||
self._maxQuality = quality
|
||||
self._minLevel = level
|
||||
self._maxLevel = level
|
||||
self._class = ItemInfo.GetClassId(self._item) or 0
|
||||
self._subClass = ItemInfo.GetSubClassId(self._item) or 0
|
||||
end
|
||||
else
|
||||
self._str = strlower(part)
|
||||
self._escapedStr = String.Escape(self._str)
|
||||
end
|
||||
elseif part == "" then
|
||||
-- ignore an empty part
|
||||
elseif tonumber(part) then
|
||||
if numLevelParts == 0 then
|
||||
self._minLevel = tonumber(part)
|
||||
elseif numLevelParts == 1 then
|
||||
self._maxLevel = tonumber(part)
|
||||
else
|
||||
-- already have min / max level
|
||||
errMsg = L["The same filter was specified multiple times."]
|
||||
self._isValid = false
|
||||
end
|
||||
numLevelParts = numLevelParts + 1
|
||||
hasNonCraftingPart = true
|
||||
elseif tonumber(strmatch(part, "^i(%d+)$")) then
|
||||
if numItemLevelParts == 0 then
|
||||
self._minItemLevel = tonumber(strmatch(part, "^i(%d+)$"))
|
||||
elseif numItemLevelParts == 1 then
|
||||
self._maxItemLevel = tonumber(strmatch(part, "^i(%d+)$"))
|
||||
else
|
||||
-- already have min / max item level
|
||||
errMsg = L["The same filter was specified multiple times."]
|
||||
self._isValid = false
|
||||
end
|
||||
numItemLevelParts = numItemLevelParts + 1
|
||||
hasNonCraftingPart = true
|
||||
elseif ItemClass.GetClassIdFromClassString(part) then
|
||||
self._class = ItemClass.GetClassIdFromClassString(part)
|
||||
hasNonCraftingPart = true
|
||||
elseif self._class and ItemClass.GetSubClassIdFromSubClassString(part, self._class) then
|
||||
self._subClass = ItemClass.GetSubClassIdFromSubClassString(part, self._class)
|
||||
hasNonCraftingPart = true
|
||||
elseif ItemClass.GetInventorySlotIdFromInventorySlotString(part) then
|
||||
self._invSlotId = ItemClass.GetInventorySlotIdFromInventorySlotString(part)
|
||||
hasNonCraftingPart = true
|
||||
elseif self:_ItemQualityToIndex(part) then
|
||||
if numQualityParts == 0 then
|
||||
self._minQuality = self:_ItemQualityToIndex(part)
|
||||
elseif numQualityParts == 1 then
|
||||
self._maxQuality = self:_ItemQualityToIndex(part)
|
||||
else
|
||||
-- already have min / max quality
|
||||
errMsg = L["The same filter was specified multiple times."]
|
||||
self._isValid = false
|
||||
end
|
||||
numQualityParts = numQualityParts + 1
|
||||
hasNonCraftingPart = true
|
||||
elseif Money.FromString(part) then
|
||||
if numPriceParts == 0 then
|
||||
self._maxPrice = Money.FromString(part)
|
||||
elseif numPriceParts == 1 then
|
||||
self._minPrice = self._maxPrice
|
||||
self._maxPrice = Money.FromString(part)
|
||||
else
|
||||
-- already have min / max price
|
||||
errMsg = L["The same filter was specified multiple times."]
|
||||
self._isValid = false
|
||||
end
|
||||
numPriceParts = numPriceParts + 1
|
||||
hasNonCraftingPart = true
|
||||
elseif not TSM.IsWowClassic() and strlower(part) == "uncollected" then
|
||||
if self._uncollected then
|
||||
errMsg = L["The same filter was specified multiple times."]
|
||||
self._isValid = false
|
||||
end
|
||||
self._uncollected = true
|
||||
hasNonCraftingPart = true
|
||||
elseif strlower(part) == "usable" then
|
||||
if self._usable then
|
||||
errMsg = L["The same filter was specified multiple times."]
|
||||
self._isValid = false
|
||||
end
|
||||
self._usable = true
|
||||
hasNonCraftingPart = true
|
||||
elseif not TSM.IsWowClassic() and strlower(part) == "upgrades" then
|
||||
if self._upgrades then
|
||||
errMsg = L["The same filter was specified multiple times."]
|
||||
self._isValid = false
|
||||
end
|
||||
self._upgrades = true
|
||||
hasNonCraftingPart = true
|
||||
elseif strlower(part) == "unlearned" then
|
||||
if self._unlearned then
|
||||
errMsg = L["The same filter was specified multiple times."]
|
||||
self._isValid = false
|
||||
end
|
||||
if CanIMogIt and CanIMogIt.PlayerKnowsTransmog then
|
||||
self._unlearned = true
|
||||
else
|
||||
Log.PrintUser(L["The unlearned filter was ignored because the CanIMogIt addon was not found."])
|
||||
end
|
||||
hasNonCraftingPart = true
|
||||
elseif strlower(part) == "canlearn" then
|
||||
if self._canlearn then
|
||||
errMsg = L["The same filter was specified multiple times."]
|
||||
self._isValid = false
|
||||
end
|
||||
if CanIMogIt and CanIMogIt.CharacterCanLearnTransmog then
|
||||
self._canlearn = true
|
||||
else
|
||||
Log.PrintUser(L["The canlearn filter was ignored because the CanIMogIt addon was not found."])
|
||||
end
|
||||
hasNonCraftingPart = true
|
||||
elseif strlower(part) == "exact" then
|
||||
if self._exactOnly then
|
||||
errMsg = L["The same filter was specified multiple times."]
|
||||
self._isValid = false
|
||||
end
|
||||
self._exactOnly = true
|
||||
hasNonCraftingPart = true
|
||||
elseif tonumber(strmatch(part, "^x(%d+)$")) then
|
||||
self._maxQuantity = tonumber(strmatch(part, "^x(%d+)$"))
|
||||
if self._maxQuantity == 0 then
|
||||
errMsg = L["The max quantity cannot be zero."]
|
||||
self._isValid = false
|
||||
end
|
||||
elseif strlower(part) == "crafting" then
|
||||
if self._crafting or self._disenchant then
|
||||
errMsg = L["The same filter was specified multiple times."]
|
||||
self._isValid = false
|
||||
end
|
||||
self._crafting = true
|
||||
elseif strlower(part) == "disenchant" then
|
||||
if self._disenchant or self._crafting then
|
||||
errMsg = L["The same filter was specified multiple times."]
|
||||
self._isValid = false
|
||||
end
|
||||
self._disenchant = true
|
||||
else
|
||||
-- invalid part
|
||||
errMsg = format(L["Unknown word (%s)."], part)
|
||||
self._isValid = false
|
||||
end
|
||||
end
|
||||
|
||||
if (self._crafting or self._disenchant) and hasNonCraftingPart then
|
||||
errMsg = L["Cannot use additional filters with /crafting or /disenchant."]
|
||||
self._isValid = false
|
||||
end
|
||||
|
||||
if self._isValid == nil then
|
||||
self._isValid = true
|
||||
end
|
||||
return self._isValid, errMsg
|
||||
end
|
||||
|
||||
function Filter.GetStr(self)
|
||||
return self._str ~= "" and self._str or nil
|
||||
end
|
||||
|
||||
function Filter.GetItem(self)
|
||||
return self._item
|
||||
end
|
||||
|
||||
function Filter.GetMinQuality(self)
|
||||
return self._minQuality ~= -math.huge and self._minQuality or nil
|
||||
end
|
||||
|
||||
function Filter.GetMaxQuality(self)
|
||||
return self._maxQuality ~= math.huge and self._maxQuality or nil
|
||||
end
|
||||
|
||||
function Filter.GetClass(self)
|
||||
return self._class
|
||||
end
|
||||
|
||||
function Filter.GetSubClass(self)
|
||||
return self._subClass
|
||||
end
|
||||
|
||||
function Filter.GetInvSlotId(self)
|
||||
return self._invSlotId
|
||||
end
|
||||
|
||||
function Filter.GetMinLevel(self)
|
||||
return self._minLevel ~= 0 and self._minLevel or nil
|
||||
end
|
||||
|
||||
function Filter.GetMaxLevel(self)
|
||||
return self._maxLevel ~= math.huge and self._maxLevel or nil
|
||||
end
|
||||
|
||||
function Filter.GetMinItemLevel(self)
|
||||
return self._minItemLevel ~= 0 and self._minItemLevel or nil
|
||||
end
|
||||
|
||||
function Filter.GetMaxItemLevel(self)
|
||||
return self._maxItemLevel ~= math.huge and self._maxItemLevel or nil
|
||||
end
|
||||
|
||||
function Filter.GetUncollected(self)
|
||||
return self._uncollected
|
||||
end
|
||||
|
||||
function Filter.GetUsableOnly(self)
|
||||
return self._usable
|
||||
end
|
||||
|
||||
function Filter.GetUpgrades(self)
|
||||
return self._upgrades
|
||||
end
|
||||
|
||||
function Filter.GetUnlearned(self)
|
||||
return self._unlearned
|
||||
end
|
||||
|
||||
function Filter.GetCanLearn(self)
|
||||
return self._canlearn
|
||||
end
|
||||
|
||||
function Filter.GetExactOnly(self)
|
||||
return self._exactOnly
|
||||
end
|
||||
|
||||
function Filter.GetMaxQuantity(self)
|
||||
return self._maxQuantity ~= math.huge and self._maxQuantity or nil
|
||||
end
|
||||
|
||||
function Filter.GetMinPrice(self)
|
||||
return self._minPrice ~= 0 and self._minPrice or nil
|
||||
end
|
||||
|
||||
function Filter.GetMaxPrice(self)
|
||||
return self._maxPrice ~= math.huge and self._maxPrice or nil
|
||||
end
|
||||
|
||||
function Filter.GetCrafting(self)
|
||||
return self._crafting
|
||||
end
|
||||
|
||||
function Filter.GetDisenchant(self)
|
||||
return self._disenchant
|
||||
end
|
||||
|
||||
function Filter.Matches(self, item, price)
|
||||
if not self._isValid then
|
||||
return false
|
||||
end
|
||||
|
||||
-- check the name
|
||||
local name = ItemInfo.GetName(item)
|
||||
name = name and strlower(name)
|
||||
if not name or not strfind(name, self._escapedStr) or (self._exactOnly and name ~= self._str) then
|
||||
return false
|
||||
end
|
||||
|
||||
-- check the quality
|
||||
local quality = ItemInfo.GetQuality(item)
|
||||
if not quality or quality < self._minQuality or quality > self._maxQuality then
|
||||
return false
|
||||
end
|
||||
|
||||
-- check the item level
|
||||
local itemLevel = ItemInfo.GetItemLevel(item)
|
||||
if not itemLevel or itemLevel < self._minItemLevel or itemLevel > self._maxItemLevel then
|
||||
return false
|
||||
end
|
||||
|
||||
-- check the required level
|
||||
local level = ItemInfo.GetMinLevel(item)
|
||||
if not level or level < self._minLevel or level > self._maxLevel then
|
||||
return false
|
||||
end
|
||||
|
||||
-- check the item class
|
||||
if self._class and ItemInfo.GetClassId(item) ~= self._class then
|
||||
return false
|
||||
end
|
||||
|
||||
-- check the item subclass
|
||||
if self._subClass and ItemInfo.GetSubClassId(item) ~= self._subClass then
|
||||
return false
|
||||
end
|
||||
|
||||
-- check the inventory slot
|
||||
if self._invSlotId and ItemInfo.GetInvSlotId(item) ~= self._invSlotId then
|
||||
return false
|
||||
end
|
||||
|
||||
-- check unlearned
|
||||
if self._unlearned and CanIMogIt:PlayerKnowsTransmog(ItemInfo.GetLink(item)) then
|
||||
return false
|
||||
end
|
||||
|
||||
-- check canlearn
|
||||
if self._canlearn and CanIMogIt:CharacterCanLearnTransmog(ItemInfo.GetLink(item)) then
|
||||
return false
|
||||
end
|
||||
|
||||
-- check the price
|
||||
price = price or 0
|
||||
if price < self._minPrice or price > self._maxPrice then
|
||||
return false
|
||||
end
|
||||
|
||||
-- it passed!
|
||||
return true
|
||||
end
|
||||
1368
LibTSM/Service/ItemInfo.lua
Normal file
1368
LibTSM/Service/ItemInfo.lua
Normal file
File diff suppressed because it is too large
Load Diff
73
LibTSM/Service/ItemLinked.lua
Normal file
73
LibTSM/Service/ItemLinked.lua
Normal file
@@ -0,0 +1,73 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- ItemLinked Functions.
|
||||
-- @module ItemLinked
|
||||
|
||||
local _, TSM = ...
|
||||
local ItemLinked = TSM.Init("Service.ItemLinked")
|
||||
local Table = TSM.Include("Util.Table")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local private = {
|
||||
callbacks = {},
|
||||
priorityLookup = {}
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
ItemLinked:OnModuleLoad(function()
|
||||
local origHandleModifiedItemClick = HandleModifiedItemClick
|
||||
HandleModifiedItemClick = function(link)
|
||||
return private.ItemLinkedHook(origHandleModifiedItemClick, link)
|
||||
end
|
||||
local origChatEdit_InsertLink = ChatEdit_InsertLink
|
||||
ChatEdit_InsertLink = function(link)
|
||||
return private.ItemLinkedHook(origChatEdit_InsertLink, link)
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function ItemLinked.RegisterCallback(callback, priority)
|
||||
assert(type(callback) == "function")
|
||||
tinsert(private.callbacks, callback)
|
||||
private.priorityLookup[callback] = (priority or 0) + #private.callbacks * 0.01
|
||||
Table.SortWithValueLookup(private.callbacks, private.priorityLookup)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.ItemLinkedHook(origFunc, itemLink)
|
||||
local putIntoChat = origFunc(itemLink)
|
||||
if putIntoChat then
|
||||
return putIntoChat
|
||||
end
|
||||
local name = ItemInfo.GetName(itemLink)
|
||||
if not name or not private.HandleItemLinked(name, itemLink) then
|
||||
return putIntoChat
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function private.HandleItemLinked(name, itemLink)
|
||||
for _, callback in ipairs(private.callbacks) do
|
||||
if callback(name, itemLink) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
24
LibTSM/Service/ItemTooltip.lua
Normal file
24
LibTSM/Service/ItemTooltip.lua
Normal file
@@ -0,0 +1,24 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local ItemTooltip = TSM.Init("Service.ItemTooltip")
|
||||
local Builder = TSM.Include("Service.ItemTooltipClasses.Builder")
|
||||
local Wrapper = TSM.Include("Service.ItemTooltipClasses.Wrapper")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function ItemTooltip.CreateBuilder()
|
||||
return Builder.Create()
|
||||
end
|
||||
|
||||
function ItemTooltip.SetWrapperPopulateFunction(func)
|
||||
Wrapper.SetPopulateFunction(func)
|
||||
end
|
||||
270
LibTSM/Service/ItemTooltipClasses/Builder.lua
Normal file
270
LibTSM/Service/ItemTooltipClasses/Builder.lua
Normal file
@@ -0,0 +1,270 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Builder = TSM.Init("Service.ItemTooltipClasses.Builder")
|
||||
local L = TSM.Include("Locale").GetTable()
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
local Money = TSM.Include("Util.Money")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local Theme = TSM.Include("Util.Theme")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local LibTSMClass = TSM.Include("LibTSMClass")
|
||||
local TooltipBuilder = LibTSMClass.DefineClass("TooltipBuilder")
|
||||
local private = {}
|
||||
local TOOLTIP_CACHE_TIME = 5
|
||||
local LINE_PART_SEP = "\t"
|
||||
local DISABLED_TINT = -30
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- TooltipBuilder - Class Meta Methods
|
||||
-- ============================================================================
|
||||
|
||||
function Builder.Create()
|
||||
return TooltipBuilder()
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- TooltipBuilder - Class Meta Methods
|
||||
-- ============================================================================
|
||||
|
||||
function TooltipBuilder.__init(self)
|
||||
self._lines = {}
|
||||
self._lineColors = {}
|
||||
self._level = 0
|
||||
self._levelNumLines = {}
|
||||
self._levelHasHeading = {}
|
||||
self._lastUpdate = 0
|
||||
self._modifier = nil
|
||||
self._inCombat = false
|
||||
self._itemString = nil
|
||||
self._quantity = nil
|
||||
self._disabled = nil
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- TooltipBuilder - Public Methods
|
||||
-- ============================================================================
|
||||
|
||||
function TooltipBuilder.ApplyValueColor(self, text)
|
||||
local color = Theme.GetColor("TEXT")
|
||||
if self._disabled then
|
||||
color = color:GetTint(DISABLED_TINT)
|
||||
end
|
||||
return color:ColorText(text)
|
||||
end
|
||||
|
||||
function TooltipBuilder.FormatMoney(self, value, color)
|
||||
color = color or Theme.GetColor("TEXT")
|
||||
if self._disabled then
|
||||
color = color:GetTint(DISABLED_TINT)
|
||||
end
|
||||
if not value then
|
||||
return self:ApplyValueColor("---")
|
||||
end
|
||||
return Money.ToString(value, color:GetTextColorPrefix(), TSM.db.global.tooltipOptions.tooltipPriceFormat == "icon" and "OPT_ICON" or nil, self._disabled and "OPT_DISABLE" or nil)
|
||||
end
|
||||
|
||||
function TooltipBuilder.AddLine(self, lineLeft, lineRight, color)
|
||||
if lineRight then
|
||||
lineLeft = strjoin(LINE_PART_SEP, lineLeft, lineRight)
|
||||
end
|
||||
local line = strrep(" ", self._level)..lineLeft
|
||||
color = color or Theme.GetColor("INDICATOR_ALT")
|
||||
if self._disabled then
|
||||
color = color:GetTint(DISABLED_TINT)
|
||||
end
|
||||
assert(not self._lineColors[line] or self._lineColors[line] == color)
|
||||
self._lineColors[line] = color
|
||||
tinsert(self._lines, line)
|
||||
self._levelNumLines[self._level] = self._levelNumLines[self._level] + 1
|
||||
end
|
||||
|
||||
function TooltipBuilder.AddTextLine(self, label, text, ...)
|
||||
if select("#", ...) == 0 then
|
||||
self:AddLine(label, self:ApplyValueColor(text))
|
||||
else
|
||||
self:AddLine(label, self:ApplyValueColor(text).." ("..self:_TextsToStr(...)..")")
|
||||
end
|
||||
end
|
||||
|
||||
function TooltipBuilder.AddValueLine(self, label, ...)
|
||||
self:AddLine(label, self:_ValuesToStr(...))
|
||||
end
|
||||
|
||||
function TooltipBuilder.AddItemValueLine(self, label, value)
|
||||
if self._quantity > 1 then
|
||||
label = label.." x"..self._quantity
|
||||
value = value * self._quantity
|
||||
end
|
||||
self:AddLine(label, self:FormatMoney(value))
|
||||
end
|
||||
|
||||
function TooltipBuilder.AddQuantityValueLine(self, label, quantity, ...)
|
||||
self:AddLine(label, self:ApplyValueColor(quantity).." ("..self:_ValuesToStr(...)..")")
|
||||
end
|
||||
|
||||
function TooltipBuilder.AddSubItemValueLine(self, itemString, value, multiplier, matRate, minAmount, maxAmount)
|
||||
local name = ItemInfo.GetName(itemString)
|
||||
local color = ItemInfo.GetQualityColor(itemString)
|
||||
if not name or not color then
|
||||
return
|
||||
end
|
||||
multiplier = Math.Round(multiplier * self._quantity, 0.001)
|
||||
matRate = matRate and matRate * 100
|
||||
matRate = matRate and matRate.."% " or ""
|
||||
local range = (minAmount and maxAmount) and (minAmount ~= maxAmount and "|cffffff00 ["..minAmount.."-"..maxAmount.."]|r" or "|cffffff00 ["..minAmount.."]|r") or ""
|
||||
value = value and (value * multiplier) or nil
|
||||
self:AddLine(color..matRate..name.." x"..multiplier.."|r"..range, self:FormatMoney(value))
|
||||
end
|
||||
|
||||
function TooltipBuilder.StartSection(self, headingTextLeft, headingTextRight)
|
||||
if self._level == 0 then
|
||||
headingTextLeft = Theme.GetColor("INDICATOR"):ColorText(headingTextLeft)
|
||||
end
|
||||
if headingTextRight then
|
||||
self:AddLine(headingTextLeft, headingTextRight)
|
||||
elseif headingTextLeft then
|
||||
self:AddLine(headingTextLeft)
|
||||
end
|
||||
self._level = self._level + 1
|
||||
self._levelHasHeading[self._level] = headingTextLeft and true or false
|
||||
self._levelNumLines[self._level] = 0
|
||||
end
|
||||
|
||||
function TooltipBuilder.EndSection(self)
|
||||
if self._levelNumLines[self._level] == 0 and self._levelHasHeading[self._level] then
|
||||
-- remove the previous heading line
|
||||
local headingLine = tremove(self._lines)
|
||||
assert(headingLine)
|
||||
self._lineColors[headingLine] = nil
|
||||
self._levelNumLines[self._level - 1] = self._levelNumLines[self._level - 1] - 1
|
||||
end
|
||||
self._level = self._level - 1
|
||||
assert(self._level >= 0)
|
||||
end
|
||||
|
||||
function TooltipBuilder.SetDisabled(self, disabled)
|
||||
self._disabled = disabled
|
||||
end
|
||||
|
||||
function TooltipBuilder.GetNumLines(self)
|
||||
return #self._lines
|
||||
end
|
||||
|
||||
function TooltipBuilder.GetLine(self, index)
|
||||
local line = self._lines[index]
|
||||
local left, right = strsplit(LINE_PART_SEP, line)
|
||||
return left, right, self._lineColors[line]
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- TooltipBuilder - Private Methods
|
||||
-- ============================================================================
|
||||
|
||||
function TooltipBuilder._GetModifierHash(self)
|
||||
return (IsShiftKeyDown() and 4 or 0) + (IsAltKeyDown() and 2 or 0) + (IsControlKeyDown() and 1 or 0)
|
||||
end
|
||||
|
||||
function TooltipBuilder._IsCached(self, itemString, quantity)
|
||||
if itemString == ItemString.GetPlaceholder() then
|
||||
return false
|
||||
end
|
||||
if self:_GetModifierHash() ~= self._modifier then
|
||||
return false
|
||||
end
|
||||
if self._itemString ~= itemString or self._quantity ~= quantity then
|
||||
return false
|
||||
end
|
||||
if GetTime() - self._lastUpdate >= TOOLTIP_CACHE_TIME then
|
||||
return false
|
||||
end
|
||||
if InCombatLockdown() ~= self._inCombat then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function TooltipBuilder._Prepare(self, itemString, quantity)
|
||||
if self:_IsCached(itemString, quantity) then
|
||||
-- have the lines cached already
|
||||
return true
|
||||
end
|
||||
|
||||
wipe(self._lines)
|
||||
wipe(self._lineColors)
|
||||
wipe(self._levelNumLines)
|
||||
wipe(self._levelHasHeading)
|
||||
self._level = 0
|
||||
self._levelNumLines[self._level] = 0
|
||||
self._lastUpdate = GetTime()
|
||||
self._modifier = self:_GetModifierHash()
|
||||
self._inCombat = InCombatLockdown()
|
||||
self._itemString = itemString
|
||||
self._quantity = quantity
|
||||
self._disabled = false
|
||||
|
||||
if InCombatLockdown() and itemString ~= ItemString.GetPlaceholder() then
|
||||
self:AddLine(L["Can't load TSM tooltip while in combat"])
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function TooltipBuilder._IsEmpty(self)
|
||||
assert(self._level == 0)
|
||||
return #self._lines == 0
|
||||
end
|
||||
|
||||
function TooltipBuilder._LineIterator(self)
|
||||
return private.LineIteratorHelper, self, 0
|
||||
end
|
||||
|
||||
function TooltipBuilder._TextsToStr(self, ...)
|
||||
local valueParts = TempTable.Acquire(...)
|
||||
for i = 1, #valueParts do
|
||||
valueParts[i] = self:ApplyValueColor(valueParts[i])
|
||||
end
|
||||
local result = table.concat(valueParts, " / ")
|
||||
TempTable.Release(valueParts)
|
||||
return result
|
||||
end
|
||||
|
||||
function TooltipBuilder._ValuesToStr(self, ...)
|
||||
-- can't just build the table with the vararg as there may be nils
|
||||
local valueParts = TempTable.Acquire()
|
||||
for i = 1, select("#", ...) do
|
||||
local value = select(i, ...)
|
||||
valueParts[i] = self:FormatMoney(value)
|
||||
end
|
||||
local result = table.concat(valueParts, " / ")
|
||||
TempTable.Release(valueParts)
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.LineIteratorHelper(self, index)
|
||||
index = index + 1
|
||||
if index > #self._lines then
|
||||
return
|
||||
end
|
||||
return index, self:GetLine(index)
|
||||
end
|
||||
114
LibTSM/Service/ItemTooltipClasses/ExtraTip.lua
Normal file
114
LibTSM/Service/ItemTooltipClasses/ExtraTip.lua
Normal file
@@ -0,0 +1,114 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local ExtraTip = TSM.Init("Service.ItemTooltipClasses.ExtraTip")
|
||||
local private = {
|
||||
numExtraTips = 0,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function ExtraTip.Create(tooltip)
|
||||
return private.CreateExtraTip(tooltip)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Extra Tip Methods
|
||||
-- ============================================================================
|
||||
|
||||
local EXTRA_TIP_METHODS = {
|
||||
Attach = function(self, tooltip)
|
||||
self:SetOwner(tooltip, "ANCHOR_PRESERVE")
|
||||
self.anchorFrame = nil
|
||||
self:OnUpdate()
|
||||
end,
|
||||
|
||||
Show = function(self)
|
||||
self.anchorFrame = nil
|
||||
GameTooltip.Show(self)
|
||||
local bottom = self:GetBottom()
|
||||
if bottom and self:GetParent():IsShown() then
|
||||
self.isTop = bottom <= 0
|
||||
end
|
||||
self:OnUpdate()
|
||||
local numLines = self:NumLines()
|
||||
local changedLines = self.changedLines or 0
|
||||
if changedLines >= numLines then return end
|
||||
for i = changedLines + 1, numLines do
|
||||
local left, right = self.Left[i], self.Right[i]
|
||||
local font = i == 1 and GameTooltipHeader or GameFontNormal
|
||||
|
||||
local r, g, b, a = left:GetTextColor()
|
||||
left:SetFontObject(font)
|
||||
left:SetTextColor(r, g, b, a)
|
||||
|
||||
r, g, b, a = right:GetTextColor()
|
||||
right:SetFontObject(font)
|
||||
right:SetTextColor(r, g, b, a)
|
||||
end
|
||||
self.changedLines = numLines
|
||||
GameTooltip.Show(self)
|
||||
self:OnUpdate()
|
||||
end,
|
||||
|
||||
OnUpdate = function(self)
|
||||
local tooltip = self:GetParent()
|
||||
local anchorFrame = private.GetExtraTipFrame(tooltip, tooltip:GetChildren()) or tooltip
|
||||
if anchorFrame ~= self.anchorFrame then
|
||||
self:ClearAllPoints()
|
||||
if self.isTop then
|
||||
self:SetPoint("BOTTOM", anchorFrame, "TOP")
|
||||
else
|
||||
self:SetPoint("TOP", anchorFrame, "BOTTOM")
|
||||
end
|
||||
self.anchorFrame = anchorFrame
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
local LINE_METATABLE = {
|
||||
__index = function(t, k)
|
||||
local v = _G[t.name..k]
|
||||
rawset(t, k, v)
|
||||
return v
|
||||
end,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.CreateExtraTip(tooltip)
|
||||
private.numExtraTips = private.numExtraTips + 1
|
||||
local extraTip = CreateFrame("GameTooltip", "TSMExtraTip"..private.numExtraTips, tooltip, "GameTooltipTemplate")
|
||||
extraTip:SetClampedToScreen(true)
|
||||
|
||||
for name, func in pairs(EXTRA_TIP_METHODS) do
|
||||
extraTip[name] = func
|
||||
end
|
||||
|
||||
extraTip.Left = setmetatable({name = extraTip:GetName().."TextLeft"}, LINE_METATABLE)
|
||||
extraTip.Right = setmetatable({name = extraTip:GetName().."TextRight"}, LINE_METATABLE)
|
||||
return extraTip
|
||||
end
|
||||
|
||||
function private.GetExtraTipFrame(tooltip, ...)
|
||||
for i = 1, select('#', ...) do
|
||||
local frame = select(i, ...)
|
||||
if frame.InitLines and frame:GetParent() == tooltip and frame:IsVisible() then
|
||||
return frame
|
||||
end
|
||||
end
|
||||
end
|
||||
320
LibTSM/Service/ItemTooltipClasses/Wrapper.lua
Normal file
320
LibTSM/Service/ItemTooltipClasses/Wrapper.lua
Normal file
@@ -0,0 +1,320 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Wrapper = TSM.Init("Service.ItemTooltipClasses.Wrapper")
|
||||
local ExtraTip = TSM.Include("Service.ItemTooltipClasses.ExtraTip")
|
||||
local Builder = TSM.Include("Service.ItemTooltipClasses.Builder")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local private = {
|
||||
builder = nil,
|
||||
settings = nil,
|
||||
tooltipRegistry = {},
|
||||
hookedBattlepetGlobal = nil,
|
||||
tooltipMethodPrehooks = nil,
|
||||
tooltipMethodPosthooks = {},
|
||||
lastMailTooltipUpdate = nil,
|
||||
lastMailTooltipIndex = nil,
|
||||
populateFunc = nil,
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Wrapper:OnSettingsLoad(function()
|
||||
private.builder = Builder.Create()
|
||||
private.settings = Settings.NewView()
|
||||
:AddKey("global", "tooltipOptions", "embeddedTooltip")
|
||||
:AddKey("global", "tooltipOptions", "enabled")
|
||||
:AddKey("global", "tooltipOptions", "tooltipShowModifier")
|
||||
private.RegisterTooltip(GameTooltip)
|
||||
private.RegisterTooltip(ItemRefTooltip)
|
||||
if not TSM.IsWowClassic() then
|
||||
private.RegisterTooltip(BattlePetTooltip)
|
||||
private.RegisterTooltip(FloatingBattlePetTooltip)
|
||||
end
|
||||
local orig = OpenMailAttachment_OnEnter
|
||||
OpenMailAttachment_OnEnter = function(self, index)
|
||||
private.lastMailTooltipUpdate = private.lastMailTooltipUpdate or 0
|
||||
if private.lastMailTooltipIndex ~= index or private.lastMailTooltipUpdate + 0.1 < GetTime() then
|
||||
private.lastMailTooltipUpdate = GetTime()
|
||||
private.lastMailTooltipIndex = index
|
||||
orig(self, index)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Wrapper.SetPopulateFunction(func)
|
||||
assert(type(func) == "function" and not private.populateFunc)
|
||||
private.populateFunc = func
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.RegisterTooltip(tooltip)
|
||||
local reg = {}
|
||||
reg.extraTip = ExtraTip.Create(tooltip)
|
||||
private.tooltipRegistry[tooltip] = reg
|
||||
|
||||
if private.IsBattlePetTooltip(tooltip) then
|
||||
if not private.hookedBattlepetGlobal then
|
||||
private.hookedBattlepetGlobal = true
|
||||
hooksecurefunc("BattlePetTooltipTemplate_SetBattlePet", private.OnTooltipSetBattlePet)
|
||||
hooksecurefunc("BattlePetToolTip_Show", private.OnBattlePetTooltipShow)
|
||||
end
|
||||
tooltip:HookScript("OnHide", private.OnTooltipCleared)
|
||||
else
|
||||
local scriptHooks = {
|
||||
OnTooltipSetItem = private.OnTooltipSetItem,
|
||||
OnTooltipCleared = private.OnTooltipCleared
|
||||
}
|
||||
for script, prehook in pairs(scriptHooks) do
|
||||
tooltip:HookScript(script, prehook)
|
||||
end
|
||||
|
||||
for method, prehook in pairs(private.tooltipMethodPrehooks) do
|
||||
local posthook = private.tooltipMethodPosthooks[method]
|
||||
local orig = tooltip[method]
|
||||
tooltip[method] = function(...)
|
||||
prehook(...)
|
||||
local a, b, c, d, e, f, g, h, i, j, k = orig(...)
|
||||
posthook(...)
|
||||
return a, b, c, d, e, f, g, h, i, j, k
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function private.IsBattlePetTooltip(tooltip)
|
||||
if TSM.IsWowClassic() then
|
||||
return false
|
||||
end
|
||||
return tooltip == BattlePetTooltip or tooltip == FloatingBattlePetTooltip
|
||||
end
|
||||
|
||||
function private.OnTooltipSetItem(tooltip)
|
||||
local reg = private.tooltipRegistry[tooltip]
|
||||
if reg.hasItem then
|
||||
return
|
||||
end
|
||||
|
||||
tooltip:Show()
|
||||
local testName, item = tooltip:GetItem()
|
||||
if not item then
|
||||
item = reg.item
|
||||
elseif testName == "" then
|
||||
-- this is likely a case where :GetItem() is broken for recipes - detect and try to fix it
|
||||
if strmatch(item, "item:([0-9]*):") == "" then
|
||||
item = reg.item
|
||||
end
|
||||
end
|
||||
if not item then
|
||||
return
|
||||
end
|
||||
|
||||
private.SetTooltipItem(tooltip, item)
|
||||
end
|
||||
|
||||
function private.OnTooltipSetBattlePet(tooltip, data)
|
||||
local reg = private.tooltipRegistry[tooltip]
|
||||
if reg.hasItem then
|
||||
private.OnTooltipCleared(tooltip)
|
||||
end
|
||||
|
||||
local link = reg.item
|
||||
if not link then
|
||||
-- extract values from data
|
||||
local speciesID = data.speciesID
|
||||
local level = data.level
|
||||
local maxHealth = data.maxHealth
|
||||
local power = data.power
|
||||
local speed = data.speed
|
||||
local battlePetID = data.battlePetID or "0x0000000000000000"
|
||||
local name = data.name
|
||||
local customName = data.customName
|
||||
local breedQuality = data.breedQuality
|
||||
local colorCode = breedQuality == -1 and NORMAL_FONT_COLOR_CODE or (ITEM_QUALITY_COLORS[breedQuality] or ITEM_QUALITY_COLORS[0]).hex
|
||||
link = format("%s|Hbattlepet:%d:%d:%d:%d:%d:%d:%s|h[%s]|h|r", colorCode, speciesID, level, breedQuality, maxHealth, power, speed, battlePetID, customName or name)
|
||||
end
|
||||
|
||||
private.SetTooltipItem(tooltip, link)
|
||||
end
|
||||
|
||||
function private.OnTooltipCleared(tooltip)
|
||||
local reg = private.tooltipRegistry[tooltip]
|
||||
if reg.ignoreOnCleared then return end
|
||||
|
||||
reg.extraTipUsed = nil
|
||||
reg.minWidth = 0
|
||||
reg.quantity = nil
|
||||
reg.hasItem = nil
|
||||
reg.item = nil
|
||||
reg.extraTip:Hide()
|
||||
reg.extraTip.minWidth = 0
|
||||
reg.extraTip.isTop = nil
|
||||
end
|
||||
|
||||
function private.OnBattlePetTooltipShow()
|
||||
local reg = private.tooltipRegistry[BattlePetTooltip]
|
||||
reg.extraTip:Show()
|
||||
end
|
||||
|
||||
function private.SetTooltipItem(tooltip, link)
|
||||
local itemString = ItemString.Get(link)
|
||||
if not private.IsEnabled() or not itemString then
|
||||
return
|
||||
end
|
||||
|
||||
local reg = private.tooltipRegistry[tooltip]
|
||||
local quantity = max(IsShiftKeyDown() and reg.quantity or 1, 1)
|
||||
local isCached = private.builder:_Prepare(itemString, quantity)
|
||||
if not isCached then
|
||||
-- populate all the lines
|
||||
private.populateFunc(private.builder, itemString)
|
||||
end
|
||||
if private.builder:_IsEmpty() then
|
||||
return
|
||||
end
|
||||
reg.hasItem = true
|
||||
local useExtraTip = private.IsBattlePetTooltip(tooltip) or not private.settings.embeddedTooltip
|
||||
|
||||
-- setup the extra tip if necessary
|
||||
if useExtraTip then
|
||||
reg.extraTip:Attach(tooltip)
|
||||
local r, g, b = GetItemQualityColor(ItemInfo.GetQuality(link) or 0)
|
||||
reg.extraTip:AddLine(ItemInfo.GetName(link), r, g, b)
|
||||
end
|
||||
|
||||
-- add all the lines
|
||||
local targetTip = useExtraTip and reg.extraTip or tooltip
|
||||
targetTip:AddLine(" ")
|
||||
for _, left, right, lineColor in private.builder:_LineIterator() do
|
||||
local r, g, b = lineColor:GetFractionalRGBA()
|
||||
if right then
|
||||
targetTip:AddDoubleLine(left, right, r, g, b, r, g, b)
|
||||
else
|
||||
targetTip:AddLine(left, r, g, b)
|
||||
end
|
||||
end
|
||||
|
||||
-- show the tooltip / extra tip as necessary
|
||||
if not private.IsBattlePetTooltip(tooltip) then
|
||||
tooltip:Show()
|
||||
end
|
||||
if useExtraTip then
|
||||
reg.extraTip:Show()
|
||||
end
|
||||
end
|
||||
|
||||
function private.IsEnabled()
|
||||
if not private.settings.enabled then
|
||||
return false
|
||||
elseif private.settings.tooltipShowModifier == "alt" and not IsAltKeyDown() then
|
||||
return false
|
||||
elseif private.settings.tooltipShowModifier == "ctrl" and not IsControlKeyDown() then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Hook Setup Code
|
||||
-- ============================================================================
|
||||
|
||||
do
|
||||
local function PreHookHelper(self, quantityFunc, quantityOffset, ...)
|
||||
private.OnTooltipCleared(self)
|
||||
local reg = private.tooltipRegistry[self]
|
||||
reg.ignoreOnCleared = true
|
||||
if type(quantityFunc) == "number" then
|
||||
reg.quantity = quantityFunc
|
||||
else
|
||||
reg.quantity = select(quantityOffset, quantityFunc(...))
|
||||
end
|
||||
return reg
|
||||
end
|
||||
private.tooltipMethodPrehooks = {
|
||||
SetQuestItem = function(self, ...) PreHookHelper(self, GetQuestItemInfo, 3, ...) end,
|
||||
SetQuestLogItem = function(self, type, ...)
|
||||
local quantityFunc = type == "choice" and GetQuestLogChoiceInfo or GetQuestLogRewardInfo
|
||||
PreHookHelper(self, quantityFunc, 3, ...)
|
||||
end,
|
||||
SetRecipeReagentItem = function(self, ...)
|
||||
local reg = PreHookHelper(self, C_TradeSkillUI.GetRecipeReagentInfo, 3, ...)
|
||||
reg.item = C_TradeSkillUI.GetRecipeReagentItemLink(...)
|
||||
end,
|
||||
SetRecipeResultItem = function(self, ...)
|
||||
private.OnTooltipCleared(self)
|
||||
local reg = private.tooltipRegistry[self]
|
||||
reg.ignoreOnCleared = true
|
||||
local lNum, hNum = C_TradeSkillUI.GetRecipeNumItemsProduced(...)
|
||||
-- the quantity can be a range, so use a quantity of 1 if so
|
||||
reg.quantity = lNum == hNum and lNum or 1
|
||||
end,
|
||||
SetBagItem = function(self, ...) PreHookHelper(self, GetContainerItemInfo, 2, ...) end,
|
||||
SetGuildBankItem = function(self, ...)
|
||||
local reg = PreHookHelper(self, GetGuildBankItemInfo, 2, ...)
|
||||
reg.item = GetGuildBankItemLink(...)
|
||||
end,
|
||||
SetVoidItem = function(self, ...) PreHookHelper(self, 1) end,
|
||||
SetVoidDepositItem = function(self, ...) PreHookHelper(self, 1) end,
|
||||
SetVoidWithdrawalItem = function(self, ...) PreHookHelper(self, 1) end,
|
||||
SetInventoryItem = function(self, ...) PreHookHelper(self, GetInventoryItemCount, 1, ...) end,
|
||||
SetMerchantItem = function(self, ...)
|
||||
local reg = PreHookHelper(self, GetMerchantItemInfo, 4, ...)
|
||||
reg.item = GetMerchantItemLink(...)
|
||||
end,
|
||||
SetMerchantCostItem = function(self, ...) PreHookHelper(self, GetMerchantItemCostItem, 2, ...) end,
|
||||
SetBuybackItem = function(self, ...) PreHookHelper(self, GetBuybackItemInfo, 4, ...) end,
|
||||
SetAuctionItem = function(self, ...)
|
||||
local reg = PreHookHelper(self, GetAuctionItemInfo, 3, ...)
|
||||
reg.item = GetAuctionItemLink(...)
|
||||
end,
|
||||
SetAuctionSellItem = function(self, ...) PreHookHelper(self, GetAuctionSellItemInfo, 3, ...) end,
|
||||
SetInboxItem = function(self, index) PreHookHelper(self, GetInboxItem, 4, index, 1) end,
|
||||
SetSendMailItem = function(self, ...) PreHookHelper(self, GetSendMailItem, 4, ...) end,
|
||||
SetLootItem = function(self, ...) PreHookHelper(self, GetLootSlotInfo, 3, ...) end,
|
||||
SetLootRollItem = function(self, ...) PreHookHelper(self, GetLootRollItemInfo, 3, ...) end,
|
||||
SetTradePlayerItem = function(self, ...) PreHookHelper(self, GetTradePlayerItemInfo, 3, ...) end,
|
||||
SetTradeTargetItem = function(self, ...) PreHookHelper(self, GetTradeTargetItemInfo, 3, ...) end,
|
||||
SetHyperlink = function(self, link)
|
||||
local reg = private.tooltipRegistry[self]
|
||||
private.OnTooltipCleared(self)
|
||||
reg.ignoreOnCleared = true
|
||||
reg.item = link
|
||||
end,
|
||||
}
|
||||
|
||||
-- populate all the posthooks
|
||||
local function TooltipMethodPostHook(self)
|
||||
private.tooltipRegistry[self].ignoreOnCleared = nil
|
||||
end
|
||||
for funcName in pairs(private.tooltipMethodPrehooks) do
|
||||
private.tooltipMethodPosthooks[funcName] = TooltipMethodPostHook
|
||||
end
|
||||
-- SetHyperlink is special
|
||||
private.tooltipMethodPosthooks.SetHyperlink = function(self)
|
||||
local reg = private.tooltipRegistry[self]
|
||||
reg.ignoreOnCleared = nil
|
||||
end
|
||||
end
|
||||
398
LibTSM/Service/MailTracking.lua
Normal file
398
LibTSM/Service/MailTracking.lua
Normal file
@@ -0,0 +1,398 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local MailTracking = TSM.Init("Service.MailTracking")
|
||||
local Database = TSM.Include("Util.Database")
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local ItemString = TSM.Include("Util.ItemString")
|
||||
local ItemInfo = TSM.Include("Service.ItemInfo")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local AuctionTracking = TSM.Include("Service.AuctionTracking")
|
||||
local private = {
|
||||
settings = nil,
|
||||
mailDB = nil,
|
||||
itemDB = nil,
|
||||
quantityDB = nil,
|
||||
isOpen = false,
|
||||
tooltip = nil,
|
||||
callbacks = {},
|
||||
expiresCallbacks = {},
|
||||
cancelAuctionQuery = nil,
|
||||
}
|
||||
local PLAYER_NAME = UnitName("player")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
MailTracking:OnSettingsLoad(function()
|
||||
private.settings = Settings.NewView()
|
||||
:AddKey("factionrealm", "internalData", "pendingMail")
|
||||
:AddKey("factionrealm", "internalData", "expiringMail")
|
||||
:AddKey("sync", "internalData", "mailQuantity")
|
||||
|
||||
-- update the structure of TSM.db.factionrealm.internalData.pendingMail
|
||||
local toUpdate = TempTable.Acquire()
|
||||
for character, pendingMailData in pairs(private.settings.pendingMail) do
|
||||
if pendingMailData.items then
|
||||
Log.Info("Converting pending mail data for %s", character)
|
||||
toUpdate[character] = pendingMailData.items
|
||||
end
|
||||
end
|
||||
for character, items in pairs(toUpdate) do
|
||||
private.settings.pendingMail[character] = items
|
||||
end
|
||||
TempTable.Release(toUpdate)
|
||||
|
||||
-- remove data for characters we don't own
|
||||
local toRemove = TempTable.Acquire()
|
||||
for character in pairs(private.settings.pendingMail) do
|
||||
if Settings.GetCharacterSyncAccountKey(character) ~= Settings.GetCurrentSyncAccountKey() then
|
||||
Log.Info("Removed pending mail data for %s", character)
|
||||
tinsert(toRemove, character)
|
||||
end
|
||||
end
|
||||
for _, character in ipairs(toRemove) do
|
||||
private.settings.pendingMail[character] = nil
|
||||
end
|
||||
TempTable.Release(toRemove)
|
||||
|
||||
private.mailDB = Database.NewSchema("MAIL_TRACKING_INBOX_INFO")
|
||||
:AddUniqueNumberField("index")
|
||||
:AddStringField("icon")
|
||||
:AddStringField("sender")
|
||||
:AddStringField("subject")
|
||||
:AddStringField("itemString")
|
||||
:AddNumberField("itemCount")
|
||||
:AddNumberField("money")
|
||||
:AddNumberField("cod")
|
||||
:AddNumberField("expires")
|
||||
:AddIndex("index")
|
||||
:Commit()
|
||||
private.itemDB = Database.NewSchema("MAIL_TRACKING_INBOX_ITEMS")
|
||||
:AddNumberField("index")
|
||||
:AddNumberField("itemIndex")
|
||||
:AddStringField("itemLink")
|
||||
:AddNumberField("quantity")
|
||||
:Commit()
|
||||
private.quantityDB = Database.NewSchema("MAIL_TRACKING_QUANTITY")
|
||||
:AddUniqueStringField("itemString")
|
||||
:AddNumberField("quantity")
|
||||
:Commit()
|
||||
|
||||
private.settings.pendingMail[PLAYER_NAME] = private.settings.pendingMail[PLAYER_NAME] or {}
|
||||
Event.Register("MAIL_SHOW", private.MailShowHandler)
|
||||
Event.Register("MAIL_CLOSED", private.MailClosedHandler)
|
||||
Event.Register("MAIL_INBOX_UPDATE", private.MailInboxUpdateHandler)
|
||||
|
||||
if TSM.IsWowClassic() then
|
||||
-- handle auction buying
|
||||
hooksecurefunc("PlaceAuctionBid", function(listType, index, bidPlaced)
|
||||
local itemString = ItemString.GetBase(GetAuctionItemLink(listType, index))
|
||||
local _, _, stackSize, _, _, _, _, _, _, buyout = GetAuctionItemInfo(listType, index)
|
||||
if not itemString or bidPlaced ~= buyout then
|
||||
return
|
||||
end
|
||||
private.ChangePendingMailQuantity(itemString, stackSize)
|
||||
end)
|
||||
|
||||
-- handle auction canceling
|
||||
hooksecurefunc("CancelAuction", function(index)
|
||||
local itemString = ItemString.GetBase(GetAuctionItemLink("owner", index))
|
||||
local _, _, stackSize = GetAuctionItemInfo("owner", index)
|
||||
-- for some reason, these APIs don't always work properly, so check the return values
|
||||
if not itemString or not stackSize or stackSize == 0 then
|
||||
return
|
||||
end
|
||||
private.ChangePendingMailQuantity(itemString, stackSize)
|
||||
end)
|
||||
else
|
||||
private.cancelAuctionQuery = AuctionTracking.CreateQuery()
|
||||
:Equal("auctionId", Database.BoundQueryParam())
|
||||
:Select("itemString", "stackSize")
|
||||
|
||||
-- handle auction canceling
|
||||
hooksecurefunc(C_AuctionHouse, "CancelAuction", function(auctionId)
|
||||
private.cancelAuctionQuery:BindParams(auctionId)
|
||||
for _, itemString, stackSize in private.cancelAuctionQuery:Iterator() do
|
||||
private.ChangePendingMailQuantity(itemString, stackSize)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- handle sending mail to alts
|
||||
hooksecurefunc("SendMail", function(target)
|
||||
local character = private.ValidateCharacter(target)
|
||||
if not character then
|
||||
return
|
||||
end
|
||||
private.settings.pendingMail[character] = private.settings.pendingMail[character] or {}
|
||||
local altPendingMail = private.settings.pendingMail[character]
|
||||
for i = 1, ATTACHMENTS_MAX_SEND do
|
||||
local itemString = ItemString.GetBase(GetSendMailItemLink(i))
|
||||
local _, _, _, quantity = GetSendMailItem(i)
|
||||
if itemString and quantity then
|
||||
altPendingMail[itemString] = (altPendingMail[itemString] or 0) + quantity
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- handle returning mail to alts
|
||||
hooksecurefunc("ReturnInboxItem", function(index)
|
||||
local character = private.ValidateCharacter(select(3, GetInboxHeaderInfo(index)))
|
||||
if not character then
|
||||
return
|
||||
end
|
||||
private.settings.pendingMail[character] = private.settings.pendingMail[character] or {}
|
||||
local altPendingMail = private.settings.pendingMail[character]
|
||||
for i = 1, ATTACHMENTS_MAX_SEND do
|
||||
local _, _, _, quantity = GetInboxItem(index, i)
|
||||
local itemLink = quantity and quantity > 0 and private.GetInboxItemLink(index, i) or nil
|
||||
local itemString = itemLink and ItemString.GetBase(itemLink) or nil
|
||||
if itemString then
|
||||
altPendingMail[itemString] = (altPendingMail[itemString] or 0) + quantity
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
private.quantityDB:BulkInsertStart()
|
||||
for itemString, quantity in pairs(private.settings.pendingMail[PLAYER_NAME]) do
|
||||
if quantity > 0 then
|
||||
private.quantityDB:BulkInsertNewRow(itemString, quantity)
|
||||
else
|
||||
private.settings.pendingMail[PLAYER_NAME][itemString] = nil
|
||||
end
|
||||
end
|
||||
private.quantityDB:BulkInsertEnd()
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function MailTracking.RegisterCallback(callback)
|
||||
tinsert(private.callbacks, callback)
|
||||
end
|
||||
|
||||
function MailTracking.RegisterExpiresCallback(callback)
|
||||
tinsert(private.expiresCallbacks, callback)
|
||||
end
|
||||
|
||||
function MailTracking.BaseItemIterator()
|
||||
return private.quantityDB:NewQuery()
|
||||
:Select("itemString")
|
||||
:IteratorAndRelease()
|
||||
end
|
||||
|
||||
function MailTracking.CreateMailInboxQuery()
|
||||
return private.mailDB:NewQuery()
|
||||
end
|
||||
|
||||
function MailTracking.CreateMailItemQuery()
|
||||
return private.itemDB:NewQuery()
|
||||
end
|
||||
|
||||
function MailTracking.GetInboxItemLink(index)
|
||||
return private.GetInboxItemLink(index, 1)
|
||||
end
|
||||
|
||||
function MailTracking.GetMailType(index)
|
||||
return private.GetMailType(index)
|
||||
end
|
||||
|
||||
function MailTracking.GetQuantityByBaseItemString(baseItemString)
|
||||
return private.quantityDB:GetUniqueRowField("itemString", baseItemString, "quantity") or 0
|
||||
end
|
||||
|
||||
function MailTracking.RecordAuctionBuyout(baseItemString, stackSize)
|
||||
if TSM.IsWowClassic() then
|
||||
-- on classic, we'll handle auction buys via a direct hook
|
||||
return
|
||||
end
|
||||
private.ChangePendingMailQuantity(baseItemString, stackSize)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Event Handlers
|
||||
-- ============================================================================
|
||||
|
||||
function private.MailShowHandler()
|
||||
private.isOpen = true
|
||||
end
|
||||
|
||||
function private.MailClosedHandler()
|
||||
private.isOpen = false
|
||||
end
|
||||
|
||||
function private.MailInboxUpdateHandler()
|
||||
if not private.isOpen then
|
||||
return
|
||||
end
|
||||
|
||||
Delay.AfterFrame("mailInboxScan", 1, private.MailInboxUpdateDelayed)
|
||||
end
|
||||
|
||||
function private.MailInboxUpdateDelayed()
|
||||
if not private.isOpen then
|
||||
return
|
||||
end
|
||||
|
||||
wipe(private.settings.mailQuantity)
|
||||
wipe(private.settings.pendingMail[PLAYER_NAME])
|
||||
private.mailDB:TruncateAndBulkInsertStart()
|
||||
private.itemDB:TruncateAndBulkInsertStart()
|
||||
local expiration = math.huge
|
||||
for i = 1, GetInboxNumItems() do
|
||||
local _, _, sender, subject, money, cod, daysLeft, itemCount = GetInboxHeaderInfo(i)
|
||||
if itemCount and itemCount > 0 and money and money > 0 then
|
||||
expiration = min(expiration, time() + (daysLeft * 24 * 60 * 60))
|
||||
end
|
||||
|
||||
if money and money > 0 then
|
||||
private.itemDB:BulkInsertNewRow(i, 0, tostring(money), 0)
|
||||
end
|
||||
|
||||
local firstItemString = nil
|
||||
for j = 1, ATTACHMENTS_MAX do
|
||||
local _, _, _, quantity = GetInboxItem(i, j)
|
||||
local itemLink = quantity and quantity > 0 and private.GetInboxItemLink(i, j) or nil
|
||||
local itemString = itemLink and ItemString.Get(itemLink) or nil
|
||||
if itemString then
|
||||
firstItemString = firstItemString or itemString
|
||||
local baseItemString = ItemString.GetBaseFast(itemString)
|
||||
private.settings.mailQuantity[baseItemString] = (private.settings.mailQuantity[baseItemString] or 0) + quantity
|
||||
private.itemDB:BulkInsertNewRow(i, j, itemLink, quantity)
|
||||
end
|
||||
end
|
||||
|
||||
local mailType = private.GetMailType(i, firstItemString) or ""
|
||||
if mailType == "BUY" then
|
||||
local _, _, _, bid = GetInboxInvoiceInfo(i)
|
||||
cod = bid
|
||||
end
|
||||
|
||||
private.mailDB:BulkInsertNewRow(i, mailType, sender or UNKNOWN, subject or "--", firstItemString or "", itemCount or 0, money or 0, cod or 0, daysLeft)
|
||||
end
|
||||
private.quantityDB:TruncateAndBulkInsertStart()
|
||||
for itemString, quantity in pairs(private.settings.mailQuantity) do
|
||||
private.quantityDB:BulkInsertNewRow(itemString, quantity)
|
||||
end
|
||||
private.quantityDB:BulkInsertEnd()
|
||||
private.itemDB:BulkInsertEnd()
|
||||
private.mailDB:BulkInsertEnd()
|
||||
|
||||
private.settings.expiringMail[PLAYER_NAME] = expiration ~= math.huge and expiration or nil
|
||||
for _, callback in ipairs(private.expiresCallbacks) do
|
||||
callback()
|
||||
end
|
||||
for _, callback in ipairs(private.callbacks) do
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.ChangePendingMailQuantity(itemString, quantity)
|
||||
assert(quantity ~= 0)
|
||||
private.settings.pendingMail[PLAYER_NAME][itemString] = (private.settings.pendingMail[PLAYER_NAME][itemString] or 0) + quantity
|
||||
if not private.quantityDB:HasUniqueRow("itemString", itemString) then
|
||||
-- create a new row
|
||||
private.quantityDB:NewRow()
|
||||
:SetField("itemString", itemString)
|
||||
:SetField("quantity", quantity)
|
||||
:Create()
|
||||
else
|
||||
local row = private.quantityDB:GetUniqueRow("itemString", itemString)
|
||||
local newValue = row:GetField("quantity") + quantity
|
||||
assert(newValue >= 0)
|
||||
if newValue == 0 then
|
||||
-- remove this row
|
||||
private.quantityDB:DeleteRow(row)
|
||||
else
|
||||
-- update this row
|
||||
row:SetField("quantity", newValue)
|
||||
:Update()
|
||||
end
|
||||
row:Release()
|
||||
end
|
||||
for _, callback in ipairs(private.callbacks) do
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
function private.ValidateCharacter(character)
|
||||
if not character then
|
||||
return
|
||||
end
|
||||
local characterName, realm = strsplit("-", strlower(character))
|
||||
-- we only care to track mails with characters on this realm
|
||||
if realm and realm ~= strlower(GetRealmName()) then
|
||||
return
|
||||
end
|
||||
-- we only care to track mails with characters on this account
|
||||
local result = nil
|
||||
for _, name in Settings.CharacterByAccountFactionrealmIterator() do
|
||||
if strlower(name) == characterName then
|
||||
result = name
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function private.GetInboxItemLink(index, num)
|
||||
local link = GetInboxItemLink(index, num)
|
||||
if ItemString.GetBase(link) ~= ItemString.GetPetCage() then
|
||||
return link
|
||||
end
|
||||
|
||||
-- need to do tooltip scanning to get battlepet links
|
||||
private.tooltip = private.tooltip or CreateFrame("GameTooltip", "TSM4MailingInboxTooltip", UIParent, "GameTooltipTemplate")
|
||||
private.tooltip:SetOwner(UIParent, "ANCHOR_NONE")
|
||||
private.tooltip:ClearLines()
|
||||
|
||||
local _, speciesId, level, breedQuality = private.tooltip:SetInboxItem(index, num)
|
||||
assert(speciesId and speciesId > 0)
|
||||
private.tooltip:Hide()
|
||||
return ItemInfo.GetLink(strjoin(":", "p", speciesId, level, breedQuality))
|
||||
end
|
||||
|
||||
function private.GetMailType(index, firstItemString)
|
||||
local _, _, _, subject, money, cod, _, numItems, _, _, _, _, isGM = GetInboxHeaderInfo(index)
|
||||
if isGM or (cod and cod > 0) or (money == 0 and (not numItems or numItems == 0)) then
|
||||
return nil
|
||||
end
|
||||
|
||||
local info = GetInboxInvoiceInfo(index)
|
||||
if money and money > 0 and info == "seller" then
|
||||
return "SALE"
|
||||
elseif numItems and numItems > 0 and info == "buyer" then
|
||||
return "BUY"
|
||||
elseif not info then
|
||||
if strfind(subject, string.gsub("^"..AUCTION_REMOVED_MAIL_SUBJECT, "%%s", "")) then
|
||||
return "CANCEL"
|
||||
end
|
||||
|
||||
if strfind(subject, string.gsub("^"..AUCTION_EXPIRED_MAIL_SUBJECT, "%%s", "")) then
|
||||
return "EXPIRE"
|
||||
end
|
||||
end
|
||||
|
||||
return "OTHER"
|
||||
end
|
||||
157
LibTSM/Service/PlayerInfo.lua
Normal file
157
LibTSM/Service/PlayerInfo.lua
Normal file
@@ -0,0 +1,157 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- PlayerInfo Functions
|
||||
-- @module PlayerInfo
|
||||
|
||||
local _, TSM = ...
|
||||
local PlayerInfo = TSM.Init("Service.PlayerInfo")
|
||||
local String = TSM.Include("Util.String")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local private = {
|
||||
connectedAlts = {},
|
||||
settings = nil,
|
||||
isPlayerCache = {},
|
||||
}
|
||||
local PLAYER_NAME = UnitName("player")
|
||||
local PLAYER_LOWER = strlower(PLAYER_NAME)
|
||||
local FACTION_LOWER = strlower(UnitFactionGroup("player"))
|
||||
local REALM_LOWER = strlower(GetRealmName())
|
||||
local PLAYER_REALM_LOWER = PLAYER_LOWER.." - "..REALM_LOWER
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
PlayerInfo:OnSettingsLoad(function()
|
||||
private.settings = Settings.NewView()
|
||||
:AddKey("factionrealm", "internalData", "guildVaults")
|
||||
:AddKey("factionrealm", "coreOptions", "ignoreGuilds")
|
||||
:AddKey("factionrealm", "internalData", "characterGuilds")
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Return all connected realm alternative characters.
|
||||
-- @return table The populated alternative characters.
|
||||
function PlayerInfo.GetConnectedAlts()
|
||||
wipe(private.connectedAlts)
|
||||
for factionrealm in TSM.db:GetConnectedRealmIterator("factionrealm") do
|
||||
for _, character in TSM.db:FactionrealmCharacterIterator(factionrealm) do
|
||||
local realm = strmatch(factionrealm, ".+ %- (.+)")
|
||||
character = Ambiguate(gsub(strmatch(character, "(.*) ?"..String.Escape("-").."?").."-"..gsub(realm, String.Escape("-"), ""), " ", ""), "none")
|
||||
if character ~= UnitName("player") then
|
||||
tinsert(private.connectedAlts, character)
|
||||
end
|
||||
end
|
||||
end
|
||||
sort(private.connectedAlts)
|
||||
return private.connectedAlts
|
||||
end
|
||||
|
||||
--- Iterate over all characters on this factionrealm.
|
||||
-- @tparam[opt=false] boolean currentAccountOnly If true, will only include the current account
|
||||
-- @return An iterator with the following fields: `index, name`
|
||||
function PlayerInfo.CharacterIterator(currentAccountOnly)
|
||||
if currentAccountOnly then
|
||||
return Settings.CharacterByAccountFactionrealmIterator()
|
||||
else
|
||||
return Settings.FactionrealmCharacterIterator()
|
||||
end
|
||||
end
|
||||
|
||||
--- Iterate over all guilds on this factionrealm.
|
||||
-- @tparam[opt=false] boolean includeIgnored If true, will include guilds which have been set to be ignored
|
||||
-- @return An iterator with the following fields: `index, guildName`
|
||||
function PlayerInfo.GuildIterator(includeIgnored)
|
||||
if includeIgnored then
|
||||
return private.GuildIteratorIgnoreIncluded, private.settings.guildVaults
|
||||
else
|
||||
return private.GuildIterator, private.settings.guildVaults
|
||||
end
|
||||
end
|
||||
|
||||
--- Get the player's guild.
|
||||
-- @tparam string player The name of the player
|
||||
-- @treturn ?string The name of the player's guilde or nil if it's not in one
|
||||
function PlayerInfo.GetPlayerGuild(player)
|
||||
return player and private.settings.characterGuilds[player] or nil
|
||||
end
|
||||
|
||||
--- Check whether or not a player belongs to the user.
|
||||
-- @tparam string target The name of the player
|
||||
-- @tparam boolean includeAlts Whether or not to include alts
|
||||
-- @tparam boolean includeOtherFaction Whether or not to include players on the other faction
|
||||
-- @tparam boolean includeOtherAccounts Whether or not to include connected accounts
|
||||
-- @treturn boolean Whether or not the player belongs to the user
|
||||
function PlayerInfo.IsPlayer(target, includeAlts, includeOtherFaction, includeOtherAccounts)
|
||||
local cacheKey = strjoin("%", target, includeAlts and "1" or "0", includeOtherFaction and "1" or "0", includeOtherAccounts and "1" or "0")
|
||||
if private.isPlayerCache.lastUpdate ~= GetTime() then
|
||||
wipe(private.isPlayerCache)
|
||||
private.isPlayerCache.lastUpdate = GetTime()
|
||||
end
|
||||
if private.isPlayerCache[cacheKey] == nil then
|
||||
private.isPlayerCache[cacheKey] = private.IsPlayerHelper(target, includeAlts, includeOtherFaction, includeOtherAccounts)
|
||||
end
|
||||
return private.isPlayerCache[cacheKey]
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.IsPlayerHelper(target, includeAlts, includeOtherFaction, includeOtherAccounts)
|
||||
target = strlower(target)
|
||||
if not strfind(target, " %- ") then
|
||||
target = gsub(target, "%-", " - ", 1)
|
||||
end
|
||||
if target == PLAYER_LOWER then
|
||||
return true
|
||||
elseif strfind(target, " %- ") and target == PLAYER_REALM_LOWER then
|
||||
return true
|
||||
end
|
||||
if not strfind(target, " %- ") then
|
||||
target = target.." - "..REALM_LOWER
|
||||
end
|
||||
if includeAlts then
|
||||
local result = false
|
||||
for _, factionrealm, character in Settings.ConnectedFactionrealmAltCharacterIterator() do
|
||||
local factionKey, realm = strmatch(factionrealm, "(.+) %- (.+)")
|
||||
factionKey = strlower(factionKey)
|
||||
if not result and target == strlower(character).." - "..strlower(realm) and (includeOtherFaction or factionKey == FACTION_LOWER) and (includeOtherAccounts or Settings.IsCurrentAccountOwner(character)) then
|
||||
result = true
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function private.GuildIterator(tbl, prevName)
|
||||
while true do
|
||||
local name = next(tbl, prevName)
|
||||
if not name then
|
||||
return nil
|
||||
end
|
||||
if not private.settings.ignoreGuilds[name] then
|
||||
return name
|
||||
end
|
||||
prevName = name
|
||||
end
|
||||
end
|
||||
|
||||
function private.GuildIteratorIgnoreIncluded(tbl, prevName)
|
||||
local name = next(tbl, prevName)
|
||||
return name
|
||||
end
|
||||
1630
LibTSM/Service/Settings.lua
Normal file
1630
LibTSM/Service/Settings.lua
Normal file
File diff suppressed because it is too large
Load Diff
83
LibTSM/Service/SlashCommands.lua
Normal file
83
LibTSM/Service/SlashCommands.lua
Normal file
@@ -0,0 +1,83 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local SlashCommands = TSM.Init("Service.SlashCommands")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local L = TSM.Include("Locale").GetTable()
|
||||
local private = {
|
||||
commandInfo = {},
|
||||
commandOrder = {},
|
||||
}
|
||||
local CALLBACK_TIME_WARNING_MS = 20
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
SlashCommands:OnModuleLoad(function()
|
||||
-- register the TSM slash commands
|
||||
SlashCmdList["TSM"] = private.OnChatCommand
|
||||
SlashCmdList["TRADESKILLMASTER"] = private.OnChatCommand
|
||||
_G["SLASH_TSM1"] = "/tsm"
|
||||
_G["SLASH_TRADESKILLMASTER1"] = "/tradeskillmaster"
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function SlashCommands.Register(key, callback, label)
|
||||
assert(key and callback)
|
||||
local keyLower = strlower(key)
|
||||
private.commandInfo[keyLower] = {
|
||||
key = key,
|
||||
label = label,
|
||||
callback = callback,
|
||||
}
|
||||
tinsert(private.commandOrder, keyLower)
|
||||
end
|
||||
|
||||
function SlashCommands.PrintHelp()
|
||||
Log.PrintUser(L["Slash Commands:"])
|
||||
for _, key in ipairs(private.commandOrder) do
|
||||
local info = private.commandInfo[key]
|
||||
if info.label then
|
||||
if info.key == "" then
|
||||
Log.PrintfUserRaw("|cffffaa00/tsm|r - %s", info.label)
|
||||
else
|
||||
Log.PrintfUserRaw("|cffffaa00/tsm %s|r - %s", info.key, info.label)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.OnChatCommand(input)
|
||||
input = strtrim(input)
|
||||
local cmd, args = strmatch(input, "^([^ ]*) ?(.*)$")
|
||||
cmd = strlower(cmd)
|
||||
if private.commandInfo[cmd] then
|
||||
local startTime = debugprofilestop()
|
||||
private.commandInfo[cmd].callback(args)
|
||||
local timeTaken = debugprofilestop() - startTime
|
||||
if timeTaken > CALLBACK_TIME_WARNING_MS then
|
||||
Log.Warn("Handler for slash command (/tsm%s) took %0.2fms", input ~= "" and " "..input or input, timeTaken)
|
||||
end
|
||||
else
|
||||
-- We weren't able to handle this command so print out the help
|
||||
SlashCommands.PrintHelp()
|
||||
end
|
||||
end
|
||||
57
LibTSM/Service/Sync.lua
Normal file
57
LibTSM/Service/Sync.lua
Normal file
@@ -0,0 +1,57 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Sync = TSM.Init("Service.Sync")
|
||||
local Connection = TSM.Include("Service.SyncClasses.Connection")
|
||||
local RPC = TSM.Include("Service.SyncClasses.RPC")
|
||||
local Mirror = TSM.Include("Service.SyncClasses.Mirror")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Sync.RegisterConnectionChangedCallback(callback)
|
||||
Connection.RegisterConnectionChangedCallback(callback)
|
||||
end
|
||||
|
||||
function Sync.RegisterRPC(name, func)
|
||||
RPC.Register(name, func)
|
||||
end
|
||||
|
||||
function Sync.CallRPC(name, targetPlayer, handler, ...)
|
||||
return RPC.Call(name, targetPlayer, handler, ...)
|
||||
end
|
||||
|
||||
function Sync.GetConnectionStatus(account)
|
||||
return Connection.GetStatus(account)
|
||||
end
|
||||
|
||||
function Sync.GetConnectedCharacterByAccount(account)
|
||||
return Connection.GetConnectedCharacterByAccount(account)
|
||||
end
|
||||
|
||||
function Sync.GetMirrorStatus(account)
|
||||
return Mirror.GetStatus(account)
|
||||
end
|
||||
|
||||
function Sync.RegisterMirrorCallback(callback)
|
||||
Mirror.RegisterCallback(callback)
|
||||
end
|
||||
|
||||
function Sync.EstablishConnection(character)
|
||||
return Connection.Establish(character)
|
||||
end
|
||||
|
||||
function Sync.GetNewAccountStatus()
|
||||
return Connection.GetNewAccountStatus()
|
||||
end
|
||||
|
||||
function Sync.RemoveAccount(account)
|
||||
Connection.Remove(account)
|
||||
end
|
||||
134
LibTSM/Service/SyncClasses/Comm.lua
Normal file
134
LibTSM/Service/SyncClasses/Comm.lua
Normal file
@@ -0,0 +1,134 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Comm = TSM.Init("Service.SyncClasses.Comm")
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local Table = TSM.Include("Util.Table")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local Constants = TSM.Include("Service.SyncClasses.Constants")
|
||||
local private = {
|
||||
handler = {},
|
||||
queuedPacket = {},
|
||||
queuedSourceCharacter = {},
|
||||
}
|
||||
-- load libraries
|
||||
LibStub("AceComm-3.0"):Embed(Comm)
|
||||
local LibSerialize = LibStub("LibSerialize")
|
||||
local LibDeflate = LibStub("LibDeflate")
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Comm:OnModuleLoad(function()
|
||||
Comm:RegisterComm("TSMSyncData", private.OnCommReceived)
|
||||
end)
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Comm.RegisterHandler(dataType, handler)
|
||||
assert(Table.KeyByValue(Constants.DATA_TYPES, dataType) ~= nil)
|
||||
assert(not private.handler[dataType])
|
||||
private.handler[dataType] = handler
|
||||
end
|
||||
|
||||
function Comm.SendData(dataType, targetCharacter, data)
|
||||
assert(type(dataType) == "string" and #dataType == 1)
|
||||
local packet = TempTable.Acquire()
|
||||
packet.dt = dataType
|
||||
packet.sa = Settings.GetCurrentSyncAccountKey()
|
||||
packet.v = Constants.VERSION
|
||||
packet.d = data
|
||||
local serialized = LibSerialize:Serialize(packet)
|
||||
TempTable.Release(packet)
|
||||
local compressed = LibDeflate:EncodeForWoWAddonChannel(LibDeflate:CompressDeflate(serialized))
|
||||
assert(LibDeflate:DecompressDeflate(LibDeflate:DecodeForWoWAddonChannel(compressed)) == serialized)
|
||||
|
||||
-- give heartbeats and rpc preambles a higher priority
|
||||
local priority = (dataType == Constants.DATA_TYPES.HEARTBEAT or dataType == Constants.DATA_TYPES.RPC_PREAMBLE) and "ALERT" or nil
|
||||
-- send the message
|
||||
Comm:SendCommMessage("TSMSyncData", compressed, "WHISPER", targetCharacter, priority)
|
||||
return #compressed
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.OnCommReceived(_, packet, _, sourceCharacter)
|
||||
-- delay the processing to make sure it happens within a debuggable context (this function is called via pcall)
|
||||
tinsert(private.queuedPacket, packet)
|
||||
tinsert(private.queuedSourceCharacter, sourceCharacter)
|
||||
Delay.AfterFrame("commReceiveQueue", 0, private.ProcessReceiveQueue)
|
||||
end
|
||||
|
||||
function private.ProcessReceiveQueue()
|
||||
assert(#private.queuedPacket == #private.queuedSourceCharacter)
|
||||
while #private.queuedPacket > 0 do
|
||||
local packet = tremove(private.queuedPacket, 1)
|
||||
local sourceCharacter = tremove(private.queuedSourceCharacter, 1)
|
||||
private.ProcessReceivedPacket(packet, sourceCharacter)
|
||||
end
|
||||
end
|
||||
|
||||
function private.ProcessReceivedPacket(msg, sourceCharacter)
|
||||
-- remove realm name from source player
|
||||
sourceCharacter = strsplit("-", sourceCharacter)
|
||||
sourceCharacter = strtrim(sourceCharacter)
|
||||
local sourceCharacterAccountKey = Settings.GetCharacterSyncAccountKey(sourceCharacter)
|
||||
if sourceCharacterAccountKey and sourceCharacterAccountKey == Settings.GetCurrentSyncAccountKey() then
|
||||
Log.Err("We own the source character")
|
||||
Settings.ShowSyncSVCopyError()
|
||||
return
|
||||
end
|
||||
|
||||
-- decode and decompress
|
||||
msg = LibDeflate:DecompressDeflate(LibDeflate:DecodeForWoWAddonChannel(msg))
|
||||
if not msg then
|
||||
Log.Err("Invalid packet")
|
||||
return
|
||||
end
|
||||
local success, packet = LibSerialize:Deserialize(msg)
|
||||
if not success then
|
||||
Log.Err("Invalid packet")
|
||||
return
|
||||
end
|
||||
|
||||
-- validate the packet
|
||||
local dataType = packet.dt
|
||||
local sourceAccount = packet.sa
|
||||
local version = packet.v
|
||||
local data = packet.d
|
||||
if type(dataType) ~= "string" or #dataType > 1 or not sourceAccount or version ~= Constants.VERSION then
|
||||
Log.Info("Invalid message received")
|
||||
return
|
||||
elseif sourceAccount == Settings.GetCurrentSyncAccountKey() then
|
||||
Log.Err("We are the source account (SV copy)")
|
||||
Settings.ShowSyncSVCopyError()
|
||||
return
|
||||
elseif sourceCharacterAccountKey and sourceCharacterAccountKey ~= sourceAccount then
|
||||
-- the source player now belongs to a different account than what we expect
|
||||
Log.Err("Unexpected source account")
|
||||
Settings.ShowSyncSVCopyError()
|
||||
return
|
||||
end
|
||||
|
||||
if private.handler[dataType] then
|
||||
private.handler[dataType](dataType, sourceAccount, sourceCharacter, data)
|
||||
else
|
||||
Log.Info("Received unhandled message of type: "..strbyte(dataType))
|
||||
end
|
||||
end
|
||||
443
LibTSM/Service/SyncClasses/Connection.lua
Normal file
443
LibTSM/Service/SyncClasses/Connection.lua
Normal file
@@ -0,0 +1,443 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Connection = TSM.Init("Service.SyncClasses.Connection")
|
||||
local L = TSM.Include("Locale").GetTable()
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Event = TSM.Include("Util.Event")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local Threading = TSM.Include("Service.Threading")
|
||||
local Constants = TSM.Include("Service.SyncClasses.Constants")
|
||||
local Comm = TSM.Include("Service.SyncClasses.Comm")
|
||||
local private = {
|
||||
isActive = false,
|
||||
hasFriendsInfo = false,
|
||||
newCharacter = nil,
|
||||
newAccount = nil,
|
||||
newSyncAcked = nil,
|
||||
connectionChangedCallbacks = {},
|
||||
threadId = {},
|
||||
threadRunning = {},
|
||||
connectedCharacter = {},
|
||||
lastHeartbeat = {},
|
||||
suppressThreadTime = {},
|
||||
connectionRequestReceived = {},
|
||||
addedFriends = {},
|
||||
invalidCharacters = {},
|
||||
}
|
||||
local RECEIVE_TIMEOUT = 5
|
||||
local HEARTBEAT_TIMEOUT = 10
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Connection:OnSettingsLoad(function()
|
||||
Event.Register("CHAT_MSG_SYSTEM", private.ChatMsgSystemEventHandler)
|
||||
Event.Register("FRIENDLIST_UPDATE", private.PrepareFriendsInfo)
|
||||
for _ in Settings.SyncAccountIterator() do
|
||||
private.isActive = true
|
||||
end
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.WHOAMI_ACCOUNT, private.WhoAmIAccountHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.WHOAMI_ACK, private.WhoAmIAckHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.CONNECTION_REQUEST, private.ConnectionHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.CONNECTION_REQUEST_ACK, private.ConnectionHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.DISCONNECT, private.DisconnectHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.HEARTBEAT, private.HeartbeatHandler)
|
||||
|
||||
private.PrepareFriendsInfo()
|
||||
end)
|
||||
|
||||
Connection:OnModuleUnload(function()
|
||||
for _, player in pairs(private.connectedCharacter) do
|
||||
Comm.SendData(Constants.DATA_TYPES.DISCONNECT, player)
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Connection.RegisterConnectionChangedCallback(handler)
|
||||
tinsert(private.connectionChangedCallbacks, handler)
|
||||
end
|
||||
|
||||
function Connection.IsCharacterConnected(targetCharacter)
|
||||
for _, player in pairs(private.connectedCharacter) do
|
||||
if player == targetCharacter then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function Connection.ConnectedAccountIterator()
|
||||
return pairs(private.connectedCharacter)
|
||||
end
|
||||
|
||||
function Connection.Establish(targetCharacter)
|
||||
if not private.hasFriendsInfo then
|
||||
Log.PrintUser(L["TSM is not yet ready to establish a new sync connection. Please try again later."])
|
||||
return false
|
||||
end
|
||||
local wasFriend = C_FriendList.GetFriendInfo(targetCharacter) and true or false
|
||||
if strlower(targetCharacter) == strlower(UnitName("player")) then
|
||||
Log.PrintUser(L["Sync Setup Error: You entered the name of the current character and not the character on the other account."])
|
||||
return false
|
||||
elseif not private.IsOnline(targetCharacter) and wasFriend then
|
||||
Log.PrintUser(L["Sync Setup Error: The specified player on the other account is not currently online."])
|
||||
return false
|
||||
end
|
||||
local invalidCharacter = false
|
||||
for _, player in Settings.CharacterByFactionrealmIterator() do
|
||||
if strlower(player) == strlower(targetCharacter) then
|
||||
invalidCharacter = true
|
||||
end
|
||||
end
|
||||
if invalidCharacter then
|
||||
Log.PrintUser(L["Sync Setup Error: This character is already part of a known account."])
|
||||
return false
|
||||
end
|
||||
if not private.isActive then
|
||||
private.isActive = true
|
||||
Delay.AfterTime("SYNC_CONNECTION_MANAGEMENT", 1, private.ManagementLoop, 1)
|
||||
end
|
||||
private.newCharacter = targetCharacter
|
||||
private.newAccount = nil
|
||||
private.newSyncAcked = nil
|
||||
Delay.Cancel("syncNewAccount")
|
||||
Delay.AfterTime("syncNewAccount", 0, private.SendNewAccountWhoAmI, 1)
|
||||
return true
|
||||
end
|
||||
|
||||
function Connection.GetNewAccountStatus()
|
||||
if not private.newCharacter then
|
||||
return nil
|
||||
end
|
||||
return format(L["Connecting to %s"], private.newCharacter)
|
||||
end
|
||||
|
||||
function Connection.GetStatus(account)
|
||||
if private.connectedCharacter[account] then
|
||||
return true, private.connectedCharacter[account]
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function Connection.Remove(account)
|
||||
if private.threadRunning[account] then
|
||||
Threading.Kill(private.threadId[account])
|
||||
private.ConnectionThreadDone(account)
|
||||
end
|
||||
Settings.RemoveSyncAccount(account)
|
||||
end
|
||||
|
||||
function Connection.GetConnectedCharacterByAccount(account)
|
||||
return private.connectedCharacter[account]
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Message Handlers
|
||||
-- ============================================================================
|
||||
|
||||
function private.WhoAmIAckHandler(dataType, sourceAccount, sourceCharacter, data)
|
||||
assert(dataType == Constants.DATA_TYPES.WHOAMI_ACK)
|
||||
if not private.newCharacter or strlower(private.newCharacter) ~= strlower(sourceCharacter) then
|
||||
-- we aren't trying to connect with a new account
|
||||
return
|
||||
end
|
||||
Log.Info("WHOAMI_ACK '%s'", tostring(private.newCharacter))
|
||||
private.newSyncAcked = true
|
||||
private.CheckNewAccountStatus()
|
||||
end
|
||||
|
||||
function private.WhoAmIAccountHandler(dataType, sourceAccount, sourceCharacter, data)
|
||||
assert(dataType == Constants.DATA_TYPES.WHOAMI_ACCOUNT)
|
||||
if not private.newCharacter then
|
||||
-- we aren't trying to connect with a new account
|
||||
return
|
||||
elseif strlower(private.newCharacter) ~= strlower(sourceCharacter) then
|
||||
Log.Info("WHOAMI_ACCOUNT from unknown player \"%s\", expected \"%s\"", private.newCharacter, sourceCharacter)
|
||||
return
|
||||
end
|
||||
private.newCharacter = sourceCharacter -- get correct capatilization
|
||||
private.newAccount = sourceAccount
|
||||
Log.Info("WHOAMI_ACCOUNT '%s' '%s'", private.newCharacter, private.newAccount)
|
||||
Comm.SendData(Constants.DATA_TYPES.WHOAMI_ACK, private.newCharacter)
|
||||
private.CheckNewAccountStatus()
|
||||
end
|
||||
|
||||
function private.ConnectionHandler(dataType, sourceAccount, sourceCharacter, data)
|
||||
if not private.threadRunning[sourceAccount] then
|
||||
return
|
||||
end
|
||||
private.connectionRequestReceived[sourceAccount] = true
|
||||
end
|
||||
|
||||
function private.DisconnectHandler(dataType, sourceAccount, sourceCharacter, data)
|
||||
assert(dataType == Constants.DATA_TYPES.DISCONNECT)
|
||||
if not private.threadRunning[sourceAccount] then
|
||||
return
|
||||
end
|
||||
|
||||
-- kill the thread and prevent it from running again for 2 seconds
|
||||
Threading.Kill(private.threadId[sourceAccount])
|
||||
private.ConnectionThreadDone(sourceAccount)
|
||||
private.suppressThreadTime[sourceAccount] = time() + 2
|
||||
end
|
||||
|
||||
function private.HeartbeatHandler(dataType, sourceAccount, sourceCharacter)
|
||||
assert(dataType == Constants.DATA_TYPES.HEARTBEAT)
|
||||
if not Connection.IsCharacterConnected(sourceCharacter) then
|
||||
-- we're not connected to this player
|
||||
return
|
||||
end
|
||||
private.lastHeartbeat[sourceAccount] = time()
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Management Loop / Sync Thread
|
||||
-- ============================================================================
|
||||
|
||||
function private.RequestFriendsInfo()
|
||||
C_FriendList.ShowFriends()
|
||||
end
|
||||
|
||||
function private.PrepareFriendsInfo()
|
||||
-- wait for friend info to populate
|
||||
local isValid
|
||||
local num = C_FriendList.GetNumFriends()
|
||||
if not num then
|
||||
isValid = false
|
||||
else
|
||||
isValid = true
|
||||
end
|
||||
for i = 1, num or 0 do
|
||||
if not C_FriendList.GetFriendInfoByIndex(i) then
|
||||
isValid = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if isValid then
|
||||
if not private.hasFriendsInfo and private.isActive then
|
||||
-- start the management loop
|
||||
Delay.AfterTime("SYNC_CONNECTION_MANAGEMENT", 1, private.ManagementLoop, 1)
|
||||
end
|
||||
private.hasFriendsInfo = true
|
||||
else
|
||||
-- try again
|
||||
Log.Err("Missing friends info - will try again")
|
||||
Delay.AfterTime("SYNC_PREPARE_FRIENDS_INFO", 0.5, private.RequestFriendsInfo)
|
||||
end
|
||||
end
|
||||
|
||||
function private.ManagementLoop()
|
||||
-- continuously spawn connection threads with online players as necessary
|
||||
private.RequestFriendsInfo()
|
||||
local hasAccount = false
|
||||
for _, account in Settings.SyncAccountIterator() do
|
||||
hasAccount = true
|
||||
local targetCharacter = private.GetTargetCharacter(account)
|
||||
if targetCharacter then
|
||||
if not private.threadId[account] then
|
||||
private.threadId[account] = Threading.New("SYNC_"..strmatch(account, "(%d+)$"), private.ConnectionThread)
|
||||
end
|
||||
if not private.threadRunning[account] and (private.suppressThreadTime[account] or 0) < time() then
|
||||
private.threadRunning[account] = true
|
||||
Threading.Start(private.threadId[account], account, targetCharacter)
|
||||
end
|
||||
end
|
||||
end
|
||||
if not hasAccount then
|
||||
Log.Info("No more sync accounts.")
|
||||
private.isActive = false
|
||||
if not private.newCharacter then
|
||||
Delay.Cancel("SYNC_CONNECTION_MANAGEMENT")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function private.ConnectionThreadInner(account, targetCharacter)
|
||||
-- for the initial handshake, the lower account key is the server, other is the client - after this it doesn't matter
|
||||
-- add some randomness to the timeout so we don't get stuck in a race condition
|
||||
local timeout = GetTime() + RECEIVE_TIMEOUT + random(0, 1000) / 1000
|
||||
if account < Settings.GetCurrentSyncAccountKey() then
|
||||
-- wait for the connection request from the client
|
||||
while not private.connectionRequestReceived[account] do
|
||||
if GetTime() > timeout then
|
||||
-- timed out on the connection - don't try again for a bit
|
||||
Log.Warn("Timed out")
|
||||
return
|
||||
end
|
||||
Threading.Yield(true)
|
||||
end
|
||||
-- send an connection request ACK back to the client
|
||||
Comm.SendData(Constants.DATA_TYPES.CONNECTION_REQUEST_ACK, targetCharacter)
|
||||
else
|
||||
-- send a connection request to the server
|
||||
Comm.SendData(Constants.DATA_TYPES.CONNECTION_REQUEST, targetCharacter)
|
||||
-- wait for the connection request ACK
|
||||
while not private.connectionRequestReceived[account] do
|
||||
if GetTime() > timeout then
|
||||
-- timed out on the connection - don't try again for a bit
|
||||
Log.Warn("Timed out")
|
||||
private.suppressThreadTime[account] = time() + RECEIVE_TIMEOUT
|
||||
return
|
||||
end
|
||||
Threading.Yield(true)
|
||||
end
|
||||
end
|
||||
|
||||
-- we are now connected
|
||||
Log.Info("Connected to: %s %s", account, targetCharacter)
|
||||
private.connectedCharacter[account] = targetCharacter
|
||||
private.lastHeartbeat[account] = time()
|
||||
for _, callback in ipairs(private.connectionChangedCallbacks) do
|
||||
callback(account, targetCharacter, true)
|
||||
end
|
||||
|
||||
-- now that we are connected, data can flow in both directions freely
|
||||
local lastHeartbeatSend = time()
|
||||
while true do
|
||||
-- check if they either logged off or the heartbeats have timed-out
|
||||
if not private.IsOnline(targetCharacter, true) or time() - private.lastHeartbeat[account] > HEARTBEAT_TIMEOUT then
|
||||
return
|
||||
end
|
||||
|
||||
-- check if we should send a heartbeat
|
||||
if time() - lastHeartbeatSend > floor(HEARTBEAT_TIMEOUT / 2) then
|
||||
Comm.SendData(Constants.DATA_TYPES.HEARTBEAT, targetCharacter)
|
||||
lastHeartbeatSend = time()
|
||||
end
|
||||
|
||||
Threading.Yield(true)
|
||||
end
|
||||
end
|
||||
|
||||
function private.ConnectionThread(account, targetCharacter)
|
||||
private.ConnectionThreadInner(account, targetCharacter)
|
||||
private.ConnectionThreadDone(account)
|
||||
end
|
||||
|
||||
function private.ConnectionThreadDone(account)
|
||||
Log.Info("Connection ended to %s", account)
|
||||
local player = private.connectedCharacter[account]
|
||||
private.connectedCharacter[account] = nil
|
||||
if player then
|
||||
for _, callback in ipairs(private.connectionChangedCallbacks) do
|
||||
callback(account, player, false)
|
||||
end
|
||||
end
|
||||
private.threadRunning[account] = nil
|
||||
private.connectionRequestReceived[account] = nil
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.SendNewAccountWhoAmI()
|
||||
if not private.newCharacter then
|
||||
Delay.Cancel("syncNewAccount")
|
||||
elseif not C_FriendList.GetFriendInfo(private.newCharacter) then
|
||||
Log.Info("Waiting for friends list to update")
|
||||
elseif not private.IsOnline(private.newCharacter) then
|
||||
Delay.Cancel("syncNewAccount")
|
||||
private.newCharacter = nil
|
||||
private.newAccount = nil
|
||||
private.newSyncAcked = nil
|
||||
Log.Err("New player went offline")
|
||||
else
|
||||
Comm.SendData(Constants.DATA_TYPES.WHOAMI_ACCOUNT, private.newCharacter)
|
||||
Log.Info("Sent WHOAMI_ACCOUNT")
|
||||
end
|
||||
end
|
||||
|
||||
function private.CheckNewAccountStatus()
|
||||
if not private.newCharacter or not private.newAccount or not private.newSyncAcked then
|
||||
return
|
||||
end
|
||||
Log.Info("New sync character: '%s' '%s'", private.newCharacter, private.newAccount)
|
||||
-- the other account ACK'd so setup a connection
|
||||
Settings.NewSyncCharacter(private.newAccount, private.newCharacter)
|
||||
|
||||
-- call the callbacks for this new account
|
||||
for _, callback in ipairs(private.connectionChangedCallbacks) do
|
||||
callback(private.newAccount, private.newCharacter, nil)
|
||||
end
|
||||
|
||||
private.newCharacter = nil
|
||||
private.newAccount = nil
|
||||
private.newSyncAcked = nil
|
||||
end
|
||||
|
||||
function private.GetTargetCharacter(account)
|
||||
local tempTbl = TempTable.Acquire()
|
||||
for _, character in Settings.CharacterByAccountFactionrealmIterator(account) do
|
||||
tinsert(tempTbl, character)
|
||||
end
|
||||
|
||||
-- find the player to connect to without adding to the friends list
|
||||
for _, player in ipairs(tempTbl) do
|
||||
if private.IsOnline(player, true) then
|
||||
TempTable.Release(tempTbl)
|
||||
return player
|
||||
end
|
||||
end
|
||||
-- if we failed, try again with adding to friends list
|
||||
for _, player in ipairs(tempTbl) do
|
||||
if private.IsOnline(player) then
|
||||
TempTable.Release(tempTbl)
|
||||
return player
|
||||
end
|
||||
end
|
||||
TempTable.Release(tempTbl)
|
||||
end
|
||||
|
||||
function private.IsOnline(target, noAdd)
|
||||
local info = C_FriendList.GetFriendInfo(target)
|
||||
if not info and not noAdd and not private.invalidCharacters[strlower(target)] and C_FriendList.GetNumFriends() ~= 50 then
|
||||
-- add them as a friend
|
||||
C_FriendList.AddFriend(target)
|
||||
private.RequestFriendsInfo()
|
||||
tinsert(private.addedFriends, target)
|
||||
info = C_FriendList.GetFriendInfo(target)
|
||||
end
|
||||
return info and info.connected or false
|
||||
end
|
||||
|
||||
function private.ChatMsgSystemEventHandler(_, msg)
|
||||
if #private.addedFriends == 0 then
|
||||
return
|
||||
end
|
||||
if msg == ERR_FRIEND_NOT_FOUND then
|
||||
if #private.addedFriends > 0 then
|
||||
private.invalidCharacters[strlower(tremove(private.addedFriends, 1))] = true
|
||||
end
|
||||
else
|
||||
for i, v in ipairs(private.addedFriends) do
|
||||
if format(ERR_FRIEND_ADDED_S, v) == msg then
|
||||
tremove(private.addedFriends, i)
|
||||
private.invalidCharacters[strlower(v)] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
29
LibTSM/Service/SyncClasses/Constants.lua
Normal file
29
LibTSM/Service/SyncClasses/Constants.lua
Normal file
@@ -0,0 +1,29 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Constants = TSM.Init("Service.SyncClasses.Constants")
|
||||
Constants.VERSION = 11
|
||||
Constants.DATA_TYPES = {
|
||||
-- new connection types (40-49)
|
||||
WHOAMI_ACCOUNT = strchar(40),
|
||||
WHOAMI_ACK = strchar(41),
|
||||
-- connection status types (50-69)
|
||||
CONNECTION_REQUEST = strchar(50),
|
||||
CONNECTION_REQUEST_ACK = strchar(51),
|
||||
DISCONNECT = strchar(52),
|
||||
HEARTBEAT = strchar(53),
|
||||
-- data mirroring types (70-99)
|
||||
CHARACTER_HASHES_BROADCAST = strchar(70),
|
||||
CHARACTER_SETTING_HASHES_REQUEST = strchar(71),
|
||||
CHARACTER_SETTING_HASHES_RESPONSE = strchar(72),
|
||||
CHARACTER_SETTING_DATA_REQUEST = strchar(73),
|
||||
CHARACTER_SETTING_DATA_RESPONSE = strchar(74),
|
||||
-- RPC types (100-109)
|
||||
RPC_CALL = strchar(100),
|
||||
RPC_RETURN = strchar(101),
|
||||
RPC_PREAMBLE = strchar(102),
|
||||
}
|
||||
268
LibTSM/Service/SyncClasses/Mirror.lua
Normal file
268
LibTSM/Service/SyncClasses/Mirror.lua
Normal file
@@ -0,0 +1,268 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local Mirror = TSM.Init("Service.SyncClasses.Mirror")
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local Settings = TSM.Include("Service.Settings")
|
||||
local Constants = TSM.Include("Service.SyncClasses.Constants")
|
||||
local Comm = TSM.Include("Service.SyncClasses.Comm")
|
||||
local Connection = TSM.Include("Service.SyncClasses.Connection")
|
||||
local private = {
|
||||
numConnected = 0,
|
||||
accountStatus = {},
|
||||
callbacks = {},
|
||||
}
|
||||
local BROADCAST_INTERVAL = 3
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
Mirror:OnModuleLoad(function()
|
||||
Connection.RegisterConnectionChangedCallback(private.ConnectionChangedHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.CHARACTER_HASHES_BROADCAST, private.CharacterHashesBroadcastHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.CHARACTER_SETTING_HASHES_REQUEST, private.CharacterSettingHashesRequestHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.CHARACTER_SETTING_HASHES_RESPONSE, private.CharacterSettingHashesResponseHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.CHARACTER_SETTING_DATA_REQUEST, private.CharacterSettingDataRequestHandler)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.CHARACTER_SETTING_DATA_RESPONSE, private.CharacterSettingDataResponseHandler)
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function Mirror.GetStatus(account)
|
||||
local status = private.accountStatus[account]
|
||||
if not status then
|
||||
return false, false
|
||||
elseif status == "UPDATING" then
|
||||
return true, false
|
||||
elseif status == "SYNCED" then
|
||||
return true, true
|
||||
else
|
||||
error("Invalid status: "..tostring(status))
|
||||
end
|
||||
end
|
||||
|
||||
function Mirror.RegisterCallback(callback)
|
||||
tinsert(private.callbacks, callback)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Connection Callback Handlers
|
||||
-- ============================================================================
|
||||
|
||||
function private.ConnectionChangedHandler(account, player, connected)
|
||||
if connected == nil then
|
||||
-- new account, but not yet connected
|
||||
return
|
||||
end
|
||||
if connected then
|
||||
Log.Info("Connected to %s (%s)", account, player)
|
||||
else
|
||||
Log.Info("Disconnected from %s (%s)", account, player)
|
||||
end
|
||||
private.numConnected = private.numConnected + (connected and 1 or -1)
|
||||
assert(private.numConnected >= 0)
|
||||
if connected then
|
||||
private.accountStatus[account] = "UPDATING"
|
||||
Delay.AfterTime("mirrorCharacterHashes", 0, private.SendCharacterHashes, BROADCAST_INTERVAL)
|
||||
else
|
||||
private.accountStatus[account] = nil
|
||||
if private.numConnected == 0 then
|
||||
Delay.Cancel("mirrorCharacterHashes")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Delay-Based Last Update Send Function
|
||||
-- ============================================================================
|
||||
|
||||
function private.SendCharacterHashes()
|
||||
assert(private.numConnected > 0)
|
||||
|
||||
-- calculate the hashes of the sync settings for all characters on this account
|
||||
local hashes = TempTable.Acquire()
|
||||
for _, character in Settings.CharacterByAccountFactionrealmIterator() do
|
||||
hashes[character] = private.CalculateCharacterHash(character)
|
||||
end
|
||||
|
||||
-- send the hashes to all connected accounts
|
||||
for _, character in Connection.ConnectedAccountIterator() do
|
||||
Comm.SendData(Constants.DATA_TYPES.CHARACTER_HASHES_BROADCAST, character, hashes)
|
||||
end
|
||||
TempTable.Release(hashes)
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Message Handlers
|
||||
-- ============================================================================
|
||||
|
||||
function private.CharacterHashesBroadcastHandler(dataType, sourceAccount, sourcePlayer, data)
|
||||
assert(dataType == Constants.DATA_TYPES.CHARACTER_HASHES_BROADCAST)
|
||||
if not Connection.IsCharacterConnected(sourcePlayer) then
|
||||
-- we're not connected to this player
|
||||
Log.Warn("Got CHARACTER_HASHES_BROADCAST for player which isn't connected")
|
||||
return
|
||||
end
|
||||
local didChange = false
|
||||
for _, character in Settings.CharacterByAccountFactionrealmIterator(sourceAccount) do
|
||||
if not data[character] then
|
||||
-- this character doesn't exist anymore, so remove it
|
||||
Log.Info("Removed character: '%s'", character)
|
||||
Settings.RemoveSyncCharacter(character)
|
||||
didChange = true
|
||||
end
|
||||
end
|
||||
for character, hash in pairs(data) do
|
||||
if not Settings.GetCharacterSyncAccountKey(character) then
|
||||
-- this is a new character, so add it to our DB
|
||||
Log.Info("New character: '%s' '%s'", character, sourceAccount)
|
||||
Settings.NewSyncCharacter(sourceAccount, character)
|
||||
didChange = true
|
||||
end
|
||||
if hash ~= private.CalculateCharacterHash(character) then
|
||||
-- this character's data has changed so request a hash of each of the keys
|
||||
Log.Info("Character data has changed: '%s'", character)
|
||||
Comm.SendData(Constants.DATA_TYPES.CHARACTER_SETTING_HASHES_REQUEST, sourcePlayer, character)
|
||||
didChange = true
|
||||
end
|
||||
end
|
||||
if didChange then
|
||||
private.accountStatus[sourceAccount] = "UPDATING"
|
||||
else
|
||||
private.accountStatus[sourceAccount] = "SYNCED"
|
||||
end
|
||||
end
|
||||
|
||||
function private.CharacterSettingHashesRequestHandler(dataType, sourceAccount, sourcePlayer, data)
|
||||
assert(dataType == Constants.DATA_TYPES.CHARACTER_SETTING_HASHES_REQUEST)
|
||||
if not Connection.IsCharacterConnected(sourcePlayer) then
|
||||
-- we're not connected to this player
|
||||
Log.Warn("Got CHARACTER_HASHES_BROADCAST for player which isn't connected")
|
||||
return
|
||||
elseif Settings.GetCharacterSyncAccountKey(data) ~= Settings.GetCurrentSyncAccountKey() then
|
||||
-- we don't own this character
|
||||
Log.Err("Request for character we don't own ('%s', '%s')", tostring(data), tostring(Settings.GetCharacterSyncAccountKey(data)))
|
||||
return
|
||||
end
|
||||
Log.Info("CHARACTER_SETTING_HASHES_REQUEST (%s)", data)
|
||||
local responseData = TempTable.Acquire()
|
||||
responseData._character = data
|
||||
for _, namespace, settingKey in Settings.SyncSettingIterator() do
|
||||
responseData[namespace.."."..settingKey] = private.CalculateCharacterSettingHash(data, namespace, settingKey)
|
||||
end
|
||||
Comm.SendData(Constants.DATA_TYPES.CHARACTER_SETTING_HASHES_RESPONSE, sourcePlayer, responseData)
|
||||
TempTable.Release(responseData)
|
||||
end
|
||||
|
||||
function private.CharacterSettingHashesResponseHandler(dataType, sourceAccount, sourcePlayer, data)
|
||||
assert(dataType == Constants.DATA_TYPES.CHARACTER_SETTING_HASHES_RESPONSE)
|
||||
if not Connection.IsCharacterConnected(sourcePlayer) then
|
||||
-- we're not connected to this player
|
||||
Log.Warn("Got CHARACTER_HASHES_BROADCAST for player which isn't connected")
|
||||
return
|
||||
end
|
||||
local character = data._character
|
||||
data._character = nil
|
||||
Log.Info("CHARACTER_SETTING_HASHES_RESPONSE (%s)", character)
|
||||
for key, hash in pairs(data) do
|
||||
local namespace, settingKey = strsplit(".", key)
|
||||
if private.CalculateCharacterSettingHash(character, namespace, settingKey) ~= hash then
|
||||
-- the settings data for key changed, so request the latest data for it
|
||||
Log.Info("Setting data has changed: '%s', '%s'", character, key)
|
||||
Comm.SendData(Constants.DATA_TYPES.CHARACTER_SETTING_DATA_REQUEST, sourcePlayer, character.."."..key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function private.CharacterSettingDataRequestHandler(dataType, sourceAccount, sourcePlayer, data)
|
||||
assert(dataType == Constants.DATA_TYPES.CHARACTER_SETTING_DATA_REQUEST)
|
||||
local character, namespace, settingKey = strsplit(".", data)
|
||||
if not Connection.IsCharacterConnected(sourcePlayer) then
|
||||
-- we're not connected to this player
|
||||
Log.Warn("Got CHARACTER_HASHES_BROADCAST for player which isn't connected")
|
||||
return
|
||||
elseif Settings.GetCharacterSyncAccountKey(character) ~= Settings.GetCurrentSyncAccountKey() then
|
||||
-- we don't own this character
|
||||
Log.Err("Request for character we don't own ('%s', '%s')", tostring(character), tostring(Settings.GetCharacterSyncAccountKey(character)))
|
||||
return
|
||||
end
|
||||
Log.Info("CHARACTER_SETTING_DATA_REQUEST (%s,%s,%s)", character, namespace, settingKey)
|
||||
local responseData = TempTable.Acquire()
|
||||
responseData.character = character
|
||||
responseData.namespace = namespace
|
||||
responseData.settingKey = settingKey
|
||||
responseData.data = Settings.Get("sync", Settings.GetSyncScopeKeyByCharacter(character), namespace, settingKey)
|
||||
Comm.SendData(Constants.DATA_TYPES.CHARACTER_SETTING_DATA_RESPONSE, sourcePlayer, responseData)
|
||||
TempTable.Release(responseData)
|
||||
end
|
||||
|
||||
function private.CharacterSettingDataResponseHandler(dataType, sourceAccount, sourcePlayer, data)
|
||||
assert(dataType == Constants.DATA_TYPES.CHARACTER_SETTING_DATA_RESPONSE)
|
||||
if not Connection.IsCharacterConnected(sourcePlayer) then
|
||||
-- we're not connected to this player
|
||||
Log.Warn("Got CHARACTER_HASHES_BROADCAST for player which isn't connected")
|
||||
return
|
||||
end
|
||||
local dataValueType = type(data.data)
|
||||
Log.Info("CHARACTER_SETTING_DATA_RESPONSE (%s,%s,%s,%s,%s)", data.character, data.namespace, data.settingKey, dataValueType, (dataValueType == "string" or dataValueType == "table") and #dataValueType or "-")
|
||||
if dataValueType == "table" then
|
||||
local tbl = Settings.Get("sync", Settings.GetSyncScopeKeyByCharacter(data.character), data.namespace, data.settingKey)
|
||||
wipe(tbl)
|
||||
for i, v in pairs(data.data) do
|
||||
tbl[i] = v
|
||||
end
|
||||
else
|
||||
Settings.Set("sync", Settings.GetSyncScopeKeyByCharacter(data.character), data.namespace, data.settingKey, data.data)
|
||||
end
|
||||
for _, callback in ipairs(private.callbacks) do
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.CalculateCharacterHash(character)
|
||||
local hash = nil
|
||||
local settingKeys = TempTable.Acquire()
|
||||
for _, namespace, settingKey in Settings.SyncSettingIterator() do
|
||||
tinsert(settingKeys, strjoin(".", namespace, settingKey))
|
||||
end
|
||||
sort(settingKeys)
|
||||
for _, key in ipairs(settingKeys) do
|
||||
hash = Math.CalculateHash(key, hash)
|
||||
local namespace, settingKey = strsplit(".", key)
|
||||
local settingValue = Settings.Get("sync", Settings.GetSyncScopeKeyByCharacter(character), namespace, settingKey)
|
||||
hash = Math.CalculateHash(settingValue, hash)
|
||||
end
|
||||
assert(hash)
|
||||
TempTable.Release(settingKeys)
|
||||
return hash
|
||||
end
|
||||
|
||||
function private.CalculateCharacterSettingHash(character, namespace, settingKey)
|
||||
return Math.CalculateHash(Settings.Get("sync", Settings.GetSyncScopeKeyByCharacter(character), namespace, settingKey))
|
||||
end
|
||||
177
LibTSM/Service/SyncClasses/RPC.lua
Normal file
177
LibTSM/Service/SyncClasses/RPC.lua
Normal file
@@ -0,0 +1,177 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
local _, TSM = ...
|
||||
local RPC = TSM.Init("Service.SyncClasses.RPC")
|
||||
local Delay = TSM.Include("Util.Delay")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local Constants = TSM.Include("Service.SyncClasses.Constants")
|
||||
local Comm = TSM.Include("Service.SyncClasses.Comm")
|
||||
local Connection = TSM.Include("Service.SyncClasses.Connection")
|
||||
local private = {
|
||||
rpcFunctions = {},
|
||||
pendingRPC = {},
|
||||
rpcSeqNum = 0,
|
||||
}
|
||||
local RPC_EXTRA_TIMEOUT = 15
|
||||
local CALLBACK_TIME_WARNING_THRESHOLD_MS = 20
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Loading
|
||||
-- ============================================================================
|
||||
|
||||
RPC:OnModuleLoad(function()
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.RPC_CALL, private.HandleCall)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.RPC_RETURN, private.HandleReturn)
|
||||
Comm.RegisterHandler(Constants.DATA_TYPES.RPC_PREAMBLE, private.HandlePreamble)
|
||||
end)
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
function RPC.Register(name, func)
|
||||
assert(name)
|
||||
private.rpcFunctions[name] = func
|
||||
end
|
||||
|
||||
function RPC.Call(name, targetPlayer, handler, ...)
|
||||
assert(targetPlayer)
|
||||
if not Connection.IsCharacterConnected(targetPlayer) then
|
||||
return false
|
||||
end
|
||||
|
||||
assert(private.rpcFunctions[name], "Cannot call an RPC which is not also registered locally.")
|
||||
private.rpcSeqNum = private.rpcSeqNum + 1
|
||||
|
||||
local requestData = TempTable.Acquire()
|
||||
requestData.name = name
|
||||
requestData.args = TempTable.Acquire(...)
|
||||
requestData.seq = private.rpcSeqNum
|
||||
local numBytes = Comm.SendData(Constants.DATA_TYPES.RPC_CALL, targetPlayer, requestData)
|
||||
TempTable.Release(requestData.args)
|
||||
TempTable.Release(requestData)
|
||||
|
||||
local context = TempTable.Acquire()
|
||||
context.name = name
|
||||
context.handler = handler
|
||||
context.timeoutTime = time() + RPC_EXTRA_TIMEOUT + private.EstimateTransferTime(numBytes)
|
||||
private.pendingRPC[private.rpcSeqNum] = context
|
||||
Delay.AfterTime("SYNC_PENDING_RPC", 1, private.HandlePendingRPC)
|
||||
|
||||
return true, (context.timeoutTime - time()) * 2 / 3
|
||||
end
|
||||
|
||||
function RPC.Cancel(name, handler)
|
||||
for seq, info in pairs(private.pendingRPC) do
|
||||
if info.name == name and info.handler == handler then
|
||||
TempTable.Release(info)
|
||||
private.pendingRPC[seq] = nil
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Message Handlers
|
||||
-- ============================================================================
|
||||
|
||||
function private.HandleCall(dataType, _, sourcePlayer, data)
|
||||
assert(dataType == Constants.DATA_TYPES.RPC_CALL)
|
||||
if type(data) ~= "table" or type(data.name) ~= "string" or type(data.seq) ~= "number" or type(data.args) ~= "table" then
|
||||
return
|
||||
end
|
||||
if not private.rpcFunctions[data.name] then
|
||||
return
|
||||
end
|
||||
local responseData = TempTable.Acquire()
|
||||
|
||||
local startTime = debugprofilestop()
|
||||
responseData.result = TempTable.Acquire(private.rpcFunctions[data.name](unpack(data.args)))
|
||||
local timeTaken = debugprofilestop() - startTime
|
||||
if timeTaken > CALLBACK_TIME_WARNING_THRESHOLD_MS then
|
||||
Log.Warn("RPC (%s) took %0.2fms", tostring(data.name), timeTaken)
|
||||
end
|
||||
responseData.seq = data.seq
|
||||
local numBytes = Comm.SendData(Constants.DATA_TYPES.RPC_RETURN, sourcePlayer, responseData)
|
||||
TempTable.Release(responseData.result)
|
||||
TempTable.Release(responseData)
|
||||
|
||||
local transferTime = private.EstimateTransferTime(numBytes)
|
||||
if transferTime > 1 then
|
||||
-- We sent more than 1 second worth of data back, so send a preamble to allow the source to adjust its timeout accordingly.
|
||||
local preambleData = TempTable.Acquire()
|
||||
preambleData.transferTime = transferTime
|
||||
preambleData.seq = data.seq
|
||||
Comm.SendData(Constants.DATA_TYPES.RPC_PREAMBLE, sourcePlayer, preambleData)
|
||||
TempTable.Release(preambleData)
|
||||
end
|
||||
end
|
||||
|
||||
function private.HandleReturn(dataType, _, _, data)
|
||||
assert(dataType == Constants.DATA_TYPES.RPC_RETURN)
|
||||
if type(data.seq) ~= "number" or type(data.result) ~= "table" then
|
||||
return
|
||||
elseif not private.pendingRPC[data.seq] then
|
||||
return
|
||||
end
|
||||
local startTime = debugprofilestop()
|
||||
private.pendingRPC[data.seq].handler(unpack(data.result))
|
||||
local timeTaken = debugprofilestop() - startTime
|
||||
if timeTaken > CALLBACK_TIME_WARNING_THRESHOLD_MS then
|
||||
Log.Warn("RPC (%s) result handler took %0.2fms", tostring(private.pendingRPC[data.seq].name), timeTaken)
|
||||
end
|
||||
TempTable.Release(private.pendingRPC[data.seq])
|
||||
private.pendingRPC[data.seq] = nil
|
||||
end
|
||||
|
||||
function private.HandlePreamble(dataType, _, _, data)
|
||||
assert(dataType == Constants.DATA_TYPES.RPC_PREAMBLE)
|
||||
if type(data.seq) ~= "number" or type(data.transferTime) ~= "number" then
|
||||
return
|
||||
elseif not private.pendingRPC[data.seq] then
|
||||
return
|
||||
end
|
||||
-- extend the timeout
|
||||
private.pendingRPC[data.seq].timeoutTime = time() + RPC_EXTRA_TIMEOUT + data.transferTime
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.EstimateTransferTime(numBytes)
|
||||
return ceil(numBytes / (ChatThrottleLib.MAX_CPS / 2))
|
||||
end
|
||||
|
||||
function private.HandlePendingRPC()
|
||||
if not next(private.pendingRPC) then
|
||||
return
|
||||
end
|
||||
local timedOut = TempTable.Acquire()
|
||||
for seq, info in pairs(private.pendingRPC) do
|
||||
if time() > info.timeoutTime then
|
||||
tinsert(timedOut, seq)
|
||||
end
|
||||
end
|
||||
for _, seq in ipairs(timedOut) do
|
||||
local info = private.pendingRPC[seq]
|
||||
Log.Warn("RPC timed out (%s)", info.name)
|
||||
info.handler()
|
||||
TempTable.Release(info)
|
||||
private.pendingRPC[seq] = nil
|
||||
end
|
||||
TempTable.Release(timedOut)
|
||||
end
|
||||
839
LibTSM/Service/Threading.lua
Normal file
839
LibTSM/Service/Threading.lua
Normal file
@@ -0,0 +1,839 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- TradeSkillMaster --
|
||||
-- https://tradeskillmaster.com --
|
||||
-- All Rights Reserved - Detailed license information included with addon. --
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
|
||||
--- Threading Functions
|
||||
-- @module Threading
|
||||
|
||||
local _, TSM = ...
|
||||
local Threading = TSM.Init("Service.Threading")
|
||||
local Debug = TSM.Include("Util.Debug")
|
||||
local Math = TSM.Include("Util.Math")
|
||||
local TempTable = TSM.Include("Util.TempTable")
|
||||
local Vararg = TSM.Include("Util.Vararg")
|
||||
local Log = TSM.Include("Util.Log")
|
||||
local ErrorHandler = TSM.Include("Service.ErrorHandler")
|
||||
local Thread = TSM.Include("LibTSMClass").DefineClass("Thread")
|
||||
local private = {
|
||||
threads = {},
|
||||
queue = {},
|
||||
runningThread = nil,
|
||||
schedulerFrame = nil,
|
||||
}
|
||||
local MAX_TIME_USAGE_RATIO = 0.25
|
||||
local EXCESSIVE_TIME_USED_RATIO = 1.2
|
||||
local EXCESSIVE_TIME_LOG_THRESHOLD_MS = 100
|
||||
local MAX_QUANTUM_MS = 10
|
||||
local SEND_MSG_SYNC_TIMEOUT_MS = 3000
|
||||
local YIELD_VALUE_START = {}
|
||||
local YIELD_VALUE = {}
|
||||
local SCHEDULER_TIME_WARNING_THRESHOLD_MS = 100
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Module Functions
|
||||
-- ============================================================================
|
||||
|
||||
--- Create a new thread.
|
||||
-- @tparam string name The name of the thread (for debugging purposes)
|
||||
-- @tparam function func The thread's main function
|
||||
-- @treturn string The thread id
|
||||
function Threading.New(name, func)
|
||||
assert(name and func)
|
||||
local thread = Thread(name, func)
|
||||
local threadId = strjoin("-", tostringall(thread, func))
|
||||
private.threads[threadId] = thread
|
||||
return threadId
|
||||
end
|
||||
|
||||
--- Set the callback for a thread.
|
||||
-- The callback is called when a thread finishes running and is passed all values returned by the thread's main function.
|
||||
-- @tparam string threadId The thread id
|
||||
-- @tparam function callback The callback function
|
||||
function Threading.SetCallback(threadId, callback)
|
||||
private.threads[threadId]:_SetCallback(callback)
|
||||
end
|
||||
|
||||
--- Start a thread.
|
||||
-- The thread will not actually be run until the next run of the scheduler (next frame).
|
||||
-- @tparam string threadId The thread id
|
||||
-- @tparam vararg ... Arguments to pass to the thread's main function
|
||||
function Threading.Start(threadId, ...)
|
||||
local thread = private.threads[threadId]
|
||||
assert(not thread:_IsAlive())
|
||||
-- make sure the scheduler is running
|
||||
private.StartScheduler()
|
||||
thread:_Start(...)
|
||||
end
|
||||
|
||||
--- Send a message to a thread.
|
||||
-- @tparam string threadId The thread id
|
||||
-- @tparam vararg ... The contents of the message
|
||||
function Threading.SendMessage(threadId, ...)
|
||||
local thread = private.threads[threadId]
|
||||
assert(thread:_IsAlive())
|
||||
tinsert(thread._messages, TempTable.Acquire(...))
|
||||
end
|
||||
|
||||
--- Send a synchronous message to a thread.
|
||||
-- The current execution context will be blocked until the message is delivered.
|
||||
-- @tparam string threadId The thread id
|
||||
-- @tparam vararg ... The contents of the message
|
||||
function Threading.SendSyncMessage(threadId, ...)
|
||||
if Threading.IsThreadContext() then
|
||||
-- we can't (sanely) run a thread from within a thread context, so we'll yield from the current thread first
|
||||
private.runningThread:_SendSyncMessage(threadId, ...)
|
||||
else
|
||||
local errMsg = private.threads[threadId]:_HandleSyncMessage(...)
|
||||
if errMsg then
|
||||
error(errMsg)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Kill a thread.
|
||||
-- @tparam string threadId The thread id
|
||||
function Threading.Kill(threadId)
|
||||
local thread = private.threads[threadId]
|
||||
if thread:_IsAlive() then
|
||||
thread:_Exit()
|
||||
end
|
||||
end
|
||||
|
||||
--- Check if we're currently in a thread context.
|
||||
-- @treturn boolean Whether or not we're currently in a thread context
|
||||
function Threading.IsThreadContext()
|
||||
return private.runningThread ~= nil
|
||||
end
|
||||
|
||||
--- Get the name of the current thread.
|
||||
-- @treturn ?string The name of the currently-running thread or nil if no thread is running
|
||||
function Threading.GetCurrentThreadName()
|
||||
return private.runningThread and private.runningThread._name or nil
|
||||
end
|
||||
|
||||
--- Check if the current thread has any pending messages.
|
||||
-- This must be called from a thread context.
|
||||
-- @treturn boolean Whether or not this thread has any pending messages
|
||||
function Threading.HasPendingMessage()
|
||||
return private.runningThread:_HasPendingMessage()
|
||||
end
|
||||
|
||||
--- Receive the next message.
|
||||
-- This must be called from a thread context.
|
||||
-- @return The context of the message
|
||||
function Threading.ReceiveMessage()
|
||||
return private.runningThread:_ReceiveMessage()
|
||||
end
|
||||
|
||||
--- Performs a yield.
|
||||
-- This must be called from a thread context. This function should be called regularly by threads to allow them the
|
||||
-- scheduler to switch to another thread if this thread's quantum is up. If this thread has not exceeded its quantum,
|
||||
-- this function will return right away.
|
||||
-- @tparam[opt=false] boolean force If true, forces this thread to yield, regardless of whether or not it needs to
|
||||
function Threading.Yield(force)
|
||||
private.runningThread:_Yield(force)
|
||||
end
|
||||
|
||||
--- Sleep the thread.
|
||||
-- This must be called from a thread context.
|
||||
-- @tparam number seconds The number of seconds to sleep for (may be a deciaml number)
|
||||
function Threading.Sleep(seconds)
|
||||
private.runningThread:_Sleep(seconds)
|
||||
end
|
||||
|
||||
--- Wait for a WoW event.
|
||||
-- This must be called from a thread context.
|
||||
-- @tparam string event The WoW event to wait for
|
||||
function Threading.WaitForEvent(...)
|
||||
return private.runningThread:_WaitForEvent(...)
|
||||
end
|
||||
|
||||
--- Wait for a function.
|
||||
-- This must be called from a thread context. It will block the thread until the specified function returns a true value.
|
||||
-- @tparam function func The function to wait for
|
||||
-- @tparam vararg ... Additional arguments to pass to the function
|
||||
function Threading.WaitForFunction(func, ...)
|
||||
return private.runningThread:_WaitForFunction(func, ...)
|
||||
end
|
||||
|
||||
--- Wait for a future.
|
||||
-- This must be called from a thread context. It will block the thread until the specified future is done.
|
||||
-- @tparam Future future The future to wait for
|
||||
function Threading.WaitForFuture(future)
|
||||
return private.runningThread:_WaitForFuture(future)
|
||||
end
|
||||
|
||||
--- Acquire a temp table.
|
||||
-- This must be called from a thread context. Any time a thread needs to maintain a temp table across a potential yield,
|
||||
-- it should use this API. This API will release the temp tables in the case that the thread gets killed.
|
||||
-- @see TempTable.Acquire
|
||||
-- @tparam vararg ... Values to insert into the temp table
|
||||
function Threading.AcquireSafeTempTable(...)
|
||||
return private.runningThread:_AcquireSafeTempTable(...)
|
||||
end
|
||||
|
||||
--- Release a temp table.
|
||||
-- This must be called from a thread context. This is used to release temp tables acquired with
|
||||
-- @{Threading.AcquireSafeTempTable}.
|
||||
-- @see TempTable.Release
|
||||
-- @tparam table tbl The temp table to release
|
||||
function Threading.ReleaseSafeTempTable(tbl)
|
||||
return private.runningThread:_ReleaseSafeTempTable(tbl)
|
||||
end
|
||||
|
||||
--- Release a temp table and returns its contents.
|
||||
-- This must be called from a thread context. This is used to release and unpack temp tables acquired with
|
||||
-- @{Threading.AcquireSafeTempTable}.
|
||||
-- @see TempTable.UnpackAndRelease
|
||||
-- @tparam table tbl The temp table to release and unpack
|
||||
-- @return The contents of the temp table
|
||||
function Threading.UnpackAndReleaseSafeTempTable(tbl)
|
||||
return private.runningThread:_UnpackAndReleaseSafeTempTable(tbl)
|
||||
end
|
||||
|
||||
--- Assign ownership of a database query to the thread so that if the thread is killed it'll get released safely.
|
||||
-- This must be called from a thread context.
|
||||
-- @tparam DatabaseQuery query The query object
|
||||
function Threading.GuardDatabaseQuery(query)
|
||||
return private.runningThread:_GuardDatabaseQuery(query)
|
||||
end
|
||||
|
||||
--- Removes ownership of a database query from the thread.
|
||||
-- This must be called from a thread context.
|
||||
-- @tparam DatabaseQuery query The query object
|
||||
function Threading.UnguardDatabaseQuery(query)
|
||||
return private.runningThread:_UnguardDatabaseQuery(query)
|
||||
end
|
||||
|
||||
function Threading.GetDebugStr()
|
||||
local lines = {}
|
||||
for _, thread in pairs(private.threads) do
|
||||
if thread:_IsAlive() then
|
||||
local name, stateStr, timeStr, createStr, startStr, backtrace = thread:_GetDebugInfo()
|
||||
tinsert(lines, " "..name)
|
||||
tinsert(lines, " "..stateStr)
|
||||
tinsert(lines, " "..timeStr)
|
||||
tinsert(lines, " "..createStr)
|
||||
tinsert(lines, " "..startStr)
|
||||
if #backtrace > 0 then
|
||||
tinsert(lines, " Backtrace:")
|
||||
for _, line in ipairs(backtrace) do
|
||||
tinsert(lines, " "..line)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Thread Class - General Methods
|
||||
-- ============================================================================
|
||||
|
||||
function Thread.__init(self, name, func)
|
||||
-- core fields
|
||||
self._func = func
|
||||
self._co = nil
|
||||
self._state = "DEAD"
|
||||
self._endTime = nil
|
||||
self._sleepTime = nil
|
||||
self._eventNames = {}
|
||||
self._eventArgs = nil
|
||||
self._waitFunction = nil
|
||||
self._waitFunctionArgs = nil
|
||||
self._waitFunctionResult = nil
|
||||
self._waitFuture = nil
|
||||
self._waitFutureResult = nil
|
||||
self._waitFutureDone = false
|
||||
self._syncMessage = nil
|
||||
self._syncMessageDest = nil
|
||||
self._messages = {}
|
||||
self._callback = nil
|
||||
self._returnValue = nil
|
||||
self._safeTempTables = {}
|
||||
self._guardedQueries = {}
|
||||
|
||||
-- debug fields
|
||||
self._startTime = 0
|
||||
self._cpuTimeUsed = 0
|
||||
self._realTimeUsed = 0
|
||||
self._name = name or tostring(self)
|
||||
self._createCaller = Debug.GetStackLevelLocation(3) or "?"
|
||||
self._startCaller = nil
|
||||
end
|
||||
|
||||
function Thread._Start(self, ...)
|
||||
self._co = coroutine.create(self._Main)
|
||||
self._state = "READY"
|
||||
self._endTime = 0
|
||||
self._sleepTime = nil
|
||||
wipe(self._eventNames)
|
||||
self._eventArgs = nil
|
||||
self._waitFunction = nil
|
||||
self._waitFunctionArgs = nil
|
||||
self._waitFunctionResult = nil
|
||||
self._waitFuture = nil
|
||||
self._waitFutureResult = nil
|
||||
self._waitFutureDone = false
|
||||
self._syncMessage = nil
|
||||
self._syncMessageDest = nil
|
||||
assert(not next(self._messages))
|
||||
assert(not next(self._safeTempTables))
|
||||
assert(not next(self._guardedQueries))
|
||||
self._startTime = 0
|
||||
self._cpuTimeUsed = 0
|
||||
self._realTimeUsed = 0
|
||||
self._startCaller = self._startCaller or Debug.GetStackLevelLocation(3)
|
||||
|
||||
-- run the thread once (will yield right away) to pass in self and the arguments
|
||||
local noErr, retValue = coroutine.resume(self._co, self, ...)
|
||||
assert(noErr and retValue == YIELD_VALUE_START)
|
||||
end
|
||||
|
||||
function Thread._SetCallback(self, callback)
|
||||
self._callback = callback
|
||||
end
|
||||
|
||||
function Thread._IsAlive(self)
|
||||
return self._state ~= "DEAD"
|
||||
end
|
||||
|
||||
function Thread._ToLogStr(self)
|
||||
if self._startTime then
|
||||
self._realTimeUsed = debugprofilestop() - self._startTime
|
||||
local pctStr = format("%.1f%%", Math.Round(self._cpuTimeUsed / self._realTimeUsed, 0.001) * 100)
|
||||
return format("%s [%s,%s]", self._name, self._state, pctStr)
|
||||
else
|
||||
return format("%s [%s]", self._name, self._state)
|
||||
end
|
||||
end
|
||||
|
||||
function Thread._Cleanup(self)
|
||||
for _, msg in ipairs(self._messages) do
|
||||
TempTable.Release(msg)
|
||||
end
|
||||
wipe(self._messages)
|
||||
for tbl in pairs(self._safeTempTables) do
|
||||
TempTable.Release(tbl)
|
||||
end
|
||||
wipe(self._safeTempTables)
|
||||
for query in pairs(self._guardedQueries) do
|
||||
query:Release(true)
|
||||
end
|
||||
wipe(self._guardedQueries)
|
||||
self._waitFunction = nil
|
||||
if self._waitFunctionArgs then
|
||||
TempTable.Release(self._waitFunctionArgs)
|
||||
self._waitFunctionArgs = nil
|
||||
end
|
||||
if self._waitFuture then
|
||||
self._waitFuture:Cancel()
|
||||
self._waitFuture = nil
|
||||
self._waitFutureResult = nil
|
||||
self._waitFutureDone = false
|
||||
end
|
||||
if self._waitFunctionResult then
|
||||
TempTable.Release(self._waitFunctionResult)
|
||||
self._waitFunctionResult = nil
|
||||
end
|
||||
if self._syncMessage then
|
||||
TempTable.Release(self._syncMessage)
|
||||
self._syncMessage = nil
|
||||
self._syncMessageDest = nil
|
||||
end
|
||||
end
|
||||
|
||||
function Thread._GetDebugInfo(self)
|
||||
local stateStr = nil
|
||||
if self._state == "SLEEPING" then
|
||||
stateStr = format("Sleeping for %d seconds", Math.Round(self._sleepTime, 0.001))
|
||||
elseif self._state == "WAITING_FOR_MSG" then
|
||||
if #self._messages > 0 then
|
||||
stateStr = "Got message"
|
||||
else
|
||||
stateStr = "Waiting for message"
|
||||
end
|
||||
elseif self._state == "WAITING_FOR_EVENT" then
|
||||
if next(self._eventNames) then
|
||||
local eventList = {}
|
||||
for event in pairs(self._eventNames) do
|
||||
tinsert(eventList, event)
|
||||
end
|
||||
stateStr = format("Waiting for %s", table.concat(eventList, "|"))
|
||||
else
|
||||
stateStr = format("Got %s", self._eventArgs[1])
|
||||
end
|
||||
elseif self._state == "WAITING_FOR_FUNCTION" then
|
||||
local functionName = nil
|
||||
-- look up to 2 levels deep in the globals table for the name of this function
|
||||
for k, v in pairs(_G) do
|
||||
if type(v) == "table" then
|
||||
for k2, v2 in pairs(v) do
|
||||
if v2 == self._waitFunction then
|
||||
functionName = tostring(k).."."..tostring(k2)
|
||||
break
|
||||
end
|
||||
end
|
||||
if functionName then
|
||||
break
|
||||
end
|
||||
elseif v == self._waitFunction then
|
||||
functionName = tostring(k)
|
||||
break
|
||||
end
|
||||
end
|
||||
stateStr = format("Waiting for %s", functionName or tostring(self._waitFunction))
|
||||
elseif self._state == "WAITING_FOR_FUTURE" then
|
||||
stateStr = format("Waiting for future (%s)", self._waitFuture:GetName())
|
||||
elseif self._state == "FORCED_YIELD" then
|
||||
stateStr = "Forced yield"
|
||||
elseif self._state == "SENDING_SYNC_MESSAGE" then
|
||||
stateStr = format("Sending sync message to %s", self._syncMessageDest and private.threads[self._syncMessageDest]._name or "?")
|
||||
elseif self._state == "RUNNING" then
|
||||
stateStr = "Running"
|
||||
elseif self._state == "DEAD" then
|
||||
stateStr = "Dead"
|
||||
elseif self._state == "READY" then
|
||||
stateStr = "Ready"
|
||||
else
|
||||
error("Invalid thread state: "..tostring(self._state))
|
||||
end
|
||||
if #self._messages > 0 then
|
||||
stateStr = format("%s (%d messages)", stateStr, #self._messages)
|
||||
end
|
||||
|
||||
local timeStr = "<Not Started>"
|
||||
if self._startTime then
|
||||
local wallTime = debugprofilestop() - self._startTime
|
||||
local cpuTime = self._cpuTimeUsed
|
||||
timeStr = format("Running for %.1f seconds (CPU: %dms, %.2f%%)", wallTime / 1000, cpuTime, (cpuTime / wallTime) * 100)
|
||||
end
|
||||
|
||||
local createStr = "Created @"..self._createCaller
|
||||
local startStr = "Started @"..(self._startCaller or "<Not Started>")
|
||||
|
||||
local backtrace = {}
|
||||
local level = 2
|
||||
local line = Debug.GetStackLevelLocation(level, self._co)
|
||||
while line do
|
||||
tinsert(backtrace, line)
|
||||
level = level + 1
|
||||
line = Debug.GetStackLevelLocation(level, self._co)
|
||||
end
|
||||
|
||||
|
||||
return self._name, stateStr, timeStr, createStr, startStr, backtrace
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Thread Class - Scheduler Helper Methods
|
||||
-- ============================================================================
|
||||
|
||||
function Thread._CanRun(self)
|
||||
return self._state == "READY"
|
||||
end
|
||||
|
||||
function Thread._Run(self, quantum)
|
||||
assert(not Threading.IsThreadContext())
|
||||
if not self:_CanRun() then
|
||||
return 0
|
||||
end
|
||||
private.runningThread = self
|
||||
self._state = "RUNNING"
|
||||
local startTime = debugprofilestop()
|
||||
self._endTime = startTime + quantum
|
||||
local noErr, returnVal = coroutine.resume(self._co)
|
||||
local elapsedTime = debugprofilestop() - startTime
|
||||
private.runningThread = nil
|
||||
|
||||
assert(not noErr or returnVal == YIELD_VALUE)
|
||||
if noErr and self._state == "SENDING_SYNC_MESSAGE" then
|
||||
-- yielded to send a sync message to another thread
|
||||
local destThread = private.threads[self._syncMessageDest]
|
||||
local msg = self._syncMessage
|
||||
self._syncMessage = nil
|
||||
self._syncMessageDest = nil
|
||||
local errMsg = destThread:_HandleSyncMessage(TempTable.UnpackAndRelease(msg))
|
||||
if errMsg then
|
||||
noErr = false
|
||||
returnVal = errMsg
|
||||
elseif self._state == "SENDING_SYNC_MESSAGE" then
|
||||
self._state = "READY"
|
||||
end
|
||||
end
|
||||
if not noErr then
|
||||
returnVal = returnVal or "UNKNOWN ERROR"
|
||||
ErrorHandler.ShowForThread(returnVal, self._co)
|
||||
self._state = "DEAD"
|
||||
end
|
||||
if self._state == "DEAD" then
|
||||
self:_Cleanup()
|
||||
local returnValue = self._returnValue
|
||||
self._returnValue = nil
|
||||
if self._callback and returnValue then
|
||||
self._callback(TempTable.UnpackAndRelease(returnValue))
|
||||
elseif returnValue then
|
||||
TempTable.Release(returnValue)
|
||||
end
|
||||
end
|
||||
return elapsedTime
|
||||
end
|
||||
|
||||
function Thread._UpdateState(self, elapsed)
|
||||
-- check what the thread state is
|
||||
if self._state == "SLEEPING" then
|
||||
self._sleepTime = self._sleepTime - elapsed
|
||||
if self._sleepTime <= 0 then
|
||||
self._sleepTime = nil
|
||||
self._state = "READY"
|
||||
end
|
||||
elseif self._state == "WAITING_FOR_MSG" then
|
||||
if #self._messages > 0 then
|
||||
self._state = "READY"
|
||||
end
|
||||
elseif self._state == "WAITING_FOR_EVENT" then
|
||||
assert(self._eventNames or self._eventArgs)
|
||||
if self._eventArgs then
|
||||
self._state = "READY"
|
||||
end
|
||||
elseif self._state == "WAITING_FOR_FUNCTION" then
|
||||
assert(self._waitFunction, "Waiting for function without waitFunction set")
|
||||
local result = TempTable.Acquire(self._waitFunction(unpack(self._waitFunctionArgs)))
|
||||
if result[1] then
|
||||
self._waitFunctionResult = result
|
||||
self._state = "READY"
|
||||
else
|
||||
TempTable.Release(result)
|
||||
end
|
||||
elseif self._state == "WAITING_FOR_FUTURE" then
|
||||
if self._waitFutureDone then
|
||||
assert(not self._waitFuture)
|
||||
self._waitFutureDone = false
|
||||
self._state = "READY"
|
||||
else
|
||||
assert(self._waitFuture)
|
||||
end
|
||||
elseif self._state == "FORCED_YIELD" then
|
||||
self._state = "READY"
|
||||
elseif self._state == "RUNNING" then
|
||||
-- this shouldn't happen, so just kill this thread
|
||||
self:_Exit()
|
||||
elseif self._state == "DEAD" then
|
||||
-- pass
|
||||
elseif self._state == "READY" then
|
||||
-- pass
|
||||
else
|
||||
error("Invalid thread state: "..tostring(self._state))
|
||||
end
|
||||
end
|
||||
|
||||
function Thread._ProcessEvent(self, event, ...)
|
||||
if self._state ~= "WAITING_FOR_EVENT" then
|
||||
return
|
||||
end
|
||||
assert(next(self._eventNames) or self._eventArgs)
|
||||
if not self._eventNames[event] then
|
||||
return
|
||||
end
|
||||
wipe(self._eventNames) -- only trigger the event once then clear all
|
||||
self._eventArgs = TempTable.Acquire(event, ...)
|
||||
end
|
||||
|
||||
function Thread._OnFutureDone(self, future)
|
||||
if self._state ~= "WAITING_FOR_FUTURE" then
|
||||
return
|
||||
end
|
||||
assert(self._waitFuture or self._waitFutureDone)
|
||||
if future ~= self._waitFuture then
|
||||
return
|
||||
end
|
||||
self._waitFutureDone = true
|
||||
self._waitFuture = nil
|
||||
self._waitFutureResult = future:GetValue()
|
||||
self:_UpdateState(0)
|
||||
self:_Run(0)
|
||||
end
|
||||
|
||||
function Thread._HandleSyncMessage(self, ...)
|
||||
assert(not Threading.IsThreadContext())
|
||||
local msg = TempTable.Acquire(...)
|
||||
tinsert(self._messages, 1, msg) -- this message should be received first
|
||||
-- run the thread for up to 3 seconds to get it to process the sync message
|
||||
local startTime = debugprofilestop()
|
||||
while self._messages[1] == msg do
|
||||
if debugprofilestop() - startTime > SEND_MSG_SYNC_TIMEOUT_MS or not self:_IsAlive() then
|
||||
-- want to error from the sending context, so just return the error
|
||||
return format("ERROR: A sync message was not able to be delivered! (%s)", self._name)
|
||||
end
|
||||
assert(self._state ~= "SENDING_SYNC_MESSAGE", "Circular sync message detected")
|
||||
if self._state == "WAITING_FOR_MSG" then
|
||||
self._state = "READY"
|
||||
end
|
||||
self:_Run(0)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Thread Class - Thread Context Methods
|
||||
-- ============================================================================
|
||||
|
||||
function Thread._Main(self, ...)
|
||||
self._startTime = debugprofilestop()
|
||||
coroutine.yield(YIELD_VALUE_START)
|
||||
self._returnValue = TempTable.Acquire(self._func(...))
|
||||
self:_Exit()
|
||||
end
|
||||
|
||||
function Thread._Yield(self, force)
|
||||
if force or self._state ~= "RUNNING" or debugprofilestop() > self._endTime then
|
||||
-- only change the state if it's currently set to RUNNING
|
||||
if self._state == "RUNNING" then
|
||||
self._state = force and "FORCED_YIELD" or "READY"
|
||||
end
|
||||
coroutine.yield(YIELD_VALUE)
|
||||
end
|
||||
end
|
||||
|
||||
function Thread._Sleep(self, seconds)
|
||||
self._state = "SLEEPING"
|
||||
self._sleepTime = seconds
|
||||
self:_Yield()
|
||||
end
|
||||
|
||||
function Thread._HasPendingMessage(self)
|
||||
return #self._messages > 0
|
||||
end
|
||||
|
||||
function Thread._ReceiveMessage(self)
|
||||
if #self._messages == 0 then
|
||||
-- change the state if there's no messages ready
|
||||
self._state = "WAITING_FOR_MSG"
|
||||
elseif debugprofilestop() > self._endTime then
|
||||
-- If we're about to yield, set the state to WAITING_FOR_MSG even if we have messages in the queue
|
||||
-- to allow sync messages to be sent to us.
|
||||
self._state = "WAITING_FOR_MSG"
|
||||
end
|
||||
self:_Yield()
|
||||
return TempTable.UnpackAndRelease(tremove(self._messages, 1))
|
||||
end
|
||||
|
||||
function Thread._SendSyncMessage(self, destThread, ...)
|
||||
assert(destThread ~= self)
|
||||
self._state = "SENDING_SYNC_MESSAGE"
|
||||
self._syncMessageDest = destThread
|
||||
self._syncMessage = TempTable.Acquire(...)
|
||||
self:_Yield()
|
||||
end
|
||||
|
||||
function Thread._WaitForEvent(self, ...)
|
||||
self._state = "WAITING_FOR_EVENT"
|
||||
self._eventArgs = nil
|
||||
for _, event in Vararg.Iterator(...) do
|
||||
self._eventNames[event] = true
|
||||
private.schedulerFrame:RegisterEvent(event)
|
||||
end
|
||||
local st = GetTime()
|
||||
self:_Yield()
|
||||
local yieldTime = GetTime() - st
|
||||
if yieldTime > 5 then
|
||||
local firstEvent = ...
|
||||
Log.Warn("Waited %d seconds for %s", yieldTime, firstEvent)
|
||||
end
|
||||
local result = self._eventArgs
|
||||
self._eventArgs = nil
|
||||
return TempTable.UnpackAndRelease(result)
|
||||
end
|
||||
|
||||
function Thread._WaitForFunction(self, func, ...)
|
||||
-- try the function once before yielding
|
||||
local result = TempTable.Acquire(func(...))
|
||||
if result[1] then
|
||||
return TempTable.UnpackAndRelease(result)
|
||||
end
|
||||
TempTable.Release(result)
|
||||
-- do the yield
|
||||
self._state = "WAITING_FOR_FUNCTION"
|
||||
self._waitFunction = func
|
||||
self._waitFunctionArgs = TempTable.Acquire(...)
|
||||
self:_Yield()
|
||||
result = self._waitFunctionResult
|
||||
self._waitFunction = nil
|
||||
TempTable.Release(self._waitFunctionArgs)
|
||||
self._waitFunctionArgs = nil
|
||||
self._waitFunctionResult = nil
|
||||
return TempTable.UnpackAndRelease(result)
|
||||
end
|
||||
|
||||
function Thread._WaitForFuture(self, future)
|
||||
-- try the future once before yielding
|
||||
if future:IsDone() then
|
||||
return future:GetValue()
|
||||
end
|
||||
-- register our OnDone handler and do the yield
|
||||
future:SetScript("OnDone", private.OnFutureDone)
|
||||
self._state = "WAITING_FOR_FUTURE"
|
||||
self._waitFuture = future
|
||||
self:_Yield()
|
||||
local result = self._waitFutureResult
|
||||
self._waitFutureResult = nil
|
||||
return result
|
||||
end
|
||||
|
||||
function Thread._AcquireSafeTempTable(self, ...)
|
||||
local tbl = TempTable.Acquire(...)
|
||||
assert(not self._safeTempTables[tbl])
|
||||
self._safeTempTables[tbl] = true
|
||||
return tbl
|
||||
end
|
||||
|
||||
function Thread._ReleaseSafeTempTable(self, tbl)
|
||||
assert(self._safeTempTables[tbl])
|
||||
self._safeTempTables[tbl] = nil
|
||||
return TempTable.Release(tbl)
|
||||
end
|
||||
|
||||
function Thread._UnpackAndReleaseSafeTempTable(self, tbl)
|
||||
assert(self._safeTempTables[tbl])
|
||||
self._safeTempTables[tbl] = nil
|
||||
return TempTable.UnpackAndRelease(tbl)
|
||||
end
|
||||
|
||||
function Thread._GuardDatabaseQuery(self, query)
|
||||
assert(not self._guardedQueries[query])
|
||||
self._guardedQueries[query] = true
|
||||
end
|
||||
|
||||
function Thread._UnguardDatabaseQuery(self, query)
|
||||
assert(self._guardedQueries[query])
|
||||
self._guardedQueries[query] = nil
|
||||
end
|
||||
|
||||
function Thread._Exit(self)
|
||||
assert(self:_IsAlive())
|
||||
self._state = "DEAD"
|
||||
self:_Cleanup()
|
||||
Log.Info("Thread finished: %s", self:_ToLogStr())
|
||||
if self == private.runningThread then
|
||||
coroutine.yield(YIELD_VALUE)
|
||||
error("Shouldn't get here")
|
||||
elseif self._returnValue then
|
||||
TempTable.Release(self._returnValue)
|
||||
self._returnValue = nil
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Private Helper Functions
|
||||
-- ============================================================================
|
||||
|
||||
function private.StartScheduler()
|
||||
if private.schedulerFrame:IsVisible() then
|
||||
return
|
||||
end
|
||||
Log.Info("Starting scheduler")
|
||||
private.schedulerFrame:Show()
|
||||
end
|
||||
|
||||
function private.RunScheduler(_, elapsed)
|
||||
-- don't run any threads while in combat
|
||||
if InCombatLockdown() then
|
||||
return
|
||||
end
|
||||
local startTime = debugprofilestop()
|
||||
local numReadyThreads = 0
|
||||
wipe(private.queue)
|
||||
|
||||
-- go through all the threads, update their state, and add the ready ones into the queue
|
||||
for _, thread in pairs(private.threads) do
|
||||
thread:_UpdateState(elapsed)
|
||||
if thread:_CanRun() then
|
||||
numReadyThreads = numReadyThreads + 1
|
||||
tinsert(private.queue, thread)
|
||||
end
|
||||
end
|
||||
|
||||
local remainingTime = min(elapsed * 1000 * MAX_TIME_USAGE_RATIO, MAX_QUANTUM_MS)
|
||||
while remainingTime > 0.01 do
|
||||
local ranThread = false
|
||||
for i = #private.queue, 1, -1 do
|
||||
local thread = private.queue[i]
|
||||
if thread:_CanRun() then
|
||||
local quantum = remainingTime / numReadyThreads
|
||||
local elapsedTime = thread:_Run(quantum)
|
||||
thread._cpuTimeUsed = thread._cpuTimeUsed + elapsedTime
|
||||
remainingTime = remainingTime - min(elapsedTime, quantum)
|
||||
-- any thread which ran excessively long should be ignored for future loops
|
||||
if elapsedTime > EXCESSIVE_TIME_USED_RATIO * quantum and elapsedTime > quantum + 1 then
|
||||
if elapsedTime > EXCESSIVE_TIME_LOG_THRESHOLD_MS then
|
||||
local line = Debug.GetStackLevelLocation(2, thread._co)
|
||||
Log.Warn("Thread %s ran too long (%.1f/%.1f): %s", thread._name, elapsedTime, quantum, line or "?")
|
||||
end
|
||||
tremove(private.queue, i)
|
||||
end
|
||||
ranThread = true
|
||||
end
|
||||
end
|
||||
if not ranThread then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local hasAliveThread = false
|
||||
for _, thread in pairs(private.threads) do
|
||||
if thread:_IsAlive() then
|
||||
hasAliveThread = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not hasAliveThread then
|
||||
Log.Info("Stopping the scheduler")
|
||||
private.schedulerFrame:Hide()
|
||||
end
|
||||
|
||||
local timeTaken = debugprofilestop() - startTime
|
||||
if timeTaken > SCHEDULER_TIME_WARNING_THRESHOLD_MS then
|
||||
Log.Warn("Scheduler took %.2fms", timeTaken)
|
||||
end
|
||||
end
|
||||
|
||||
function private.ProcessEvent(self, event, ...)
|
||||
local startTime = debugprofilestop()
|
||||
for _, thread in pairs(private.threads) do
|
||||
thread:_ProcessEvent(event, ...)
|
||||
end
|
||||
local timeTaken = debugprofilestop() - startTime
|
||||
if timeTaken > SCHEDULER_TIME_WARNING_THRESHOLD_MS then
|
||||
Log.Warn("Scheduler took %.2fms to process %s", timeTaken, tostring(event))
|
||||
end
|
||||
end
|
||||
|
||||
function private.OnFutureDone(future)
|
||||
for _, thread in pairs(private.threads) do
|
||||
thread:_OnFutureDone(future)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- Driver Frame
|
||||
-- ============================================================================
|
||||
|
||||
do
|
||||
private.schedulerFrame = CreateFrame("Frame")
|
||||
private.schedulerFrame:Hide()
|
||||
private.schedulerFrame:SetScript("OnUpdate", private.RunScheduler)
|
||||
private.schedulerFrame:SetScript("OnEvent", private.ProcessEvent)
|
||||
end
|
||||
Reference in New Issue
Block a user