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, chunkStart, chunkEnd local function itemParseCoroutine(start, tooltip, threadNumber) if threadNumber == 1 then chunkStart, chunkEnd = chunkSize * math.floor(start / chunkSize), chunkSize * math.floor((start + chunkSize) / chunkSize) - 1 IS_status.items.scan_active = true IS_status.items.finished = false coroutine.yield() startTime = GetTime() roundStartTime = startTime IS_status.items.last_scanned = start - 1 numProcessed, roundNumProcessed, valid, roundValid = 0, 0, 0, 0 print(string.format("ItemScanner: starting scan at item %d", start)) else coroutine.yield() end local function scan(i, scanningAll) IS_status.items.last_scanned = i local itemStartTime = GetTime() if scanItemLink("item:" .. i .. ":0:0:0:0:0:0:0:85", tooltip, threadNumber) then if scanningAll then if IS_status.items.invalid[i] ~= false then valid, roundValid = valid + 1, roundValid + 1 end else if IS_status.items.scan_active then IS_status.items.last_valid = i end valid, roundValid = valid + 1, roundValid + 1 end -- Reset invalid count IS_status.items.invalid[i] = false else -- start with 3 if no previous value IS_status.items.invalid[i] = (IS_status.items.invalid[i] or 2) + 1 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 while IS_status.items.scan_active and not IS_status.items.finished do local i = IS_status.items.last_scanned if i == nil then break end repeat i = i + 1 if i > chunkEnd or i > maxItem then IS_status.items.finished = true break end until not IS_status.items.invalid[i] or (type(IS_status.items.invalid[i]) == "number" and IS_status.items.invalid[i] < CONSECUTIVE_INVALID_TO_IGNORE) if IS_status.items.finished then break end scan(i, false) end local chunkName = chunkStart .. "-" .. chunkEnd while IS_status.items.scan_active do local i = (IS_status.items.last_invalid[chunkName] or chunkStart - 1) repeat i = i + 1 if i > chunkEnd or i > maxItem then i = chunkStart end until IS_status.items.invalid[i] ~= true IS_status.items.last_invalid[chunkName] = i scan(i, true) end if threadNumber == 1 then 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)) end return true end local function suffixParseCoroutine(start, tooltip) IS_status.suffixes.scan_active = true IS_status.suffixes.finished = false coroutine.yield() local startTime = GetTime() local roundStartTime = startTime local numProcessed, roundNumProcessed, valid, roundValid = 0, 0, 0, 0 print(string.format("ItemScanner: starting scan at suffix %d", start)) local function scan(i, scanningAll) IS_status.suffixes.last_scanned = i local itemStartTime = GetTime() if scanItemLink("item:11993:0:0:0:0:0:" .. i .. ":100:85", tooltip) then if scanningAll then if IS_status.suffixes.invalid[i] ~= false then valid, roundValid = valid + 1, roundValid + 1 end else IS_status.suffixes.last_valid = i valid, roundValid = valid + 1, roundValid + 1 end -- Reset invalid count IS_status.suffixes.invalid[i] = false else -- start with 3 if no previous value IS_status.suffixes.invalid[i] = (IS_status.suffixes.invalid[i] or 2) + 1 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 suffix %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 for i = start, maxSuffix do if not IS_status.suffixes.invalid[i] or (type(IS_status.suffixes.invalid[i]) == "number" and IS_status.suffixes.invalid[i] < CONSECUTIVE_INVALID_TO_IGNORE) then if not IS_status.suffixes.scan_active then break end scan(i, false) end end IS_status.suffixes.finished = true while IS_status.suffixes.scan_active do local i = (IS_status.suffixes.last_invalid or minSuffix - 1) repeat i = i + 1 if i > maxSuffix then i = minSuffix end until IS_status.suffixes.invalid[i] ~= true IS_status.suffixes.last_invalid = i scan(i, true) end local elapsedTime = GetTime() - startTime print(string.format("ItemScanner: scan stopped at suffix %d, %d in %.1f sec. (%.2f/min.) (%d valid)", IS_status.suffixes.last_scanned, numProcessed, elapsedTime, numProcessed / elapsedTime * 60, valid)) 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")) local currentTime = GetTime() print("roundTime = " .. tostring(currentTime - roundStartTime)) print("elapsedTime = " .. tostring(currentTime - startTime)) 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 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 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(itemParseCoroutine)) frame.itemco[i](start + i - 1, 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 frame.suffco = coroutine.wrap(suffixParseCoroutine) local tooltip = _G["ItemScannerHiddenTooltip1"] if not tooltip then tooltip = CreateFrame("GameTooltip", "ItemScannerHiddenTooltip1", nil, "ItemScannerHiddenTooltip") end frame.suffco(start, 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.last_invalid then IS_status.items.last_invalid = {} end 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)