local minItem, maxItem, chunkSize, minSuffix, maxSuffix, minGem, maxGem = 1, 99999, 20000, -500, 3000, 1, 6000 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 function scanGem(gemId) local results = {GetItemGem("item:25:0:" .. gemId .. ":0:0:0:0:0:0:0", 1)} for _ in pairs(results) do IS_gem_info[gemId] = results return true end return false 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 elseif self.gemco then status, err = pcall(self.gemco) if status == false then print("ItemScanner: gem scan error, aborting.") self.gemco = nil IS_status.gems.scan_active = false error(err) return elseif err == true then self.gemco = 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 gemParseCoroutine(start) IS_status.gems.scan_active = true IS_status.gems.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 gem %d", start)) local function scan(i, scanningAll) IS_status.gems.last_scanned = i local itemStartTime = GetTime() if scanGem(i) then if scanningAll then if IS_status.gems.invalid[i] ~= false then valid, roundValid = valid + 1, roundValid + 1 end else IS_status.gems.last_valid = i valid, roundValid = valid + 1, roundValid + 1 end -- Reset invalid count IS_status.gems.invalid[i] = false else -- start with 3 if no previous value IS_status.gems.invalid[i] = (IS_status.gems.invalid[i] or 2) + 1 end while GetTime() - itemStartTime < VALID_DELAY do coroutine.yield() end numProcessed = numProcessed + 1 roundNumProcessed = roundNumProcessed + 1 local currentTime = GetTime() if currentTime - roundStartTime >= 120 then local elapsedTime = currentTime - startTime print(string.format("ItemScanner: at gem %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, maxGem do if not IS_status.gems.invalid[i] or (type(IS_status.gems.invalid[i]) == "number" and IS_status.gems.invalid[i] < CONSECUTIVE_INVALID_TO_IGNORE) then if not IS_status.gems.scan_active then break end scan(i, false) end end IS_status.gems.finished = true while IS_status.gems.scan_active do local i = (IS_status.gems.last_invalid or minGem - 1) repeat i = i + 1 if i > maxGem then i = minGem end until IS_status.gems.invalid[i] ~= true IS_status.gems.last_invalid = i scan(i, true) end local elapsedTime = GetTime() - startTime if elapsedTime == 0 then elapsedTime = 0.001 end print(string.format("ItemScanner: scan stopped at gem %d, %d in %.1f sec. (%.2f/min.) (%d valid)", IS_status.gems.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")) commandHandler(string.gsub(msg, "all", "gems")) 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")) elseif IS_status.gems.scan_active then if IS_status.gems.finished then print("Scanning all gems") else print("Scanning known valid gems") end print("Last gem: " .. tostring(IS_status.gems.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, "^gems") then local start if msg == "gems" or msg == "gems start" then start = IS_status.gems.last_valid or IS_status.gems.last_scanned or minGem elseif msg == "gems clear" then IS_status.gems.last_scanned = nil IS_status.gems.last_valid = nil IS_gem_info = {} return elseif msg == "gems reset" then commandHandler("gems clear") commandHandler("gems restart") return elseif msg == "gems restart" then start = minGem IS_status.gems.last_scanned = nil IS_status.gems.last_valid = nil elseif msg == "gems stop" then IS_status.gems.scan_active = false return else print("Usage: /is gems <arg> (or /itemscanner gems <arg>") print(" clear clears gem data but does not stop a running scan") print(" reset clears gem 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.gemco = coroutine.wrap(gemParseCoroutine) frame.gemco(start) frame:SetScript("OnUpdate", OnUpdate) 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(" gems scans all gem ids") 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_gem_info then IS_gem_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 if not IS_status.gems then IS_status.gems = {} end if not IS_status.gems.invalid then IS_status.gems.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 IS_status.gems.scan_active then if IS_status.gems.finished then IS_status.gems.scan_active = false IS_status.gems.finished = false else commandHandler("gems") end end if not frame.itemco and not frame.suffco and not frame.gemco then print("Nothing seems to be active, restarting all") commandHandler("all reset") end end frame:SetScript("OnUpdate", resume)