local minItem, maxItem, chunkSize = 1, 99999, 20000 local CONSECUTIVE_INVALID_TO_IGNORE = 5 local VALID_DELAY = 0.025 local INVALID_DELAY = 5 local patterns = { "^Design:", "^Formula:", "^Manual:", "^Pattern:", "^Plans:", "^Recipe:", "^Schematic:", "^Technique:", } local skippedLines = { "^Already known$", "^Prospectable$", "^Disenchanting requires Enchanting %(%d+%)$", "^Cannot be disenchanted", "^Collected %(%d/%d%)$", } local function scanItemLink(link) local textL, textR -- Populate hidden tooltip ItemScannerHiddenTooltip:ClearLines() ItemScannerHiddenTooltip:SetHyperlink(link) local startTime = GetTime() local isPattern, hasReq = false, false while true do local numLines = ItemScannerHiddenTooltip:NumLines() local done = numLines > 1 if done then if not isPattern then local text = _G["ItemScannerHiddenTooltipTextLeft1"]:GetText() for _, pattern in ipairs(patterns) do if text:find(pattern) then isPattern = true break end end end -- We can skip the first line because it will only be RETRIEVING_ITEM_INFO if we only have one line for i = 2, numLines do local text = _G["ItemScannerHiddenTooltipTextLeft" .. i]:GetText() if text:find(RETRIEVING_ITEM_INFO) then done = false break elseif text:find("%(%d/[01]%)$") then done = false break elseif isPattern and not hasReq then hasReq = text:find("^\nRequires") end end if isPattern and not hasReq then done = false end if done then break end end coroutine.yield() if GetTime() - startTime >= INVALID_DELAY then -- If we got anything useful, keep it if numLines > 1 then break end return false end end if IS_item_info[link] and #(IS_item_info[link]) >= ItemScannerHiddenTooltip:NumLines() then return true end IS_item_info[link] = { itemInfo = {GetItemInfo(link)}, itemSpell = {GetItemSpell(link)}, statTable = GetItemStats(link), itemUniqueness = {GetItemUniqueness(link)}, consumableItem = IsConsumableItem(link), equippable = IsEquippableItem(link), helpful = IsHelpfulItem(link), harmful = IsHarmfulItem(link), } local numSkipped = 0 for i = 1, ItemScannerHiddenTooltip:NumLines() do textL = _G["ItemScannerHiddenTooltipTextLeft" .. i]:GetText() textR = _G["ItemScannerHiddenTooltipTextRight" .. i]:GetText() local skip = false for _, pattern in ipairs(skippedLines) do if textL:find(pattern) then skip = true numSkipped = numSkipped + 1 break end end if not skip and (i ~= ItemScannerHiddenTooltip:NumLines() or (textL ~= " " and textL ~= "" and textR ~= " " and textR ~= "")) then IS_item_info[link][i - numSkipped] = { left = textL, right = textR, } end end return true end local frame = CreateFrame("Frame") local function OnUpdate(self) local status, err if self.itemco then status, err = pcall(self.itemco) if status == false then print("ItemScanner: item scan error, aborting.") self.itemco = nil IS_status.items.scan_active = false error(err) return elseif err == true then self.itemco = nil end else frame:SetScript("OnUpdate", nil) end end local function itemParseCoroutine(start) local chunkStart, chunkEnd = chunkSize * math.floor(start / chunkSize), chunkSize * math.floor((start + chunkSize) / chunkSize) - 1 IS_status.items.scan_active = true coroutine.yield() local startTime = GetTime() local roundStartTime = startTime local numProcessed, roundNumProcessed, valid, roundValid = 0, 0, 0, 0 print(string.format("ItemScanner: starting scan at item %d", start)) for i = start, math.min(chunkEnd, maxItem) do if not IS_status.items.invalid[i] or (type(IS_status.items.invalid[i]) == "number" and IS_status.items.invalid[i] < CONSECUTIVE_INVALID_TO_IGNORE) then IS_status.items.last_scanned = i local itemStartTime = GetTime() if scanItemLink("item:" .. i .. ":0:0:0:0:0:0:0:85") then IS_status.items.last_valid = i -- Reset invalid count IS_status.items.invalid[i] = false valid, roundValid = valid + 1, roundValid + 1 else -- start with 3 if no previous value IS_status.items.invalid[i] = (IS_status.items.invalid[i] or 2) + 1 end if not IS_status.items.scan_active then break end numProcessed = numProcessed + 1 roundNumProcessed = roundNumProcessed + 1 while GetTime() - itemStartTime < VALID_DELAY do coroutine.yield() end local currentTime = GetTime() if currentTime - roundStartTime >= 120 then local elapsedTime = currentTime - startTime print(string.format("ItemScanner: at item %d, %d in %.1f sec. (%.2f/min.) (%d valid), %d this round (%d valid)", i, numProcessed, elapsedTime, numProcessed / elapsedTime * 60, valid, roundNumProcessed, roundValid)) roundStartTime = roundStartTime + 120 roundNumProcessed, roundValid = 0, 0 end end end local elapsedTime = GetTime() - startTime if elapsedTime == 0 then elapsedTime = 0.001 end print(string.format("ItemScanner: scan stopped at item %d, %d in %.1f sec. (%.2f/min.) (%d valid)", IS_status.items.last_scanned, numProcessed, elapsedTime, numProcessed / elapsedTime * 60, valid)) IS_status.items.scan_active = false return true end local function commandHandler(msg) if string.match(msg, "^items") then local start if msg == "items" or msg == "items start" then start = IS_status.items.last_valid or IS_status.items.last_scanned or minItem elseif msg == "items clear" then IS_item_info = {} IS_status.items.last_scanned = nil IS_status.items.last_valid = nil return elseif msg == "items next-chunk" then start = IS_status.items.last_valid or IS_status.items.last_scanned or minItem start = chunkSize * math.ceil(start / chunkSize) if start > maxItem then commandHandler("items stop") return end commandHandler("items clear") elseif msg == "items reset" then commandHandler("items clear") commandHandler("items restart") return elseif msg == "items restart" then start = minItem IS_status.items.last_scanned = nil IS_status.items.last_valid = nil elseif msg == "items restart-chunk" then start = (IS_status.items.last_valid or IS_status.items.last_scanned or minItem) start = math.max(chunkSize * math.floor(start / chunkSize), minItem) elseif msg == "items stop" then IS_status.items.scan_active = false return else print("Usage: /is items <arg> (or /itemscanner items <arg>") print(" clear clears item data but does not stop a running scan") print(" reset clears item data and starts over") print(" restart restarts the current scan but leaves existing data intact") print(" start (default) starts where you last left off") print(" stop stops scanning but leaves existing data intact") return end frame.itemco = coroutine.wrap(itemParseCoroutine) frame.itemco(start) frame:SetScript("OnUpdate", OnUpdate) else print("Usage: /is <arg> (or /itemscanner <arg>") print(" items scans all items") end end if not IS_item_info then IS_item_info = {} end if not IS_status then IS_status = {} end if not IS_status.items then IS_status.items = {} end if not IS_status.items.invalid then IS_status.items.invalid = {} end SLASH_ITEMSCANNER1="/is" SLASH_ITEMSCANNER2="/itemscanner" SlashCmdList["ITEMSCANNER"] = commandHandler -- Autoresumes on login local function resume() if IS_status.items.scan_active then if IS_status.items.finished then commandHandler("items next-chunk") else commandHandler("items") end end if not frame.itemco then print("Nothing seems to be active, restarting") commandHandler("items reset") end end frame:SetScript("OnUpdate", resume)