Advertisement
Guest User

Auc-Advanced/CoreScan.lua

a guest
Jul 20th, 2016
1,716
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 118.99 KB | None | 0 0
  1. --[[
  2.     Auctioneer
  3.     Version: 5.21f.5579 (SanctimoniousSwamprat)
  4.     Revision: $Id: CoreScan.lua 5577 2015-12-16 18:46:12Z brykrys $
  5.     URL: http://auctioneeraddon.com/
  6.  
  7.     This is an addon for World of Warcraft that adds statistical history to the auction data that is collected
  8.     when the auction is scanned, so that you can easily determine what price
  9.     you will be able to sell an item for at auction or at a vendor whenever you
  10.     mouse-over an item in the game
  11.  
  12.     License:
  13.         This program is free software; you can redistribute it and/or
  14.         modify it under the terms of the GNU General Public License
  15.         as published by the Free Software Foundation; either version 2
  16.         of the License, or (at your option) any later version.
  17.  
  18.         This program is distributed in the hope that it will be useful,
  19.         but WITHOUT ANY WARRANTY; without even the implied warranty of
  20.         MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  21.         GNU General Public License for more details.
  22.  
  23.         You should have received a copy of the GNU General Public License
  24.         along with this program(see GPL.txt); if not, write to the Free Software
  25.         Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  26.  
  27.     Note:
  28.         This AddOn's source code is specifically designed to work with
  29.         World of Warcraft's interpreted AddOn system.
  30.         You have an implicit license to use this AddOn with these facilities
  31.         since that is its designated purpose as per:
  32.         http://www.fsf.org/licensing/licenses/gpl-faq.html#InterpreterIncompat
  33. --]]
  34.  
  35. --[[
  36.     Auctioneer Scanning Engine.
  37.  
  38.     Provides a service to walk through an AH Query, reporting changes in the AH to registered utilities and stats modules
  39.  
  40.     System Overview
  41.  
  42.         Overloads function QueryAuctionItems.
  43.             when called checks to see if a scan is in progress.
  44.             If we are currently in a scan, store the last recieved page if not saved just in case (shouldn't be necessary).
  45.             Scrubs parameters for when called directly to keep D/Cs from happening (like Blizzard does via UI)
  46.             If in a scan and the current call doesn't match it, commits current scan.
  47.             Else if not in a scan, record the start of a new scan including whether a manual or automated scan.
  48.             if first page of scan, indicate to all clients that a scan has started.
  49.             calls original Blizzard QueryAuctionItems.
  50.  
  51.         We add a listener on the AH window, and fire every frame it is opened.
  52.             If commit coroutine is asleep, resume it.
  53.             If commit routine is dead and there are scans needing committed, wake it up.
  54.             If current scan is paused or an error occurs, exit out of routine to cut loading on system.
  55.             If scans are queued and we are not currently active scanning, start the next scan and exit.
  56.             If an ah request has been sent and we haven't started to store page, exit if if know data isn't ready to be stored.
  57.             If a store routine is going and is suspended, restart it (if been stopped long enough)
  58.             If it is time to get the next page in an automated scan, send the next page query and exit.
  59.             If AH is open and we were unexpected closed earlier, restart last scan here and exit.
  60.             If AH is open and we made it here start to Store last page recieved.
  61.             IF AH is not open, pause the current scan and put it on the scan stack.
  62.  
  63.         Storing a Page.
  64.             When a store page is requested, it starts a coroutine.  The coroutine is responsible for all work.
  65.             Note: store page keeps verifying throughout that store hasn't been requested to stop.  It exits immediately if it has.
  66.             Also, this area uses defaults for items from GetItemInfo instead of calling for each auction.
  67.             If a getAll scan, turn off updating of AH Windows.
  68.             Updates scan processors that the page is being stored.
  69.             skips walking through and storing auctions if page isn't later than pages already done.
  70.             walks through all auctions returned.  If data is ready, stores it in page store.  If not, records location # in retry list.
  71.             while retry list has items and retries left is greater than 0, do following
  72.                 pauses for 1 second
  73.                 decrement retries left
  74.                 walks through all auctions in retries list.
  75.                     If data is ready, stores it in page store and resets retry count to max.
  76.                     If not, records location # in newRetry list.
  77.                 replaces retry list with newRetry list.
  78.  
  79.             add the items to the scan store and record what the next page we need is.
  80.             if isGetAll, put AH Window back in normal state.
  81.             otherwise if an automated scan and there are more pages, start the scan of the next page.
  82.             otherwise if an automated scan then Commit the scan.
  83.             otherwise if a manual scan and on the last page, commit the scan.
  84.  
  85.             this routine means that a page store can take up to NumActionsOnPage*RetryCount seconds to complete
  86.             for a default page, that is 5 minutes.  For a getall, it could take forever.
  87.             In practice, it adds up to 15 seconds to what already happens for a regular page, and 5 minutes for getAll.
  88.  
  89.         Committing a scan.
  90.             the commit routine is responsible for storing the scan store into a list and then starting a coroutine (if not already running) that can commit it.
  91.             The coroutine runs in a loop until there are no more items in the commit queue.
  92.             Pull first item from commit queue.
  93.  
  94.             Stage 1.  Pre-Process new scan. Retrieve item info
  95.             While itemlink lookup has entries and retry count not 0
  96.                 create empty next itemlink lookup table
  97.                 decrement retry count
  98.                 Walk through items in itemlink lookup
  99.                     call GetItemInfo for item
  100.                     If GetItemInfo returns data
  101.                         walk through auction list and fix up data returned for each.
  102.                         reset retry count to max.
  103.                     else add item to next itemLink lookup
  104.                 replace itemlink lookup with next itemLink lookup
  105.             if there are still items in itemlink lookup
  106.                 walk scan table from back to front, and remove any item with an ILEVEL of -1
  107.                 mark scan an an incomplete scan.
  108.  
  109.  
  110.             Stage 2.  Prep AH Image
  111.                 mark all auctions in auction house image that match current scan as still needing resolved against scan
  112.                 mark all auctions in auction house image that don't match current scan as NOT needing resolved against scan
  113.                 build a look-up table for the next stage
  114.  
  115.             Stage 3.  Merge new scan with AH Image
  116.                 walk through all items in new scan.
  117.                 if a match is found in AH Image that still needs resolved
  118.                     if auction in AH Image was not filtered then
  119.                         check if AH exactly matches current info.
  120.                         if not, then send 'update' processor message.
  121.                         otherwise send 'leave' processor message.
  122.                     update item in AH Image from scan info and mark entry as resolved.
  123.                 otherwise
  124.                     send 'filter' processor message.
  125.                     If no filters indicate auction to be filtered, then
  126.                         send 'create' processor message.
  127.                     otherwise
  128.                         add flag to auction to indicate it is filtered.
  129.                     add auction to AH Image.
  130.  
  131.             Stage 4.  Remove unseen from AH Image
  132.                 walk through all items in AH Image
  133.                 if needs resolved then
  134.                     if expired, then remove from image, sending 'delete' processor message
  135.                     otherwise if not expired and a complete scan, remove from image, sending 'delete' process message
  136.                     otherwise if flagged unseen prior scan, remove from image sending 'delete' processor message
  137.                     otherwise mark unseen in AH Scan image.
  138. ]]
  139. local _G = _G
  140.  
  141. if not _G.AucAdvanced then return end
  142. local coremodule, internal = _G.AucAdvanced.GetCoreModule("CoreScan")
  143. if not coremodule or not internal then return end -- Someone has explicitely broken us
  144.  
  145. if (not _G.AucAdvanced.Scan) then _G.AucAdvanced.Scan = {} end
  146.  
  147. local SCANDATA_VERSION = "A" -- must match Auc-ScanData INTERFACE_VERSION
  148.  
  149. local TOLERANCE_LOWERLIMIT = 50
  150. local TOLERANCE_TAPERLIMIT = 10000
  151.  
  152. local lib = _G.AucAdvanced.Scan
  153. local private = {}
  154.  
  155. local Const = _G.AucAdvanced.Const
  156. local Resources = AucAdvanced.Resources
  157. local _print,decode,_,_,replicate,empty,get,set,default,debugPrint,fill, _TRANS = _G.AucAdvanced.GetModuleLocals()
  158. local GetFaction = _G.AucAdvanced.GetFaction
  159. local ResolveServerKey = AucAdvanced.ResolveServerKey
  160. local EquipCodeToInvIndex = _G.AucAdvanced.Const.EquipCodeToInvIndex
  161.  
  162. local table, tinsert, tremove, gsub, string, coroutine, pcall, time = _G.table, _G.tinsert, _G.tremove, _G.gsub, _G.string, _G.coroutine, _G.pcall, _G.time
  163. local ceil, math, mod, floor = _G.ceil, _G.math, _G.mod, _G.floor
  164. local unpack, select = _G.unpack, _G.select
  165. local bitand, bitor, bitnot = bit.band, bit.bor, bit.bnot
  166. local type, wipe = type, wipe
  167. local pairs, ipairs, next = _G.pairs, _G.ipairs, _G.next
  168. local tonumber = tonumber
  169. local GetTime = GetTime
  170.  
  171. private.isScanning = false
  172. private.auctionItemListUpdated = false
  173.  
  174. -- Only the following entries in a valid itemdata table are permitted to be nil or false
  175. local ItemDataAllowedNil = {
  176.     [Const.IEQUIP] = true,
  177.     [Const.AMHIGH] = true,
  178.     [Const.CANUSE] = true,
  179.     [Const.DEP2] = true,
  180. }
  181.  
  182. function private.LoadScanData()
  183.     if not private.loadingScanData then
  184.         local _, _, _, _, reason = GetAddOnInfo("Auc-ScanData")
  185.         if IsAddOnLoaded("Auc-ScanData") then
  186.             -- another AddOn has force-loaded Auc-ScanData
  187.             private.loadingScanData = "loading"
  188.         elseif reason ~= "DEMAND_LOADED" then -- unable to be loaded
  189.             private.loadingScanData = "fallback"
  190.             if reason then
  191.                 reason = _G["ADDON_"..reason] or reason
  192.             else
  193.                 reason = "Unknown reason"
  194.             end
  195.             private.FallbackScanData = reason
  196.         else
  197.             private.loadingScanData = "block" -- prevents re-entry to this function during the LoadAddOn call
  198.             load, reason = LoadAddOn("Auc-ScanData")
  199.             if load then
  200.                 private.loadingScanData = "loading"
  201.             elseif reason then
  202.                 private.loadingScanData = "fallback"
  203.                 private.FallbackScanData = _G["ADDON_"..reason] or reason
  204.             else
  205.                 -- LoadAddOn sometimes returns nil, nil if called too early during game startup
  206.                 -- assume it needs to be called again at a later stage
  207.                 private.loadingScanData = nil
  208.             end
  209.         end
  210.     end
  211.     if private.loadingScanData == "loading" then
  212.         local ready, version
  213.         local scanmodule = _G.AucAdvanced.Modules.Util.ScanData
  214.         if scanmodule and scanmodule.GetAddOnInfo then
  215.             ready, version = scanmodule.GetAddOnInfo()
  216.         end
  217.         if version ~= SCANDATA_VERSION then
  218.             private.loadingScanData = "fallback"
  219.             private.FallbackScanData = "Incorrect version"
  220.         elseif ready then
  221.             -- install functions from Auc-ScanData
  222.             private.GetScanData = scanmodule.GetScanData
  223.             lib.ClearScanData = scanmodule.ClearScanData
  224.             -- cleanup
  225.             private.loadingScanData = nil
  226.             private.LoadScanData = nil
  227.             -- signal success
  228.             return private.GetScanData
  229.         end
  230.     end
  231.     if private.loadingScanData == "fallback" then
  232.         -- cannot load Auc-ScanData, go to fallback image handler
  233.         local fallbackscandata = {}
  234.         private.GetScanData = function(serverKey)
  235.             serverKey = ResolveServerKey(serverKey)
  236.             if not serverKey then
  237.                 debugPrint("Fallback-ScanData: invalid serverKey passed to GetScanData", "ScanData", "Invalid serverKey", "Error")
  238.                 return
  239.             end
  240.             local scandata = fallbackscandata[serverKey]
  241.             if scandata then return scandata end
  242.             local test = AucAdvanced.SplitServerKey(serverKey)
  243.             if not test then return end
  244.             scandata = {image = {}, scanstats = {ImageUpdated = time()}}
  245.             fallbackscandata[serverKey] = scandata
  246.             return scandata
  247.         end
  248.         -- fallback message
  249.         local text = format(_TRANS("ADV_Interface_ScanDataNotLoaded"), private.FallbackScanData) --The Auc-ScanData storage module could not be loaded: %s
  250.         if get("core.scan.disable_scandatawarning") then
  251.             _print("|cffff7f3f"..text.."|r")
  252.         else
  253.             message(text)
  254.         end
  255.         -- cleanup
  256.         private.loadingScanData = nil
  257.         private.LoadScanData = nil
  258.         -- signal success
  259.         return private.GetScanData
  260.     end
  261. end
  262.  
  263. function lib.LoadScanData()
  264.     if private.LoadScanData then private.LoadScanData() end
  265. end
  266.  
  267. -- scandataTable = private.GetScanData(serverKey)
  268. -- parameter: serverKey (defaults to home serverKey)
  269. -- returns: scandataTable = {image = imageTable, scanstats = scanstatsTable} for the specified serverKey
  270. --     scandataTable should only be modified for home serverKey
  271. -- returns: nil if there is no data for serverKey or if serverKey is invalid; home serverKey will always return a scandataTable
  272. -- CAUTION: the following is a stub function, which will be overloaded with the real function by LoadScanData
  273. function private.GetScanData(serverKey)
  274.     if private.LoadScanData then
  275.         local newfunc = private.LoadScanData()
  276.         if newfunc then
  277.             return newfunc(serverKey)
  278.         end
  279.     end
  280. end
  281.  
  282. -- AucAdvanced.Scan.ClearScanData(serverKey)
  283. -- AucAdvanced.Scan.ClearScanData(realmName)
  284. -- AucAdvanced.Scan.ClearScanData("SERVER") -- all data for current server (default)
  285. -- AucAdvanced.Scan.ClearScanData("ALL")
  286. -- CAUTION: the following is a stub function, which will be overloaded with the real function by LoadScanData
  287. function lib.ClearScanData(key)
  288.     _print(_TRANS("ADV_Interface_ScanDataNotCleared")) --Scan Data cannot be cleared because {{Auc-ScanData}} is not loaded
  289. end
  290.  
  291. function lib.StartPushedScan(name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex, exactMatch, options)
  292.     name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex, exactMatch = private.QueryScrubParameters(
  293.         name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex, exactMatch)
  294.  
  295.     if private.scanStack then
  296.         for _, scan in ipairs(private.scanStack) do
  297.             if not scan[8] and private.QueryCompareParameters(scan[3], name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex, exactMatch) then
  298.                 -- duplicate of exisiting queued query
  299.                 if (_G.nLog) then
  300.                     _G.nLog.AddMessage("Auctioneer", "Scan", _G.N_INFO, "Duplicate pushed scan detected, cancelling duplicate")
  301.                 end
  302.                 return
  303.             end
  304.         end
  305.     else
  306.         private.scanStack = {}
  307.     end
  308.  
  309.     local query = private.NewQueryTable(name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex, exactMatch)
  310.     query.qryinfo.pushed = true
  311.  
  312.     local NoSummary
  313.     if type(options) == "table" then
  314.         -- only 1 option so far
  315.         if options.NoSummary or options.nosummary then
  316.             NoSummary = true
  317.         end
  318.     end
  319.  
  320.     query.qryinfo.nosummary = NoSummary
  321.  
  322.     if (_G.nLog) then
  323.         _G.nLog.AddMessage("Auctioneer", "Scan", _G.N_INFO, ("Starting pushed scan %d (%s)"):format(query.qryinfo.id, query.qryinfo.sig))
  324.     end
  325.  
  326.     tinsert(private.scanStack, {time(), false, query, {}, {}, false, 0, false, 0})
  327. end
  328.  
  329. function lib.PushScan()
  330.     if private.isGetAll then
  331.         -- A GetAll scan cannot be Popped; do not allow it to be Pushed
  332.         _print("Warning: Scan cannot be Pushed because it is a GetAll scan")
  333.         return
  334.     end
  335.     if private.isScanning then
  336.         if (_G.nLog) then
  337.             _G.nLog.AddMessage("Auctioneer", "Scan", _G.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))
  338.         end
  339.         -- _print(("Pausing current scan at page {{%d}}."):format(private.curQuery.qryinfo.page+1))
  340.         if not private.scanStack then private.scanStack = {} end
  341.         private.StopStorePage()
  342.         tinsert(private.scanStack, {
  343.             private.scanStartTime,
  344.             private.sentQuery,
  345.             private.curQuery,
  346.             private.curPages,
  347.             private.curScan,
  348.             private.scanStarted,
  349.             private.totalPaused,
  350.             GetTime(),
  351.             private.storeTime
  352.         })
  353.         local oldquery = private.curQuery
  354.         private.curQuery = nil
  355.         private.scanStartTime = nil
  356.         private.scanStarted = nil
  357.         private.totalPaused = nil
  358.         private.curScan = nil
  359.         private.storeTime = nil
  360.         private.curPages = nil
  361.         private.sentQuery = nil
  362.         private.isScanning = false
  363.         private.UpdateScanProgress(false, nil, nil, nil, nil, nil, oldquery)
  364.     end
  365. end
  366.  
  367. function lib.PopScan()
  368.     if private.scanStack and #private.scanStack > 0 then
  369.         local now, pauseTime = GetTime()
  370.         private.scanStartTime,
  371.         private.sentQuery,
  372.         private.curQuery,
  373.         private.curPages,
  374.         private.curScan,
  375.         private.scanStarted,
  376.         private.totalPaused,
  377.         pauseTime,
  378.         private.storeTime = unpack(private.scanStack[1])
  379.         tremove(private.scanStack, 1)
  380.  
  381.         private.scanStarted = private.scanStarted or now -- scans created by StartPushedScan measure start time from when first popped
  382.         local elapsed = pauseTime and (now - pauseTime) or 0
  383.         if elapsed > 300 then
  384.             -- 5 minutes old
  385.             --_print("Paused scan is older than 5 minutes, commiting what we have and aborting")
  386.             if (_G.nLog) then
  387.                 _G.nLog.AddMessage("Auctioneer", "Scan", _G.N_WARNING, ("Scan %d Too Old, committing what we have and aborting"):format(private.curQuery.qryinfo.id))
  388.             end
  389.             private.Commit(true, false, false) -- Scan terminated early.
  390.             return
  391.         end
  392.  
  393.         private.totalPaused = private.totalPaused + elapsed
  394.         if (_G.nLog) then
  395.             _G.nLog.AddMessage("Auctioneer", "Scan", _G.N_INFO, ("Scan %d Resumed, next page to scan is %d"):format(private.curQuery.qryinfo.id, private.curQuery.qryinfo.page+1))
  396.         end
  397.         --_print(("Resuming paused scan at page {{%d}}..."):format(private.curQuery.qryinfo.page+1))
  398.         private.isScanning = true
  399.         private.sentQuery = false
  400.         private.ScanPage(private.curQuery.qryinfo.page+1)
  401.         private.UpdateScanProgress(true, nil, nil, nil, nil, nil, private.curQuery)
  402.     end
  403. end
  404.  
  405. --[[This function is now in core API]]
  406. function lib.ProgressBars(name, value, show, text, options)
  407.     _G.AucAdvanced.API.ProgressBars(name, value, show, text, options)
  408. end
  409.  
  410. function lib.StartScan(name, minUseLevel, maxUseLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex, GetAll, exactMatch, options)
  411.     if _G.AuctionFrame and _G.AuctionFrame:IsVisible() then
  412.         if private.isPaused then
  413.             _G.message("Scanning is currently paused")
  414.             return
  415.         end
  416.         if private.isScanning then
  417.             _G.message("Scan is currently in progress")
  418.             return
  419.         end
  420.         local CanQuery, CanQueryAll = CanSendAuctionQuery()
  421.         if GetAll then
  422.             local now = time()
  423.             if not CanQueryAll then
  424.                 local text = "You cannot do a GetAll scan at this time."
  425.                 if private.LastGetAll then
  426.                     local timeleft = 900 - (now - private.LastGetAll) -- 900 = 15 * 60 sec = 15 min
  427.                     if timeleft > 0 then
  428.                         local minleft = floor(timeleft / 60)
  429.                         local secleft = timeleft - minleft * 60
  430.                         text = text.." You must wait "..minleft..":"..secleft.." until you can scan again."
  431.                     end
  432.                 end
  433.                 _G.message(text)
  434.                 return
  435.             end
  436.  
  437.             _G.AucAdvanced.API.BlockUpdate(true, false)
  438.             BrowseSearchButton:Hide()
  439.             lib.ProgressBars("GetAllProgressBar", 0, true, "Auctioneer: Scanning")
  440.             private.isGetAll = true -- indicates that certain functions must take special action, and that the above changes need to be undone
  441.  
  442.             private.LastGetAll = now
  443.         else
  444.             if not CanQuery then
  445.                 private.queueScan = {
  446.                     name, minUseLevel, maxUseLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex, GetAll, exactMatch, options
  447.                 }
  448.                 private.queueScanParams = 11 -- must match the number of entries we put into the table, including nils. Used when unpacking
  449.                 return
  450.             end
  451.         end
  452.  
  453.         local NoSummary
  454.         if type(options) == "table" then
  455.             -- only 1 option so far
  456.             if options.NoSummary or options.nosummary then
  457.                 NoSummary = true
  458.             end
  459.         end
  460.  
  461.         if private.curQuery then
  462.             private.Commit(true, false, false) -- sets private.curQuery to nil and commits prior cancelled query
  463.         end
  464.  
  465.         private.isScanning = true
  466.         local startPage = 0
  467.  
  468.         lib.SetAuctioneerQuery() -- flag the following query as coming from Auctioneer
  469.         SortAuctionClearSort("list")
  470.         QueryAuctionItems(name or "", minUseLevel or "", maxUseLevel or "",
  471.                 invTypeIndex, classIndex, subclassIndex, startPage, isUsable, qualityIndex, GetAll, exactMatch)
  472.         if not private.curQuery then
  473.             -- private.curQuery will have been set if QueryAuctionItems succeeded
  474.             -- this should never fail? we checked CanSendAuctionQuery() earlier
  475.             _G.message("Scan failed: unable to send query")
  476.             if private.isGetAll then
  477.                 lib.ProgressBars("GetAllProgressBar", nil, false)
  478.                 BrowseSearchButton:Show()
  479.                 _G.AucAdvanced.API.BlockUpdate(false)
  480.                 private.isGetAll = nil
  481.             end
  482.             return
  483.         end
  484.         _G.AuctionFrameBrowse.page = startPage
  485.         private.curQuery.qryinfo.nosummary = NoSummary
  486.         if GetAll then
  487.             private.curQuery.qryinfo.getall = true
  488.         end
  489.  
  490.         --Show the progress indicator
  491.         private.UpdateScanProgress(true, nil, nil, nil, nil, nil, private.curQuery)
  492.     else
  493.         _G.message("Steady on; You'll need to talk to the auctioneer first!")
  494.     end
  495. end
  496.  
  497. function lib.IsScanning()
  498.     return private.isScanning or (private.queueScan ~= nil)
  499. end
  500.  
  501. function lib.IsPaused()
  502.     return private.isPaused
  503. end
  504.  
  505. function private.Unpack(item, storage)
  506.     if not storage then storage = {} end
  507.     storage.link = item[Const.LINK]
  508.     storage.useLevel = item[Const.ULEVEL]
  509.     storage.itemLevel = item[Const.ILEVEL]
  510.     storage.itemType = item[Const.ITYPE]
  511.     storage.subType = item[Const.ISUB]
  512.     storage.equipPos = item[Const.IEQUIP]
  513.     storage.price = item[Const.PRICE]
  514.     storage.timeLeft = item[Const.TLEFT]
  515.     storage.seenTime = item[Const.TIME]
  516.     storage.itemName = item[Const.NAME]
  517.     storage.stackSize = item[Const.COUNT]
  518.     storage.quality = item[Const.QUALITY]
  519.     storage.canUse = item[Const.CANUSE]
  520.     storage.minBid = item[Const.MINBID]
  521.     storage.curBid = item[Const.CURBID]
  522.     storage.increment = item[Const.MININC]
  523.     storage.sellerName = item[Const.SELLER]
  524.     storage.buyoutPrice = item[Const.BUYOUT]
  525.     storage.amBidder = item[Const.AMHIGH]
  526.     storage.dataFlag = item[Const.FLAG]
  527.     storage.itemId = item[Const.ITEMID]
  528.     storage.bonusString = item[Const.BONUSES]
  529.     storage.itemSuffix = item[Const.SUFFIX]
  530.     storage.itemFactor = item[Const.FACTOR]
  531.     storage.itemEnchant = item[Const.ENCHANT]
  532.     storage.itemSeed = item[Const.SEED]
  533.  
  534.     return storage
  535. end
  536. -- Define a public accessor for the above upack function
  537. lib.UnpackImageItem = private.Unpack
  538.  
  539. --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.
  540. --The second parameter will be a number that is the max number of items in the scan.
  541. --The third parameter is the current progress of the scan.
  542. function private.UpdateScanProgress(state, totalAuctions, scannedAuctions, elapsedTime, page, maxPages, query)
  543.     if (lib.IsScanning() or (state == false)) then
  544.         if (_G.nLog) then
  545.             _G.nLog.AddMessage("Auctioneer", "Scan", _G.N_INFO, "UpdateScanProgress Called", state)
  546.         end
  547.         local scanCount = 0
  548.         if (private.scanStack) then scanCount=#private.scanStack end
  549.         _G.AucAdvanced.SendProcessorMessage("scanprogress", state, totalAuctions, scannedAuctions, elapsedTime, page, maxPages, query, scanCount)
  550.     end
  551. end
  552.  
  553. function private.IsIdentical(focus, compare)
  554.     for i = 1, Const.SELLER do
  555.         if (i ~= Const.TIME and i ~= Const.CANUSE and focus[i] ~= compare[i]) then
  556.             return false
  557.         end
  558.     end
  559.     return true
  560. end
  561.  
  562. function private.IsSameItem(focus, compare, onlyDirt)
  563.     if onlyDirt then
  564.         local flag = focus[Const.FLAG]
  565.         if not flag or bitand(flag, Const.FLAG_DIRTY) == 0 then
  566.             return false
  567.         end
  568.     end
  569.     if (focus[Const.LINK] ~= compare[Const.LINK]) then return false end
  570.     if (focus[Const.COUNT] ~= compare[Const.COUNT]) then return false end
  571.     if (focus[Const.MINBID] ~= compare[Const.MINBID]) then return false end
  572.     if (focus[Const.BUYOUT] ~= compare[Const.BUYOUT]) then return false end
  573.     if (focus[Const.CURBID] > compare[Const.CURBID]) then return false end
  574.     return true
  575. end
  576.  
  577. function lib.FindItem(item, image, lut)
  578.     local focus
  579.     -- If we have a lookuptable, then we don't need to scan the whole lot
  580.     if (lut) then
  581.         local list = lut[item[Const.LINK]]
  582.         if not list then return false
  583.         elseif type(list) == "number" then
  584.             if (private.IsSameItem(image[list], item, true)) then return list end
  585.         else
  586.             local pos
  587.             for i=1, #list do
  588.                 pos = list[i]
  589.                 if (private.IsSameItem(image[pos], item, true)) then return pos end
  590.             end
  591.         end
  592.     else
  593.         -- We need to scan the whole thing cause there's no lookup table
  594.         for i = 1, #image do
  595.             if (private.IsSameItem(image[i], item, true)) then return i end
  596.         end
  597.     end
  598. end
  599.  
  600.  
  601. local function processBeginEndStats(processors, operation, querySizeInfo, TempcurScanStats)
  602.     if (not processors) then return end
  603.     local po = processors[operation]
  604.     if (po) then
  605.         for i=1,#po do
  606.             local x = po[i]
  607.             local f = x.Func
  608.             local pOK, errormsg = pcall(f, operation, querySizeInfo, TempcurScanStats)
  609.             if (not pOK) then
  610.                 collectgarbage() -- ### trial to see if this helps recover from Memory Allocation Errors
  611.                 local text = ("Error trapped for ScanProcessor '%s' in module %s:\n%s"):format(operation, x.Name, errormsg)
  612.                 if (_G.nLog) then _G.nLog.AddMessage("Auctioneer", "Scan", _G.N_ERROR, "ScanProcessor Error", text) end
  613.                 geterrorhandler()(text)
  614.             end
  615.         end
  616.     end
  617.     return true
  618. end
  619.  
  620. local statItem = { }
  621. local statItemOld = { }
  622.  
  623. local function processStats(processors, operation, curItem, oldItem)
  624.     local filtered = false
  625.     if (not processors) then return end
  626.     if (curItem) then private.Unpack(curItem, statItem) end
  627.     if (oldItem) then private.Unpack(oldItem, statItemOld) end
  628.     if (operation == "create" and processors.Filter) then
  629.         --[[
  630.             Filtering out happens here so we only have to do Unpack once.
  631.             Only filter on create because once its in the system, dropping it can give the wrong impression to other mods.
  632.             (it could think it was sold, for instance)
  633.         ]]
  634.         local pf = processors.Filter
  635.         for i=1,#pf do
  636.             local x = pf[i]
  637.             local f = x.Func
  638.             local pOK, result=pcall(f, operation, statItem)
  639.             if (pOK) then
  640.                 if (result) then
  641.                     curItem[Const.FLAG] = bitor(curItem[Const.FLAG] or 0, Const.FLAG_FILTER)
  642.                     filtered = true
  643.                     break
  644.                 end
  645.             else
  646.                 collectgarbage() -- ### trial to see if this helps recover from Memory Allocation Errors
  647.                 local text = ("Error trapped for AuctionFilter in module %s:\n%s"):format(x.Name, result)
  648.                 if (_G.nLog) then _G.nLog.AddMessage("Auctioneer", "Scan", _G.N_ERROR, "AuctionFilter Error", text) end
  649.                 geterrorhandler()(text)
  650.             end
  651.         end
  652.     elseif curItem and bitand(curItem[Const.FLAG] or 0, Const.FLAG_FILTER) == Const.FLAG_FILTER then
  653.         -- This item is a filtered item
  654.         filtered = true
  655.     end
  656.     if filtered then
  657.         return false
  658.     end
  659.  
  660.     local po = processors[operation]
  661.     if (po) then
  662.         for i=1,#po do
  663.             local x = po[i]
  664.             local f = x.Func
  665.             local pOK, errormsg = pcall(f, operation, statItem, oldItem and statItemOld or nil)
  666.             if (not pOK) then
  667.                 collectgarbage() -- ### trial to see if this helps recover from Memory Allocation Errors
  668.                 local text = ("Error trapped for ScanProcessor '%s' in module %s:\n%s"):format(operation, x.Name, errormsg)
  669.                 if (_G.nLog) then _G.nLog.AddMessage("Auctioneer", "Scan", _G.N_ERROR, "ScanProcessor Error", text) end
  670.                 geterrorhandler()(text)
  671.             end
  672.         end
  673.     end
  674.     return true
  675. end
  676.  
  677. function private.IsInQuery(curQuery, data)
  678.     if  (not curQuery.class or curQuery.class == data[Const.ITYPE])
  679.             and (not curQuery.subclass or (curQuery.subclass == data[Const.ISUB]))
  680.             and (not curQuery.minUseLevel or (data[Const.ULEVEL] >= curQuery.minUseLevel))
  681.             and (not curQuery.maxUseLevel or (data[Const.ULEVEL] <= curQuery.maxUseLevel))
  682.             and (not curQuery.isUsable or (private.CanUse(data[Const.LINK])))
  683.             and (not curQuery.invType or (EquipCodeToInvIndex[data[Const.IEQUIP]] == curQuery.invType)) -- must convert iEquip code to invTypeIndex for comparison
  684.             and (not curQuery.quality or (data[Const.QUALITY] >= curQuery.quality))
  685.             then
  686.         if curQuery.name then
  687.             local dataname = data[Const.NAME]:lower() -- case insensitive; curQuery.name is already lowercased
  688.             if dataname ~= curQuery.name and (curQuery.exactMatch or not dataname:find(curQuery.name, 1, true)) then
  689.                 return false
  690.             end
  691.         end
  692.         return true
  693.     end
  694.     return false
  695. end
  696.  
  697. function lib.GetScanStats(serverKey)
  698.     local scandata = private.GetScanData(serverKey)
  699.     if scandata then
  700.         return scandata.scanstats
  701.     end
  702. end
  703.  
  704. function lib.GetImageCopy(serverKey)
  705.     -- Create a fully independent copy of the image - intended for use by coroutines
  706.     local scandata = private.GetScanData(serverKey)
  707.     if scandata then
  708.         local image = scandata.image
  709.         local size = Const.LASTENTRY
  710.         local copy = {}
  711.         for i = 1, #image do
  712.             tinsert(copy, {unpack(image[i], 1, size)})
  713.         end
  714.         return copy
  715.     end
  716. end
  717.  
  718. function lib.GetImageSize(serverKey)
  719.     local scandata = private.GetScanData(serverKey)
  720.     if scandata then
  721.         return #scandata.image
  722.     end
  723. end
  724.  
  725. function lib.GetImageItem(index, serverKey, reserved)
  726.     -- reserved flag for possible future expansion
  727.     local scandata = private.GetScanData(serverKey)
  728.     if scandata then
  729.         local item = scandata.image[index]
  730.         if item then return {unpack(item, 1, Const.LASTENTRY)} end
  731.     end
  732. end
  733.  
  734.  
  735. private.scandataIndex = {}
  736. private.prevQuery = {}
  737. -- private.queryResults is nil initially
  738. -- private.prevQueryServerKey is nil initially
  739.  
  740. function private.clearImageCaches(event, scanstats)
  741.     if event == "factionselect" then
  742.         -- if cache for home serverKey exists at this point it is a weak table - just dump the cache and let it rebuild
  743.         private.scandataIndex[Resources.ServerKeyHome] = nil
  744.     else
  745.         local serverKey = scanstats and scanstats.serverKey
  746.         if serverKey then
  747.             local cache = private.scandataIndex[serverKey]
  748.             if cache then
  749.                 wipe(cache)
  750.             end
  751.         else -- no serverKey provided: affects multiple serverKeys (or unknown source), dump all caches
  752.             wipe(private.scandataIndex)
  753.         end
  754.     end
  755.  
  756.     private.prevQueryServerKey = nil
  757.     private.queryResults = nil -- not required but frees some memory
  758. end
  759.  
  760. local weaktablemeta = {__mode="kv"}
  761. function private.SubImageCache(itemId, serverKey)
  762.     local indexResults = private.scandataIndex[serverKey]
  763.     if not indexResults then
  764.         if not _G.AucAdvanced.SplitServerKey(serverKey) then return end -- valid serverKey format?
  765.         indexResults = {}
  766.         if serverKey ~= Resources.ServerKeyHome and serverKey ~= Resources.ServerKeyNeutral then
  767.             -- use weak tables for other serverKeys
  768.             indexResults = setmetatable(indexResults, weaktablemeta)
  769.         end
  770.         private.scandataIndex[serverKey] = indexResults
  771.     end
  772.  
  773.     local itemResults = indexResults[itemId]
  774.     if not itemResults then
  775.         local scandata = private.GetScanData(serverKey)
  776.         if not scandata then return end
  777.         itemResults = {}
  778.         for pos, data in ipairs(scandata.image) do
  779.             if data[Const.ITEMID] == itemId then
  780.                 tinsert(itemResults, data)
  781.             end
  782.         end
  783.         indexResults[itemId] = itemResults
  784.     end
  785.  
  786.     return itemResults
  787. end
  788.  
  789. function lib.QueryImage(query, serverKey, reserved, ...)
  790.     serverKey = serverKey or GetFaction()
  791.     local prevQuery = private.prevQuery
  792.     local queryResults = private.queryResults
  793.  
  794.     -- is this the same query as last time?
  795.     if serverKey == private.prevQueryServerKey then
  796.         local samequery = true
  797.         for k,v in pairs(prevQuery) do
  798.             if v ~= query[k] then
  799.                 samequery = false
  800.                 break
  801.             end
  802.         end
  803.         if samequery then
  804.             for k,v in pairs(query) do
  805.                 if v ~= prevQuery[k] then
  806.                     samequery = false
  807.                     break
  808.                 end
  809.             end
  810.             if samequery then
  811.                 return queryResults
  812.             end
  813.         end
  814.     end
  815.  
  816.     -- reset results and save a copy of query
  817.     queryResults = {} -- cannot use wipe; needs to be a new table here {ADV-534}
  818.     private.queryResults = queryResults
  819.     wipe(prevQuery)
  820.     for k, v in pairs(query) do prevQuery[k] = v end
  821.     private.prevQueryServerKey = serverKey
  822.  
  823.     -- get image to search - may be the whole snapshot or a subset
  824.     local itemId = tonumber(query.itemId)
  825.     local stringSpeciesID
  826.     if query.speciesID then
  827.         -- looking for a battlepet
  828.         -- we will need to split the link for each data item, resulting in a _string containing the speciesID_
  829.         -- make sure the test value is also a string
  830.         stringSpeciesID = tostring(query.speciesID)
  831.         -- also, all battlepets have the same itemId
  832.         if not itemId then
  833.             itemId = 82800
  834.         elseif itemId ~= 82800 then
  835.             -- wrong itemId! return empty results table
  836.             return queryResults
  837.         end
  838.     end
  839.     local saneQueryLink
  840.     if query.link then
  841.         saneQueryLink = SanitizeLink(query.link)
  842.         if not itemId then
  843.             -- it should be more efficient to extract itemId from the link
  844.             -- so we can use SubImageCache
  845.             local header, id = strsplit(":", saneQueryLink)
  846.             itemId = tonumber(id)
  847.         end
  848.     end
  849.     local image
  850.     if itemId then
  851.         image = private.SubImageCache(itemId, serverKey)
  852.     else
  853.         local scandata = private.GetScanData(serverKey)
  854.         if scandata then
  855.             image = scandata.image
  856.         end
  857.     end
  858.     if not image then return queryResults end -- return empty results table
  859.  
  860.     local lowerName
  861.     if query.name then
  862.         lowerName = query.name:lower()
  863.     end
  864.  
  865.     -- scan image to build a table of auctions that match query
  866.     local ptr, finish = 1, #image
  867.     while ptr <= finish do
  868.         repeat
  869.             local data = image[ptr]
  870.             ptr = ptr + 1
  871.             if not data then break end
  872.             if bitand(data[Const.FLAG] or 0, Const.FLAG_UNSEEN) == Const.FLAG_UNSEEN then break end
  873.             if query.filter and query.filter(data, ...) then break end
  874.             if saneQueryLink and data[Const.LINK] ~= saneQueryLink then break end
  875.             if query.suffix and data[Const.SUFFIX] ~= query.suffix then break end
  876.             if query.factor and data[Const.FACTOR] ~= query.factor then break end
  877.             if query.minUseLevel and data[Const.ULEVEL] < query.minUseLevel then break end
  878.             if query.maxUseLevel and data[Const.ULEVEL] > query.maxUseLevel then break end
  879.             if query.minItemLevel and data[Const.ILEVEL] < query.minItemLevel then break end
  880.             if query.maxItemLevel and data[Const.ILEVEL] > query.maxItemLevel then break end
  881.             if query.class and data[Const.ITYPE] ~= query.class then break end
  882.             if query.subclass and data[Const.ISUB] ~= query.subclass then break end
  883.             if query.quality and data[Const.QUALITY] ~= query.quality then break end
  884.             if query.invType and data[Const.IEQUIP] ~= query.invType then break end
  885.             if query.seller and data[Const.SELLER] ~= query.seller then break end
  886.             if lowerName then
  887.                 local name = data[Const.NAME]
  888.                 if not (name and name:lower():find(lowerName, 1, true)) then break end
  889.             end
  890.             if stringSpeciesID then
  891.                 local _, id = strsplit(":", data[Const.LINK])
  892.                 if id ~= stringSpeciesID then
  893.                     break
  894.                 end
  895.             end
  896.  
  897.             local stack = data[Const.COUNT]
  898.             local nextBid = data[Const.PRICE]
  899.             local buyout = data[Const.BUYOUT]
  900.             if query.perItem and stack > 1 then
  901.                 nextBid = ceil(nextBid / stack)
  902.                 buyout = ceil(buyout / stack)
  903.             end
  904.             if query.minStack and stack < query.minStack then break end
  905.             if query.maxStack and stack > query.maxStack then break end
  906.             if query.minBid and nextBid < query.minBid then break end
  907.             if query.maxBid and nextBid > query.maxBid then break end
  908.             if query.minBuyout and buyout < query.minBuyout then break end
  909.             if query.maxBuyout and buyout > query.maxBuyout then break end
  910.  
  911.             -- If we're still here, then we've got a winner
  912.             tinsert(queryResults, data)
  913.         until true
  914.     end
  915.  
  916.     return queryResults
  917. end
  918.  
  919.  
  920. private.CommitQueue = {}
  921.  
  922. local CommitRunning = false
  923. local Commitfunction = function()
  924.     local commitStarted = GetTime()
  925.     --local totalProcessingTime = 0 -- temp disabled, going to take some work to thread this back in with the broken GetTime / time changes
  926.  
  927.     -- coroutine speed limiter using debugprofilestop
  928.     -- time in milliseconds: 1000/FPS * 0.8 (80% rough adjustment to allow for other stuff happening during the frame)
  929.     local processingTime = 800 / get("scancommit.targetFPS")
  930.     local debugprofilestop = debugprofilestop
  931.     local nextPause -- gets set before each processing loop, and after each yield within the loop
  932.     -- backup timer, in case debugprofilestop fails - can occur under (currently unknown) circumstances - only used in the merge and cleanup loops {ADV-637}
  933.     local time = time
  934.     local lastTime
  935.  
  936.     local inscount, delcount = 0, 0
  937.     if #private.CommitQueue == 0 then CommitRunning = false return end
  938.     CommitRunning = true
  939.  
  940.     --grab the first item in the commit queue, and bump everything else down
  941.     local TempcurCommit = tremove(private.CommitQueue)
  942.     -- setup various locals for later use
  943.     local TempcurScan = TempcurCommit.Scan
  944.     local TempcurQuery = TempcurCommit.Query
  945.  
  946.     local wasIncomplete = TempcurCommit.wasIncomplete
  947.     local wasEarlyTerm = TempcurCommit.wasEarlyTerm
  948.     local wasEndPagesOnly = TempcurCommit.wasEndPagesOnly
  949.  
  950.     local wasGetAll = TempcurCommit.wasGetAll
  951.     local scanStarted = TempcurCommit.scanStarted
  952.     local scanStartTime = TempcurCommit.scanStartTime
  953.     local totalPaused = TempcurCommit.totalPaused
  954.     local scanCommitTime = TempcurCommit.scanCommitTime
  955.     local scanStoreTime = scanCommitTime - scanStarted - totalPaused
  956.     local storeTime = TempcurCommit.storeTime
  957.     local wasOnePage = wasGetAll or (TempcurQuery.qryinfo.page == 0) -- retrieved all records in single pull (only one page scanned or was GetAll)
  958.     local wasUnrestricted = not (TempcurQuery.class or TempcurQuery.subclass or TempcurQuery.minUseLevel
  959.         or TempcurQuery.name or TempcurQuery.isUsable or TempcurQuery.invType or TempcurQuery.quality) -- no restrictions, potentially a full scan
  960.  
  961.     -- ### temp fix, until we figure out what isUsable flag is now doing
  962.     if TempcurQuery.isUsable then
  963.         wasIncomplete = true -- always treat as incomplete
  964.     end
  965.  
  966.  
  967.     local serverKey = TempcurQuery.qryinfo.serverKey or GetFaction()
  968.     local scandata = private.GetScanData(serverKey)
  969.     assert(scandata, "Critical error: scandata does not exist for serverKey "..serverKey)
  970.     local now = time()
  971.     if get("scancommit.progressbar") then
  972.         lib.ProgressBars("CommitProgressBar", 0, true)
  973.     end
  974.     local hadGetError = false
  975.     local oldCount = #scandata.image
  976.     local scanCount = #TempcurScan
  977.  
  978.     local progresscounter = 0
  979.     local progresstotal = 3*oldCount + 7*scanCount
  980.     if progresstotal == 0 then progresstotal = 1 end -- dummy value to avoid potential div0. ### this needs a better solution
  981.  
  982.     local filterOldCount, filterNewCount, updateCount, sameCount, newCount, updateRecoveredCount, sameRecoveredCount, missedCount = 0,0,0,0, 0,0,0,0
  983.     local unresolvedCount = 0
  984.     local dirtyCount, undirtyCount, expiredCount, corruptCount, matchedCount = 0, 0, 0, 0, 0
  985.     local filterDeleteCount, earlyDeleteCount, expiredDeleteCount, corruptDeleteCount = 0, 0, 0, 0
  986.  
  987.     do --[[ *** Stage 1 : pre-process the new scan ]]--
  988.         lib.ProgressBars("CommitProgressBar", 100*progresscounter/progresstotal, true, "Auctioneer: Processing Stage 1")
  989.         coroutine.yield() -- yield here to allow the bar to display, and help the frame rate a little
  990.         local breakinterval, timeadjust
  991.         local stage1throttle = get("core.scan.stage1throttle")
  992.         if stage1throttle >= Const.ALEVEL_HI then
  993.             breakinterval, timeadjust = 500, 0.1
  994.         elseif stage1throttle >= Const.ALEVEL_MED then
  995.             breakinterval, timeadjust = 2000, 0.4
  996.         elseif stage1throttle >= Const.ALEVEL_LOW then
  997.             breakinterval, timeadjust = 5000, 1
  998.         else -- OFF
  999.             breakinterval, timeadjust = nil, 1
  1000.         end
  1001.         local breakcount = 0
  1002.         local doYield = false
  1003.         local battlepetYield = true
  1004.         local firstfailureYield = true
  1005.         nextPause = debugprofilestop() + processingTime * timeadjust
  1006.         -- Stage 1 First Pass
  1007.         private.InitItemInfoCache()
  1008.         for pos = scanCount, 1, -1 do
  1009.             if breakinterval then
  1010.                 breakcount = (breakcount + 1) % breakinterval
  1011.                 if breakcount == 0 then
  1012.                     doYield = true
  1013.                 end
  1014.             end
  1015.             if doYield or debugprofilestop() > nextPause then
  1016.                 lib.ProgressBars("CommitProgressBar", 100*progresscounter/progresstotal, true, "Auctioneer: Processing Stage 1")
  1017.                 coroutine.yield()
  1018.                 nextPause = debugprofilestop() + processingTime * timeadjust
  1019.                 doYield = false
  1020.             end
  1021.             local success, reason, linkType = private.GetAuctionItemFillIn(TempcurScan[pos], true)
  1022.             progresscounter = progresscounter + 1
  1023.             if breakinterval then
  1024.                 if linkType == "battlepet" and battlepetYield then
  1025.                     -- experimental: yield on finding first battlepet
  1026.                     -- first time battlepet API is used, it appears to trigger a small amount of lag
  1027.                     doYield = true
  1028.                     battlepetYield = false
  1029.                 elseif not success and firstfailureYield then
  1030.                     -- experimental: yield after first failure detected
  1031.                     -- todo: fiddle with this, perhaps yielding every X failures, see if it appears to help
  1032.                     doYield = true
  1033.                     firstfailureYield = false
  1034.                 end
  1035.             end
  1036.         end
  1037.  
  1038.         -- Stage 1 Second Pass
  1039.         breakcount = 0
  1040.         doYield = false
  1041.         private.InitItemInfoCache()
  1042.         for pos = scanCount, 1, -1 do
  1043.             if breakinterval then
  1044.                 breakcount = (breakcount + 1) % breakinterval
  1045.                 if breakcount == 0 then
  1046.                     doYield = true
  1047.                 end
  1048.             end
  1049.             if doYield or debugprofilestop() > nextPause then
  1050.                 lib.ProgressBars("CommitProgressBar", 100*progresscounter/progresstotal, true, "Auctioneer: Processing Stage 1")
  1051.                 coroutine.yield()
  1052.                 nextPause = debugprofilestop() + processingTime * timeadjust
  1053.                 doYield = false
  1054.             end
  1055.  
  1056.             local entryUnusable = false
  1057.             local data = TempcurScan[pos]
  1058.             local success, reason, linkType = private.GetAuctionItemFillIn(data, true)
  1059.             progresscounter = progresscounter + 1
  1060.  
  1061.             if not success then
  1062.                 entryUnusable = true
  1063.             else
  1064.                 -- full test
  1065.                 for i = 1, Const.LASTENTRY, 1 do
  1066.                     if not (data[i] or ItemDataAllowedNil[i]) then
  1067.                         entryUnusable = true
  1068.                         break
  1069.                     end
  1070.                 end
  1071.             end
  1072.  
  1073.  
  1074.             if entryUnusable then
  1075.                 if _G.nLog then
  1076.                     -- Yes this is a mess.  However, it gives enough information to let us resolve problems in the future when blizzard breaks in a new way.
  1077.                     _G.nLog.AddMessage("Auctioneer", "Scan", _G.N_WARNING, "Incomplete Auction Seen",
  1078.                         (("%s%s%s%s%s%s"):format(
  1079.                         "Page %d, Index %d -- %s\n %s -- %d of %s sold by %s\n",
  1080.                         "Level %d, Quality %s, Item Level %s\n",
  1081.                         "Item Type %s, Sub Type %s, Equipment Position %s\n",
  1082.                         "Price %s, Bid %s, NextBid %s, MinInc %s, Buyout %s\n Time Left %s, Time %s\n",
  1083.                         "High Bidder %s  Can Use: %s  Bonuses %s  Item ID %s  Suffix %s  Factor %s  Enchant %s  Seed %s\n",
  1084.                         "Deprecated2: %s")):format(
  1085.                         data.PAGE, data.PAGEINDEX, "too broken, can not use at all",
  1086.                         data[Const.LINK] or "(nil)", data[Const.COUNT] or -1, data[Const.NAME] or "(nil)", data[Const.SELLER] or "(UNKNOWN)",
  1087.                         data[Const.ULEVEL] or -1, data[Const.QUALITY] or -1, data[Const.ILEVEL] or -1,data[Const.ITYPE] or "(UNKNOWN)", data[Const.ISUB] or "(UNKNOWN)", data[Const.IEQUIP] or '(n/a)',
  1088.                         data[Const.PRICE] or -1, data[Const.CURBID] or -1, data[Const.MINBID] or -1, data[Const.MININC] or -1, data[Const.BUYOUT] or -1,
  1089.                         data[Const.TLEFT] or -1, data[Const.TIME] or "(nil)", data[Const.AMHIGH] and "Yes" or "No",
  1090.                         (data[Const.CANUSE]==false and "Yes") or (data[Const.CANUSE] and "No" or "(nil)"), data[Const.BONUSES] or '(nil)', data[Const.ITEMID] or '(nil)',
  1091.                         data[Const.SUFFIX] or '(nil)', data[Const.FACTOR] or '(nil)', data[Const.ENCHANT] or '(nil)', data[Const.SEED] or '(nil)', data[Const.DEP2] or '(nil)'))
  1092.                 end
  1093.                 tremove(TempcurScan, pos)
  1094.                 unresolvedCount = unresolvedCount + 1
  1095.                 progresscounter = progresscounter + 5 -- We just wiped the entry from the db, so other steps won't see it.
  1096.             end
  1097.         end
  1098.         private.ResetItemInfoCache() -- free up cache memory
  1099.         local tolerance = 0
  1100.         if scanCount > TOLERANCE_LOWERLIMIT then -- don't use tolerance for tiny scans
  1101.             tolerance = get("core.scan.unresolvedtolerance")
  1102.             if scanCount < TOLERANCE_TAPERLIMIT then -- taper tolerance for smaller scans
  1103.                 tolerance = ceil(tolerance * scanCount / TOLERANCE_TAPERLIMIT)
  1104.             end
  1105.         end
  1106.         if unresolvedCount > tolerance then
  1107.             hadGetError = true
  1108.             wasIncomplete = true
  1109.         end
  1110.  
  1111.     end --[[ of Stage 1 ]]--
  1112.  
  1113.     --[[ *** Stage 2 : Pre-process image table : Mark all matching auctions as DIRTY, and build a LookUpTable *** ]]--
  1114.     lib.ProgressBars("CommitProgressBar", 100*progresscounter/progresstotal, true, "Auctioneer: Processing Stage 2")
  1115.     coroutine.yield() -- yield to allow updated bar to display
  1116.  
  1117.     local lut = {}
  1118.  
  1119.     nextPause = debugprofilestop() + processingTime
  1120.     for pos, data in ipairs(scandata.image) do
  1121.         if debugprofilestop() > nextPause then
  1122.             lib.ProgressBars("CommitProgressBar", 100*progresscounter/progresstotal, true, "Auctioneer: Processing Stage 2")
  1123.             coroutine.yield()
  1124.             nextPause = debugprofilestop() + processingTime
  1125.         end
  1126.         local link = data[Const.LINK]
  1127.         local flags = data[Const.FLAG]
  1128.         progresscounter = progresscounter + 1
  1129.         if link and flags then
  1130.             if private.IsInQuery(TempcurQuery, data) then
  1131.                 matchedCount = matchedCount + 1
  1132.  
  1133.                 -- Test for expired
  1134.                 local auctionmaxtime = Const.AucMaxTimes[data[Const.TLEFT]] or 172800
  1135.                 if not data[Const.TIME] or (now - data[Const.TIME] > auctionmaxtime) then
  1136.                     data[Const.FLAG] = bitor(flags, Const.FLAG_EXPIRED)
  1137.                     expiredCount = expiredCount + 1
  1138.                 else
  1139.                     -- Mark dirty
  1140.                     data[Const.FLAG] = bitor(flags, Const.FLAG_DIRTY)
  1141.                     dirtyCount = dirtyCount+1
  1142.  
  1143.                     -- Build lookup table
  1144.                     local list = lut[link]
  1145.                     if (not list) then
  1146.                         lut[link] = pos
  1147.                     else
  1148.                         if (type(list) == "number") then
  1149.                             lut[link] = {}
  1150.                             tinsert(lut[link], list)
  1151.                         end
  1152.                         tinsert(lut[link], pos)
  1153.                     end
  1154.                 end
  1155.             else
  1156.                 -- Mark NOT dirty
  1157.                 data[Const.FLAG] = bitand(flags, bitnot(Const.FLAG_DIRTY))
  1158.             end
  1159.         else
  1160.             -- corrupt entry
  1161.             data[Const.FLAG] = bitor(flags or 0, Const.FLAG_CORRUPT)
  1162.             corruptCount = corruptCount + 1
  1163.         end
  1164.     end
  1165.  
  1166.  
  1167.     --[[ *** Stage 3 : Merge new scan into ScanData *** ]]
  1168.     lib.ProgressBars("CommitProgressBar", 100*progresscounter/progresstotal, true, "Auctioneer: Processing Stage 3")
  1169.     coroutine.yield()
  1170.  
  1171.     local processors = {}
  1172.     local modules = _G.AucAdvanced.GetAllModules("AuctionFilter", "Filter")
  1173.     for pos, engineLib in ipairs(modules) do
  1174.         if (not processors.Filter) then processors.Filter = {} end
  1175.         local x = {}
  1176.         x.Name = engineLib.GetName()
  1177.         x.Func = engineLib.AuctionFilter
  1178.         tinsert(processors.Filter, x)
  1179.     end
  1180.     modules = _G.AucAdvanced.GetAllModules("ScanProcessors")
  1181.     for pos, engineLib in ipairs(modules) do
  1182.         for op, func in pairs(engineLib.ScanProcessors) do
  1183.             if (not processors[op]) then processors[op] = {} end
  1184.             local x = {}
  1185.             x.Name = engineLib.GetName()
  1186.             x.Func = func
  1187.             tinsert(processors[op], x)
  1188.         end
  1189.     end
  1190.  
  1191.     local printSummary, scanSize = false, ""
  1192.     scanSize = TempcurQuery.qryinfo.scanSize
  1193.     if scanSize=="Full" then
  1194.         printSummary = get("scandata.summaryonfull");
  1195.     elseif scanSize=="Partial" then
  1196.         printSummary = get("scandata.summaryonpartial")
  1197.     else -- scanSize=="Micro"
  1198.         printSummary = get("scandata.summaryonmicro")
  1199.     end
  1200.     if (wasEndPagesOnly) then
  1201.         scanSize = "TailScan-"..scanSize
  1202.         printSummary = get("scandata.summaryonpartial") -- todo: do we want a separate "summary on end pages only" option?
  1203.     elseif (TempcurQuery.qryinfo.nosummary) then
  1204.         printSummary = false
  1205.         scanSize = "NoSum-"..scanSize
  1206.     end
  1207.  
  1208.     local querySizeInfo = { }
  1209.     querySizeInfo.wasIncomplete = wasIncomplete
  1210.     querySizeInfo.wasGetAll = wasGetAll
  1211.     querySizeInfo.scanStarted = scanStarted
  1212.     querySizeInfo.wasUnrestricted = wasUnrestricted
  1213.     querySizeInfo.wasEarlyTerm = wasEarlyTerm
  1214.     querySizeInfo.hadGetError = hadGetError
  1215.     querySizeInfo.wasEndPagesOnly = wasEndPagesOnly
  1216.     querySizeInfo.Query = TempcurCommit.Query
  1217.     querySizeInfo.matchCount = dirtyCount
  1218.     querySizeInfo.scanCount = scanCount
  1219.     querySizeInfo.printSummary = printSummary
  1220.     querySizeInfo.FallbackScanData = private.FallbackScanData
  1221.  
  1222.     local maskNotDirtyUnseen = bitnot(bitor(Const.FLAG_DIRTY, Const.FLAG_UNSEEN)) -- only calculate mask for clearing these flags once
  1223.     local messageCreate = private.FallbackScanData and "fallbackcreate" or "create"
  1224.  
  1225.     local garbageinterval
  1226.     local stage3garbage = get("core.scan.stage3garbage")
  1227.     if stage3garbage >= Const.ALEVEL_HI then
  1228.         garbageinterval = 1000
  1229.     elseif stage3garbage >= Const.ALEVEL_MED then
  1230.         garbageinterval = 5000
  1231.     elseif stage3garbage >= Const.ALEVEL_LOW then
  1232.         garbageinterval = 10000
  1233.     end
  1234.  
  1235.     processBeginEndStats(processors, "begin", querySizeInfo, nil)
  1236.  
  1237.     coroutine.yield()
  1238.     nextPause = debugprofilestop() + processingTime
  1239.     lastTime = time()
  1240.     for index, data in ipairs(TempcurScan) do
  1241.         local doYield = false
  1242.         if garbageinterval and index % garbageinterval == 0 then
  1243.             coroutine.yield() -- yield before and after collectgarbage to smooth things a little, as it tends to cause small freezes
  1244.             collectgarbage()
  1245.             doYield = true
  1246.         else
  1247.             local checkprofile = debugprofilestop()
  1248.             if checkprofile > nextPause then
  1249.                 checkprofile = (checkprofile - nextPause) / processingTime
  1250.                 if checkprofile > 2 then
  1251.                     -- double yield if last processing cycle took more than 2 * the permitted time
  1252.                     coroutine.yield()
  1253.                 end
  1254.                 if checkprofile > 4 then
  1255.                     -- triple yield if last processing cycle took more than 4 * the permitted time
  1256.                     coroutine.yield()
  1257.                 end
  1258.                 doYield = true
  1259.             elseif time() > lastTime then
  1260.                 doYield = true
  1261.             end
  1262.         end
  1263.         if doYield then
  1264.             lib.ProgressBars("CommitProgressBar", 100*progresscounter/progresstotal, true, "Auctioneer: Processing Stage 3")
  1265.             coroutine.yield()
  1266.             nextPause = debugprofilestop() + processingTime
  1267.             lastTime = time()
  1268.         end
  1269.         local itemPos = lib.FindItem(data, scandata.image, lut)
  1270.         progresscounter = progresscounter + 4
  1271.  
  1272.         if (itemPos) then
  1273.             local oldItem = scandata.image[itemPos]
  1274.             data[Const.FLAG] = bitand(oldItem[Const.FLAG], maskNotDirtyUnseen)
  1275.             undirtyCount = undirtyCount + 1
  1276.             if data[Const.SELLER] == "" then -- unknown seller name in new data; copy the old name if it exists
  1277.                 data[Const.SELLER] = oldItem[Const.SELLER]
  1278.             end
  1279.             if (bitand(data[Const.FLAG], Const.FLAG_FILTER)==Const.FLAG_FILTER) then
  1280.                 filterOldCount = filterOldCount + 1
  1281.             else
  1282.                 if not private.IsIdentical(oldItem, data) then
  1283.                     if processStats(processors, "update", data, oldItem) then
  1284.                         updateCount = updateCount + 1
  1285.                     end
  1286.                     if bitand(oldItem[Const.FLAG] or 0, Const.FLAG_UNSEEN) == Const.FLAG_UNSEEN then
  1287.                         updateRecoveredCount = updateRecoveredCount + 1
  1288.                     end
  1289.                 else
  1290.                     if processStats(processors, "leave", data) then
  1291.                         sameCount = sameCount + 1
  1292.                     end
  1293.                     if bitand(oldItem[Const.FLAG] or 0, Const.FLAG_UNSEEN) == Const.FLAG_UNSEEN then
  1294.                         sameRecoveredCount = sameRecoveredCount + 1
  1295.                     end
  1296.                 end
  1297.             end
  1298.             scandata.image[itemPos] = data
  1299.         else
  1300.             if (processStats(processors, messageCreate, data)) then
  1301.                 newCount = newCount + 1
  1302.             else -- processStats(processors, "create"...) filtered the auction: flag it
  1303.                 data[Const.FLAG] = bitor(data[Const.FLAG] or 0, Const.FLAG_FILTER)
  1304.                 filterNewCount = filterNewCount + 1
  1305.             end
  1306.             data[Const.FLAG] = bitand(data[Const.FLAG], maskNotDirtyUnseen)
  1307.             tinsert(scandata.image, data)
  1308.         end
  1309.     end
  1310.     lut = nil -- release some memory
  1311.  
  1312.  
  1313.     --[[ *** Stage 4 : Cleanup deleted auctions *** ]]
  1314.     lib.ProgressBars("CommitProgressBar", 100*progresscounter/progresstotal, true, "Auctioneer: Processing Stage 4")
  1315.     coroutine.yield() -- as above
  1316.     local progressstep = 1
  1317.     if #scandata.image > 0 then -- (avoid potential div0)
  1318.         -- #scandata.image is probably now larger than when we originally calculated progresstotal -- adjust the step size to compensate
  1319.         progressstep = (progresstotal - progresscounter) / #scandata.image
  1320.     end
  1321.     local loopBegin, loopEnd, loopDirection = #scandata.image, 1, -1
  1322.     local keepmodeImage
  1323.     --[[ Keep mode test
  1324.         Using tremove is extremely inefficient when there are a large number of deletions (particularly if the number of kept entries is also large).
  1325.         If we estimate this will be the case, switch to Keep mode, where we copy the entries we want to keep into a new image, which then replaces the old one.
  1326.  
  1327.         Test version 1: use keep mode if scan is complete, and if number of remaining dirty entries exceeds a fixed threshold
  1328.         Test version 2: Test for expired in Stage 2; expiredCount is now available to decide whether to use keep mode
  1329.     --]]
  1330.     if (expiredCount + corruptCount) > 1000 or (not wasIncomplete and (expiredCount + corruptCount + dirtyCount - undirtyCount) > 1000) then
  1331.         loopBegin, loopEnd, loopDirection = 1, #scandata.image, 1 -- process Keep mode in ascending order to keep the new table in the same order
  1332.         keepmodeImage = {} -- new image table; also acts as a flag for Keep mode
  1333.     end
  1334.     nextPause = debugprofilestop() + processingTime
  1335.     lastTime = time()
  1336.     for pos = loopBegin, loopEnd, loopDirection do
  1337.         if debugprofilestop() > nextPause or time() > lastTime then
  1338.             lib.ProgressBars("CommitProgressBar", 100*progresscounter/progresstotal, true, "Auctioneer: Processing Stage 4")
  1339.             coroutine.yield()
  1340.             nextPause = debugprofilestop() + processingTime
  1341.             lastTime = time()
  1342.         end
  1343.         local data = scandata.image[pos]
  1344.         local flags = data[Const.FLAG] -- *caution* if we modify data[Const.FLAG] below, be sure to set flags to the same value again
  1345.         local dodelete = false
  1346.         progresscounter = progresscounter + progressstep
  1347.         if bitand(flags, Const.FLAG_CORRUPT) ~= 0 then
  1348.             dodelete = true
  1349.             corruptDeleteCount = corruptDeleteCount + 1
  1350.         elseif bitand(flags, Const.FLAG_EXPIRED) ~= 0 then
  1351.             dodelete = true
  1352.             if bitand(flags, Const.FLAG_FILTER) ~= 0 then
  1353.                 filterDeleteCount = filterDeleteCount + 1
  1354.             else
  1355.                 expiredDeleteCount = expiredDeleteCount + 1
  1356.             end
  1357.         elseif bitand(flags, Const.FLAG_DIRTY) ~= 0  then
  1358.             if wasIncomplete then
  1359.                 missedCount = missedCount + 1
  1360.             elseif wasOnePage then
  1361.                 -- a *completed* one-page scan should not have missed any auctions
  1362.                 dodelete = true
  1363.                 if bitand(flags, Const.FLAG_FILTER) ~= 0 then
  1364.                     filterDeleteCount = filterDeleteCount + 1
  1365.                 else
  1366.                     earlyDeleteCount = earlyDeleteCount + 1
  1367.                 end
  1368.             else
  1369.                 if bitand(flags, Const.FLAG_UNSEEN) ~= 0 then
  1370.                     dodelete = true
  1371.                     if bitand(flags, Const.FLAG_FILTER) ~= 0 then
  1372.                         filterDeleteCount = filterDeleteCount + 1
  1373.                     else
  1374.                         earlyDeleteCount = earlyDeleteCount + 1
  1375.                     end
  1376.                 else
  1377.                     flags = bitor(bitand(flags, bitnot(Const.FLAG_DIRTY)), Const.FLAG_UNSEEN)
  1378.                     data[Const.FLAG] = flags
  1379.                     missedCount = missedCount + 1
  1380.                 end
  1381.             end
  1382.         end
  1383.         if dodelete then
  1384.             if bitand(flags, Const.FLAG_FILTER) == 0 then
  1385.                 processStats(processors, "delete", data)
  1386.             end
  1387.             if not keepmodeImage then
  1388.                 tremove(scandata.image, pos)
  1389.             end
  1390.         else -- keep
  1391.             if keepmodeImage then
  1392.                 tinsert(keepmodeImage, data)
  1393.             end
  1394.         end
  1395.     end
  1396.     if keepmodeImage then
  1397.         scandata.image = keepmodeImage
  1398.     end
  1399.  
  1400.  
  1401.     --[[ *** Stage 5 : Reports *** ]]
  1402.     lib.ProgressBars("CommitProgressBar", 100, true, "Auctioneer: Processing Finished")
  1403.     -- final yield to update GetTime for the stats
  1404.     -- (though we should be aware that whatever else happens during this yield gets added to our final time, we can't get an update of GetTime *without* yielding here!)
  1405.     coroutine.yield()
  1406.     local endTimeStamp = time()
  1407.     local scanTimeSecs, scanTimeMins, scanTimeHours = GetTime() - scanStarted - totalPaused, 0, 0
  1408.  
  1409.     -- optionally do a final collection here (as above, we want it surrounded by yields)
  1410.     if get("core.scan.stage5garbage") then
  1411.         collectgarbage()
  1412.         coroutine.yield()
  1413.     end
  1414.  
  1415.     if scanTimeSecs < 1 then
  1416.         scanTimeSecs = floor(scanTimeSecs*10)/10
  1417.     else
  1418.         scanTimeSecs = floor(scanTimeSecs)
  1419.         scanTimeMins = floor(scanTimeSecs / 60)
  1420.         scanTimeSecs =  mod(scanTimeSecs, 60)
  1421.         scanTimeHours = floor(scanTimeMins / 60)
  1422.         scanTimeMins = mod(scanTimeMins, 60)
  1423.     end
  1424.  
  1425.     local currentCount = #scandata.image
  1426.     if (updateCount + sameCount + newCount + filterNewCount + filterOldCount + unresolvedCount ~= scanCount) then
  1427.         if _G.nLog then
  1428.             _G.nLog.AddMessage("Auctioneer", "Scan", _G.N_WARNING, "Scan Count Discrepency Seen",
  1429.                 ("%d updated + %d same + %d new + %d filtered + %d unresolved != %d scanned"):format(updateCount, sameCount,
  1430.                     newCount, filterOldCount+filterNewCount, unresolvedCount, scanCount))
  1431.         end
  1432.     end
  1433.  
  1434.     -- image contains filtered items now.  Need to account for new entries that are flagged as filtered (not shown to stats modules)
  1435.     if (oldCount - earlyDeleteCount - expiredDeleteCount - corruptDeleteCount + newCount + filterNewCount - filterDeleteCount ~= currentCount) then
  1436.         if _G.nLog then
  1437.             _G.nLog.AddMessage("Auctioneer", "Scan", _G.N_WARNING, "Current Count Discrepency Seen",
  1438.                 ("%d - %d - %d + %d + %d - %d != %d"):format(oldCount, earlyDeleteCount, expiredDeleteCount,
  1439.                     newCount, filterNewCount, filterDeleteCount, currentCount))
  1440.         end
  1441.     end
  1442.  
  1443.     --Hides the end of scan summary if user is not interested
  1444.     if (_G.nLog or printSummary) then
  1445.  
  1446.         local scanTime = " "
  1447.         local summaryLine
  1448.         local summary
  1449.  
  1450.         if scanTimeHours ~= 0 then
  1451.             scanTime = scanTime..scanTimeHours.._TRANS("PSS_Hours")
  1452.         end
  1453.         if scanTimeMins ~= 0 then
  1454.             scanTime = scanTime..scanTimeMins.._TRANS("PSS_Minutes")
  1455.         end
  1456.         if scanTimeSecs ~= 0 or (scanTimeHours == 0 and scanTimeMins == 0) then
  1457.             scanTime = scanTime..scanTimeSecs.._TRANS("PSS_Seconds")
  1458.         end
  1459.  
  1460.         if (wasEndPagesOnly) then
  1461.             summaryLine = (_TRANS("PSS_TailScan")):format(scanCount, scanTime)
  1462.         elseif (wasEarlyTerm) then
  1463.             summaryLine = (_TRANS("PSS_Incomplete")):format(scanCount, scanTime) --Auctioneer finished scanning {{%d}} auctions over {{%s}} before being stopped
  1464.         elseif (hadGetError) then
  1465.             summaryLine = (_TRANS("PSS_GetError")):format(scanCount, scanTime) --Auctioneer finished scanning {{%d}} auctions over {{%s}} but was not able to retrieve some auctions
  1466.         elseif wasIncomplete then -- any other incomplete (unknown reason)
  1467.             summaryLine = (_TRANS("PSS_Incomplete")):format(scanCount, scanTime) --Auctioneer finished scanning {{%d}} auctions over {{%s}} before being stopped
  1468.         else
  1469.             summaryLine = (_TRANS("PSS_Complete")):format(scanCount, scanTime) --Auctioneer finished scanning {{%d}} auctions over {{%s}}
  1470.         end
  1471.         if (printSummary) then _print(summaryLine) end
  1472.         summary = summaryLine
  1473.  
  1474.         summaryLine = "  {{"..oldCount.."}} ".._TRANS("PSS_StartItems").."{{"..matchedCount.."}} ".._TRANS("PSS_MatchedItems").." {{"..currentCount.."}} ".._TRANS("PSS_AtEnd")
  1475.         if (printSummary) then _print(summaryLine) end
  1476.         summary = summary.."\n"..summaryLine
  1477.  
  1478.         if (sameCount > 0) then
  1479.             if (sameRecoveredCount > 0) then
  1480.                 summaryLine = "  {{"..sameCount.."}} ".._TRANS("PSS_Unchanged_Missed")..sameRecoveredCount.._TRANS("PSS_Missed")
  1481.             else
  1482.                 summaryLine = "  {{"..sameCount.."}} ".._TRANS("PSS_Unchanged_NoMissed")
  1483.             end
  1484.             if (printSummary) then _print(summaryLine) end
  1485.             summary = summary.."\n"..summaryLine
  1486.         end
  1487.         if (updateCount > 0) then
  1488.             if (updateRecoveredCount > 0) then
  1489.                 summaryLine = "  {{"..updateCount.."}} ".._TRANS("PSS_Updated_Missed")..updateRecoveredCount.._TRANS("PSS_Missed")
  1490.             else
  1491.                 summaryLine = "  {{"..updateCount.."}} ".._TRANS("PSS_Updated_NoMissed")
  1492.             end
  1493.             if (printSummary) then _print(summaryLine) end
  1494.             summary = summary.."\n"..summaryLine
  1495.         end
  1496.         if (newCount > 0) then
  1497.             summaryLine = "  {{"..newCount.."}} ".._TRANS("PSS_NewItems")
  1498.             if (printSummary) then _print(summaryLine) end
  1499.             summary = summary.."\n"..summaryLine
  1500.         end
  1501.         if (earlyDeleteCount+expiredDeleteCount > 0) then
  1502.             if expiredDeleteCount > 0 then
  1503.                 summaryLine = "  {{"..earlyDeleteCount+expiredDeleteCount.."}} ".._TRANS("PSS_Removed_Expired").." {{"..expiredDeleteCount.."}} ".._TRANS("PSS_Expired")
  1504.             else
  1505.                 summaryLine = "  {{"..earlyDeleteCount+expiredDeleteCount.."}} ".._TRANS("PSS_Removed_NoExpired")
  1506.             end
  1507.             if (printSummary) then _print(summaryLine) end
  1508.             summary = summary.."\n"..summaryLine
  1509.         end
  1510.         if (filterNewCount+filterOldCount > 0) then
  1511.             summaryLine = "  {{"..filterNewCount+filterOldCount.."}} ".._TRANS("PSS_Filtered")
  1512.             if (printSummary) then _print(summaryLine) end
  1513.             summary = summary.."\n"..summaryLine
  1514.         end
  1515.         if (filterDeleteCount > 0) then
  1516.             summaryLine = "  {{"..filterDeleteCount.."}} ".._TRANS("PSS_Filtered_Removed")
  1517.             if (printSummary) then _print(summaryLine) end
  1518.             summary = summary.."\n"..summaryLine
  1519.         end
  1520.         if (missedCount > 0 and not wasEndPagesOnly) then
  1521.             if wasIncomplete then
  1522.                 summaryLine = "  ".._TRANS("PSS_Incomplete_Missed_1").." "..missedCount.."}} ".._TRANS("PSS_Incomplete_Missed_2")
  1523.             else
  1524.                 summaryLine = "  {{"..missedCount.."}} ".._TRANS("PSS_MissedItems")
  1525.             end
  1526.             if (printSummary) then _print(summaryLine) end
  1527.             summary = summary.."\n"..summaryLine
  1528.         end
  1529.         if unresolvedCount > 0 then
  1530.             summaryLine = "  {{"..unresolvedCount.."}} Unresolved entries"
  1531.             if (printSummary) then _print(summaryLine) end
  1532.             summary = summary.."\n"..summaryLine
  1533.         end
  1534.         if corruptDeleteCount > 0 then
  1535.             summaryLine = "  {{"..corruptDeleteCount.."}} Corrupt entries found and removed"
  1536.             if (printSummary) then _print(summaryLine) end
  1537.             summary = summary.."\n"..summaryLine
  1538.         end
  1539.  
  1540.         if (_G.nLog) then
  1541.             local eTime = GetTime()
  1542.             _G.nLog.AddMessage("Auctioneer", "Scan", _G.N_INFO,
  1543.             "Scan "..TempcurQuery.qryinfo.id.."("..TempcurQuery.qryinfo.sig..") Committed",
  1544.             --("%s\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(summary, eTime-scanStarted, totalPaused, scanStoreTime, storeTime, GetTime()-commitStarted, totalProcessingTime))
  1545.             -- temporarily removed totalProcessingTime until other fixes go in and we can calculate it again
  1546.             ("%s\nTotal Time: %f\nPaused Time: %f\nData Storage Time: %f\nData Store Time (our processing): %f\nTotal Commit Coroutine Execution Time: %f"):format(summary, eTime-scanStarted, totalPaused, scanStoreTime, storeTime, GetTime()-commitStarted))
  1547.         end
  1548.     end
  1549.  
  1550.  
  1551.     local TempcurScanStats = {
  1552.         source = "scan",
  1553.         serverKey = serverKey,
  1554.         scanCount = scanCount,
  1555.         oldCount = oldCount,
  1556.         sameCount = sameCount,
  1557.         newCount = newCount,
  1558.         updateCount = updateCount,
  1559.         matchedCount = matchedCount,
  1560.         earlyDeleteCount = earlyDeleteCount,
  1561.         expiredDeleteCount = expiredDeleteCount,
  1562.         currentCount = currentCount,
  1563.         missedCount = missedCount,
  1564.         filteredCount = filterNewCount+filterOldCount,
  1565.         wasIncomplete = wasIncomplete or false,
  1566.         wasGetAll = wasGetAll or false,
  1567.         startTime = scanStartTime,
  1568.         endTime = endTimeStamp,
  1569.         started = scanStarted,
  1570.         paused = totalPaused,
  1571.         ended = GetTime(),
  1572.         elapsed = GetTime() - scanStarted - totalPaused,
  1573.         query = TempcurQuery,
  1574.         scanStoreTime = scanStoreTime,
  1575.         storeTime = storeTime
  1576.     }
  1577.  
  1578.     local scanstats = scandata.scanstats
  1579.     if not scanstats then
  1580.         scanstats = {}
  1581.         scandata.scanstats = scanstats
  1582.     end
  1583.  
  1584.     scanstats.LastScan = endTimeStamp
  1585.     if oldCount ~= currentCount or scanCount > 0 or dirtyCount > 0 or expiredCount > 0 or corruptCount > 0 then
  1586.         scanstats.ImageUpdated = endTimeStamp
  1587.     end
  1588.     if wasUnrestricted and not wasIncomplete then scanstats.LastFullScan = endTimeStamp end
  1589.  
  1590.     -- keep 2 old copies for compatibility
  1591.     scanstats[2] = scandata.scanstats[1]
  1592.     scanstats[1] = scandata.scanstats[0]
  1593.     scanstats[0] = TempcurScanStats
  1594.  
  1595.     -- Tell everyone that our stats are updated
  1596.     TempcurQuery.qryinfo.finished = true
  1597.  
  1598.     processBeginEndStats(processors, "complete", querySizeInfo, TempcurScanStats)
  1599.  
  1600.     _G.AucAdvanced.SendProcessorMessage("scanstats", TempcurScanStats)
  1601.  
  1602.     --Hide the progress indicator
  1603.     lib.ProgressBars("CommitProgressBar", nil, false)
  1604.     private.UpdateScanProgress(false, nil, nil, nil, nil, nil, TempcurQuery)
  1605.     lib.PopScan()
  1606.     CommitRunning = false
  1607.     if not private.curQuery then
  1608.         private.ResetAll()
  1609.     end
  1610.     _G.AucAdvanced.SendProcessorMessage("scanfinish", scanSize, TempcurQuery.qryinfo.sig, TempcurQuery.qryinfo, not wasIncomplete, TempcurQuery, TempcurScanStats)
  1611. end
  1612.  
  1613. local CoCommit, CoStore
  1614.  
  1615. local function CoroutineResume(...)
  1616.     local status, result = coroutine.resume(...)
  1617.     if not status and result then
  1618.         collectgarbage() -- ### trial to see if this helps recover from Memory Allocation Errors
  1619.         local msg = "Error occurred in coroutine: "..result
  1620.         if Swatter then
  1621.             Swatter.OnError(msg, nil, debugstack((...)))
  1622.         else
  1623.             geterrorhandler()(msg)
  1624.         end
  1625.     end
  1626.     return status, result
  1627. end
  1628.  
  1629. function private.Commit(wasEarlyTerm, wasEndPagesOnly, wasGetAll)
  1630.     private.StopStorePage()
  1631.     local curScan, curQuery, storeTime = private.curScan, private.curQuery, private.storeTime
  1632.     local scanStarted, scanStartTime, totalPaused = private.scanStarted, private.scanStartTime, private.totalPaused
  1633.     private.curQuery = nil
  1634.     private.curScan = nil
  1635.     private.isScanning = false
  1636.     if not (curQuery and curScan) then return end
  1637.  
  1638.     tinsert(private.CommitQueue, {
  1639.         Query = curQuery,
  1640.         Scan = curScan,
  1641.         wasIncomplete = wasEarlyTerm or wasEndPagesOnly,
  1642.         wasEarlyTerm = wasEarlyTerm,
  1643.         wasEndPagesOnly = wasEndPagesOnly,
  1644.         wasGetAll = wasGetAll,
  1645.         scanStarted = scanStarted,
  1646.         scanStartTime = scanStartTime,
  1647.         totalPaused = totalPaused,
  1648.         scanCommitTime = GetTime(),
  1649.         storeTime = storeTime
  1650.     })
  1651.  
  1652.     if not CoCommit or coroutine.status(CoCommit) == "dead" then
  1653.         CoCommit = coroutine.create(Commitfunction)
  1654.     end
  1655.     -- wait for the next update to resume CoCommit
  1656. end
  1657.  
  1658. function private.QuerySent(query, isSearch, ...)
  1659.     -- Tell everyone that our stats are updated
  1660.     _G.AucAdvanced.SendProcessorMessage("querysent", query, isSearch, ...)
  1661.     return ...
  1662. end
  1663.  
  1664. function private.FinishedPage(nextPage)
  1665.     -- Tell everyone that our stats are updated
  1666.     local modules = _G.AucAdvanced.GetAllModules("FinishedPage")
  1667.     for pos, engineLib in ipairs(modules) do
  1668.         local pOK, finished = pcall(engineLib.FinishedPage,nextPage)
  1669.         if (pOK) then
  1670.             if (finished~=nil) and (finished==false) then
  1671.                 return false
  1672.             end
  1673.         else
  1674.             if (_G.nLog) then
  1675.                 _G.nLog.AddMessage("Auctioneer", "Scan", _G.N_WARNING, ("FinishedPage %s Returned Error %s"):format(engineLib.GetName(), finished))
  1676.             end
  1677.         end
  1678.     end
  1679.     return true
  1680. end
  1681.  
  1682. function private.ScanPage(nextPage, really)
  1683.     if (private.isScanning) then
  1684.         local CanQuery, CanQueryAll = CanSendAuctionQuery()
  1685.         if not (CanQuery and private.FinishedPage(nextPage) and really) then
  1686.             private.scanNext = GetTime()
  1687.             private.scanNextPage = nextPage
  1688.             return
  1689.         end
  1690.         private.sentQuery = true
  1691.         private.queryStarted = GetTime()
  1692.         private.auctionItemListUpdated = false
  1693.         SortAuctionClearSort("list")
  1694.         private.Hook.QueryAuctionItems(private.curQuery.name or "",
  1695.             private.curQuery.minUseLevel or "", private.curQuery.maxUseLevel or "",
  1696.             private.curQuery.invType, private.curQuery.classIndex, private.curQuery.subclassIndex, nextPage,
  1697.             private.curQuery.isUsable, private.curQuery.quality, nil, private.curQuery.exactMatch)
  1698.  
  1699.         _G.AuctionFrameBrowse.page = nextPage
  1700.  
  1701.         private.verifyStart = nil
  1702.     end
  1703. end
  1704.  
  1705. -- Mechanism to limit repeated calls to GetItemInfo and C_PetJournal.GetPetInfoBySpeciesID during processing
  1706. do
  1707.     local ItemInfoCache, PetInfoCache = {}, {}
  1708.     local ItemTried, PetTried
  1709.     local cageType, cageSubtypeLookup, cageUseLevel
  1710.  
  1711.     function private.ResetItemInfoCache()
  1712.         wipe(ItemInfoCache)
  1713.         wipe(PetInfoCache)
  1714.         ItemTried, PetTried = nil, nil
  1715.     end
  1716.     function private.InitItemInfoCache()
  1717.         ItemTried, PetTried = {}, {}
  1718.     end
  1719.     local function GetItemInfoCache(link, itemID, bonuses, scanthrottle)
  1720.         local cachekey = itemID
  1721.         if bonuses and bonuses ~= "" then cachekey = cachekey .. ":" .. bonuses end
  1722.         local data = ItemInfoCache[cachekey]
  1723.         if data then
  1724.             return data
  1725.         end
  1726.         if scanthrottle and ItemTried[cachekey] then
  1727.             -- if GetItemInfo previously failed for a link with this cachekey in this processing pass (ItemTried is reset each pass)
  1728.             return
  1729.         end
  1730.         local _,_,_,iLevel,uLevel,iType,iSubtype,_,equipLoc = GetItemInfo(link)
  1731.         if not iType then
  1732.             if scanthrottle then
  1733.                 ItemTried[cachekey] = true
  1734.             end
  1735.             return
  1736.         end
  1737.         -- not all values are used; only store the ones we want
  1738.         data = {iType, iSubtype, Const.EquipEncode[equipLoc], iLevel, uLevel or 0}
  1739.         ItemInfoCache[cachekey] = data
  1740.         return data
  1741.     end
  1742.  
  1743.     local function GetPetInfoCache(speciesID, scanthrottle)
  1744.         if not cageType then
  1745.             -- generic info for the Pet Cage item 82800 - info that is the same for every pet
  1746.             -- cageSubtypeLookup can be used to convert numeric subtype to localized string for ISUB
  1747.             local _,_,_,_,uLevel, iType = GetItemInfo(82800)
  1748.             if iType and uLevel then
  1749.                 cageSubtypeLookup = Const.SUBCLASSES[Const.CLASSESREV[iType]]
  1750.                 if cageSubtypeLookup then
  1751.                     cageType = iType
  1752.                     cageUseLevel = uLevel -- always 0, but we check here in case Blizzard changes it
  1753.                 end
  1754.             end
  1755.             if not cageType then return end
  1756.         end
  1757.         local subtype = PetInfoCache[speciesID]
  1758.         if not subtype then
  1759.             if scanthrottle and PetTried[speciesID] then
  1760.                 -- GetPetInfoBySpeciesID previously failed for this speciesID in this processing pass
  1761.                 return
  1762.             end
  1763.             local _, _, petType = C_PetJournal.GetPetInfoBySpeciesID(speciesID)
  1764.             if petType then
  1765.                 subtype = cageSubtypeLookup[petType]
  1766.                 PetInfoCache[speciesID] = subtype
  1767.             else
  1768.                 if scanthrottle then
  1769.                     PetTried[speciesID] = true
  1770.                 end
  1771.                 return
  1772.             end
  1773.         end
  1774.  
  1775.         return cageType, subtype, cageUseLevel
  1776.     end
  1777.  
  1778.     -- Called on a data table that has been processed by private.GetAuctionItem
  1779.     -- Fills in certain entries that can be calculated from other entries, and so do not need to be determined at scan time
  1780.     -- In particular, we avoid calling APIs GetItemInfo or C_PetJournal.GetPetInfoBySpeciesID duing the scan; we can delay them until processing
  1781.     function private.GetAuctionItemFillIn(itemData, scanthrottle)
  1782.         local linkType
  1783.         local itemID = itemData[Const.ITEMID]
  1784.         local itemLink = itemData[Const.LINK]
  1785.         if not (itemID and itemLink and itemData[Const.TLEFT]) then
  1786.             return nil, "Invalid", "unknown"
  1787.         end
  1788.  
  1789.         if itemID == 82800 then -- "Pet Cage"
  1790.             linkType = "battlepet"
  1791.             if not itemData[Const.SEED] then
  1792.                 -- following entries are not relevant to battlepets
  1793.                 itemData[Const.BONUSES] = ""
  1794.                 itemData[Const.SUFFIX] = 0
  1795.                 itemData[Const.FACTOR] = 0
  1796.                 itemData[Const.ENCHANT] = 0
  1797.                 itemData[Const.SEED] = 0 -- there must be a hidden unique seed, but I can't find a way to access it
  1798.             end
  1799.             if not itemData[Const.ITYPE] then
  1800.                 local _, speciesID = strsplit(":", itemLink)
  1801.                 speciesID = tonumber(speciesID)
  1802.                 if speciesID then
  1803.                     local cType, cSubtype, cUseLevel = GetPetInfoCache(speciesID, scanthrottle)
  1804.                     if cType then
  1805.                         itemData[Const.ITYPE] = cType -- string, localized to client
  1806.                         itemData[Const.IEQUIP] = nil -- always nil for Pet Cages
  1807.                         itemData[Const.ULEVEL] = itemData[Const.ULEVEL] or cUseLevel
  1808.                         itemData[Const.ISUB] = cSubtype
  1809.                         -- iLevel should have been obtained from GetAuctionItemInfo (could be extracted from link, if required though)
  1810.                     end
  1811.                 end
  1812.             end
  1813.         else
  1814.             linkType = "item"
  1815.             if not itemData[Const.SEED] then
  1816.                 local lkt, id, suffix, factor, enchant, seed, _, _, _, _, bonuses = AucAdvanced.DecodeLink(itemLink)
  1817.                 if lkt == "item" and id == itemID then
  1818.                     itemData[Const.BONUSES] = bonuses or ""
  1819.                     itemData[Const.SUFFIX] = suffix
  1820.                     itemData[Const.FACTOR] = factor
  1821.                     itemData[Const.ENCHANT] = enchant
  1822.                     itemData[Const.SEED] = seed
  1823.                 else
  1824.                     -- unrecognised link type - if this is happening then we'll need to investigate the link type
  1825.                     -- and install a special exception for it (similar to battlepet links, as above)
  1826.                     if nLog then
  1827.                         nLog.AddMessage("Auctioneer", "Scan", N_WARNING, "GetAuctionItemFillIn could not decode link",
  1828.                             ("Page %d, Index %d\nLink %s, itemID %d (from GetAuctionItemInfo)\ntype %s, id %s, suffix %s, factor %s, enchant %s, seed %s (from Decode)"):format(
  1829.                                 page, index, itemLink, itemID, tostringall(lkt, id, suffix, factor, enchant, seed)))
  1830.                     end
  1831.                     -- Note: SEED is still set to nil
  1832.                     return nil, "UnknownLinkType", "unknown"
  1833.                 end
  1834.             end
  1835.             if not itemData[Const.ITYPE] then
  1836.                 local itemInfo = GetItemInfoCache(itemLink, itemID, itemData[Const.BONUSES], scanthrottle) -- {iType, iSubtype, Const.EquipEncode[equipLoc], iLevel, uLevel}
  1837.                 if itemInfo then
  1838.                     itemData[Const.ITYPE] = itemInfo[1]
  1839.                     itemData[Const.ISUB] = itemInfo[2]
  1840.                     itemData[Const.IEQUIP] = itemInfo[3]
  1841.                     -- Prefer iLevel and/or uLevel values provided by GetAuctionItemInfo over those from GetItemInfo
  1842.                     -- (because we used itemID, it is possible for values from GetItemInfo to be incorrect)
  1843.                     itemData[Const.ILEVEL] = itemData[Const.ILEVEL] or itemInfo[4]
  1844.                     itemData[Const.ULEVEL] = itemData[Const.ULEVEL] or itemInfo[5]
  1845.                 end
  1846.             end
  1847.         end
  1848.  
  1849.         if not itemData[Const.ITYPE] then
  1850.             return nil, "Retry", linkType
  1851.         end
  1852.  
  1853.         if not itemData[Const.SELLER] then itemData[Const.SELLER] = "" end
  1854.  
  1855.         return true, nil, linkType
  1856.     end
  1857. end
  1858.  
  1859. --[[ private.GetAuctionItem(list, page, index, itemData)
  1860.     Returns itemData, with entries filled in from the GetAuctionItemX & GetItemInfo functions, plus some additional processing
  1861.     If page is provided, requires the same itemData table as was used for the same page/index combination previously in the current scan
  1862.         This is used during scanning, when retrying an auction entry - also enables some error checking
  1863.     If page is not provided, itemData may be an empty table (if reusing a table, wipe it first)
  1864.  
  1865.     When checking itemData for completeness, check the following entries:
  1866.     Const.LINK : if this is missing, most other entries will be missing too. Auction is unresolvable, but may be possible to resolve after a delay
  1867.         if it is present, it is likely that most other entries will be present too
  1868.     Const.ITEMID : if present, most useful entries should be present, particularly all prices
  1869.     Const.TLEFT : is one of the last entries to get resolved - only happens if no failures were detected
  1870.  
  1871.     Const.ITYPE, Const.SEED : left blank
  1872.         private.GetAuctionItemFillIn should be called after to fill these in
  1873.  
  1874.     Const.SELLER : often missing, particularly during GetAll scans - may be resolvable after a delay, but may require a very long delay
  1875.         private.GetAuctionItemFillIn will replace missing seller with ""
  1876. --]]
  1877. function private.GetAuctionItem(list, page, index, itemData)
  1878.     if not itemData then
  1879.         itemData = {}
  1880.     elseif itemData.NORETRY then
  1881.         return itemData
  1882.     end
  1883.     itemData[Const.FLAG] = itemData[Const.FLAG] or 0
  1884.  
  1885.     local isLogging = nLog and page and list == "list"
  1886.     if isLogging then
  1887.         if not itemData.PAGE then
  1888.             itemData.PAGE = page
  1889.             itemData.PAGEINDEX = index
  1890.         elseif itemData.PAGE ~= page or itemData.PAGEINDEX ~= index then
  1891.             -- We messed up the indexing - if we used the page parameter we should have used the same itemData table as before
  1892.             local msg = ("Page new=%d old=%d\nIndex new=%d old=%d"):format(page, itemData.PAGE, index, itemData.PAGEINDEX)
  1893.             nLog.AddMessage("Auctioneer", "Scan", N_ERROR, "GetAuctionItem called with invalid page/index",
  1894.                 msg)
  1895.             geterrorhandler()("GetAuctionItem called with invalid page/index\n"..msg)
  1896.             return itemData
  1897.         end
  1898.     end
  1899.  
  1900.     local itemLink = GetAuctionItemLink(list, index)
  1901.     if itemLink then
  1902.         itemLink = AucAdvanced.SanitizeLink(itemLink)
  1903.         if itemData[Const.LINK] and itemData[Const.LINK] ~= itemLink then
  1904.             -- Not the same auction as was at this position in the scan before!
  1905.             -- Log and abort so we don't corrupt it
  1906.             if isLogging then
  1907.                 nLog.AddMessage("Auctioneer", "Scan", N_ERROR, "GetAuctionItem ItemLink does not match link found previously at this index",
  1908.                     ("Page %d, Index %d\nOld link %s\nNew link %s\nOld ITEMID %s, MINBID %s, TLEFT %s, SELLER %s"):format(page, index, itemData[Const.LINK], itemLink,
  1909.                         tostringall(itemData[Const.ITEMID], itemData[Const.MINBID], itemData[Const.TLEFT], itemData[Const.SELLER]))) -- one of these must be missing for us to need to retry
  1910.             end
  1911.             itemData.NORETRY = "Link changed"
  1912.             return itemData
  1913.         end
  1914.         itemData[Const.LINK] = itemLink
  1915.     else
  1916.         return itemData
  1917.     end
  1918.  
  1919.     local name, texture, count, quality, canUse, level, levelColHeader, minBid, minIncrement, buyoutPrice, bidAmount, highBidder, bidderFullName, owner, ownerFullName, saleStatus, itemId = GetAuctionItemInfo(list, index)
  1920.     -- Check critical values (if we got those, assume we got the rest as well - except possibly owner)
  1921.     if not (itemId and minBid) then
  1922.         return itemData
  1923.     end
  1924.     if itemData[Const.MINBID] and itemData[Const.MINBID] ~= minBid then
  1925.         -- similar to itemLink changing, this means the auction is not the same one as was at this position before
  1926.         if isLogging then
  1927.             nLog.AddMessage("Auctioneer", "Scan", N_ERROR, "GetAuctionItem minBid does not match minBid found previously at this index",
  1928.                 ("Page %d, Index %d\nLink %s\nminBid old %s, new %s\nAll returns from GetAuctionItemInfo:\n%s"):format(page, index, itemLink, itemData[Const.MINBID], minBid,
  1929.                 strjoin(",", tostringall(name, texture, count, quality, canUse, level, levelColHeader, minBid, minIncrement, buyoutPrice, bidAmount, highBidder, owner, saleStatus, itemId))))
  1930.         end
  1931.         itemData.NORETRY = "MinBid changed"
  1932.         return itemData
  1933.     end
  1934.  
  1935.     itemData[Const.ITEMID] = itemId
  1936.     itemData[Const.MINBID] = minBid
  1937.  
  1938.     itemData[Const.NAME] = name
  1939.     itemData[Const.DEP2] = nil
  1940.     itemData[Const.QUALITY] = quality
  1941.     itemData[Const.CANUSE] = canUse
  1942.     itemData[Const.AMHIGH] = highBidder and true or false
  1943.     itemData[Const.MININC] = minIncrement
  1944.     itemData[Const.SELLER] = owner -- if this is nil, it will get set to "" at a later time
  1945.  
  1946.     if not count or (count == 0 and (list ~= "owner" or saleStatus ~= 1)) then -- the only time count may be 0 is for a sold auction on the "owner" list
  1947.         count = 1
  1948.     end
  1949.     itemData[Const.COUNT] = count
  1950.  
  1951.     bidAmount = bidAmount or 0
  1952.     itemData[Const.CURBID] = bidAmount
  1953.     buyoutPrice = buyoutPrice or 0
  1954.     itemData[Const.BUYOUT] = buyoutPrice
  1955.     local nextBid
  1956.     if bidAmount > 0 then
  1957.         nextBid = bidAmount + minIncrement
  1958.         if buyoutPrice > 0 and nextBid > buyoutPrice then
  1959.             nextBid = buyoutPrice
  1960.         end
  1961.     elseif minBid > 0 then
  1962.         nextBid = minBid
  1963.     else
  1964.         nextBid = 1
  1965.     end
  1966.     itemData[Const.PRICE] = nextBid
  1967.  
  1968.     -- use the iLevel or uLevel data from GetAuctionItemInfo, if available
  1969.     if level then
  1970.         if levelColHeader == "REQ_LEVEL_ABBR" then
  1971.             itemData[Const.ULEVEL] = level
  1972.         elseif levelColHeader == "ITEM_LEVEL_ABBR"  then
  1973.             itemData[Const.ILEVEL] = level
  1974.         end
  1975.         -- todo: handle other possible values for levelColHeader
  1976.     end
  1977.  
  1978.     --[[
  1979.         Returns Integer giving range of time left for query
  1980.         1 -- short time (Less than 30 mins)
  1981.         2 -- medium time (30 mins to 2 hours)
  1982.         3 -- long time (2 hours to 8 hours)
  1983.         4 -- very long time (8 hours+)
  1984.     ]]
  1985.     itemData[Const.TLEFT] = GetAuctionItemTimeLeft(list, index)
  1986.     itemData[Const.TIME] = time()
  1987.  
  1988.     return itemData
  1989. end
  1990.  
  1991. function lib.GetAuctionItem(list, index, fillTable)
  1992.     if type(fillTable) == "table" then
  1993.         wipe(fillTable)
  1994.     else
  1995.         fillTable = nil
  1996.     end
  1997.     local itemData = private.GetAuctionItem(list, nil, index, fillTable)
  1998.     if not itemData[Const.TLEFT] then
  1999.         -- missing TLEFT indicates a failure was detected for one of the GetAuctionItemX functions
  2000.         return
  2001.     end
  2002.     private.GetAuctionItemFillIn(itemData)
  2003.  
  2004.     return itemData
  2005. end
  2006.  
  2007. function lib.GetAuctionSellItem(minBid, buyoutPrice, runTime)
  2008.     local itemLink = private.auctionItem
  2009.     local name, texture, count, quality, canUse, price = GetAuctionSellItemInfo();
  2010.  
  2011.     if name and itemLink then
  2012.         local linkType, itemId, itemSuffix, itemFactor, itemEnchant, itemSeed = _G.AucAdvanced.DecodeLink(itemLink)
  2013.         if linkType == "item" then
  2014.             itemLink = _G.AucAdvanced.SanitizeLink(itemLink)
  2015.             local _,_,_,itemLevel,level,itemType,itemSubType,_,itemEquipLoc = GetItemInfo(itemLink)
  2016.             local timeLeft = 4
  2017.             if runTime <= 12*60 then timeLeft = 3 end
  2018.             local curTime = time()
  2019.  
  2020.             return {
  2021.                 itemLink, itemLevel, itemType, itemSubType, nil, minBid,
  2022.                 timeLeft, curTime, name, texture, count, quality, canUse, level,
  2023.                 minBid, 0, buyoutPrice, 0, nil, Const.PlayerName,
  2024.                 0, -1, itemId, itemSuffix, itemFactor, itemEnchant, itemSeed
  2025.             }, price
  2026.         end
  2027.     end
  2028. end
  2029.  
  2030. --[[ Used to decide if we should retry scanning this auction
  2031.     returns: IsResolved, IsResolvedExceptSeller
  2032.     notes: assumes that if certain entries are present then auction is resolved (see notes for GetAuctionItem)
  2033. --]]
  2034. function private.isComplete(itemData)
  2035.     local resolved = itemData and itemData[Const.TLEFT]
  2036.     return itemData[Const.SELLER] and resolved, resolved
  2037. end
  2038.  
  2039. local StorePageFunction = function()
  2040.     if (not private.curQuery) or (private.curQuery.name == "empty page") then
  2041.         return
  2042.     end
  2043.  
  2044.     local GetTime = GetTime
  2045.  
  2046.     if (not private.scanStarted) then private.scanStarted = GetTime() end
  2047.     local queryStarted = private.scanStarted
  2048.     local retrievalStarted = GetTime()
  2049.  
  2050.     --local RunTime = 0 -- todo: reinstate RunTime calculations
  2051.     private.sentQuery = false
  2052.     local page = _G.AuctionFrameBrowse.page
  2053.     if not private.curScan then
  2054.         private.curScan = {}
  2055.     end
  2056.     if not private.curPages then
  2057.         private.curPages = {}
  2058.     end
  2059.  
  2060.     if (_G.nLog) then
  2061.         _G.nLog.AddMessage("Auctioneer", "Scan", _G.N_INFO, ("StorePage For Page %d Started %fs after Query Start"):format(page, retrievalStarted - queryStarted), ("StorePage (Page %d) Called\n%f seconds have elapsed since scan start"):format(page, retrievalStarted - queryStarted))
  2062.     end
  2063.  
  2064.     if private.isGetAll then
  2065.         --[[
  2066.             pre-store delay before starting to store a getall query to give the client a bit of time to sort itself out
  2067.             we want to call it before GetNumAuctionItems, so we must use private.isGetAll for detection
  2068.         --]]
  2069.         coroutine.yield()
  2070.         if private.warningCanSendBug and CanSendAuctionQuery() then -- check it again after delay
  2071.             private.warningCanSendBug = nil
  2072.         end
  2073.     end
  2074.  
  2075.     local curQuery, curScan, curPages = private.curQuery, private.curScan, private.curPages
  2076.     local qryinfo = curQuery.qryinfo
  2077.  
  2078.     local EventFramesRegistered = nil
  2079.     local numBatchAuctions, totalAuctions = GetNumAuctionItems("list")
  2080.     local maxPages = ceil(totalAuctions / NUM_AUCTION_ITEMS_PER_PAGE)
  2081.     local isGetAll = false
  2082.     local isGetAllFail = false -- flag to handle certain GetAll failure situations
  2083.     if numBatchAuctions > NUM_AUCTION_ITEMS_PER_PAGE then
  2084.         isGetAll = true
  2085.         maxPages = 1
  2086.         if totalAuctions ~= numBatchAuctions then -- check for invalid values from server (residual test in case it starts to happen again...)
  2087.             isGetAllFail = true
  2088.             if nLog then
  2089.                 nLog.AddMessage("Auctioneer", "Scan", N_WARNING, "StorePage incomplete GetAll",
  2090.                     format("Batch size %d\nReported total auctions %d",
  2091.                     numBatchAuctions, totalAuctions))
  2092.             end
  2093.             totalAuctions = numBatchAuctions
  2094.             _print("|cffff7f3fThe Server has not sent all data for this GetAll scan. The scan will be incomplete.|r")
  2095.             _print("Please report this in the Auctioneer forums.")
  2096.         end
  2097.         EventFramesRegistered = {GetFramesRegisteredForEvent("AUCTION_ITEM_LIST_UPDATE")}
  2098.         for _, frame in pairs(EventFramesRegistered) do
  2099.             frame:UnregisterEvent("AUCTION_ITEM_LIST_UPDATE")
  2100.         end
  2101.         private.verifyStart = 1
  2102.         coroutine.yield()
  2103.     end
  2104.  
  2105.     --Update the progress indicator
  2106.     local elapsed = GetTime() - private.scanStarted - private.totalPaused
  2107.     --store queued scans to pass along on the callback, used by scanbutton and searchUI etc to display how many scans are still queued
  2108.  
  2109.     --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
  2110.     private.UpdateScanProgress(nil, totalAuctions, #curScan, elapsed, page+1, maxPages, curQuery) --page starts at 0 so we need to add +1
  2111.  
  2112.     -- coroutine speed limiter using debugprofilestop
  2113.     -- time in milliseconds: 1000/FPS * 0.8 (80% rough adjustment to allow for other stuff happening during the frame)
  2114.     local processingTime = 800 / get("scancommit.targetFPS")
  2115.     local debugprofilestop = debugprofilestop
  2116.     local nextPause = debugprofilestop() + processingTime
  2117.  
  2118.     local breakcount = 10000 -- additional limiter: yield every breakcount auctions scanned
  2119.     local scannerthrottle = get("core.scan.scannerthrottle")
  2120.     if scannerthrottle >= Const.ALEVEL_HI then
  2121.         breakcount = 500
  2122.         processingTime = processingTime * 0.1
  2123.     elseif scannerthrottle >= Const.ALEVEL_MED then
  2124.         breakcount = 2000
  2125.         processingTime = processingTime * 0.2
  2126.     elseif scannerthrottle >= Const.ALEVEL_LOW then
  2127.         breakcount = 5000
  2128.         processingTime = processingTime * 0.5
  2129.     end
  2130.  
  2131.     local storecount = 0
  2132.     local sellerOnly = true
  2133.  
  2134.     local missedCounts, remissedCounts, switchCounts, mc = {}, {}, nil, nil
  2135.     for i = 1, Const.LASTENTRY do
  2136.         missedCounts[i] = 0
  2137.     end
  2138.     local resolvedCounts = {}
  2139.     for i = 1, Const.LASTENTRY do
  2140.         remissedCounts[i] = 0
  2141.     end
  2142.  
  2143.     if not private.breakStorePage and (page > qryinfo.page) then
  2144.         -- First pass
  2145.         local retries = { }
  2146.         for i = 1, numBatchAuctions do
  2147.             if isGetAll then -- only yield for GetAll scans
  2148.                 if debugprofilestop() > nextPause or i % breakcount == 0 then
  2149.                     lib.ProgressBars("GetAllProgressBar", 100*storecount/numBatchAuctions, true)
  2150.                     coroutine.yield()
  2151.                     if private.breakStorePage then
  2152.                         break
  2153.                     end
  2154.                     nextPause = debugprofilestop() + processingTime
  2155.                 end
  2156.             end
  2157.  
  2158.             local itemData = private.GetAuctionItem("list", page, i)
  2159.  
  2160.             if (itemData) then
  2161.                 local isComplete, completeMinusSeller = private.isComplete(itemData)
  2162.                 if (isComplete) then
  2163.                     tinsert(curScan, itemData)
  2164.                     storecount = storecount + 1
  2165.                 else
  2166.                     for mc = 1, Const.LASTENTRY do
  2167.                         missedCounts[mc] = missedCounts[mc] + ((itemData[mc] and 0) or 1)
  2168.                     end
  2169.                     sellerOnly = sellerOnly and completeMinusSeller
  2170.                     tinsert(retries, { i, itemData })
  2171.                 end
  2172.             else
  2173.                 for mc = 1, Const.LASTENTRY do
  2174.                     missedCounts[mc] = missedCounts[mc] + 1
  2175.                 end
  2176.                 sellerOnly = false
  2177.                 tinsert(retries, { i, nil })
  2178.             end
  2179.         end
  2180.         local maxTries = get('scancommit.ttl')
  2181.         local tryCount = 0
  2182.         if _G.nLog and (#retries > 0) then
  2183.             _G.nLog.AddMessage("Auctioneer", "Scan", _G.N_INFO, ("StorePage Requires Retries Page %d"):format(page),
  2184.                 ("Page: %d\nRetries Setting: %d\nUnresolved Entries:%d\nPage Elapsed Time: %.2fs"):format(page, maxTries, #retries, GetTime() - retrievalStarted))
  2185.         end
  2186.  
  2187.         -- Second and subsequent passes
  2188.         local newRetries = { }
  2189.         local readCount = 1
  2190.         local needsRetries = #retries > 0
  2191.         while (needsRetries and tryCount < maxTries and ((not sellerOnly) or get("core.scan.sellernamedelay")) and not private.breakStorePage) do
  2192.             needsRetries = false
  2193.             sellerOnly = true
  2194.             tryCount = tryCount + 1
  2195.             -- must use GetTime to time this pause, as debugprofilestop is unsafe across yields
  2196.             local nextWait = GetTime() + 1
  2197.             while GetTime() < nextWait do
  2198.                 coroutine.yield() -- yielding updates GetTime, so this loop will still work
  2199.                 if private.breakStorePage then break end
  2200.             end
  2201.             if private.breakStorePage then break end
  2202.  
  2203.             nextPause = debugprofilestop() + processingTime
  2204.             for pos, i in ipairs(retries) do
  2205.                 if isGetAll then
  2206.                     if debugprofilestop() > nextPause or pos % breakcount == 0 then
  2207.                         lib.ProgressBars("GetAllProgressBar", 100*storecount/numBatchAuctions, true)
  2208.                         coroutine.yield()
  2209.                         if private.breakStorePage then break end
  2210.                         nextPause = debugprofilestop() + processingTime
  2211.                     end
  2212.                 end
  2213.  
  2214.                 readCount = readCount + 1
  2215.  
  2216.                 local itemData = private.GetAuctionItem("list", page, i[1], i[2])
  2217.  
  2218.                 if (itemData) then
  2219.                     local isComplete, completeMinusSeller = private.isComplete(itemData)
  2220.                     if (isComplete) then
  2221.                         tinsert(curScan, itemData)
  2222.                         storecount = storecount + 1
  2223.                     else
  2224.                         for mc = 1, Const.LASTENTRY do
  2225.                             remissedCounts[mc] = remissedCounts[mc] + ((itemData[mc] and 0) or 1)
  2226.                         end
  2227.                         sellerOnly = sellerOnly and completeMinusSeller
  2228.                         if not itemData.NORETRY then
  2229.                             needsRetries = true
  2230.                         end
  2231.                         tinsert(newRetries, { i[1], itemData })
  2232.                     end
  2233.                 else
  2234.                     for mc = 1, Const.LASTENTRY do
  2235.                         remissedCounts[mc] = remissedCounts[mc] + ((itemData[mc] and 0) or 1)
  2236.                     end
  2237.                     sellerOnly = false
  2238.                     tinsert(newRetries, i)
  2239.                 end
  2240.             end
  2241.  
  2242.             if (#retries ~= #newRetries) then
  2243.                 if _G.nLog then
  2244.                     local resolvedMap = ""
  2245.                     local missingMap = ""
  2246.                     resolvedMap = ("%d"):format(missedCounts[1]-remissedCounts[1])
  2247.                     missingMap = ("%d"):format(remissedCounts[1])
  2248.                     for mc = 2, Const.LASTENTRY do
  2249.                         resolvedMap = ("%s,%d"):format(resolvedMap,missedCounts[mc]-remissedCounts[mc])
  2250.                         missingMap = ("%s,%d"):format(missingMap,remissedCounts[mc])
  2251.                         if mc==Const.SELLER then
  2252.                             resolvedMap = resolvedMap.."*"
  2253.                             missingMap = missingMap.."*"
  2254.                         elseif mc==Const.IEQUIP then
  2255.                             resolvedMap = resolvedMap.."-"
  2256.                             missingMap = missingMap.."-"
  2257.                         end
  2258.                     end
  2259.  
  2260.                     _G.nLog.AddMessage("Auctioneer", "Scan", _G.N_INFO,
  2261.                         ("StorePage Retry Successful Page %d"):format(page),
  2262.                         ("Page: %d\nRetry Count: %d\nRecords Returned: %d\nRecords Left: %d\nPage Elapsed Time: %.2fs\nResolved:\n %s\nRemaining Unresolved:\n %s\nSeller Only Remaining: %s,   Wait on Only Seller: %s"):format(page, tryCount,
  2263.                             #retries - #newRetries, #newRetries, GetTime() - retrievalStarted, resolvedMap, missingMap, sellerOnly and "True" or "False", get("core.scan.sellernamedelay") and "True" or "False"))
  2264.                 end
  2265.                 -- Found at least one.  Reset retry delay.
  2266.                 tryCount = 0
  2267.             end
  2268.             for mc = 1, Const.LASTENTRY do
  2269.                 missedCounts[mc]=remissedCounts[mc]
  2270.                 remissedCounts[mc]=0
  2271.             end
  2272.             retries = newRetries
  2273.             newRetries = { }
  2274.         end
  2275.  
  2276.  
  2277.         local names_missed, all_missed, ld_and_names_missed, links_missed, link_data_missed = 0,0,0,0,0
  2278.         nextPause = debugprofilestop() + processingTime
  2279.         for _, i in ipairs(retries) do
  2280.             if isGetAll then
  2281.                 if debugprofilestop() > nextPause then
  2282.                     lib.ProgressBars("GetAllProgressBar", 100*storecount/numBatchAuctions, true)
  2283.                     coroutine.yield()
  2284.                     if private.breakStorePage then break end
  2285.                     nextPause = debugprofilestop() + processingTime
  2286.                 end
  2287.             end
  2288.             readCount = readCount + 1
  2289.             -- Put it to scan and let the commit routine deal with it.
  2290.             if (not i[2][Const.SELLER] and not i[2][Const.LINK]) then
  2291.                 i[2][Const.SELLER] = ""
  2292.                 all_missed = all_missed + 1
  2293.             elseif (not i[2][Const.SELLER] and not i[2][Const.ITEMID]) then
  2294.                 i[2][Const.SELLER] = ""
  2295.                 ld_and_names_missed = ld_and_names_missed + 1
  2296.             elseif (not i[2][Const.SELLER]) then
  2297.                 i[2][Const.SELLER] = ""
  2298.                 names_missed = names_missed + 1
  2299.             elseif (not i[2][Const.LINK]) then
  2300.                 links_missed = links_missed + 1
  2301.             elseif (not i[2][Const.ITEMID]) then
  2302.                 link_data_missed = link_data_missed + 1
  2303.             end
  2304.             tinsert(curScan, i[2])
  2305.         end
  2306.  
  2307.         if _G.nLog and #retries > 0 then
  2308.             _G.nLog.AddMessage("Auctioneer", "Scan", _G.N_INFO, ("StorePage Incomplete Resolution of Page %d"):format(page),
  2309.                 ("Page: %d\nRetries Setting: %d\nUnresolved Entries: %d\nMissing Everything: %d, Just Names: %d, Just Links (and link data): %d, Names and Link Data: %d, Link Data: %d"):format(page,
  2310.                 maxTries, #retries, all_missed, names_missed, links_missed, ld_and_names_missed, link_data_missed ))
  2311.         end
  2312.  
  2313.         if storecount > 0 or page == 0 then
  2314.             qryinfo.page = page
  2315.             curPages[page] = true -- we have pulled this page
  2316.         end
  2317.  
  2318.         if #retries > 0 then
  2319.             -- for info only; CommitFunction does its own 'incomplete' detection
  2320.             qryinfo.unresolved = (qryinfo.unresolved or 0) + all_missed + links_missed + link_data_missed + ld_and_names_missed
  2321.         end
  2322.     end
  2323.  
  2324.  
  2325.     if EventFramesRegistered then
  2326.         for _, frame in pairs(EventFramesRegistered) do
  2327.             frame:RegisterEvent("AUCTION_ITEM_LIST_UPDATE")
  2328.             local eventscript = frame:GetScript("OnEvent")
  2329.             if eventscript then
  2330.                 pcall(eventscript, frame, "AUCTION_ITEM_LIST_UPDATE")
  2331.             end
  2332.         end
  2333.         EventFramesRegistered = nil
  2334.     end
  2335.  
  2336.     --Send a Processor event to modules letting them know we are done with the page
  2337.     _G.AucAdvanced.SendProcessorMessage("pagefinished", page)
  2338.  
  2339.     -- Clear GetAll changes made by StartScan
  2340.     if private.isGetAll then -- in theory private.isGetAll should be true iff (local) isGetAll is true -- unless total auctions <=50 (e.g. on PTR)
  2341.         lib.ProgressBars("GetAllProgressBar", 100, false)
  2342.         BrowseSearchButton:Show()
  2343.         _G.AucAdvanced.API.BlockUpdate(false)
  2344.         private.isGetAll = nil
  2345.     end
  2346.  
  2347.     coroutine.yield() -- update GetTime
  2348.     local endTime = GetTime()
  2349.     if not private.breakStorePage then
  2350.         -- Send the next page query or finish scanning
  2351.         if isGetAll then
  2352.                 elapsed = endTime - private.scanStarted - private.totalPaused
  2353.                 private.UpdateScanProgress(nil, totalAuctions, #curScan, elapsed, page+2, maxPages, curQuery) -- page+2 signals that scan is done
  2354.                 private.Commit(isGetAllFail, false, true)
  2355.                 -- Clear the getall output. We don't want to create a new query so use the hook
  2356.                 -- ### temp fix: isUsable flag appears to be acting like a mini-getall, in this case we don't want to blank the results
  2357.                 if not curQuery.isUsable then
  2358.                     private.queryStarted = GetTime()
  2359.                     private.Hook.QueryAuctionItems("empty page", "", "", nil, nil, nil, nil, nil, nil, nil, nil)
  2360.                 end -- ###
  2361.         elseif private.isScanning then
  2362.             if (page+1 < maxPages) then
  2363.                 private.ScanPage(page + 1)
  2364.             else
  2365.                 elapsed = endTime - private.scanStarted - private.totalPaused
  2366.                 private.UpdateScanProgress(nil, totalAuctions, #curScan, elapsed, page+2, maxPages, curQuery)
  2367.                 private.Commit(false, false, false)
  2368.             end
  2369.         elseif (maxPages == page+1) then
  2370.             local incomplete = false
  2371.             for i = 0, maxPages-1 do
  2372.                 if not curPages[i] then
  2373.                     incomplete = true
  2374.                     break
  2375.                 end
  2376.             end
  2377.             local wasEndOnly = false
  2378.             if incomplete and curPages[maxPages-1] then
  2379.                 wasEndOnly = true
  2380.                 for i = 1, maxPages-3 do
  2381.                     if curPages[i] then
  2382.                         wasEndOnly = false
  2383.                         break
  2384.                     end
  2385.                 end
  2386.             end
  2387.             elapsed = endTime - private.scanStarted - private.totalPaused
  2388.             private.UpdateScanProgress(nil, totalAuctions, #curScan, elapsed, page+2, maxPages, curQuery)
  2389.             private.Commit(incomplete, wasEndOnly, false)
  2390.         elseif maxPages == 0 and page == 0 and numBatchAuctions == 0 then
  2391.             -- manual search, no auctions returned
  2392.             elapsed = endTime - private.scanStarted - private.totalPaused
  2393.             private.UpdateScanProgress(nil, totalAuctions, #curScan, elapsed, page+2, maxPages, curQuery)
  2394.             private.Commit(false, false, false)
  2395.         end
  2396.     end
  2397.     private.storeTime = endTime-retrievalStarted -- temp hack as RunTime calculation is broken - this will include paused and other non-processing time!
  2398.     if (_G.nLog) then
  2399.         _G.nLog.AddMessage("Auctioneer", "Scan", _G.N_INFO, ("StorePage Page %d Complete"):format(page),
  2400. --      ("Query Elapsed: %fs\nThis Page Store Elapsed: %fs\nThis Page Code Execution Time: %fs"):format(endTime-queryStarted, endTime-retrievalStarted, RunTime))
  2401.         ("Query Elapsed: %fs\nThis Page Store Elapsed: %fs"):format(endTime-queryStarted, endTime-retrievalStarted))
  2402.     end
  2403.  
  2404.     -- Report warning for Blizzard bug {ADV-595}
  2405.     -- (we wait til we're finished storing as much as we can, before asking the user to close the AH)
  2406.     if private.warningCanSendBug then
  2407.         private.warningCanSendBug = nil
  2408.         if not CanSendAuctionQuery() then
  2409.             --_G.message("The Server is not responding correctly.\nClosing and reopening the Auctionhouse may fix this problem.") -- ### suppressed
  2410.         end
  2411.     end
  2412. end
  2413.  
  2414. function private.StopStorePage(silent)
  2415.     if not CoStore or coroutine.status(CoStore) ~= "suspended" then return end
  2416.     local isGetAll = private.isGetAll
  2417.     -- flag to break out of the loop, or prevent the loop being entered, within the coroutine
  2418.     private.breakStorePage = true
  2419.     while coroutine.status(CoStore) == "suspended" do
  2420.         CoroutineResume(CoStore)
  2421.     end
  2422.     private.breakStorePage = nil
  2423.     if isGetAll and not silent then
  2424.         _G.message("Warning: GetAll scan is incomplete because it was interrupted")
  2425.     end
  2426. end
  2427.  
  2428. function lib.StorePage()
  2429.     if not CoStore or coroutine.status(CoStore) == "dead" then
  2430.         CoStore = coroutine.create(StorePageFunction)
  2431.         CoroutineResume(CoStore)
  2432.     elseif coroutine.status(CoStore) == "suspended" then
  2433.         CoroutineResume(CoStore)
  2434.     end
  2435. end
  2436.  
  2437. --[[ _G.AucAdvanced.Scan.QuerySafeName(name)
  2438.     Library function to convert a name to the 'normalized' form used by scan querys
  2439.     Note: performs truncation on names over 63 bytes as QueryAuctionItems cannot handle longer strings
  2440. --]]
  2441. function lib.QuerySafeName(name)
  2442.     if type(name) == "string" and #name > 0 then
  2443.         if #name > 63 then
  2444.             if name:byte(63) >= 192 then -- UTF-8 multibyte first byte
  2445.                 name = name:sub(1, 62)
  2446.             elseif name:byte(62) >= 224 then -- UTF-8 triplebyte first byte
  2447.                 name = name:sub(1, 61)
  2448.             else
  2449.                 name = name:sub(1, 63)
  2450.             end
  2451.         end
  2452.         return name:lower()
  2453.     end
  2454. end
  2455.  
  2456. --[[ _G.AucAdvanced.Scan.CreateQuerySig(name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex)
  2457.     Library function to allow other modules to obtain a query sig
  2458.     Returns the sig that would be used in a scan with the specified parameters
  2459. --]]
  2460. function lib.CreateQuerySig(...)
  2461.     return private.CreateQuerySig(private.QueryScrubParameters(...))
  2462. end
  2463.  
  2464. function private.QueryScrubParameters(name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex, exactMatch)
  2465.     -- Converts the parameters that we will store in our scanQuery table into a consistent format:
  2466.     -- converts each parameter to correct type;
  2467.     -- converts all strings to lowercase;
  2468.     -- converts all "" and 0 to nil;
  2469.     -- converts any invalid parameters to nil.
  2470.     name = lib.QuerySafeName(name)
  2471.     minLevel = tonumber(minLevel)
  2472.     if minLevel and minLevel < 1 then minLevel = nil end
  2473.     maxLevel = tonumber(maxLevel)
  2474.     if maxLevel and maxLevel < 1 then maxLevel = nil end
  2475.     classIndex = tonumber(classIndex)
  2476.     if classIndex and classIndex < 1 then classIndex = nil end
  2477.     if classIndex then
  2478.         subclassIndex = tonumber(subclassIndex)
  2479.         if subclassIndex and subclassIndex < 1 then subclassIndex = nil end
  2480.     else
  2481.         subclassIndex = nil -- subclassIndex is only valid if we have a classIndex
  2482.     end
  2483.     invTypeIndex = tonumber(invTypeIndex) or Const.EquipLocToInvIndex[invTypeIndex] -- accepts "INVTYPE_*" strings
  2484.     if invTypeIndex and invTypeIndex < 1 then invTypeIndex = nil end
  2485.     if isUsable and isUsable ~= 0 then
  2486.         isUsable = true
  2487.     else
  2488.         isUsable = nil
  2489.     end
  2490.     if name and exactMatch and exactMatch ~= 0 then
  2491.         exactMatch = true -- exactMatch is only valid if we have a name
  2492.     else
  2493.         exactMatch = nil
  2494.     end
  2495.     qualityIndex = tonumber(qualityIndex)
  2496.     if qualityIndex and qualityIndex < 1 then qualityIndex = nil end
  2497.  
  2498.     return name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex, exactMatch
  2499. end
  2500.  
  2501. function private.CreateQuerySig(name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex, exactMatch)
  2502.     return strjoin("#",
  2503.         name or "",
  2504.         minLevel or "",
  2505.         maxLevel or "",
  2506.         invTypeIndex or "",
  2507.         classIndex or "",
  2508.         subclassIndex or "",
  2509.         isUsable and "1" or "", -- can't concatenate booleans
  2510.         qualityIndex or "",
  2511.         exactMatch and "1" or ""
  2512.     ) -- can use strsplit("#", sig) to extract params
  2513. end
  2514.  
  2515. function private.QueryCompareParameters(query, name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex, exactMatch)
  2516.     -- Returns true if the parameters are identical to the values stored in the specified scanQuery table
  2517.     -- Use this function to avoid creating a duplicate scanQuery table
  2518.     -- Parameters must have been scrubbed first
  2519.     -- Note: to compare two scanQuery tables for equality, just compare the sigs
  2520.     if query.name == name -- note: both already converted to lowercase when scrubbed
  2521.     and query.minUseLevel == minLevel
  2522.     and query.maxUseLevel == maxLevel
  2523.     and query.classIndex == classIndex
  2524.     and query.subclassIndex == subclassIndex
  2525.     and query.quality == qualityIndex
  2526.     and query.invType == invTypeIndex
  2527.     and query.isUsable == isUsable
  2528.     and query.exactMatch == exactMatch
  2529.     then
  2530.         return true
  2531.     end
  2532. end
  2533.  
  2534. private.querycount = 0
  2535.  
  2536. function private.NewQueryTable(name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex, exactMatch)
  2537.     -- Assumes the parameters have already been scrubbed
  2538.     local class, subclass
  2539.     local query, qryinfo = {}, {}
  2540.     query.qryinfo = qryinfo
  2541.     qryinfo.query = query
  2542.  
  2543.     query.name = name
  2544.     query.minUseLevel = minLevel
  2545.     query.maxUseLevel = maxLevel
  2546.     query.invType = invTypeIndex
  2547.     if classIndex then
  2548.         class = Const.CLASSES[classIndex]
  2549.         query.class = class
  2550.         query.classIndex = classIndex
  2551.     end
  2552.     if subclassIndex then
  2553.         subclass = Const.SUBCLASSES[classIndex][subclassIndex]
  2554.         query.subclass = subclass
  2555.         query.subclassIndex = subclassIndex
  2556.     end
  2557.     query.isUsable = isUsable
  2558.     query.quality = qualityIndex
  2559.     query.exactMatch = exactMatch
  2560.  
  2561.     qryinfo.page = -1 -- use this to store highest page seen by query, and we haven't seen any yet.
  2562.     qryinfo.id = private.querycount
  2563.     private.querycount = private.querycount+1
  2564.     qryinfo.sig = private.CreateQuerySig(name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex, exactMatch)
  2565.  
  2566.     -- the return value from GetFaction() can change when the Auctionhouse closes
  2567.     -- (Neutral Auctionhouse and "Always Home Faction" option enabled - this is on by default)
  2568.     -- store the current return value - this will be used throughout processing to avoid problems
  2569.     qryinfo.serverKey = GetFaction()
  2570.  
  2571.     local scanSize = false, ""
  2572.     if ((not query.class) and (not query.subclass) and (not query.minUseLevel)
  2573.             and (not query.maxUseLevel)
  2574.             and (not query.name) and (not query.isUsable)
  2575.             and (not query.invType) and (not query.quality)
  2576.             and (not query.exactMatch)) then
  2577.         qryinfo.scanSize = "Full"
  2578.     elseif (query.name and query.class and query.subclass and query.quality) then
  2579.         qryinfo.scanSize = "Micro"
  2580.     else
  2581.         qryinfo.scanSize = "Partial"
  2582.     end
  2583.     query.pageIncomplete = false
  2584.     return query
  2585. end
  2586.  
  2587. private.Hook = {}
  2588. private.Hook.PlaceAuctionBid = PlaceAuctionBid
  2589. function PlaceAuctionBid(type, index, bid, ...)
  2590.     local itemData = lib.GetAuctionItem(type, index)
  2591.     if itemData then
  2592.         private.Unpack(itemData, statItem)
  2593.         local modules = _G.AucAdvanced.GetAllModules("ScanProcessors")
  2594.         for pos, engineLib in ipairs(modules) do
  2595.             if engineLib.ScanProcessors["placebid"] then
  2596.                 pcall(engineLib.ScanProcessors["placebid"],"placebid", statItem, type, index, bid)
  2597.             end
  2598.         end
  2599.     end
  2600.     return private.Hook.PlaceAuctionBid(type, index, bid, ...)
  2601. end
  2602.  
  2603. private.Hook.ClickAuctionSellItemButton = ClickAuctionSellItemButton
  2604. function ClickAuctionSellItemButton(...)
  2605.     local ctype, itemID, itemLink = GetCursorInfo()
  2606.     if ctype == "item" then
  2607.         private.auctionItem = itemLink
  2608.     else
  2609.         private.auctionItem = nil
  2610.     end
  2611.     return private.Hook.ClickAuctionSellItemButton(...)
  2612. end
  2613.  
  2614. private.Hook.StartAuction = StartAuction
  2615. function StartAuction(minBid, buyoutPrice, runTime, ...)
  2616.     local itemData, price = lib.GetAuctionSellItem(minBid, buyoutPrice, runTime)
  2617.     if itemData then
  2618.         private.Unpack(itemData, statItem)
  2619.         local modules = _G.AucAdvanced.GetAllModules("ScanProcessors")
  2620.         for pos, engineLib in ipairs(modules) do
  2621.             if engineLib.ScanProcessors["newauc"] then
  2622.                 pcall(engineLib.ScanProcessors["newauc"],"newauc", statItem, minBid, buyoutPrice, runTime, price)
  2623.             end
  2624.         end
  2625.     end
  2626.     return private.Hook.StartAuction(minBid, buyoutPrice, runTime, ...)
  2627. end
  2628.  
  2629. private.Hook.TakeInboxMoney = TakeInboxMoney
  2630. function TakeInboxMoney(index, ...)
  2631.     local invoiceType, itemName, playerName, bid, buyout, deposit, consignment = GetInboxInvoiceInfo(index)
  2632.     if invoiceType then
  2633.         local modules = _G.AucAdvanced.GetAllModules("ScanProcessors")
  2634.         local _,_, sender = GetInboxHeaderInfo(index)
  2635.  
  2636.         local faction = "Neutral"
  2637.         if sender:find(FACTION_ALLIANCE) then
  2638.             faction = "Alliance"
  2639.         elseif sender:find(FACTION_HORDE) then
  2640.             faction = "Horde"
  2641.         end
  2642.  
  2643.         for pos, engineLib in ipairs(modules) do
  2644.             if engineLib.ScanProcessors["aucsold"] then
  2645.                 pcall(engineLib.ScanProcessors["aucsold"],"aucsold", faction, itemName, playerName, bid, buyout, deposit, consignment)
  2646.             end
  2647.         end
  2648.     end
  2649.     return private.Hook.TakeInboxMoney(index, ...)
  2650. end
  2651.  
  2652. private.Hook.QueryAuctionItems = QueryAuctionItems
  2653.  
  2654. local isSecure, taint = issecurevariable("CanSendAuctionQuery")
  2655. if not isSecure then
  2656.     private.warnTaint = taint
  2657. end
  2658. private.CanSend = CanSendAuctionQuery
  2659.  
  2660. function QueryAuctionItems(name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, page, isUsable, qualityIndex, GetAll, exactMatch, ...)
  2661.     if not private.isAuctioneerQuery then
  2662.         -- Optional bypass to handle compatibility problems with other AddOns
  2663.         local doBypass = false
  2664.         if private.compatModeLocks then
  2665.             local scanbit = private.isBlizzardQuery and 2 or 1
  2666.             for lock, mode in pairs(private.compatModeLocks) do
  2667.                 if bitand(mode, scanbit) ~= 0 then -- another AddOn has requested we bypass this scan type
  2668.                     doBypass = true
  2669.                     break
  2670.                 end
  2671.             end
  2672.             private.compatModeLocks[""] = nil -- remove anonymous lock (if present)
  2673.         end
  2674.         doBypass = doBypass or not get("core.scan.scanallqueries")
  2675.         if doBypass then
  2676.             return private.Hook.QueryAuctionItems(name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, page, isUsable, qualityIndex, GetAll, exactMatch, ...)
  2677.         end
  2678.     end
  2679.  
  2680.     private.isAuctioneerQuery = nil
  2681.     private.isBlizzardQuery = nil
  2682.     if private.compatModeLocks and not next(private.compatModeLocks) then
  2683.         private.compatModeLocks = nil -- remove table if empty
  2684.     end
  2685.     if private.warnTaint then
  2686.         _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 ")
  2687.         private.warnTaint = nil
  2688.     end
  2689.     if not private.CanSend() then
  2690.         _print("Can't send query just at the moment")
  2691.         return
  2692.     end
  2693.  
  2694.     local isSearch = (BrowseSearchButton:GetButtonState() == "PUSHED")
  2695.  
  2696.     -- If we're getting called after we've sent a query, but before it's been stored, take this chance to save it.
  2697.     if private.sentQuery then
  2698.         lib.StorePage()
  2699.     end
  2700.  
  2701.     name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex, exactMatch = private.QueryScrubParameters(
  2702.         name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex, exactMatch)
  2703.  
  2704.     local query
  2705.     if private.curQuery then
  2706.         if not GetAll and not private.isGetAll
  2707.         and private.QueryCompareParameters(private.curQuery, name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex, exactMatch) then
  2708.             private.StopStorePage()
  2709.             query = private.curQuery
  2710.             if (_G.nLog) then
  2711.                 _G.nLog.AddMessage("Auctioneer", "Scan", _G.N_INFO, ("Sending existing query %d (%s)"):format(query.qryinfo.id, query.qryinfo.sig))
  2712.             end
  2713.         else
  2714.             private.Commit(true, false, false)
  2715.         end
  2716.     end
  2717.     if not query then
  2718.         query = private.NewQueryTable(name, minLevel, maxLevel, invTypeIndex, classIndex, subclassIndex, isUsable, qualityIndex, exactMatch)
  2719.         private.scanStartTime = time()
  2720.         private.scanStarted = GetTime()
  2721.         private.totalPaused = 0
  2722.         private.storeTime = 0
  2723.         private.curQuery = query
  2724.     end
  2725.  
  2726.     page = tonumber(page) or 0
  2727.     if (page==0) then
  2728.         local scanSize = query.qryinfo.scanSize
  2729.         if (query.qryinfo.NoSummary) then
  2730.             scanSize = "NoSum-"..scanSize
  2731.         end
  2732.         if (_G.nLog) then
  2733.             local queryType = "standard"
  2734.             if (GetAll) then queryType = "get all" end
  2735.             _G.nLog.AddMessage("Auctioneer", "Scan", _G.N_INFO, ("Sending new %s query %d (%s)"):format(queryType, query.qryinfo.id, query.qryinfo.sig))
  2736.         end
  2737.         _G.AucAdvanced.SendProcessorMessage("scanstart", scanSize, query.qryinfo.sig, query)
  2738.     end
  2739.  
  2740.  
  2741.     private.sentQuery = true
  2742.     lib.lastReq = GetTime()
  2743.  
  2744.     private.queryStarted = GetTime()
  2745.     private.auctionItemListUpdated = false
  2746.     return private.QuerySent(query, isSearch,
  2747.         private.Hook.QueryAuctionItems(
  2748.             name or "", minLevel or "", maxLevel or "", invTypeIndex, classIndex, subclassIndex,
  2749.             page, isUsable, qualityIndex, GetAll, exactMatch, ...))
  2750. end
  2751.  
  2752. -- Function to indicate that the next call to QueryAuctionItems comes from Auctioneer itself.
  2753. function lib.SetAuctioneerQuery()
  2754.     private.isAuctioneerQuery = true
  2755. end
  2756.  
  2757. --[[ Function for third-party AddOns to change Auctioneer's scanning behaviour to avoid compatibility issues
  2758.     Duplicates or overrides certain compatibility Config settings
  2759.     mode 1 : don't scan next raw call to QueryAuctionItems (i.e. neither isAuctioneerQuery nor isBlizzardQuery is set)
  2760.     mode 2 : don't scan next Blizzard query (i.e. isBlizzardQuery is set but isAuctioneerQuery is not set)
  2761.  
  2762.     lock : optional lock "key" (preferably string containing AddOn name for uniqueness)
  2763.         given mode will persist until cancelled: cancel by calling CompatibilityMode with mode 0 and the same lock "key"
  2764. --]]
  2765. function lib.CompatibilityMode(mode, lock)
  2766.     if type(mode) ~= "number" or floor(mode) ~= mode then
  2767.         error("AucAdvanced.Scan.CompatibilityMode(mode, lock)\nmode must be a number (bitfield)", 2)
  2768.     end
  2769.     lock = lock or "" -- use "" as key for anonymous locks, which are removed by the next call to QueryAuctionItems
  2770.     if not private.compatModeLocks then
  2771.         private.compatModeLocks = {}
  2772.     end
  2773.  
  2774.     if lock == "" then -- anonymous lock
  2775.         private.compatModeLocks[lock] = bitor(private.compatModeLocks[lock] or 0, mode) -- merge modes
  2776.     elseif mode == 0 then
  2777.         private.compatModeLocks[lock] = nil
  2778.     else
  2779.         private.compatModeLocks[lock] = mode -- overwrite mode
  2780.     end
  2781. end
  2782.  
  2783. function lib.SetPaused(pause)
  2784.     if private.isGetAll then
  2785.         -- A GetAll scan cannot be Popped or Pushed
  2786.         assert(not private.isPaused)
  2787.         if pause then
  2788.             _print("Scan cannot be paused/unpaused because it is a GetAll scan")
  2789.         end
  2790.         return
  2791.     end
  2792.     if pause then
  2793.         if private.isPaused then return end
  2794.         lib.PushScan()
  2795.         private.isPaused = true
  2796.     elseif private.isPaused then
  2797.         lib.PopScan()
  2798.         private.isPaused = false
  2799.     end
  2800. end
  2801.  
  2802. private.unexpectedClose = false
  2803. local timeoutCanSend = 0 -- part of fix for Blizzard bug {ADV-595}
  2804.  
  2805. function private.OnUpdate(me, dur)
  2806.     if CoCommit then
  2807.         local costat = coroutine.status(CoCommit)
  2808.         if costat == "suspended" then
  2809.             CoroutineResume(CoCommit)
  2810.         elseif costat == "dead" then
  2811.             if #private.CommitQueue > 0 then
  2812.                 CoCommit = coroutine.create(Commitfunction)
  2813.                 CoroutineResume(CoCommit)
  2814.             else
  2815.                 CoCommit = nil
  2816.             end
  2817.         end
  2818.     end
  2819.     local auctionFrame = _G.AuctionFrame
  2820.     if not auctionFrame then return end
  2821.     if private.isPaused then return end
  2822.     local isVisibleAucFrame = auctionFrame:IsVisible()
  2823.  
  2824.     if private.queueScan then
  2825.         if isVisibleAucFrame and CanSendAuctionQuery() then
  2826.             local queued = private.queueScan
  2827.             private.queueScan = nil
  2828.             lib.StartScan(unpack(queued, 1, private.queueScanParams)) -- explicit start and end points as some entries may be nil
  2829.         end
  2830.         return
  2831.     end
  2832.  
  2833.     if CoStore and coroutine.status(CoStore) == "suspended" and isVisibleAucFrame then
  2834.         CoroutineResume(CoStore)
  2835.     end
  2836.     if private.scanNext then
  2837.         if isVisibleAucFrame and CanSendAuctionQuery() then
  2838.             local nextPage = private.scanNextPage
  2839.             private.scanNext = nil
  2840.             private.ScanPage(nextPage, true)
  2841.         end
  2842.         return
  2843.     end
  2844.  
  2845.     if isVisibleAucFrame then
  2846.         if private.unexpectedClose then
  2847.             private.unexpectedClose = false
  2848.             lib.PopScan()
  2849.             return
  2850.         end
  2851.  
  2852.         if private.sentQuery and private.auctionItemListUpdated then
  2853.             if CanSendAuctionQuery() then
  2854.                 timeoutCanSend = 0
  2855.                 lib.StorePage()
  2856.             elseif timeoutCanSend > 15 then
  2857.                 -- Fix for Blizzard Auctionhouse bug {ADV-595}
  2858.                 -- CanSendAuctionQuery continues to return nil indefinitely. We use a timeout
  2859.                 timeoutCanSend = 0
  2860.                 private.warningCanSendBug = true -- further handling required by StorePageFunction
  2861.                 lib.StorePage()
  2862.             else
  2863.                 -- part of fix for Blizzard bug {ADV-595}
  2864.                 timeoutCanSend = timeoutCanSend + dur
  2865.             end
  2866.         end
  2867.     elseif private.curQuery then
  2868.         lib.Interrupt()
  2869.     end
  2870. end
  2871. private.updater = CreateFrame("Frame", nil, UIParent)
  2872. private.updater:SetScript("OnUpdate", private.OnUpdate)
  2873.  
  2874. function lib.Cancel()
  2875.     if (private.curQuery) then
  2876.         _print("Cancelling current scan")
  2877.         private.Commit(true, false, false)
  2878.     end
  2879.     private.ResetAll()
  2880. end
  2881.  
  2882. function lib.Interrupt()
  2883.     if private.curQuery and not _G.AuctionFrame:IsVisible() then
  2884.         if private.isGetAll then
  2885.             -- GetAll cannot be pushed/popped so we have to commit here instead
  2886.             private.Commit(true, false, true)
  2887.             private.sentQuery = false
  2888.             if private.isGetAll then
  2889.                 -- If the StorePage function didn't run, we need to cleanup here instead
  2890.                 lib.ProgressBars("GetAllProgressBar", nil, false)
  2891.                 BrowseSearchButton:Show()
  2892.                 _G.AucAdvanced.API.BlockUpdate(false)
  2893.                 private.isGetAll = nil
  2894.             end
  2895.         elseif private.isScanning then
  2896.             private.unexpectedClose = true
  2897.             lib.PushScan()
  2898.         else
  2899.             private.Commit(true, false, false)
  2900.             private.sentQuery = false
  2901.         end
  2902.     end
  2903. end
  2904.  
  2905. function lib.Abort()
  2906.     if (private.curQuery) then
  2907.         _print("Aborting current scan")
  2908.     end
  2909.     private.ResetAll()
  2910. end
  2911.  
  2912. function private.ResetAll()
  2913.     private.StopStorePage(true)
  2914.  
  2915.     -- Fallback in case private.isGetAll and related actions were not cleared during processing
  2916.     lib.ProgressBars("GetAllProgressBar", nil, false)
  2917.     BrowseSearchButton:Show()
  2918.     _G.AucAdvanced.API.BlockUpdate(false)
  2919.     private.isGetAll = nil
  2920.  
  2921.     local oldquery = private.curQuery
  2922.     private.curQuery = nil
  2923.     private.curScan = nil
  2924.     private.isPaused = nil
  2925.     private.sentQuery = nil
  2926.     private.isScanning = false
  2927.     private.unexpectedClose = false
  2928.  
  2929.     private.UpdateScanProgress(false, nil, nil, nil, nil, nil, oldquery)
  2930.     if CommitRunning then
  2931.         return
  2932.     end
  2933.     private.scanStartTime = nil
  2934.     private.scanStarted = nil
  2935.     private.totalPaused = nil
  2936.     private.storeTime = nil
  2937.     private.curPages = nil
  2938.     private.scanStack = nil
  2939.  
  2940.     private.Pausing = nil
  2941. end
  2942.  
  2943. -- In the absence of a proper API function to do it, it's necessary to inspect an item's tooltip to
  2944. -- figure out if it's usable by the player
  2945. local ItemUsableTooltip = {
  2946.     tooltipFrame = nil,
  2947.     fontString = {},
  2948.     maxLines = 100,
  2949.  
  2950.     CanUse = function(this, link)
  2951.         -- quick level check first
  2952.         local minLevel = select(5, GetItemInfo(link)) or 0
  2953.         if UnitLevel("player") < minLevel then
  2954.             return false
  2955.         end
  2956.  
  2957.         -- set up if not done already
  2958.         if not this.tooltipFrame then
  2959.             this.tooltipFrame = CreateFrame("GameTooltip")
  2960.             this.tooltipFrame:SetOwner(UIParent, "ANCHOR_NONE")
  2961.             for i = 1, this.maxLines do
  2962.                 this.fontString[i] = {}
  2963.                 for j = 1, 2 do
  2964.                     this.fontString[i][j] = this.tooltipFrame:CreateFontString()
  2965.                     this.fontString[i][j]:SetFontObject(GameFontNormal)
  2966.                 end
  2967.                 this.tooltipFrame:AddFontStrings(this.fontString[i][1], this.fontString[i][2])
  2968.             end
  2969.             this.minLevelPattern = string.gsub(ITEM_MIN_LEVEL, "(%%d)", "(.+)")
  2970.         end
  2971.  
  2972.         -- clear tooltip
  2973.         local numLines
  2974.         numLines = math.min(this.maxLines, this.tooltipFrame:NumLines())
  2975.         for i = 1, numLines do
  2976.             for j = 1, 2 do
  2977.                 this.fontString[i][j]:SetText()
  2978.                 this.fontString[i][j]:SetTextColor(0, 0, 0)
  2979.             end
  2980.         end
  2981.  
  2982.         -- populate tooltip
  2983.         this.tooltipFrame:SetHyperlink(link)
  2984.  
  2985.         -- search tooltip for red text
  2986.         numLines = math.min(this.maxLines, this.tooltipFrame:NumLines())
  2987.         for i = 1, numLines do
  2988.             for j = 1, 2 do
  2989.                 local r, g, b = this.fontString[i][j]:GetTextColor()
  2990.                 if r > 0.8 and g < 0.2 and b < 0.2 then
  2991.                     -- item is not usable, with one exception: if it doesn't have a level
  2992.                     -- requirement, red "requires level xxx" text refers to some other item,
  2993.                     -- e.g. that created by a recipe
  2994.                     local text = string.lower(this.fontString[i][j]:GetText())
  2995.                     if not (minLevel == 0 and string.find(text, this.minLevelPattern)) then
  2996.                         return false
  2997.                     end
  2998.                 end
  2999.             end
  3000.         end
  3001.  
  3002.         return true
  3003.     end,
  3004. }
  3005.  
  3006. -- Caching wrapper for ItemUsableTooltip. Invalidates cache when certain events occur
  3007. -- (player levels up, learns a new recipe, etc.)
  3008. local ItemUsableCached = {
  3009.     eventFrame = nil,
  3010.     patterns = {},
  3011.     cache = {},
  3012.     tooltip = ItemUsableTooltip,
  3013.  
  3014.     OnEvent = function(this, event, arg1, ...)
  3015.         local dirty = false
  3016.         -- print("got event " .. event .. ", arg1 " .. arg1)
  3017.         if event == "CHAT_MSG_SYSTEM" or event == "CHAT_MSG_SKILL" then
  3018.             for _, pattern in pairs(this.patterns) do
  3019.                 if string.find(arg1, pattern) then
  3020.                     dirty = true
  3021.                     break
  3022.                 end
  3023.             end
  3024.         elseif event == "PLAYER_LEVEL_UP" then
  3025.             dirty = true
  3026.         end
  3027.  
  3028.         if dirty then
  3029.             -- print("invalidating")
  3030.             this.cache = {}
  3031.         end
  3032.     end,
  3033.  
  3034.     RegisterChatString = function(this, chatString)
  3035.         local pattern = chatString
  3036.         pattern = gsub(pattern, "%%s", ".+")
  3037.         pattern = gsub(pattern, "%%d", ".+")
  3038.         pattern = gsub(pattern, "%%%d+%$s", ".+")
  3039.         pattern = gsub(pattern, "%%%d+%$d", ".+")
  3040.         pattern = gsub(pattern, "|3%-%d+%(%%s%)", ".+")
  3041.         tinsert(this.patterns, pattern)
  3042.     end,
  3043.  
  3044.     CanUse = function(this, link)
  3045.         -- set up if not done already
  3046.         if not this.eventFrame then
  3047.             this.eventFrame = CreateFrame("Frame")
  3048.  
  3049.             -- forward events from frame to self
  3050.             this.eventFrame.forwardEventsTo = this
  3051.             this.eventFrame:SetScript(
  3052.                 "OnEvent",
  3053.                 function(eventFrame, ...)
  3054.                     eventFrame.forwardEventsTo:OnEvent(...)
  3055.                 end)
  3056.  
  3057.             -- register events and chat patterns
  3058.             this.eventFrame:RegisterEvent("CHAT_MSG_SYSTEM")
  3059.             this.eventFrame:RegisterEvent("CHAT_MSG_SKILL")
  3060.             this.eventFrame:RegisterEvent("PLAYER_LEVEL_UP")
  3061.  
  3062.             this:RegisterChatString(_G.ERR_LEARN_ABILITY_S)
  3063.             this:RegisterChatString(_G.ERR_LEARN_RECIPE_S)
  3064.             this:RegisterChatString(_G.ERR_LEARN_SPELL_S)
  3065.             this:RegisterChatString(_G.ERR_SPELL_UNLEARNED_S)
  3066.             this:RegisterChatString(_G.ERR_SKILL_GAINED_S)
  3067.             this:RegisterChatString(_G.ERR_SKILL_UP_SI)
  3068.         end
  3069.  
  3070.         local linkType, id = strsplit(":", link)
  3071.         linkType = linkType:sub(-4) -- get last 4 characters
  3072.         if linkType == "epet" then
  3073.             -- battlepet : assume anyone can use it
  3074.             -- todo: do we need to check if user has enabled battlepets?
  3075.             -- I think you can still "use" the Pet Cage to learn the pet, even if you haven't enabled battlepets yet
  3076.             -- todo: what if the user has reached the max pet limit?
  3077.             return true
  3078.         elseif linkType ~= "item" then
  3079.             return
  3080.         end
  3081.         id = tonumber(id)
  3082.         if not id then return end
  3083.  
  3084.         -- check cache first. failing that, do a tooltip scan
  3085.         if this.cache[id] == nil then
  3086.             -- print("miss " .. link)
  3087.             this.cache[id] = this.tooltip:CanUse(link)
  3088.         else
  3089.             -- print("hit  " .. link)
  3090.         end
  3091.  
  3092.         return this.cache[id]
  3093.     end,
  3094. }
  3095.  
  3096. private.itemUsable = ItemUsableCached
  3097. function private.CanUse(link)
  3098.     return private.itemUsable:CanUse(link)
  3099. end
  3100.  
  3101. function lib.GetScanCount()
  3102.     local scanCount = 0
  3103.     if (private.scanStack) then scanCount = #private.scanStack end
  3104.     if (private.isScanning) then
  3105.         scanCount = scanCount + 1
  3106.     end
  3107.     return scanCount
  3108. end
  3109.  
  3110. function lib.GetStackedScanCount()
  3111.     local scanCount = 0
  3112.     if (private.scanStack) then scanCount = #private.scanStack end
  3113.     return scanCount
  3114. end
  3115.  
  3116. function coremodule.OnUnload()
  3117.     if (private.curQuery) then
  3118.         private.Commit(true, false, false)
  3119.     end
  3120.     if CoCommit then
  3121.         while coroutine.status(CoCommit) == "suspended" do
  3122.             CoroutineResume(CoCommit)
  3123.         end
  3124.     end
  3125. end
  3126.  
  3127. coremodule.Processors = {}
  3128. function coremodule.Processors.auctionui()
  3129.     private.Hook.AuctionFrameBrowse_Search = AuctionFrameBrowse_Search
  3130.     function AuctionFrameBrowse_Search()
  3131.         private.isBlizzardQuery = true
  3132.         private.Hook.AuctionFrameBrowse_Search()
  3133.         private.isBlizzardQuery = nil
  3134.     end
  3135. end
  3136.  
  3137. function coremodule.Processors.scanstats(event, scanstats)
  3138.     private.clearImageCaches(event, scanstats)
  3139. end
  3140.  
  3141. function coremodule.Processors.auctionclose(event)
  3142.     -- clearup memory usage when AH closed
  3143.     private.ResetItemInfoCache()
  3144.     private.clearImageCaches(event)
  3145.     lib.Interrupt()
  3146. end
  3147.  
  3148. if Resources.PlayerFaction == "Neutral" then
  3149.     coremodule.Processors.factionselect = function(event)
  3150.         private.clearImageCaches(event)
  3151.     end
  3152. end
  3153.  
  3154.  
  3155. internal.Scan = {}
  3156. function internal.Scan.NotifyItemListUpdated()
  3157.     if private.scanStarted then
  3158.         private.auctionItemListUpdated = true
  3159.         --[[ commented out for now - this gets really spammy
  3160.         if (_G.nLog) then
  3161.             local startTime = GetTime()
  3162.             _G.nLog.AddMessage("Auctioneer", "Scan", _G.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))
  3163.         end
  3164.         --]]
  3165.     end
  3166. end
  3167.  
  3168. function internal.Scan.NotifyOwnedListUpdated()
  3169. --  if private.scanStarted then
  3170. --      if (_G.nLog) then
  3171. --          local startTime = GetTime()
  3172. --          _G.nLog.AddMessage("Auctioneer", "Scan", _G.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))
  3173. --      end
  3174. --  end
  3175. end
  3176.  
  3177. _G.AucAdvanced.RegisterRevision("$URL: http://svn.norganna.org/auctioneer/branches/5.21f/Auc-Advanced/CoreScan.lua $", "$Rev: 5577 $")
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement