local minItem, maxItem, chunkSize, minSuffix, maxSuffix = 1, 99999, 20000, -500, 3000 local CONSECUTIVE_INVALID_TO_IGNORE = 5 local VALID_DELAY = 0.025 local INVALID_DELAY = 10 local NUM_THREADS = 4 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, tooltip, threadNumber) local textL, textR local tooltipName = tooltip:GetName() -- Populate hidden tooltip tooltip:ClearLines() tooltip:SetHyperlink(link) local startTime = GetTime() local isPattern, hasReq = false, false while true do local numLines = tooltip:NumLines() local done = numLines > 1 if done then if not isPattern then local text = _G[tooltipName .. "TextLeft1"]: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[tooltipName .. "TextLeft" .. 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]) >= tooltip:NumLines() then return true end -- Avoids recording blank random suffixes if link:find("^item:11993:") and not link:find("^item:11993:0:0:0:0:0:0:") then if _G[tooltipName .. "TextLeft1"]:GetText():find("^Clay Ring ?$") and tooltip:NumLines() == 5 then return false end 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, tooltip:NumLines() do textL = _G[tooltipName .. "TextLeft" .. i]:GetText() textR = _G[tooltipName .. "TextRight" .. 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 ~= tooltip: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 nextSlot = 1 local function OnUpdate(self) local status, err if self.itemco then if nextSlot > #(self.itemco) then nextSlot = 1 end if self.itemco[nextSlot] then status, err = pcall(self.itemco[nextSlot]) if status == false then print("ItemScanner: item scan error, aborting thread " .. nextSlot .. ".") table.remove(self.itemco, nextSlot) if #(self.itemco) == 0 then IS_status.items.scan_active = false self.itemco = nil end error(err) return elseif err == true then table.remove(self.itemco, nextSlot) if #(self.itemco) == 0 then self.itemco = nil end return end nextSlot = nextSlot + 1 else self.itemco = nil end elseif self.suffco then status, err = pcall(self.suffco) if status == false then print("ItemScanner: suffix scan error, aborting.") self.suffco = nil IS_status.suffixes.scan_active = false error(err) return elseif err == true then self.suffco = nil end else frame:SetScript("OnUpdate", nil) end end local startTime, roundStartTime, numProcessed, roundNumProcessed, valid, roundValid local function genericCoroutine(start, min, max, name, linkTemplate, delay, statusVars, tooltip, threadNumber) if not threadNumber or threadNumber == 1 then statusVars.scan_active = true statusVars.finished = false coroutine.yield() startTime = GetTime() roundStartTime = startTime statusVars.last_scanned = start - 1 numProcessed, roundNumProcessed, valid, roundValid = 0, 0, 0, 0 print(string.format("ItemScanner: starting scan at %s %d", name, start)) else coroutine.yield() end local function scan(i, scanningAll) statusVars.last_scanned = i local itemStartTime = GetTime() if scanItemLink(string.format(linkTemplate, i), tooltip, threadNumber) then if scanningAll then if statusVars.invalid[i] ~= false then valid, roundValid = valid + 1, roundValid + 1 end else if statusVars.scan_active then statusVars.last_valid = i end valid, roundValid = valid + 1, roundValid + 1 end -- Reset invalid count statusVars.invalid[i] = false else -- start with 3 if no previous value statusVars.invalid[i] = (statusVars.invalid[i] or 2) + 1 end numProcessed = numProcessed + 1 roundNumProcessed = roundNumProcessed + 1 while GetTime() - itemStartTime < delay do coroutine.yield() end local currentTime = GetTime() if currentTime - roundStartTime >= 120 then local elapsedTime = currentTime - startTime print(string.format("ItemScanner: at %s %d, %d in %.1f sec. (%.2f/min.) (%d valid), %d this round (%d valid)", name, i, numProcessed, elapsedTime, numProcessed / elapsedTime * 60, valid, roundNumProcessed, roundValid)) roundStartTime = roundStartTime + 120 roundNumProcessed, roundValid = 0, 0 end end while statusVars.scan_active and not statusVars.finished do local i = statusVars.last_scanned if i == nil then break end repeat i = i + 1 if i > max then statusVars.finished = true break end until not statusVars.invalid[i] or (type(statusVars.invalid[i]) == "number" and statusVars.invalid[i] < CONSECUTIVE_INVALID_TO_IGNORE) if statusVars.finished then break end scan(i, false) end while statusVars.scan_active do local i = (statusVars.last_invalid or min - 1) repeat i = i + 1 if i > max then i = min end until statusVars.invalid[i] ~= true statusVars.last_invalid = i scan(i, true) end if not threadNumber or threadNumber == 1 then local elapsedTime = GetTime() - startTime if elapsedTime == 0 then elapsedTime = 0.001 end print(string.format("ItemScanner: scan stopped at %s %d, %d in %.1f sec. (%.2f/min.) (%d valid)", name, statusVars.last_scanned, numProcessed, elapsedTime, numProcessed / elapsedTime * 60, valid)) end return true end local function commandHandler(msg) if string.match(msg, "^all") then commandHandler(string.gsub(msg, "all", "items")) commandHandler(string.gsub(msg, "all", "suffixes")) elseif string.match(msg, "^debug") then if IS_status.items.scan_active then if IS_status.items.finished then print("Scanning all items with " .. #(frame.itemco) .. " threads") else print("Scanning known valid items with " .. #(frame.itemco) .. " threads") end print("Last item: " .. tostring(IS_status.items.last_scanned or "none")) elseif IS_status.suffixes.scan_active then if IS_status.suffixes.finished then print("Scanning all random suffixes") else print("Scanning known valid random suffixes") end print("Last suffix: " .. tostring(IS_status.suffixes.last_scanned or "none")) else print("Scanning not active") return end local currentTime = GetTime() print("roundTime = " .. tostring(currentTime - roundStartTime)) print("elapsedTime = " .. tostring(currentTime - startTime)) elseif 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 and suffix data but does not stop a running scan") print(" reset clears item and suffix 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 local chunkStart, chunkEnd = chunkSize * math.floor(start / chunkSize), chunkSize * math.floor((start + chunkSize) / chunkSize) - 1 if chunkEnd > maxItem then chunkEnd = maxItem end frame.itemco = {} for i = 1, NUM_THREADS do local tooltip = _G["ItemScannerHiddenTooltip" .. i] if not tooltip then tooltip = CreateFrame("GameTooltip", "ItemScannerHiddenTooltip" .. i, nil, "ItemScannerHiddenTooltip") end table.insert(frame.itemco, coroutine.wrap(genericCoroutine)) frame.itemco[i](start + i - 1, chunkStart, chunkEnd, "item", "item:%d:0:0:0:0:0:0:0:85", VALID_DELAY * NUM_THREADS, IS_status.items[chunkStart .. "-" .. chunkEnd], tooltip, i) end frame:SetScript("OnUpdate", OnUpdate) elseif string.match(msg, "^suffixes") then local start if msg == "suffixes" or msg == "suffixes start" then start = IS_status.suffixes.last_valid or IS_status.suffixes.last_scanned or minSuffix elseif msg == "suffixes clear" then IS_status.suffixes.last_scanned = nil IS_status.suffixes.last_valid = nil return elseif msg == "suffixes reset" then commandHandler("suffixes restart") return elseif msg == "suffixes restart" then start = minSuffix IS_status.suffixes.last_scanned = nil IS_status.suffixes.last_valid = nil elseif msg == "suffixes stop" then IS_status.suffixes.scan_active = false return else print("Usage: /is suffixes <arg> (or /itemscanner suffixes <arg>") print(" clear clears suffix progress but does not stop a running scan") print(" reset clears suffix progress 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 local tooltip = _G["ItemScannerHiddenTooltip1"] if not tooltip then tooltip = CreateFrame("GameTooltip", "ItemScannerHiddenTooltip1", nil, "ItemScannerHiddenTooltip") end frame.suffco = coroutine.wrap(genericCoroutine) frame.suffco(start, minSuffix, maxSuffix, "suffix", "item:11993:0:0:0:0:0:%d:100:85", VALID_DELAY, IS_status.suffixes, tooltip) frame:SetScript("OnUpdate", OnUpdate) else print("Usage: /is <arg> (or /itemscanner <arg>") print(" all scans all scannable objects") print(" debug prints scanning status") print(" items scans all items") print(" suffixes scans all random suffixes") 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 if not IS_status.items.last_invalid then IS_status.items.last_invalid = {} end if not IS_status.suffixes then IS_status.suffixes = {} end if not IS_status.suffixes.invalid then IS_status.suffixes.invalid = {} end SLASH_ITEMSCANNER1="/is" SLASH_ITEMSCANNER2="/itemscanner" SlashCmdList["ITEMSCANNER"] = commandHandler -- Autoresumes on login local function resume() if not IS_status.items then IS_status.items = {} end local metatable = { __index = IS_status.items, __newindex = function(tbl, key, val) if key == "last_invalid" then rawset(tbl, key, val) else IS_status.items[key] = val end end, } local t = IS_status.items for i = 0, maxItem, chunkSize do local chunkStart, chunkEnd = chunkSize * math.floor(i / chunkSize), chunkSize * math.floor((i + chunkSize) / chunkSize) - 1 local chunkName = chunkStart .. "-" .. chunkEnd t[chunkName] = setmetatable(t[chunkName] or { last_invalid = (IS_status.items.last_invalid or {})[chunkName] }, metatable) end IS_status.items.last_invalid = nil if IS_status.items.scan_active then if IS_status.items.finished then commandHandler("items next-chunk") else commandHandler("items") end end if IS_status.suffixes.scan_active then if IS_status.suffixes.finished then IS_status.suffixes.scan_active = false IS_status.suffixes.finished = false else commandHandler("suffixes") end end if not frame.itemco and not frame.suffco then print("Nothing seems to be active, restarting all") commandHandler("all reset") end end frame:SetScript("OnUpdate", resume)