--[[ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. This is a module for BtmScan to evaluate an item for purchase. If you wish to make your own module, do the following: - Make a copy of the supplied "EvalTemplate.lua" file. - Rename your copy to a name of your choosing. - Edit your copy to do your own valuations of the item. (search for the "TODO" sections in the file) - Insert your new file's name into the "BtmScan.toc" file. - Optionally, put it up on the wiki at: http://norganna.org/wiki/BottomScanner/Evaluators "Disencrafting" evaluator Looks for cheap enchanting mats, cheap items to disenchant, and (deep breath) cheap mats for disenchantable crafted items taking into account the expected cost of the other mats. Collects available crafting recipes when tradeskill window is open. Recipe data is stored in BtmScanData (persists across sessions) ]] -- We simply cannot run without enchantrix! if not Enchantrix then LoadAddOn("Enchantrix") end if not Enchantrix then DEFAULT_CHAT_FRAME:AddMessage("Disencraft: Unable to find Enchantrix. Cannot continue!") return end local libName = "Disencraft" local lcName = libName:lower() local lib = { name = lcName, propername = libName } table.insert(BtmScan.evaluators, lcName) local define = BtmScan.Settings.SetDefault local get = BtmScan.Settings.GetSetting local set = BtmScan.Settings.SetSetting local dataVersion = 1 BtmScan.evaluators[lcName] = lib -- init recipe/ingredient data local defaultDisencraftData = { version = dataVersion, recipeTable = {}, ingredientTable = {}, priceCapTable = { [22450] = 1000000, -- Void Crystal [20725] = 1000000, -- Nexus Crystal [22449] = 1000000, -- Large Prismatic Shard [14344] = 1000000, -- Large Brilliant Shard [11178] = 1000000, -- Large Radiant Shard [11139] = 1000000, -- Large Glowing Shard [11084] = 1000000, -- Large Glimmering Shard [22448] = 1000000, -- Small Prismatic Shard [14343] = 1000000, -- Small Brilliant Shard [11177] = 1000000, -- Small Radiant Shard [11138] = 1000000, -- Small Glowing Shard [10978] = 1000000, -- Small Glimmering Shard [22446] = 1000000, -- Greater Planar Essence [16203] = 1000000, -- Greater Eternal Essence [11175] = 1000000, -- Greater Nether Essence [11135] = 1000000, -- Greater Mystic Essence [11082] = 1000000, -- Greater Astral Essence [10939] = 1000000, -- Greater Magic Essence [22447] = 1000000, -- Lesser Planar Essence [16202] = 1000000, -- Lesser Eternal Essence [11174] = 1000000, -- Lesser Nether Essence [11134] = 1000000, -- Lesser Mystic Essence [10998] = 1000000, -- Lesser Astral Essence [10938] = 1000000, -- Lesser Magic Essence [22445] = 1000000, -- Arcane Dust [16204] = 1000000, -- Illusion Dust [11176] = 1000000, -- Dream Dust [11137] = 1000000, -- Vision Dust [11083] = 1000000, -- Soul Dust [10940] = 1000000, -- Strange Dust } } BtmScanData.DisencraftData = defaultDisencraftData function lib.checkData() if not BtmScanData.DisencraftData or BtmScanData.DisencraftData.version ~= dataVersion then lib.spam("Resetting data") BtmScanData.DisencraftData = defaultDisencraftData end end -- get value of enchanting mat function lib.getMatValue(itemID) if not BtmScanData.DisencraftData.priceCapTable[itemID] then return 0 end local value = Enchantrix.Util.GetReagentPrice(itemID) return value or 0 end -- get disenchant value for item function lib.getDisenchantValue(itemID) local _, itemLink = GetItemInfo(itemID) if not itemLink then return 0 end if not Enchantrix then return 0 end local hsp, med, mkt, five = Enchantrix.Storage.GetItemDisenchantTotals(itemLink) if not hsp then return 0 end if (AucAdvanced) then return five elseif (Auctioneer and Auctioneer.Statistic) then return hsp else return 0 end end -- get cost of buying item directly, from vendor or ah function lib.getDirectCost(itemID) local itemInfo = Informant.GetItem(itemID) if itemInfo and itemInfo.vendors and itemInfo.buy then return itemInfo.buy else local _, itemLink = GetItemInfo(itemID) if not itemLink then return nil end if (AucAdvanced) then return AucAdvanced.API.GetMarketValue(itemLink) elseif (Auctioneer and Auctioneer.Statistic) then local itemKey = Auctioneer.ItemDB.CreateItemKeyFromLink(itemLink) return Auctioneer.Statistic.GetUsableMedian(itemKey) end end end -- get minimum cost of obtaining item, from vendor, ah or crafting function lib.getMinCost(itemID) local minCost = lib.getDirectCost(itemID) or 10000000 if BtmScanData.DisencraftData.recipeTable[itemID] then local matsCost = 0 for _, mat in pairs(BtmScanData.DisencraftData.recipeTable[itemID]) do local matID, matCount = mat[1], mat[2] matsCost = matsCost + lib.getMinCost(matID) * matCount end minCost = math.min(minCost, matsCost) end return minCost end -- get resale value for item function lib.getResaleValue(itemID) local _, itemLink, itemRarity, _, _, itemType, itemSubType = GetItemInfo(itemID) if not itemLink then return 0 end local value if (AucAdvanced) then value = AucAdvanced.API.GetMarketValue(itemLink) or 0 elseif (Auctioneer and Auctioneer.Statistic) then local itemKey = Auctioneer.ItemDB.CreateItemKeyFromLink(itemLink) value = Auctioneer.Statistic.GetUsableMedian(itemKey) or 0 end return value end -- get best crafting value for recipe ingredient function lib.getCraftingValue(itemID) if not BtmScanData.DisencraftData.ingredientTable[itemID] then return 0, "None" end local bestValue, bestReason = 0, "None" for _, recipeID in pairs(BtmScanData.DisencraftData.ingredientTable[itemID]) do local recipeMats = BtmScanData.DisencraftData.recipeTable[recipeID] local _, recipeLink = GetItemInfo(recipeID) local usedCount = 0 local otherMatsCost = 0 for _, mat in pairs(recipeMats) do local matID, matCount = mat[1], mat[2] if matID == itemID then usedCount = usedCount + matCount else otherMatsCost = otherMatsCost + lib.getMinCost(matID) * matCount end end local recipeValue, recipeReason = 0, "None" local ignoreString = lib.item.itemconfig.ignoreModuleList local disenchantReason = "Disencraft " .. recipeLink if ignoreString and string.find(ignoreString, disenchantReason, 0, true) then -- lib.spam("filtered: " .. disenchantReason .. "/" .. ignoreString) else local disenchantValue = lib.getDisenchantValue(recipeID) if disenchantValue > recipeValue then recipeValue = disenchantValue recipeReason = disenchantReason end end if get(lcName..".resell.enable") then local sellReason = "Craft " .. recipeLink if ignoreString and string.find(ignoreString, sellReason, 0, true) then -- lib.spam("filtered: " .. sellReason .. "/" .. ignoreString) else local sellValue = lib.getResaleValue(recipeID) if sellValue > recipeValue then recipeValue = sellValue recipeReason = sellReason end end end local craftValue, craftReason = lib.getCraftingValue(recipeID) if craftValue > recipeValue then recipeValue = craftValue recipeReason = craftReason end recipeValue = (recipeValue - otherMatsCost) / usedCount if recipeValue > bestValue then bestValue = recipeValue bestReason = recipeReason end end return bestValue, bestReason end ------------------------------------------------------------------------------ -- Given an item, work out what its value is function lib.evaluate(item) local value, reason = 0, "None" lib.checkData() lib.item = item local craftValue, craftReason = lib.getCraftingValue(item.id) craftValue = craftValue * item.count if craftValue > value then value = craftValue reason = craftReason end lib.item = nil return value, reason end ------------------------------------------------------------------------------ -- debug spam function lib.spam(message) getglobal("ChatFrame1"):AddMessage("Disencraft: "..message, 0.25, 0.25, 1.0) end -- hook to scan tradeskill window when it opens function lib.tradeUiHook() lib.checkData() local recipeTable = BtmScanData.DisencraftData.recipeTable local ingredientTable = BtmScanData.DisencraftData.ingredientTable local addedCount = 0 for recipeIdx = 1, GetNumTradeSkills() do local name, ttype = GetTradeSkillInfo(recipeIdx) if name and ttype ~= "header" then local recipeLink = GetTradeSkillItemLink(recipeIdx) if recipeLink then local recipeID = BtmScan.BreakLink(recipeLink) if not recipeTable[recipeID] then recipeTable[recipeID] = {} local reagentCount = GetTradeSkillNumReagents(recipeIdx) local madeCount = GetTradeSkillNumMade(recipeIdx) for reagentIdx = 1, reagentCount do local reagentID = BtmScan.BreakLink(GetTradeSkillReagentItemLink(recipeIdx, reagentIdx)) local _, _, reagentUsedCount = GetTradeSkillReagentInfo(recipeIdx, reagentIdx) if not ingredientTable[reagentID] then ingredientTable[reagentID] = {} end recipeTable[recipeID][reagentIdx] = { reagentID, reagentUsedCount / madeCount } table.insert(ingredientTable[reagentID], recipeID) end addedCount = addedCount + 1 end end end end if addedCount > 0 then lib.spam("Added " .. addedCount .. " recipes") end end Stubby.RegisterEventHook("TRADE_SKILL_SHOW", "BtmScan.Disencraft", lib.tradeUiHook) function lib:valuate(item, tooltip) local price = 0 -- If we're not enabled, scadaddle! if (not get(lcName..".enable")) then return end -- If this item is grey, forget about it. if (item.qual == 0) then return end -- Valuate this item local market, reason = lib.evaluate(item) -- No valuation if (market <= 0) then return end item:info(reason, market) -- Calculate the real value of this item once our profit is taken out local pct = get(lcName..".profit.pct") local min = get(lcName..".profit.min") local value, mkdown = BtmScan.Markdown(market, pct, min) item:info((" - %d%% / %s markdown"):format(pct,BtmScan.GSC(min, true)), mkdown) -- Check for tooltip evaluation if (tooltip) then item.what = self.name item.valuation = value item.reason = libName..":"..reason if (item.bid == 0) then return end end -- If the current purchase price is more than our valuation, -- another module "wins" this purchase. if (value < item.purchase) then return end -- Check to see what the most we can pay for this item is. if (item.canbuy and item.buy < value) then price = item.buy elseif (item.canbid and item.bid < value) then price = item.bid end -- Check our projected profit level local profit = 0 if price > 0 then profit = value - price end -- If what we are willing to pay for this item beats what -- other modules are willing to pay, then we "win". if (price >= item.purchase and profit > item.profit) then item.purchase = price item.reason = reason item.what = self.name item.profit = profit item.valuation = market end end define(lcName..'.enable', false) define(lcName..'.profit.min', 3000) define(lcName..'.profit.pct', 30) define(lcName..'.resell.enable', false) function lib:setup(gui) id = gui:AddTab(libName) gui:AddControl(id, "Subhead", 0, libName.." Settings") gui:AddControl(id, "Checkbox", 0, 1, lcName..".enable", "Enable purchasing for "..lcName) gui:AddControl(id, "MoneyFramePinned", 0, 1, lcName..".profit.min", 1, 99999999, "Minimum Profit") gui:AddControl(id, "WideSlider", 0, 1, lcName..".profit.pct", 0, 100, 0.5, "Percent Profit: %0.01f%%") gui:AddControl(id, "Checkbox", 0, 1, lcName..".resell.enable", "Evaluate crafted items for resale") end