AuctioneerSuite/Auc-Advanced/CoreScan.lua

2241 lines
74 KiB
Lua
Raw Permalink Normal View History

2026-04-13 17:48:13 -04:00
--[[
Auctioneer
Version: 5.9.4961 (WhackyWallaby)
Revision: $Id: CoreScan.lua 4953 2010-10-17 19:37:42Z Nechckn $
URL: http://auctioneeraddon.com/
This is an addon for World of Warcraft that adds statistical history to the auction data that is collected
when the auction is scanned, so that you can easily determine what price
you will be able to sell an item for at auction or at a vendor whenever you
mouse-over an item in the game
License:
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program(see GPL.txt); if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
Note:
This AddOn's source code is specifically designed to work with
World of Warcraft's interpreted AddOn system.
You have an implicit license to use this AddOn with these facilities
since that is its designated purpose as per:
http://www.fsf.org/licensing/licenses/gpl-faq.html#InterpreterIncompat
--]]
--[[
Auctioneer Scanning Engine.
Provides a service to walk through an AH Query, reporting changes in the AH to registered utilities and stats modules
]]
if not AucAdvanced then return end
local coremodule, internal = AucAdvanced.GetCoreModule("CoreScan")
if not coremodule or not internal then return end -- Someone has explicitely broken us
if (not AucAdvanced.Scan) then AucAdvanced.Scan = {} end
local SCANDATA_VERSION = "A" -- must match Auc-ScanData INTERFACE_VERSION
local lib = AucAdvanced.Scan
local private = {}
lib.Private = private
local Const = AucAdvanced.Const
local _print,decode,_,_,replicate,empty,get,set,default,debugPrint,fill = AucAdvanced.GetModuleLocals()
local GetFaction = AucAdvanced.GetFaction
local EquipCodeToInvIndex = AucAdvanced.Const.EquipCodeToInvIndex
local tinsert, tremove = tinsert, tremove
local bitand, bitor, bitnot = bit.band, bit.bor, bit.bnot
local type, wipe = type, wipe
local pairs, ipairs = pairs, ipairs
local tonumber = tonumber
local GetTime = GetTime
private.isScanning = false
function private.LoadScanData()
if not private.loadingScanData then
local _, _, _, enabled, load, reason = GetAddOnInfo("Auc-ScanData")
if not (enabled and load) then
private.loadingScanData = "fallback"
message("The Auc-ScanData storage module could not be loaded: "..(reason or "Unknown reason"))
elseif IsAddOnLoaded("Auc-ScanData") then
-- if another AddOn has force-loaded Auc-ScanData
private.loadingScanData = "loading"
else
private.loadingScanData = "block" -- prevents re-entry to this function during the LoadAddOn call
load, reason = LoadAddOn("Auc-ScanData")
if load then
private.loadingScanData = "loading"
elseif reason then
private.loadingScanData = "fallback"
message("The Auc-ScanData storage module could not be loaded: "..reason)
else
-- LoadAddOn sometimes returns nil, nil if called too early during game startup
-- assume it needs to be called again at a later stage
private.loadingScanData = nil
end
end
end
if private.loadingScanData == "loading" then
local ready, version
local scanmodule = AucAdvanced.Modules.Util.ScanData
if scanmodule and scanmodule.GetAddOnInfo then
ready, version = scanmodule.GetAddOnInfo()
end
if version ~= SCANDATA_VERSION then
private.loadingScanData = "fallback"
message("The Auc-ScanData storage module could not be loaded: ".."Incorrect version")
elseif ready then
-- install functions from Auc-ScanData
private.GetScanData = scanmodule.GetScanData
lib.ClearScanData = scanmodule.ClearScanData
-- cleanup
private.loadingScanData = nil
private.LoadScanData = nil
-- signal success
return private.GetScanData
end
end
if private.loadingScanData == "fallback" then
-- cannot load Auc-ScanData, go to fallback image handler
local fallbackscandata = {}
private.GetScanData = function(serverKey)
local scandata = fallbackscandata[serverKey]
if scandata then return scandata end
local test = AucAdvanced.SplitServerKey(serverKey)
if not test then return end
scandata = {image = {}, scanstats = {ImageUpdated = time()}}
fallbackscandata[serverKey] = scandata
return scandata
end
-- cleanup
private.loadingScanData = nil
private.LoadScanData = nil
-- signal success
return private.GetScanData
end
end
function lib.GetImage()
-- Deprecated
if private.LoadScanData then private.LoadScanData() end
end
function lib.LoadScanData()
if private.LoadScanData then private.LoadScanData() end
end
-- scandataTable = private.GetScanData(serverKey)
-- parameter: serverKey (required)
-- returns: scandataTable = {image = imageTable, scanstats = scanstatsTable} for the specified serverKey
-- returns: nil if there is no data for serverKey (or if serverKey is invalid)
-- CAUTION: the following is a stub function, which will be overloaded with the real function by LoadScanData
function private.GetScanData(serverKey)
if private.LoadScanData then
local newfunc = private.LoadScanData()
if newfunc then
return newfunc(serverKey)
end
end
end
-- AucAdvanced.Scan.ClearScanData(serverKey)
-- AucAdvanced.Scan.ClearScanData(realmName)
-- AucAdvanced.Scan.ClearScanData("SERVER") -- all data for current server
-- AucAdvanced.Scan.ClearScanData("FACTION") -- data for current faction (as determined by AucAdvanced.GetFaction())
-- AucAdvanced.Scan.ClearScanData("ALL")
-- CAUTION: the following is a stub function, which will be overloaded with the real function by LoadScanData
function lib.ClearScanData(key)
_print("Scan Data cannot be cleared because {{Auc-ScanData}} is not loaded")
end
function lib.StartPushedScan(name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex, GetAll, NoSummary)
if not private.scanStack then private.scanStack = {} end
name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex = private.QueryScrubParameters(
name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex)
if private.scanStack then
for _, scan in ipairs(private.scanStack) do
if not scan[8] and private.QueryCompareParameters(scan[3], name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex) then
-- duplicate of exisiting queued query
if (nLog) then
nLog.AddMessage("Auctioneer", "Scan", N_INFO, "Duplicate pushed scan detected, cancelling duplicate")
end
return
end
end
end
local query = private.NewQueryTable(name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex)
query.qryinfo.pushed = true
if NoSummary then query.qryinfo.nosummary = true end
if (nLog) then
nLog.AddMessage("Auctioneer", "Scan", N_INFO, ("Starting pushed scan %d (%s)"):format(query.qryinfo.id, query.qryinfo.sig))
end
tinsert(private.scanStack, {time(), false, query, {}, {}, GetTime(), 0, false, 0})
end
function lib.PushScan()
if private.isGetAll then
-- A GetAll scan cannot be Popped; do not allow it to be Pushed
_print("Warning: Scan cannot be Pushed because it is a GetAll scan")
return
end
if private.isScanning then
if (nLog) then
nLog.AddMessage("Auctioneer", "Scan", N_INFO, ("Scan %d (%s) Paused, next page to scan is %d"):format(private.curQuery.qryinfo.id, private.curQuery.qryinfo.sig, private.curQuery.qryinfo.page+1))
end
-- _print(("Pausing current scan at page {{%d}}."):format(private.curQuery.qryinfo.page+1))
if not private.scanStack then private.scanStack = {} end
tinsert(private.scanStack, {
private.scanStartTime,
private.sentQuery,
private.curQuery,
private.curPages,
private.curScan,
private.scanStarted,
private.totalPaused,
GetTime(),
private.storeTime
})
local oldquery = private.curQuery
private.curQuery = nil
private.scanStartTime = nil
private.scanStarted = nil
private.totalPaused = nil
private.curScan = nil
private.storeTime = nil
private.curPages = nil
private.sentQuery = nil
private.isScanning = false
private.UpdateScanProgress(false, nil, nil, nil, nil, nil, oldquery)
end
end
function lib.PopScan()
if private.scanStack and #private.scanStack > 0 then
local now, pauseTime = GetTime()
private.scanStartTime,
private.sentQuery,
private.curQuery,
private.curPages,
private.curScan,
private.scanStarted,
private.totalPaused,
pauseTime,
private.storeTime = unpack(private.scanStack[1])
tremove(private.scanStack, 1)
local elapsed = pauseTime and (now - pauseTime) or 0
if elapsed > 300 then
-- 5 minutes old
--_print("Paused scan is older than 5 minutes, commiting what we have and aborting")
if (nLog) then
nLog.AddMessage("Auctioneer", "Scan", N_WARNING, ("Scan %d Too Old, committing what we have and aborting"):format(private.curQuery.qryinfo.id))
end
private.Commit(true, false) -- Incomplete, non-GetAll Scan
return
end
private.totalPaused = private.totalPaused + elapsed
if (nLog) then
nLog.AddMessage("Auctioneer", "Scan", N_INFO, ("Scan %d Resumed, next page to scan is %d"):format(private.curQuery.qryinfo.id, private.curQuery.qryinfo.page+1))
end
--_print(("Resuming paused scan at page {{%d}}..."):format(private.curQuery.qryinfo.page+1))
private.isScanning = true
private.sentQuery = false
private.ScanPage(private.curQuery.qryinfo.page+1)
private.UpdateScanProgress(true, nil, nil, nil, nil, nil, private.curQuery)
end
end
--[[This function is now in core API]]
function lib.ProgressBars(name, value, show, text, options)
AucAdvanced.API.ProgressBars(name, value, show, text, options)
end
function lib.StartScan(name, minUseLevel, maxUseLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex, GetAll, NoSummary)
if AuctionFrame and AuctionFrame:IsVisible() then
if private.isPaused then
message("Scanning is currently paused")
return
end
if private.isScanning then
message("Scan is currently in progress")
return
end
local CanQuery, CanQueryAll = CanSendAuctionQuery()
if GetAll then
local now = time()
if not CanQueryAll then
local text = "You cannot do a GetAll scan at this time."
if private.LastGetAll then
local timeleft = 900 - (now - private.LastGetAll) -- 900 = 15 * 60 sec = 15 min
if timeleft > 0 then
local minleft = floor(timeleft / 60)
local secleft = timeleft - minleft * 60
text = text.." You must wait "..minleft..":"..secleft.." until you can scan again."
end
end
message(text)
return
end
AucAdvanced.API.BlockUpdate(true, false)
BrowseSearchButton:Hide()
lib.ProgressBars("GetAllProgressBar", 0, true, "Auctioneer: Scanning")
private.isGetAll = true -- indicates that certain functions must take special action, and that the above changes need to be undone
private.LastGetAll = now
else
if not CanQuery then
private.queueScan = {
name, minUseLevel, maxUseLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex, GetAll, NoSummary
}
private.queueScanParams = 10 -- must match the number of entries we put into the table, including nils. Used when unpacking
return
end
end
if private.curQuery then
private.Commit(true, false) -- sets private.curQuery to nil
end
private.isScanning = true
private.isNoSummary = NoSummary
local startPage = 0
QueryAuctionItems(name or "", minUseLevel or "", maxUseLevel or "",
invTypeIndex, classIndex, subclassIndex, startPage, isUsable, qualityIndex, GetAll)
if not private.curQuery then
-- private.curQuery will have been set if QueryAuctionItems succeeded
-- this should never fail? we checked CanSendAuctionQuery() earlier
message("Scan failed: unable to send query")
if private.isGetAll then
lib.ProgressBars("GetAllProgressBar", nil, false)
BrowseSearchButton:Show()
AucAdvanced.API.BlockUpdate(false)
private.isGetAll = nil
end
return
end
AuctionFrameBrowse.page = startPage
if (NoSummary) then
private.curQuery.qryinfo.nosummary = true
end
if GetAll then
private.curQuery.qryinfo.getall = true
end
private.isNoSummary = false
--Show the progress indicator
private.UpdateScanProgress(true, nil, nil, nil, nil, nil, private.curQuery)
else
message("Steady on; You'll need to talk to the auctioneer first!")
end
end
function lib.IsScanning()
return private.isScanning or (private.queueScan ~= nil)
end
function lib.IsPaused()
return private.isPaused
end
function private.Unpack(item, storage)
if not storage then storage = {} end
storage.id = item[Const.ID]
storage.link = item[Const.LINK]
storage.useLevel = item[Const.ULEVEL]
storage.itemLevel = item[Const.ILEVEL]
storage.itemType = item[Const.ITYPE]
storage.subType = item[Const.ISUB]
storage.equipPos = item[Const.IEQUIP]
storage.price = item[Const.PRICE]
storage.timeLeft = item[Const.TLEFT]
storage.seenTime = item[Const.TIME]
storage.itemName = item[Const.NAME]
storage.texture = item[Const.TEXTURE]
storage.stackSize = item[Const.COUNT]
storage.quality = item[Const.QUALITY]
storage.canUse = item[Const.CANUSE]
storage.minBid = item[Const.MINBID]
storage.curBid = item[Const.CURBID]
storage.increment = item[Const.MININC]
storage.sellerName = item[Const.SELLER]
storage.buyoutPrice = item[Const.BUYOUT]
storage.amBidder = item[Const.AMHIGH]
storage.dataFlag = item[Const.FLAG]
storage.itemId = item[Const.ITEMID]
storage.itemSuffix = item[Const.SUFFIX]
storage.itemFactor = item[Const.FACTOR]
storage.itemEnchant = item[Const.ENCHANT]
storage.itemSeed = item[Const.SEED]
return storage
end
-- Define a public accessor for the above upack function
lib.UnpackImageItem = private.Unpack
--The first parameter will be true if we want to show the process indicator, false if we want to hide it. and nil if we only want to update it.
--The second parameter will be a number that is the max number of items in the scan.
--The third parameter is the current progress of the scan.
function private.UpdateScanProgress(state, totalAuctions, scannedAuctions, elapsedTime, page, maxPages, query)
if (lib.IsScanning() or (state == false)) then
if (nLog) then
nLog.AddMessage("Auctioneer", "Scan", N_INFO, "UpdateScanProgress Called", state)
end
local scanCount = 0
if (private.scanStack) then scanCount=#private.scanStack end
AucAdvanced.SendProcessorMessage("scanprogress", state, totalAuctions, scannedAuctions, elapsedTime, page, maxPages, query, scanCount)
end
end
function private.IsIdentical(focus, compare)
for i = 1, Const.SELLER do
if (i ~= Const.TIME and i ~= Const.CANUSE and focus[i] ~= compare[i]) then
return false
end
end
return true
end
function private.IsSameItem(focus, compare, onlyDirt)
if onlyDirt then
local flag = focus[Const.FLAG]
if not flag or bitand(flag, Const.FLAG_DIRTY) == 0 then
return false
end
end
if (focus[Const.LINK] ~= compare[Const.LINK]) then return false end
if (focus[Const.COUNT] ~= compare[Const.COUNT]) then return false end
if (focus[Const.MINBID] ~= compare[Const.MINBID]) then return false end
if (focus[Const.BUYOUT] ~= compare[Const.BUYOUT]) then return false end
if (focus[Const.CURBID] > compare[Const.CURBID]) then return false end
return true
end
function lib.FindItem(item, image, lut)
local focus
-- If we have a lookuptable, then we don't need to scan the whole lot
if (lut) then
local list = lut[item[Const.LINK]]
if not list then return false
elseif type(list) == "number" then
if (private.IsSameItem(image[list], item, true)) then return list end
else
local pos
for i=1, #list do
pos = list[i]
if (private.IsSameItem(image[pos], item, true)) then return pos end
end
end
else
-- We need to scan the whole thing cause there's no lookup table
for i = 1, #image do
if (private.IsSameItem(image[i], item, true)) then return i end
end
end
end
local statItem = {}
local statItemOld = {}
local function processStats(processors, operation, curItem, oldItem)
local filtered = false
if (not processors) then return end
if (curItem) then private.Unpack(curItem, statItem) end
if (oldItem) then private.Unpack(oldItem, statItemOld) end
if (operation == "create" and processors.Filter) then
--[[
Filtering out happens here so we only have to do Unpack once.
Only filter on create because once its in the system, dropping it can give the wrong impression to other mods.
(it could think it was sold, for instance)
]]
local pf = processors.Filter
for i=1,#pf do
local x = pf[i]
local f = x.Func
local pOK, result=pcall(f, operation, statItem)
if (pOK) then
if (result) then
curItem[Const.FLAG] = bitor(curItem[Const.FLAG] or 0, Const.FLAG_FILTER)
filtered = true
break
end
else
if (nLog) then
nLog.AddMessage("Auctioneer", "Scan", N_WARNING, "AuctionFilter Error", ("AuctionFilter %s Returned Error %s"):format(x and x.Name or "??", errormsg))
end
end
end
elseif curItem and bitand(curItem[Const.FLAG] or 0, Const.FLAG_FILTER) == Const.FLAG_FILTER then
-- This item is a filtered item
filtered = true
end
if filtered then
return false
end
local po = processors[operation]
if (po) then
for i=1,#po do
local x = po[i]
local f = x.Func
local pOK, errormsg = pcall(f, operation, statItem, oldItem and statItemOld or nil)
--if (oldItem) then
-- pOK, errormsg = pcall(func,operation, statItem, statItemOld)
--else
-- pOK, errormsg = pcall(func,operation, statItem)
--end
if (not pOK) then
if (nLog) then nLog.AddMessage("Auctioneer", "Scan", N_WARNING, "ScanProcessor Error", ("ScanProcessor %s Returned Error %s"):format(x and x.Name or "??", errormsg)) end
end
end
end
return true
end
function private.IsInQuery(curQuery, data)
if (not curQuery.class or curQuery.class == data[Const.ITYPE])
and (not curQuery.subclass or (curQuery.subclass == data[Const.ISUB]))
and (not curQuery.minUseLevel or (data[Const.ULEVEL] >= curQuery.minUseLevel))
and (not curQuery.maxUseLevel or (data[Const.ULEVEL] <= curQuery.maxUseLevel))
and (not curQuery.name or (data[Const.NAME] and data[Const.NAME]:lower():find(curQuery.name, 1, true))) -- curQuery.name is already lowercased
and (not curQuery.isUsable or (private.CanUse(data[Const.LINK])))
and (not curQuery.invType or (EquipCodeToInvIndex[data[Const.IEQUIP]] == curQuery.invType)) -- must convert iEquip code to invTypeIndex for comparison
and (not curQuery.quality or (data[Const.QUALITY] >= curQuery.quality))
then
return true
end
return false
end
local idLists = {}
function private.BuildIDList(scandata, serverKey)
local idList = idLists[serverKey]
if idList then return idList end
idList = {0} -- dummy entry ensures that list is never empty and that counting starts from 1
idLists[serverKey] = idList
local image = scandata.image
for i = 1, #image do
tinsert(idList, image[i][Const.ID])
end
table.sort(idList)
return idList
end
function private.GetNextID(idList)
local nextId = idList[1] + 1
local second = idList[2]
while second == nextId do
nextId = second + 1
tremove(idList, 1)
second = idList[2]
end
idList[1] = nextId
return nextId
end
-- Library wrapper for private.GetScanData. Deprecated function
function lib.GetScanData(serverKey, reserved)
AucAdvanced.API.ShowDeprecationAlert(nil, "Direct access to the ScanData image is deprecated. Instead QueryImage, GetImageCopy or GetImageItem should be used")
if serverKey then
local realmName, faction = AucAdvanced.SplitServerKey(serverKey)
if not realmName then
if serverKey == "Alliance" or serverKey == "Horde" or serverKey == "Neutral" then
faction = serverKey
else
error("Invalid serverKey passed to GetScanData")
end
if reserved then
realmName = reserved
else
realmName = GetRealmName()
end
serverKey = realmName.."-"..faction
end
else
serverKey = GetFaction()
end
return private.GetScanData(serverKey)
end
function lib.GetScanStats(serverKey)
local scandata = private.GetScanData(serverKey or GetFaction())
if scandata then
return scandata.scanstats
end
end
function lib.GetImageCopy(serverKey)
-- Create a fully independent copy of the image - intended for use by coroutines
local scandata = private.GetScanData(serverKey or GetFaction())
if scandata then
local image = scandata.image
local size = Const.LASTENTRY
local copy = {}
for i = 1, #image do
tinsert(copy, {unpack(image[i], 1, size)})
end
return copy
end
end
function lib.GetImageSize(serverKey)
local scandata = private.GetScanData(serverKey or GetFaction())
if scandata then
return #scandata.image
end
end
function lib.GetImageItem(index, serverKey, reserved)
-- reserved flag for possible future expansion
local scandata = private.GetScanData(serverKey or GetFaction())
if scandata then
local item = scandata.image[index]
if item then return {unpack(item, 1, Const.LASTENTRY)} end
end
end
private.scandataIndex = {}
private.prevQuery = {}
-- private.queryResults is nil initially
-- private.prevQueryServerKey is nil initially
function private.clearImageCaches(scanstats)
local serverKey = scanstats and scanstats.serverKey
if serverKey then
local cache = private.scandataIndex[serverKey]
if cache then
wipe(cache)
end
else -- no serverKey provided: affects multiple serverKeys (or unknown source), clear all caches
for _, cache in pairs(private.scandataIndex) do
wipe(cache)
end
end
private.prevQueryServerKey = nil
private.queryResults = nil -- not required but frees some memory
end
-- ensure home and neutral factions for current realm are always present
-- unlike the tables for other serverKeys, these tables are *not* weak
private.scandataIndex[GetRealmName().."-"..UnitFactionGroup("player")] = {}
private.scandataIndex[GetRealmName().."-Neutral"] = {}
local weaktablemeta = {__mode="kv"}
function private.SubImageCache(itemId, serverKey)
local indexResults = private.scandataIndex[serverKey]
if not indexResults then
if not AucAdvanced.SplitServerKey(serverKey) then return end -- valid serverKey format?
indexResults = setmetatable({}, weaktablemeta) -- use weak tables for other serverKeys
private.scandataIndex[serverKey] = indexResults
end
local itemResults = indexResults[itemId]
if not itemResults then
local scandata = private.GetScanData(serverKey)
if not scandata then return end
itemResults = {}
for pos, data in ipairs(scandata.image) do
if data[Const.ITEMID] == itemId then
tinsert(itemResults, data)
end
end
indexResults[itemId] = itemResults
end
return itemResults
end
function lib.QueryImage(query, serverKey, reserved, ...)
serverKey = serverKey or GetFaction()
local prevQuery = private.prevQuery
local queryResults = private.queryResults
-- is this the same query as last time?
if serverKey == private.prevQueryServerKey then
local samequery = true
for k,v in pairs(prevQuery) do
if v ~= query[k] then
samequery = false
break
end
end
if samequery then
for k,v in pairs(query) do
if v ~= prevQuery[k] then
samequery = false
break
end
end
if samequery then
return queryResults
end
end
end
-- reset results and save a copy of query
queryResults = {} -- cannot use wipe; needs to be a new table here {ADV-534}
private.queryResults = queryResults
wipe(prevQuery)
for k, v in pairs(query) do prevQuery[k] = v end
private.prevQueryServerKey = serverKey
-- get image to search - may be the whole snapshot or a subset
local image
if query.itemId then
image = private.SubImageCache(query.itemId, serverKey)
else
local scandata = private.GetScanData(serverKey)
if scandata then
image = scandata.image
end
end
if not image then return queryResults end -- return empty results table
local saneQueryLink
if query.link then
saneQueryLink = SanitizeLink(query.link)
end
local lowerName
if query.name then
lowerName = query.name:lower()
end
-- scan image to build a table of auctions that match query
local ptr, finish = 1, #image
while ptr <= finish do
repeat
local data = image[ptr]
ptr = ptr + 1
if not data then break end
if bitand(data[Const.FLAG] or 0, Const.FLAG_UNSEEN) == Const.FLAG_UNSEEN then break end
if query.filter and query.filter(data, ...) then break end
if saneQueryLink and data[Const.LINK] ~= saneQueryLink then break end
if query.suffix and data[Const.SUFFIX] ~= query.suffix then break end
if query.factor and data[Const.FACTOR] ~= query.factor then break end
if query.minUseLevel and data[Const.ULEVEL] < query.minUseLevel then break end
if query.maxUseLevel and data[Const.ULEVEL] > query.maxUseLevel then break end
if query.minItemLevel and data[Const.ILEVEL] < query.minItemLevel then break end
if query.maxItemLevel and data[Const.ILEVEL] > query.maxItemLevel then break end
if query.class and data[Const.ITYPE] ~= query.class then break end
if query.subclass and data[Const.ISUB] ~= query.subclass then break end
if query.quality and data[Const.QUALITY] ~= query.quality then break end
if query.invType and data[Const.IEQUIP] ~= query.invType then break end
if query.seller and data[Const.SELLER] ~= query.seller then break end
if lowerName then
local name = data[Const.NAME]
if not (name and name:lower():find(lowerName, 1, true)) then break end
end
local stack = data[Const.COUNT]
local nextBid = data[Const.PRICE]
local buyout = data[Const.BUYOUT]
if query.perItem and stack > 1 then
nextBid = ceil(nextBid / stack)
buyout = ceil(buyout / stack)
end
if query.minStack and stack < query.minStack then break end
if query.maxStack and stack > query.maxStack then break end
if query.minBid and nextBid < query.minBid then break end
if query.maxBid and nextBid > query.maxBid then break end
if query.minBuyout and buyout < query.minBuyout then break end
if query.maxBuyout and buyout > query.maxBuyout then break end
-- If we're still here, then we've got a winner
tinsert(queryResults, data)
until true
end
return queryResults
end
private.CommitQueue = {}
local CommitRunning = false
local Commitfunction = function()
local startTime = GetTime()
local lastPause = startTime
local totalProcessingTime = 0
local speed = get("scancommit.speed")/100
speed = speed^2.5
local processingTime = speed * 0.1 + 0.015
-- Min (1): 0.02s (~50 fps) -- Max (100): 0.12s (~8 fps). Default (50): 0.037s (~25 fps)
local inscount, delcount = 0, 0
if #private.CommitQueue == 0 then CommitRunning = false return end
CommitRunning = true
--grab the first item in the commit queue, and bump everything else down
local TempcurCommit = tremove(private.CommitQueue)
-- setup various locals for later use
local TempcurScan = TempcurCommit.Scan
local TempcurQuery = TempcurCommit.Query
local wasIncomplete = TempcurCommit.wasIncomplete
local wasGetAll = TempcurCommit.wasGetAll
local scanStarted = TempcurCommit.scanStarted
local scanStartTime = TempcurCommit.scanStartTime
local totalPaused = TempcurCommit.totalPaused
local scanCommitTime = TempcurCommit.scanCommitTime
local scanStoreTime = scanCommitTime - scanStarted - totalPaused
local storeTime = TempcurCommit.storeTime
local wasOnePage = wasGetAll or (TempcurQuery.qryinfo.page == 0) -- retrieved all records in single pull (only one page scanned or was GetAll)
local wasUnrestricted = not (TempcurQuery.class or TempcurQuery.subclass or TempcurQuery.minUseLevel
or TempcurQuery.name or TempcurQuery.isUsable or TempcurQuery.invType or TempcurQuery.quality) -- no restrictions, potentially a full scan
local serverKey = TempcurQuery.qryinfo.serverKey or GetFaction()
local scandata = private.GetScanData(serverKey)
assert(scandata, "Critical error: scandata does not exist for serverKey "..serverKey)
local idList = private.BuildIDList(scandata, serverKey)
local now = time()
if get("scancommit.progressbar") then
lib.ProgressBars("CommitProgressBar", 0, true)
end
local oldCount = #scandata.image
local scanCount = #TempcurScan
local progresscounter = 0
local progresstotal = 3*oldCount + 4*scanCount
local filterDeleteCount,filterOldCount, filterNewCount, updateCount, sameCount, newCount, updateRecoveredCount, sameRecoveredCount, missedCount, earlyDeleteCount, expiredDeleteCount = 0,0,0,0,0,0,0,0,0,0,0
--[[ *** Stage 1: Mark all matching auctions as DIRTY, and build a LookUpTable *** ]]
local dirtyCount = 0
local lut = {}
for pos, data in ipairs(scandata.image) do
local link = data[Const.LINK]
progresscounter = progresscounter + 1
local gt = GetTime()
if gt - lastPause >= processingTime then
lib.ProgressBars("CommitProgressBar", 100*progresscounter/progresstotal, true, "Auctioneer: Processing Stage 1")
totalProcessingTime = totalProcessingTime + (gt - lastPause)
coroutine.yield()
lastPause = GetTime()
end
if link then
if private.IsInQuery(TempcurQuery, data) then
-- Mark dirty
data[Const.FLAG] = bitor(data[Const.FLAG] or 0, Const.FLAG_DIRTY)
dirtyCount = dirtyCount+1
-- Build lookup table
local list = lut[link]
if (not list) then
lut[link] = pos
else
if (type(list) == "number") then
lut[link] = {}
tinsert(lut[link], list)
end
tinsert(lut[link], pos)
end
else
-- Mark NOT dirty
data[Const.FLAG] = bitand(data[Const.FLAG] or 0, bitnot(Const.FLAG_DIRTY))
end
end
end
local processors = {}
local modules = AucAdvanced.GetAllModules("AuctionFilter", "Filter")
for pos, engineLib in ipairs(modules) do
if (not processors.Filter) then processors.Filter = {} end
local x = {}
x.Name = engineLib.GetName()
x.Func = engineLib.AuctionFilter
table.insert(processors.Filter, x)
end
modules = AucAdvanced.GetAllModules("ScanProcessors")
for pos, engineLib in ipairs(modules) do
for op, func in pairs(engineLib.ScanProcessors) do
if (not processors[op]) then processors[op] = {} end
local x = {}
x.Name = engineLib.GetName()
x.Func = func
table.insert(processors[op], x)
end
end
--[[ *** Stage 2: Merge new scan into ScanData *** ]]
lib.ProgressBars("CommitProgressBar", 100*progresscounter/progresstotal, true, "Auctioneer: Starting Stage 2") -- change displayed text for reporting purposes
processStats(processors, "begin")
for index, data in ipairs(TempcurScan) do
local itemPos
progresscounter = progresscounter + 4
local gt = GetTime()
if gt - lastPause >= processingTime then
lib.ProgressBars("CommitProgressBar", 100*progresscounter/progresstotal, true, "Auctioneer: Processing Stage 2")
totalProcessingTime = totalProcessingTime + (gt - lastPause)
coroutine.yield()
lastPause = GetTime()
end
itemPos = lib.FindItem(data, scandata.image, lut)
data[Const.FLAG] = bitand(data[Const.FLAG] or 0, bitnot(Const.FLAG_DIRTY))
data[Const.FLAG] = bitand(data[Const.FLAG], bitnot(Const.FLAG_UNSEEN))
if (itemPos) then
local oldItem = scandata.image[itemPos]
data[Const.ID] = oldItem[Const.ID]
data[Const.FLAG] = bitand(oldItem[Const.FLAG] or 0, bitnot(Const.FLAG_DIRTY+Const.FLAG_UNSEEN))
if data[Const.SELLER] == "" then -- unknown seller name in new data; copy the old name if it exists
data[Const.SELLER] = oldItem[Const.SELLER]
end
if (bitand(data[Const.FLAG], Const.FLAG_FILTER)==Const.FLAG_FILTER) then
filterOldCount = filterOldCount + 1
else
if not private.IsIdentical(oldItem, data) then
if processStats(processors, "update", data, oldItem) then
updateCount = updateCount + 1
end
if bitand(oldItem[Const.FLAG] or 0, Const.FLAG_UNSEEN) == Const.FLAG_UNSEEN then
updateRecoveredCount = updateRecoveredCount + 1
end
else
if processStats(processors, "leave", data) then
sameCount = sameCount + 1
end
if bitand(oldItem[Const.FLAG] or 0, Const.FLAG_UNSEEN) == Const.FLAG_UNSEEN then
sameRecoveredCount = sameRecoveredCount + 1
end
end
end
scandata.image[itemPos] = replicate(data)
else
if (processStats(processors, "create", data)) then
newCount = newCount + 1
else -- processStats(processors, "create"...) filtered the auction: flag it
data[Const.FLAG] = bitor(data[Const.FLAG] or 0, Const.FLAG_FILTER)
filterNewCount = filterNewCount + 1
end
data[Const.ID] = private.GetNextID(idList)
tinsert(scandata.image, replicate(data))
end
end
--[[ *** Stage 3: Cleanup deleted auctions *** ]]
local numempty = 0
local progressstep = 1
if #scandata.image > 0 then -- (avoid potential div0)
-- #scandata.image is probably now larger than when we originally calculated progresstotal -- adjust the step size to compensate
progressstep = (progresstotal - progresscounter) / #scandata.image
end
for pos = #scandata.image, 1, -1 do
local data = scandata.image[pos]
progresscounter = progresscounter + progressstep
local gt = GetTime()
if gt - lastPause >= processingTime then
lib.ProgressBars("CommitProgressBar", 100*progresscounter/progresstotal, true, "Auctioneer: Processing Stage 3")
totalProcessingTime = totalProcessingTime + (gt - lastPause)
coroutine.yield()
lastPause = GetTime()
end
if (bitand(data[Const.FLAG] or 0, Const.FLAG_DIRTY) == Const.FLAG_DIRTY) then
local auctionmaxtime = Const.AucMaxTimes[data[Const.TLEFT]] or 172800
local dodelete = false
if data[Const.TIME] and (now - data[Const.TIME] > auctionmaxtime) then
-- delete items that have passed their expiry time - even if scan was incomplete
dodelete = true
if bitand(data[Const.FLAG] or 0, Const.FLAG_FILTER) == Const.FLAG_FILTER then
filterDeleteCount = filterDeleteCount + 1
else
expiredDeleteCount = expiredDeleteCount + 1
end
elseif wasIncomplete then
missedCount = missedCount + 1
elseif wasOnePage then
-- a *completed* one-page scan should not have missed any auctions
dodelete = true
if bitand(data[Const.FLAG] or 0, Const.FLAG_FILTER) == Const.FLAG_FILTER then
filterDeleteCount = filterDeleteCount + 1
else
earlyDeleteCount = earlyDeleteCount + 1
end
else
if bitand(data[Const.FLAG] or 0, Const.FLAG_UNSEEN) == Const.FLAG_UNSEEN then
dodelete = true
if bitand(data[Const.FLAG] or 0, Const.FLAG_FILTER) == Const.FLAG_FILTER then
filterDeleteCount = filterDeleteCount + 1
else
earlyDeleteCount = earlyDeleteCount + 1
end
else
data[Const.FLAG] = bitor(data[Const.FLAG] or 0, Const.FLAG_UNSEEN)
missedCount = missedCount + 1
end
end
if dodelete then
if not (bitand(data[Const.FLAG] or 0, Const.FLAG_FILTER) == Const.FLAG_FILTER) then
processStats(processors, "delete", data)
end
tremove(scandata.image, pos)
end
elseif not data[Const.LINK] then --if there isn't a link in the data, remove the entry
tremove(scandata.image, pos)
numempty = numempty + 1
end
end
--[[ *** Stage 4: Reports *** ]]
lib.ProgressBars("CommitProgressBar", 100, true, "Auctioneer: Processing Finished")
processStats(processors, "complete")
local currentCount = #scandata.image
if (updateCount + sameCount + newCount + filterNewCount + filterOldCount ~= scanCount) then
if nLog then
nLog.AddMessage("Auctioneer", "Scan", N_WARNING, "Scan Count Discrepency Seen",
("%d updated + %d same + %d new + %d filtered != %d scanned"):format(updateCount, sameCount,
newCount, filterOldCount+filterNewCount, scanCount))
end
end
if numempty > 0 then
if nLog then
nLog.AddMessage("Auctioneer", "Scan", N_ERROR, "ScanData Missing Links",
("We saw %d entries in scandata without links"):format(numempty))
end
end
-- image contains filtered items now. Need to account for new entries that are flagged as filtered (not shown to stats modules)
if (oldCount - earlyDeleteCount - expiredDeleteCount + newCount + filterNewCount - filterDeleteCount ~= currentCount) then
if nLog then
nLog.AddMessage("Auctioneer", "Scan", N_WARNING, "Current Count Discrepency Seen",
("%d - %d - %d + %d + %d - %d != %d"):format(oldCount, earlyDeleteCount, expiredDeleteCount,
newCount, filterNewCount, filterDeleteCount, currentCount))
end
end
local now = time()
local scanTimeSecs = math.floor(GetTime() - scanStarted - totalPaused)
local scanTimeMins = floor(scanTimeSecs / 60)
scanTimeSecs = mod(scanTimeSecs, 60)
local scanTimeHours = floor(scanTimeMins / 60)
scanTimeMins = mod(scanTimeMins, 60)
--Hides the end of scan summary if user is not interested
local printSummary, scanSize = false, ""
scanSize = TempcurQuery.qryinfo.scanSize
if scanSize=="Full" then
printSummary = get("scandata.summaryonfull");
elseif scanSize=="Partial" then
printSummary = get("scandata.summaryonpartial")
else -- scanSize=="Micro"
printSummary = get("scandata.summaryonmicro")
end
if (TempcurQuery.qryinfo.nosummary) then
printSummary = false
scanSize = "NoSum-"..scansize
end
if (nLog or printSummary) then
totalProcessingTime = totalProcessingTime + (GetTime() - lastPause)
local scanTime = " "
local summaryLine
local summary
if scanTimeHours ~= 0 then
scanTime = scanTime..scanTimeHours.." Hours "
end
if scanTimeMins ~= 0 then
scanTime = scanTime..scanTimeMins.." Mins "
end
if scanTimeSecs ~= 0 or (scanTimeHours == 0 and scanTimeMins == 0) then
scanTime = scanTime..scanTimeSecs.." Secs "
end
if (wasIncomplete) then
summaryLine = "Auctioneer scanned {{"..scanCount.."}} auctions over{{"..scanTime.."}}before stopping:"
else
summaryLine = "Auctioneer finished scanning {{"..scanCount.."}} auctions over{{"..scanTime.."}}:"
end
if (printSummary) then _print(summaryLine) end
summary = summaryLine
summaryLine = " {{"..oldCount.."}} items in DB at start ({{"..dirtyCount.."}} matched query); {{"..currentCount.."}} at end"
if (printSummary) then _print(summaryLine) end
summary = summary.."\n"..summaryLine
if (sameCount > 0) then
if (sameRecoveredCount > 0) then
summaryLine = " {{"..sameCount.."}} unchanged items (of which, "..sameRecoveredCount.." were missed last scan)"
else
summaryLine = " {{"..sameCount.."}} unchanged items"
end
if (printSummary) then _print(summaryLine) end
summary = summary.."\n"..summaryLine
end
if (updateCount > 0) then
if (updateRecoveredCount > 0) then
summaryLine = " {{"..updateCount.."}} updated items (of which, "..updateRecoveredCount.." were missed last scan)"
else
summaryLine = " {{"..updateCount.."}} updated items"
end
if (printSummary) then _print(summaryLine) end
summary = summary.."\n"..summaryLine
end
if (newCount > 0) then
summaryLine = " {{"..newCount.."}} new items"
if (printSummary) then _print(summaryLine) end
summary = summary.."\n"..summaryLine
end
if (earlyDeleteCount+expiredDeleteCount > 0) then
if expiredDeleteCount > 0 then
summaryLine = " {{"..earlyDeleteCount+expiredDeleteCount.."}} items removed (of which, {{"..expiredDeleteCount.."}} were past expiry time)"
else
summaryLine = " {{"..earlyDeleteCount+expiredDeleteCount.."}} items removed"
end
if (printSummary) then _print(summaryLine) end
summary = summary.."\n"..summaryLine
end
if (filterNewCount+filterOldCount > 0) then
summaryLine = " {{"..filterNewCount+filterOldCount.."}} filtered items"
if (printSummary) then _print(summaryLine) end
summary = summary.."\n"..summaryLine
end
if (filterDeleteCount > 0) then
summaryLine = " {{"..filterDeleteCount.."}} filtered items removed"
if (printSummary) then _print(summaryLine) end
summary = summary.."\n"..summaryLine
end
if (missedCount > 0) then
if (wasIncomplete) then
summaryLine = " (Incomplete scan missed {{"..missedCount.."}} items)"
else
summaryLine = " {{"..missedCount.."}} missed items"
end
if (printSummary) then _print(summaryLine) end
summary = summary.."\n"..summaryLine
end
if (nLog) then
local eTime = GetTime()
nLog.AddMessage("Auctioneer", "Scan", N_INFO,
"Scan "..TempcurQuery.qryinfo.id.."("..TempcurQuery.qryinfo.sig..") Committed",
summary..("\nTotal Time: %f\nPaused Time: %f\nData Storage Time: %f\nData Store Time (our processing): %f\nTotal Commit Coroutine Execution Time: %f\nTotal Commit Coroutine Execution Time (excluding yields): %f"):format(eTime-scanStarted, totalPaused, scanStoreTime, storeTime, GetTime()-startTime, totalProcessingTime))
end
end
local TempcurScanStats = {
source = "scan",
serverKey = serverKey,
scanCount = scanCount,
oldCount = oldCount,
sameCount = sameCount,
newCount = newCount,
updateCount = updateCount,
matchedCount = dirtyCount,
earlyDeleteCount = earlyDeleteCount,
expiredDeleteCount = expiredDeleteCount,
currentCount = currentCount,
missedCount = missedCount,
filteredCount = filterNewCount+filterOldCount,
wasIncomplete = wasIncomplete or false,
wasGetAll = wasGetAll or false,
startTime = scanStartTime,
endTime = now,
started = scanStarted,
paused = totalPaused,
ended = GetTime(),
elapsed = GetTime() - scanStarted - totalPaused,
query = TempcurQuery,
scanStoreTime = scanStoreTime,
storeTime = storeTime
}
local scanstats = scandata.scanstats
if not scanstats then
scanstats = {}
scandata.scanstats = scanstats
end
scanstats.LastScan = now
if oldCount ~= currentCount or scanCount > 0 or dirtyCount > 0 or numempty > 0 then
scanstats.ImageUpdated = now
end
if wasUnrestricted and not wasIncomplete then scanstats.LastFullScan = now end
-- keep 2 old copies for compatibility
scanstats[2] = scandata.scanstats[1]
scanstats[1] = scandata.scanstats[0]
scanstats[0] = TempcurScanStats
-- Tell everyone that our stats are updated
TempcurQuery.qryinfo.finished = true
AucAdvanced.SendProcessorMessage("scanstats", TempcurScanStats)
--Hide the progress indicator
lib.ProgressBars("CommitProgressBar", nil, false)
private.UpdateScanProgress(false, nil, nil, nil, nil, nil, TempcurQuery)
lib.PopScan()
CommitRunning = false
if not private.curQuery then
private.ResetAll()
end
AucAdvanced.SendProcessorMessage("scanfinish", scanSize, TempcurQuery.qryinfo.sig, TempcurQuery.qryinfo, not wasIncomplete, TempcurQuery, TempcurScanStats)
end
local CoCommit, CoStore
local function CoroutineResume(...)
local status, result = coroutine.resume(...)
if not status and result then
geterrorhandler()("Error occurred in coroutine: "..result, nil, debugstack((...)));
end
return status, result
end
function private.Commit(wasIncomplete, wasGetAll)
private.StopStorePage()
if not private.curScan then return end
tinsert(private.CommitQueue, {
Query = private.curQuery,
Scan = private.curScan,
wasIncomplete = wasIncomplete,
wasGetAll = wasGetAll,
scanStarted = private.scanStarted,
scanStartTime = private.scanStartTime,
totalPaused = private.totalPaused,
scanCommitTime = GetTime(),
storeTime = private.storeTime
})
private.curQuery = nil
private.curScan = nil
private.isScanning = false
if not CoCommit or coroutine.status(CoCommit) == "dead" then
CoCommit = coroutine.create(Commitfunction)
CoroutineResume(CoCommit)
end
-- in all other cases wait for the next update to resume CoCommit
end
function private.QuerySent(query, isSearch, ...)
-- Tell everyone that our stats are updated
AucAdvanced.SendProcessorMessage("querysent", query, isSearch, ...)
return ...
end
function private.FinishedPage(nextPage)
-- Tell everyone that our stats are updated
local modules = AucAdvanced.GetAllModules("FinishedPage")
for pos, engineLib in ipairs(modules) do
local pOK, finished = pcall(engineLib.FinishedPage,nextPage)
if (pOK) then
if (finished~=nil) and (finished==false) then
return false
end
else
if (nLog) then
nLog.AddMessage("Auctioneer", "Scan", N_WARNING, ("FinishedPage %s Returned Error %s"):format(engineLib.GetName(), finished))
end
end
end
return true
end
function private.ScanPage(nextPage, really)
if (private.isScanning) then
local CanQuery, CanQueryAll = CanSendAuctionQuery()
if not (CanQuery and private.FinishedPage(nextPage) and really) then
private.scanNext = GetTime()
private.scanNextPage = nextPage
return
end
private.sentQuery = true
private.queryStarted = GetTime()
private.Hook.QueryAuctionItems(private.curQuery.name or "",
private.curQuery.minUseLevel or "", private.curQuery.maxUseLevel or "",
private.curQuery.invType, private.curQuery.classIndex, private.curQuery.subclassIndex, nextPage,
private.curQuery.isUsable, private.curQuery.quality)
AuctionFrameBrowse.page = nextPage
-- The maximum time we'll wait for the pagedata to be returned to us:
local now = GetTime()
private.scanDelay = now + 8 -- Only wait for up to ?? seconds
private.nextCheck = now + 0.5 -- Check complete in ?? seconds
private.verifyStart = nil
end
end
function private.HasAllData()
local check = private.nextCheck
if not check then return true end
local now = GetTime()
if now > check then -- Wait at least 1 second before checking
-- Check to see if we have all the page data
local numBatchAuctions, totalAuctions = GetNumAuctionItems("list")
if not private.NoOwnerList then
private.NoOwnerList = {}
for i = 1, numBatchAuctions do
private.NoOwnerList[i] = i
end
end
local _, owner = 0, {}
for i, j in ipairs(private.NoOwnerList) do
_,_,_,_,_,_,_,_,_,_,_,owner[j] = GetAuctionItemInfo("list", j)
end
for i = #private.NoOwnerList, 1, -1 do
local j = private.NoOwnerList[i]
if owner[j] then
-- Remove from the lookuptable
tremove(private.NoOwnerList, i)
end
end
if #private.NoOwnerList ~= 0 then
private.nextCheck = now + 0.1
return false
end
private.NoOwnerList = nil
return true
end
return false
end
--[[ Not currently used
function private.NoDupes(pageData, compare)
if not pageData then return true end
for pos, pageItem in ipairs(pageData) do
if (compare[Const.LINK] == pageItem[Const.LINK]) then
if (private.IsSameItem(pageItem, compare)) then
return false
end
end
end
return true
end
--]]
function lib.GetAuctionItem(list, i)
local itemLink = GetAuctionItemLink(list, i)
if itemLink then
itemLink = AucAdvanced.SanitizeLink(itemLink)
local _,_,_,itemLevel,_,itemType,itemSubType,_,itemEquipLoc = GetItemInfo(itemLink)
local _, itemId, itemSuffix, itemFactor, itemEnchant, itemSeed = AucAdvanced.DecodeLink(itemLink)
--[[
Returns Integer giving range of time left for query
1 -- short time (Less than 30 mins)
2 -- medium time (30 mins to 2 hours)
3 -- long time (2 hours to 8 hours)
4 -- very long time (8 hours+)
]]
local timeLeft = GetAuctionItemTimeLeft(list, i)
local name, texture, count, quality, canUse, level, minBid, minIncrement, buyoutPrice, bidAmount, highBidder, owner, saleStatus = GetAuctionItemInfo(list, i)
local invType = Const.EquipEncode[itemEquipLoc]
buyoutPrice = buyoutPrice or 0
minBid = minBid or 0
local nextBid
if bidAmount > 0 then
nextBid = bidAmount + minIncrement
if buyoutPrice > 0 and nextBid > buyoutPrice then
nextBid = buyoutPrice
end
elseif minBid > 0 then
nextBid = minBid
else
nextBid = 1
end
if not count or count == 0 then count = 1 end
if not highBidder then highBidder = false
else highBidder = true end
if not owner then owner = "" end
local curTime = time()
return {
itemLink, itemLevel, itemType, itemSubType, invType, nextBid,
timeLeft, curTime, name, texture, count, quality, canUse, level,
minBid, minIncrement, buyoutPrice, bidAmount, highBidder, owner,
0, -1, itemId, itemSuffix, itemFactor, itemEnchant, itemSeed
}
end
end
function lib.GetAuctionSellItem(minBid, buyoutPrice, runTime)
local itemLink = private.auctionItem
local name, texture, count, quality, canUse, price = GetAuctionSellItemInfo();
if name and itemLink then
itemLink = AucAdvanced.SanitizeLink(itemLink)
local _,_,_,itemLevel,level,itemType,itemSubType,_,itemEquipLoc = GetItemInfo(itemLink)
local _, itemId, itemSuffix, itemFactor, itemEnchant, itemSeed = AucAdvanced.DecodeLink(itemLink)
local timeLeft = 4
if runTime <= 12*60 then timeLeft = 3 end
local curTime = time()
return {
itemLink, itemLevel, itemType, itemSubType, invType, minBid,
timeLeft, curTime, name, texture, count, quality, canUse, level,
minBid, 0, buyoutPrice, 0, nil, UnitName("player"),
0, -1, itemId, itemSuffix, itemFactor, itemEnchant, itemSeed
}, price
end
end
local StorePageFunction = function()
local queryStarted = private.scanStarted
if not queryStarted then queryStarted = GetTime() end
if (not private.curQuery) or (private.curQuery.name == "empty page") then
return
end
local startTime = GetTime()
local lastPause = startTime
localRunTime = 0
private.sentQuery = false
local page = AuctionFrameBrowse.page
if not private.curScan then
private.curScan = {}
end
if not private.curPages then
private.curPages = {}
end
if (nLog) then
nLog.AddMessage("Auctioneer", "Scan", N_INFO, ("StorePage Started %fs after Query Start"):format(startTime - queryStarted), ("StorePage Called %f seconds from query to be called"):format(startTime - queryStarted))
end
local curQuery, curScan, curPages = private.curQuery, private.curScan, private.curPages
local speed = get("scancommit.speed")/100
speed = speed^2.5
local processingTime = speed * 0.1 + 0.015
local EventFramesRegistered = {}
local numBatchAuctions, totalAuctions = GetNumAuctionItems("list")
local maxPages = ceil(totalAuctions / 50)
local isGetAll = false
if (numBatchAuctions > 50) then
isGetAll = true
maxPages = 1
EventFramesRegistered = {GetFramesRegisteredForEvent("AUCTION_ITEM_LIST_UPDATE")}
for _, frame in pairs(EventFramesRegistered) do
frame:UnregisterEvent("AUCTION_ITEM_LIST_UPDATE")
end
private.verifyStart = 1
local now = GetTime()
private.nextCheck = now
private.scanDelay = now + 30
localRunTime = GetTime()-lastPause
coroutine.yield()
lastPause = GetTime()
end
--Update the progress indicator
local elapsed = GetTime() - private.scanStarted - private.totalPaused
--store queued scans to pass along on the callback, used by scanbutton and searchUI etc to display how many scans are still queued
--page, maxpages, name lets a module know when a "scan" they have queued is actually in progress. scansQueued lets a module know how may scans are left to go
private.UpdateScanProgress(nil, totalAuctions, #curScan, elapsed, page+1, maxPages, curQuery) --page starts at 0 so we need to add +1
local curTime = time()
local getallspeed = (get("GetAllSpeed") or 500)*4
local storecount = 0
if not private.breakStorePage and (page > curQuery.qryinfo.page) then
for i = 1, numBatchAuctions do
if isGetAll and ((i % getallspeed) == 0) then --only start yielding once the first page is done, so it won't affect normal scanning
local gt = GetTime()
if (gt-lastPause >= processingTime) then
lib.ProgressBars("GetAllProgressBar", 100*i/numBatchAuctions, true)
localRunTime = localRunTime + GetTime()-lastPause
coroutine.yield()
lastPause = GetTime()
if private.breakStorePage then
break
end
end
end
local itemData = lib.GetAuctionItem("list", i)
if itemData then
-- local legacyScanning = private.legacyScanning
-- if legacyScanning == nil then
-- if Auctioneer and Auctioneer.ScanManager and Auctioneer.ScanManager.IsScanning then
-- legacyScanning = Auctioneer.ScanManager.IsScanning
-- else
-- legacyScanning = function () return false end
-- end
-- private.legacyScanning = legacyScanning
-- end
-- We only store one of the same item/owner/price/quantity in the scan
-- unless we are doing a forward scan (in which case we can be sure they
-- are not duplicate entries.
-- if private.isScanning
-- or totalAuctions <= 50
-- or numBatchAuctions > 50 --if GetAll, we can be sure they aren't duplicates
-- or legacyScanning() -- Is AucClassic scanning?
-- or private.NoDupes(curScan, itemData) then
tinsert(curScan, itemData)
storecount = storecount + 1
end
end
if (storecount > 0) then
curQuery.qryinfo.page = page
curPages[page] = true -- we have pulled this page
end
end
if isGetAll then
for _, frame in pairs(EventFramesRegistered) do
frame:RegisterEvent("AUCTION_ITEM_LIST_UPDATE")
local eventscript = frame:GetScript("OnEvent")
if eventscript then
pcall(eventscript, frame, "AUCTION_ITEM_LIST_UPDATE")
end
end
EventFramesRegistered=nil
end
-- Just updated the page if it was a new page, so record it as latest page.
if (page > curQuery.qryinfo.page) then
curQuery.qryinfo.page = page
end
--Send a Processor event to modules letting them know we are done with the page
AucAdvanced.SendProcessorMessage("pagefinished", page)
-- Clear GetAll changes made by StartScan
if private.isGetAll then -- in theory private.isGetAll should be true iff (local) isGetAll is true -- unless total auctions <=50 (e.g. on PTR)
lib.ProgressBars("GetAllProgressBar", 100, false)
BrowseSearchButton:Show()
AucAdvanced.API.BlockUpdate(false)
private.isGetAll = nil
end
-- Send the next page query or finish scanning
if isGetAll then
if not private.breakStorePage then
private.Commit((#curScan < totalAuctions - 100), true)
-- Clear the getall output. We don't want to create a new query so use the hook
private.queryStarted = GetTime()
private.Hook.QueryAuctionItems("empty page", "", "", nil, nil, nil, nil, nil, nil)
end
elseif private.isScanning then
if (page+1 < maxPages) then
private.ScanPage(page + 1)
else
local incomplete = false
if (#curScan < totalAuctions - 10) then -- we just got scan size above, so they should be close.
incomplete = true
end
private.Commit(incomplete, false)
end
elseif (maxPages == page+1) then
local incomplete = false
for i = 0, maxPages-1 do
if not curPages[i] then
incomplete = true
break
end
end
private.Commit(incomplete, false)
end
local endTime = GetTime()
localRunTime = localRunTime + endTime-lastPause
private.storeTime = (private.storeTime or 0) + localRunTime
if (nLog) then
nLog.AddMessage("Auctioneer", "Scan", N_INFO, ("StorePage %fs"):format(localRunTime), ("StorePage Took %f seconds from request to complete, %f seconds of that was to store, and %f seconds of the time to store was processing time"):format(endTime-queryStarted, endTime-startTime, localRunTime))
end
end
function private.StopStorePage(silent)
if not CoStore or coroutine.status(CoStore) ~= "suspended" then return end
-- flag to break out of the loop, or prevent the loop being entered, within the coroutine
private.breakStorePage = true
while coroutine.status(CoStore) == "suspended" do
CoroutineResume(CoStore)
end
private.breakStorePage = nil
if not silent then
message("Warning: GetAll scan is incomplete because it was interrupted")
end
end
function lib.StorePage()
if not CoStore or coroutine.status(CoStore) == "dead" then
CoStore = coroutine.create(StorePageFunction)
CoroutineResume(CoStore)
elseif coroutine.status(CoStore) == "suspended" then
CoroutineResume(CoStore)
end
end
--[[ AucAdvanced.Scan.QuerySafeName(name)
Library function to convert a name to the 'normalized' form used by scan querys
Note: performs truncation on names over 63 bytes as QueryAuctionItems cannot handle longer strings
--]]
function lib.QuerySafeName(name)
if type(name) == "string" and #name > 0 then
if #name > 63 then
if name:byte(63) >= 192 then -- UTF-8 multibyte first byte
name = name:sub(1, 62)
elseif name:byte(62) >= 224 then -- UTF-8 triplebyte first byte
name = name:sub(1, 61)
else
name = name:sub(1, 63)
end
end
return name:lower()
end
end
--[[ AucAdvanced.Scan.CreateQuerySig(name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex)
Library function to allow other modules to obtain a query sig
Returns the sig that would be used in a scan with the specified parameters
--]]
function lib.CreateQuerySig(...)
return private.CreateQuerySig(private.QueryScrubParameters(...))
end
function private.QueryScrubParameters(name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex)
-- Converts the parameters that we will store in our scanQuery table into a consistent format:
-- converts each parameter to correct type;
-- converts all strings to lowercase;
-- converts all "" and 0 to nil;
-- converts any invalid parameters to nil.
name = lib.QuerySafeName(name)
minLevel = tonumber(minLevel)
if minLevel and minLevel < 1 then minLevel = nil end
maxLevel = tonumber(maxLevel)
if maxLevel and maxLevel < 1 then maxLevel = nil end
classIndex = tonumber(classIndex)
if classIndex and classIndex < 1 then classIndex = nil end
if classIndex then
subclassIndex = tonumber(subclassIndex)
if subclassIndex and subclassIndex < 1 then subclassIndex = nil end
else
subclassIndex = nil -- subclassIndex is only valid if we have a classIndex
end
invTypeIndex = tonumber(invTypeIndex) or Const.EquipLocToInvIndex[invTypeIndex] -- accepts "INVTYPE_*" strings
if invTypeIndex and invTypeIndex < 1 then invTypeIndex = nil end
if isUsable and isUsable ~= 0 then
isUsable = 1
else
isUsable = nil
end
qualityIndex = tonumber(qualityIndex)
if qualityIndex and qualityIndex < 1 then qualityIndex = nil end
return name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex
end
function private.CreateQuerySig(name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex)
return strjoin("#",
name or "",
minLevel or "",
maxLevel or "",
invTypeIndex or "",
classIndex or "",
subclassIndex or "",
isUsable or "",
qualityIndex or ""
) -- can use strsplit("#", sig) to extract params
end
function private.QueryCompareParameters(query, name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex)
-- Returns true if the parameters are identical to the values stored in the specified scanQuery table
-- Use this function to avoid creating a duplicate scanQuery table
-- Parameters must have been scrubbed first
-- Note: to compare two scanQuery tables for equality, just compare the sigs
if query.name == name -- note: both already converted to lowercase when scrubbed
and query.minUseLevel == minLevel
and query.maxUseLevel == maxLevel
and query.classIndex == classIndex
and query.subclassIndex == subclassIndex
and query.quality == qualityIndex
and query.invType == invTypeIndex
and query.isUsable == isUsable
then
return true
end
end
private.querycount = 0
function private.NewQueryTable(name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex)
-- Assumes the parameters have already been scrubbed
local class, subclass
local query, qryinfo = {}, {}
query.qryinfo = qryinfo
query.name = name
query.minUseLevel = minLevel
query.maxUseLevel = maxLevel
query.invType = invTypeIndex
if classIndex then
class = Const.CLASSES[classIndex]
query.class = class
query.classIndex = classIndex
end
if subclassIndex then
subclass = Const.SUBCLASSES[classIndex][subclassIndex]
query.subclass = subclass
query.subclassIndex = subclassIndex
end
query.isUsable = isUsable
query.quality = qualityIndex
qryinfo.page = -1 -- use this to store highest page seen by query, and we haven't seen any yet.
qryinfo.id = private.querycount
private.querycount = private.querycount+1
qryinfo.sig = private.CreateQuerySig(name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex)
-- the return value from GetFaction() can change when the Auctionhouse closes
-- (Neutral Auctionhouse and "Always Home Faction" option enabled - this is on by default)
-- store the current return value - this will be used throughout processing to avoid problems
qryinfo.serverKey = GetFaction()
local scanSize = false, ""
if ((not query.class) and (not query.subclass) and (not query.minUseLevel)
and (not query.maxUseLevel)
and (not query.name) and (not query.isUsable)
and (not query.invType) and (not query.quality)) then
qryinfo.scanSize = "Full"
elseif (query.name and query.class and query.subclass and query.quality) then
qryinfo.scanSize = "Micro"
else
qryinfo.scanSize = "Partial"
end
return query
end
private.Hook = {}
private.Hook.PlaceAuctionBid = PlaceAuctionBid
function PlaceAuctionBid(type, index, bid, ...)
local itemData = lib.GetAuctionItem(type, index)
if itemData then
private.Unpack(itemData, statItem)
local modules = AucAdvanced.GetAllModules("ScanProcessors")
for pos, engineLib in ipairs(modules) do
if engineLib.ScanProcessors["placebid"] then
pcall(engineLib.ScanProcessors["placebid"],"placebid", statItem, type, index, bid)
end
end
end
return private.Hook.PlaceAuctionBid(type, index, bid, ...)
end
private.Hook.ClickAuctionSellItemButton = ClickAuctionSellItemButton
function ClickAuctionSellItemButton(...)
local ctype, itemID, itemLink = GetCursorInfo()
if ctype == "item" then
private.auctionItem = itemLink
else
private.auctionItem = nil
end
return private.Hook.ClickAuctionSellItemButton(...)
end
private.Hook.StartAuction = StartAuction
function StartAuction(minBid, buyoutPrice, runTime, ...)
local itemData, price = lib.GetAuctionSellItem(minBid, buyoutPrice, runTime)
if itemData then
private.Unpack(itemData, statItem)
local modules = AucAdvanced.GetAllModules("ScanProcessors")
for pos, engineLib in ipairs(modules) do
if engineLib.ScanProcessors["newauc"] then
pcall(engineLib.ScanProcessors["newauc"],"newauc", statItem, minBid, buyoutPrice, runTime, price)
end
end
end
return private.Hook.StartAuction(minBid, buyoutPrice, runTime, ...)
end
private.Hook.TakeInboxMoney = TakeInboxMoney
function TakeInboxMoney(index, ...)
local invoiceType, itemName, playerName, bid, buyout, deposit, consignment = GetInboxInvoiceInfo(index)
if invoiceType then
local modules = AucAdvanced.GetAllModules("ScanProcessors")
local _,_, sender = GetInboxHeaderInfo(index)
local faction = "Neutral"
if sender:find(FACTION_ALLIANCE) then
faction = "Alliance"
elseif sender:find(FACTION_HORDE) then
faction = "Horde"
end
for pos, engineLib in ipairs(modules) do
if engineLib.ScanProcessors["aucsold"] then
pcall(engineLib.ScanProcessors["aucsold"],"aucsold", faction, itemName, playerName, bid, buyout, deposit, consignment)
end
end
end
return private.Hook.TakeInboxMoney(index, ...)
end
private.Hook.QueryAuctionItems = QueryAuctionItems
local isSecure, taint = issecurevariable("CanSendAuctionQuery")
if not isSecure then
private.warnTaint = taint
end
private.CanSend = CanSendAuctionQuery
function QueryAuctionItems(name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, page, isUsable, qualityIndex, GetAll, ...)
if private.warnTaint then
_print("\nAuctioneer:\n WARNING, The CanSendAuctionQuery() function was tainted by the addon: {{"..private.warnTaint.."}}.\n This may cause minor inconsistencies with scanning.\n If possible, adjust the load order to get me to load first.\n ")
private.warnTaint = nil
end
if not private.CanSend() then
_print("Can't send query just at the moment")
return
end
local isSearch = (BrowseSearchButton:GetButtonState() == "PUSHED")
-- If we're getting called after we've sent a query, but before it's been stored, take this chance to save it.
if private.sentQuery then
lib.StorePage()
end
name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex = private.QueryScrubParameters(
name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex)
local query
if private.curQuery then
if private.QueryCompareParameters(private.curQuery, name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex) then
query = private.curQuery
if (nLog) then
nLog.AddMessage("Auctioneer", "Scan", N_INFO, ("Sending exisiting query %d (%s)"):format(query.qryinfo.id, query.qryinfo.sig))
end
else
private.Commit(true, false)
end
end
if not query then
query = private.NewQueryTable(name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex)
private.scanStartTime = time()
private.scanStarted = GetTime()
private.totalPaused = 0
private.storeTime = 0
private.curQuery = query
end
page = tonumber(page) or 0
if (page==0) then
local scanSize = query.qryinfo.scanSize
if (query.qryinfo.NoSummary) then
scanSize = "NoSum-"..scansize
end
if (nLog) then
local queryType = "standard"
if (GetAll) then queryType = "get all" end
nLog.AddMessage("Auctioneer", "Scan", N_INFO, ("Sending new %s query %d (%s)"):format(queryType, query.qryinfo.id, query.qryinfo.sig))
end
AucAdvanced.SendProcessorMessage("scanstart", scanSize, query.qryinfo.sig, query)
end
private.sentQuery = true
lib.lastReq = GetTime()
private.queryStarted = GetTime()
return private.QuerySent(query, isSearch,
private.Hook.QueryAuctionItems(
name or "", minLevel or "", maxLevel or "", invTypeIndex, classIndex, subclassIndex,
page, isUsable, qualityIndex, GetAll, ...))
end
function lib.SetPaused(pause)
if private.isGetAll then
-- A GetAll scan cannot be Popped or Pushed
_print("Scan cannot be paused/unpaused because it is a GetAll scan")
return
end
if pause then
if private.isPaused then return end
lib.PushScan()
private.isPaused = true
elseif private.isPaused then
lib.PopScan()
private.isPaused = false
end
end
private.unexpectedClose = false
local flipb, flopb = false, false
function private.OnUpdate(me, dur)
if CoCommit then
local costat = coroutine.status(CoCommit)
if costat == "suspended" then
CoroutineResume(CoCommit)
elseif costat == "dead" then
if #private.CommitQueue > 0 then
CoCommit = coroutine.create(Commitfunction)
CoroutineResume(CoCommit)
else
CoCommit = nil
end
end
end
local now = GetTime()
if not AuctionFrame then return end
if private.isPaused then return end
if private.queueScan then
if CanSendAuctionQuery() and (not private.CanSend or private.CanSend()) then
local queued = private.queueScan
private.queueScan = nil
lib.StartScan(unpack(queued, 1, private.queueScanParams)) -- explicit start and end points as some entries may be nil
end
return
end
if private.scanDelay then
-- If we are within the delay interval
if now < private.scanDelay then
-- Check to see if all the auctions have fully populated
if not private.HasAllData() then
-- If not, we still have time to wait
return
end
end
private.NoOwnerList = nil
private.scanDelay = nil
end
if CoStore and coroutine.status(CoStore) == "suspended" and AuctionFrame and AuctionFrame:IsVisible() then
flipb = not flipb
if flipb then
flopb = not flopb
if flopb then
CoroutineResume(CoStore)
end
end
end
if private.scanNext then
--if now > private.scanNext and CanSendAuctionQuery() then
if CanSendAuctionQuery() then
local nextPage = private.scanNextPage
private.scanNext = nil
private.ScanPage(nextPage, true)
end
return
end
if AuctionFrame:IsVisible() then
if private.unexpectedClose then
private.unexpectedClose = false
lib.PopScan()
return
end
if private.sentQuery and CanSendAuctionQuery() then
lib.StorePage()
end
elseif private.curQuery then
lib.Interrupt()
end
end
private.updater = CreateFrame("Frame", nil, UIParent)
private.updater:SetScript("OnUpdate", private.OnUpdate)
function lib.Cancel()
if (private.curQuery) then
_print("Cancelling current scan")
private.Commit(true, false)
end
private.ResetAll()
end
function lib.Interrupt()
if private.curQuery and not AuctionFrame:IsVisible() then
if private.isGetAll then
-- GetAll cannot be pushed/popped so we have to commit here instead
private.Commit(true, true)
private.sentQuery = false
if private.isGetAll then
-- If the StorePage function didn't run, we need to cleanup here instead
lib.ProgressBars("GetAllProgressBar", nil, false)
BrowseSearchButton:Show()
AucAdvanced.API.BlockUpdate(false)
private.isGetAll = nil
end
elseif private.isScanning then
private.unexpectedClose = true
lib.PushScan()
else
private.Commit(true, false)
private.sentQuery = false
end
end
end
function lib.Abort()
if (private.curQuery) then
_print("Aborting current scan")
end
private.ResetAll()
end
function private.ResetAll()
private.StopStorePage(true)
-- Fallback in case private.isGetAll and related actions were not cleared during processing
lib.ProgressBars("GetAllProgressBar", nil, false)
BrowseSearchButton:Show()
AucAdvanced.API.BlockUpdate(false)
private.isGetAll = nil
local oldquery = private.curQuery
private.curQuery = nil
private.curScan = nil
private.isPaused = nil
private.sentQuery = nil
private.isScanning = false
private.unexpectedClose = false
private.UpdateScanProgress(false, nil, nil, nil, nil, nil, oldquery)
if CommitRunning then
return
end
private.scanStartTime = nil
private.scanStarted = nil
private.totalPaused = nil
private.storeTime = nil
private.curPages = nil
private.scanStack = nil
private.Pausing = nil
end
-- In the absence of a proper API function to do it, it's necessary to inspect an item's tooltip to
-- figure out if it's usable by the player
local ItemUsableTooltip = {
tooltipFrame = nil,
fontString = {},
maxLines = 100,
CanUse = function(this, link)
-- quick level check first
local minLevel = select(5, GetItemInfo(link)) or 0
if UnitLevel("player") < minLevel then
return false
end
-- set up if not done already
if not this.tooltipFrame then
this.tooltipFrame = CreateFrame("GameTooltip")
this.tooltipFrame:SetOwner(UIParent, "ANCHOR_NONE")
for i = 1, this.maxLines do
this.fontString[i] = {}
for j = 1, 2 do
this.fontString[i][j] = this.tooltipFrame:CreateFontString()
this.fontString[i][j]:SetFontObject(GameFontNormal)
end
this.tooltipFrame:AddFontStrings(this.fontString[i][1], this.fontString[i][2])
end
this.minLevelPattern = string.gsub(ITEM_MIN_LEVEL, "(%%d)", "(.+)")
end
-- clear tooltip
local numLines
numLines = math.min(this.maxLines, this.tooltipFrame:NumLines())
for i = 1, numLines do
for j = 1, 2 do
this.fontString[i][j]:SetText()
this.fontString[i][j]:SetTextColor(0, 0, 0)
end
end
-- populate tooltip
this.tooltipFrame:SetHyperlink(link)
-- search tooltip for red text
numLines = math.min(this.maxLines, this.tooltipFrame:NumLines())
for i = 1, numLines do
for j = 1, 2 do
local r, g, b = this.fontString[i][j]:GetTextColor()
if r > 0.8 and g < 0.2 and b < 0.2 then
-- item is not usable, with one exception: if it doesn't have a level
-- requirement, red "requires level xxx" text refers to some other item,
-- e.g. that created by a recipe
local text = string.lower(this.fontString[i][j]:GetText())
if not (minLevel == 0 and string.find(text, this.minLevelPattern)) then
return false
end
end
end
end
return true
end,
}
-- Caching wrapper for ItemUsableTooltip. Invalidates cache when certain events occur
-- (player levels up, learns a new recipe, etc.)
local ItemUsableCached = {
eventFrame = nil,
patterns = {},
cache = {},
tooltip = ItemUsableTooltip,
OnEvent = function(this, event, arg1, ...)
local dirty = false
-- print("got event " .. event .. ", arg1 " .. arg1)
if event == "CHAT_MSG_SYSTEM" or event == "CHAT_MSG_SKILL" then
for _, pattern in pairs(this.patterns) do
if string.find(arg1, pattern) then
dirty = true
break
end
end
elseif event == "PLAYER_LEVEL_UP" then
dirty = true
end
if dirty then
-- print("invalidating")
this.cache = {}
end
end,
RegisterChatString = function(this, chatString)
local pattern = chatString
pattern = gsub(pattern, "%%s", ".+")
pattern = gsub(pattern, "%%d", ".+")
pattern = gsub(pattern, "%%%d+%$s", ".+")
pattern = gsub(pattern, "%%%d+%$d", ".+")
pattern = gsub(pattern, "|3%-%d+%(%%s%)", ".+")
tinsert(this.patterns, pattern)
end,
CanUse = function(this, link)
-- set up if not done already
if not this.eventFrame then
this.eventFrame = CreateFrame("Frame")
-- forward events from frame to self
this.eventFrame.forwardEventsTo = this
this.eventFrame:SetScript(
"OnEvent",
function(eventFrame, ...)
eventFrame.forwardEventsTo:OnEvent(...)
end)
-- register events and chat patterns
this.eventFrame:RegisterEvent("CHAT_MSG_SYSTEM")
this.eventFrame:RegisterEvent("CHAT_MSG_SKILL")
this.eventFrame:RegisterEvent("PLAYER_LEVEL_UP")
this:RegisterChatString(ERR_LEARN_ABILITY_S)
this:RegisterChatString(ERR_LEARN_RECIPE_S)
this:RegisterChatString(ERR_LEARN_SPELL_S)
this:RegisterChatString(ERR_SPELL_UNLEARNED_S)
this:RegisterChatString(ERR_SKILL_GAINED_S)
this:RegisterChatString(ERR_SKILL_UP_SI)
end
local itemType, id = AucAdvanced.DecodeLink(link)
if not itemType or itemType ~= "item" then return end
-- check cache first. failing that, do a tooltip scan
if this.cache[id] == nil then
-- print("miss " .. link)
this.cache[id] = this.tooltip:CanUse(link)
else
-- print("hit " .. link)
end
return this.cache[id]
end,
}
private.itemUsable = ItemUsableCached
function private.CanUse(link)
return private.itemUsable:CanUse(link)
end
function lib.GetScanCount()
local scanCount = 0
if (private.scanStack) then scanCount = #private.scanStack end
if (private.isScanning) then
scanCount = scanCount + 1
end
return scanCount
end
function lib.GetStackedScanCount()
local scanCount = private.scanStack or 0
if (private.scanStack) then scanCount = #private.scanStack end
return scanCount
end
function lib.AHClosed()
lib.Interrupt()
end
function lib.Logout()
AucAdvancedData.Scandata = nil -- delete obsolete data. it's here because CoreScan doesn't have an OnLoad processor
private.Commit(true, false)
if CoCommit then
while coroutine.status(CoCommit) == "suspended" do
CoroutineResume(CoCommit)
end
end
end
function coremodule.Processor(event, ...)
if event == "scanstats" then
private.clearImageCaches(...)
end
end
coremodule.Processors = {}
function coremodule.Processors.scanstats(event, ...)
private.clearImageCaches(...)
end
internal.Scan = {}
function internal.Scan.NotifyItemListUpdated()
if private.scanStarted then
if (nLog) then
local startTime = GetTime()
nLog.AddMessage("Auctioneer", "Scan", N_INFO, ("NotifyItemListUpdated Called %fs after Query Start"):format(startTime - private.scanStarted), ("NotifyItemListUpdated Called %f seconds from query to be called"):format(startTime - private.scanStarted))
end
end
end
function internal.Scan.NotifyOwnedListUpdated()
if private.scanStarted then
if (nLog) then
local startTime = GetTime()
nLog.AddMessage("Auctioneer", "Scan", N_INFO, ("NotifyOwnedListUpdated Called %fs after Query Start"):format(startTime - private.scanStarted), ("NotifyOwnedListUpdated Called %f seconds from query to be called"):format(startTime - private.scanStarted))
end
end
end
internal.Scan.Logout = lib.Logout
internal.Scan.AHClosed = lib.AHClosed
AucAdvanced.RegisterRevision("$URL: http://svn.norganna.org/auctioneer/branches/5.9/Auc-Advanced/CoreScan.lua $", "$Rev: 4953 $")